Repository: python-pillow/Pillow

Issue:
ImageFont.getlength dramatically slower in some environments
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?
There's this [script](https://github.com/avalonv/literaryclock/blob/main/quote%20to%20image/quote_to_image.py) which reads some text and recursively wraps/scales a TrueType font to fit an image. I was porting it to Windows and noticed that it ran almost 3x faster than Linux on the same machine, which greatly hurt my sense of superiority as a penguin user. 

Never profiled python code before but what I did was `python -m cProfile -o quote_to_image.prof quote_to_image.py`, and then checked the output with [snakeviz](https://github.com/jiffyclub/snakeviz). Unless I'm wildly mistaken about how this stuff works, there's a massive discrepancy in time spent on getlength calls, despite the number of calls being very similar, with Linux spending more than 3 minutes to handle methods that Windows handled in 30 seconds.
```
ncalls  tottime percall         cumtime percall         filename:lineno(function)
1482429	187.6	0.0001265	187.6	0.0001265	~:0(<method 'getlength' of 'Font' objects>) Linux
1485834	26.45	1.78e-05	26.45	1.78e-05	~:0(<method 'getlength' of 'Font' objects>) Windows
```

To isolate the issue a bit more I decided to profile a shorter [script](https://gist.github.com/avalonv/34486658cc56e5b96dce509f7604b9ba) with only the function that does most of these calls, and interestingly, the discrepancy isn't anywhere as large. However, it's still very obviously there:
```
ncalls  tottime percall         cumtime percall         filename:lineno(function)
96726	16.64	0.000172	16.64	0.000172	~:0(<method 'getlength' of 'Font' objects>) Linux
96726	12.72	0.0001315	12.72	0.0001315	~:0(<method 'getlength' of 'Font' objects>) Windows
```

I was able to reproduce both tests on different dual-booting machines with different python/pillow versions, however I didn't bother to save the results cause dual-booting on a HDD sucks. If I'm an idiot and you need more information let me know.

### What did you expect to happen? 
Similar time spent ImageFont getlength method calls on both operating systems.

### What actually happened? 
Windows was faster, ranging from 500% to 25% (increases exponentially with the number of calls?)

### What are your OS, Python and Pillow versions?

* OS: Windows 10/Arch linux 5.19
* Python: 3.10.7
* Pillow: 9.2.0



Discussion:
I [tested this out with `quote_to_image.py 100`](https://github.com/radarhere/Pillow/commit/dfe9d932cd9e0d5f79493953bccc49926b30df23) on GitHub Actions, and found that the `Test` part of the run [took 5 minutes on Windows](https://github.com/radarhere/Pillow/actions/runs/3166250871/jobs/5155842795), but [took a few seconds on Linux.](https://github.com/radarhere/Pillow/actions/runs/3166250879/jobs/5155842565)

I considered that perhaps the Windows environment had some sort of emulation making it run slower, so I added in a `comparison.py` script to compare the speeds by counting to 100 million, but they're relatively similar.

So given that I've seen the opposite results to you, I don't think this is as simple as Windows vs Linux.
Huh that's super interesting! I'm not at all familiar with Github actions, is there a way you could profile how much time was spent on each call from there?
To reiterate, this is like my first time ever reading this genre of logs, but it seems like the cache on the Linux test is much bigger than the Windows one, could that mean something?
Linux
![image](https://user-images.githubusercontent.com/29720696/193433568-ebb66132-5bb6-43c1-918d-864065490363.png)
Windows
![image](https://user-images.githubusercontent.com/29720696/193433565-da57e51a-8364-4416-8b4b-0ab7758a7ebf.png)

The test run by @radarhere was with heap verification enabled on Windows. After [disabling it](https://github.com/nulano/Pillow/commit/ac82d2609ad5e32540b935914a5aa0209ac573f8), [Windows](https://github.com/nulano/Pillow/actions/runs/3167499455/jobs/5158034631) is only about 2x slower than [Ubuntu](https://github.com/nulano/Pillow/actions/runs/3167499459/jobs/5158034641). I also added a call to `PIL.features.pilinfo`, which shows that Windows is using Raqm layout and Ubuntu is using basic layout. Given this fact, I think a factor of 2x slower is about exactly what I would expect.

I'd expect the opposite is true for @avalonv since Raqm is much more likely to be available by default on Linux systems than on Windows. However, the difference observed in your first test is larger than what I'd expect for a typical font with a line of English text. It could be that the resulting image is different due to more complex layout rules used by Raqm. What font are you using, and what is the text being measured? Edit: I think I found your font and text, but it's a massive list. It might be helpful if you could narrow it down a bit.

If you don't care about complex layout, you could use `font = ImageFont.truetype(..., layout_engine=ImageFont.Layout.BASIC)` to force basic layout, but note that it might give worse results for non-English text.

> To reiterate, this is like my first time ever reading this genre of logs, but it seems like the cache on the Linux test is much bigger than the Windows one, could that mean something?

Different test environments cache different things, there is no way this is related.
> which shows that Windows is using Raqm layout and Ubuntu is using basic layout. Given this fact, I think a factor of 2x slower is about exactly what I would expect.

yeaaaah, that seems to be the culprit here! After setting layout_engine to ImageFont.Layout.BASIC the total processing time on my Linux machine fell to 108s, ten seconds faster than Windows, and the callstack graph looks very similar to the Windows profile I did earlier:

<details><summary>Linux</summary><img src="https://user-images.githubusercontent.com/29720696/193464073-2e0e1abd-889b-4fd2-895e-731d856968b1.png"></details>

<details><summary>Windows</summary><img src="https://user-images.githubusercontent.com/29720696/193464084-9618174b-d6c0-46d3-ab82-ac3a6b15f5c5.png"></details>

I can check `PIL.features.pilinfo` for the sake of thoroughness, but I think it's safe to assume that my Windows environment is using Layout.BASIC and the discrepancy stems almost entirely from that.

Thanks a lot for finding it :D
As for this issue: it would be nice to better document the pros and cons of each layout engine in the ImageFont docs. I skimmed through it quite a few times while trying to solve this, and sure enough, Layout.BASIC is there, but it's not completely clear which one is the default, or why you might prefer Layout.BASIC for some reason (i.e. better performance in certain tasks).
![image](https://user-images.githubusercontent.com/29720696/193465960-15f178d7-f470-4491-bca8-52a6d8ff3952.png)


If I'm getting this right, Raqm is the default if available, but that's not really explicit in its [documentation](https://pillow.readthedocs.io/en/stable/reference/ImageFont.html?highlight=raqm#PIL.ImageFont.PIL.ImageFont.Layout.RAQM), just that you can check if it's available.

Resolving patch:
diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py
--- a/src/PIL/ImageFont.py
+++ b/src/PIL/ImageFont.py
@@ -945,6 +945,11 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
                      encoding of any text provided in subsequent operations.
     :param layout_engine: Which layout engine to use, if available:
                      :data:`.ImageFont.Layout.BASIC` or :data:`.ImageFont.Layout.RAQM`.
+                     If it is available, Raqm layout will be used by default.
+                     Otherwise, basic layout will be used.
+
+                     Raqm layout is recommended for all non-English text. If Raqm layout
+                     is not required, basic layout will have better performance.
 
                      You can check support for Raqm layout using
                      :py:func:`PIL.features.check_feature` with ``feature="raqm"``.


Test patch: