toga icon indicating copy to clipboard operation
toga copied to clipboard

Performance issue with update of Label.text

Open em-oaken opened this issue 10 months ago • 6 comments

Describe the bug

Updating a label takes between 5-6ms per label. This is a problem for an application that has many labels and willing to have refresh rate 25Hz.

Steps to reproduce

Execution of program with py -m my_module.

Code snipet with line_profiler:

Timer unit: 1e-07 s

Line # Hits Time Per Hit % Time Line Contents

46                                               @profile
47                                               def render(self):
48        85       2655.0     31.2      0.0          context = self.canvas.context
49        85     134875.0   1586.8      0.5          context.clear()
50                                           (...)
58        85    4838747.0  56926.4     19.1          self.top_bar_infos[0].text = f'{self.tau.loopno:04}  |  {self.tau.game_duration:.2f}s'
59        85    4524132.0  53225.1     17.8          self.top_bar_infos[1].text = f'Day {self.tau.vtime:%d %H:%M:%S}'
60        85    4483225.0  52743.8     17.7          self.top_bar_infos[2].text = f'{len(self.colony.population)} ants'
61        85    4750631.0  55889.8     18.7          self.top_bar_infos[4].text = f'{1/self.tau.rt_loop_duration.total_seconds():.0f} fps'

self.top_bar_infos defined with self.top_bar_infos = [toga.Label('...', style=Pack(flex=1)) for _ in range(4)], where self is a toga.Box

Testing by replacing to self.infolabel.text = 'abc' leads to the same performance hit.

Expected behavior

I expect update time <1ms per label.

Screenshots

No response

Environment

  • Operating System: Windows 11
  • Python version: 3.13.1
  • Software versions:
    • Briefcase: 0.3.20
    • Toga: 0.4.8

Logs

No response

Additional context

Many thanks for your help in advance

Em

em-oaken avatar Feb 05 '25 20:02 em-oaken

Thanks for the report.

There's four possible sources of slowdown here:

  1. Windows/Winforms itself. If the actual underlying Winforms Label.Text assignment is slow... there's not much we can do about it.
  2. Python.net. If the process of marshalling from Python to the .Net runtime is slow, that's a problem - but it's not a problem that we can do much about. It might be fixable, but it's a performance issue for python.net to resolve.
  3. Toga's layered architecture. This is under our control... but to some extent, it's also "the price of doing business". Abstraction layers always come with a performance penalty; it's possible you might just be hitting the limits of Toga's abstraction layer.
  4. The Winforms event loop. There is a known issue with Toga's Winforms backend - see #2613. We'd very much like to fix this, but it's not completely clear how.

Unfortunately, the profiling trace you've provided doesn't give enough detail to work out which of these is the culprit.

In terms of ways to narrow this down:

It's difficult to rule out (1) or (2) in isolation; however, writing a bare bones Python.net app that just updates a label will at least give you an indication of the maximum possible refresh rate.

You can rule out (3) by using `my_label._impl.native.Text = "new label" to change the label. That sidesteps Toga's abstraction layer entirely, and jumps directly to the native assignment; it's not ideal as a solution, but at least points out if that's where the performance lag is.

You can test (4) by modifying this line of code in the Winforms backend. If using Delay(1) significantly increases the refresh rate, then you've found the culprit.

Finally, it's also worth pointing out that your expectations here might be unreasonable. Toga isn't a game framework. If you're updating text 25 times a second... a human isn't actually reading that text. They can't. 1/25th of a second isn't enough time to read anything of significance. If you think of other examples where desktop GUIs display content, text will often have 1Hz or 2Hz refresh rates so that the content can be read, or a graphical representation (such as a progress bar) will be used.

freakboy3742 avatar Feb 05 '25 23:02 freakboy3742

These timings imply a maximum of 200 text updates per second across all labels. That does seem unreasonably low.

It would require deeper profiling to find the root cause, but it's also possible that this is caused by Toga running redundant layout calculations (#2938).

mhsmith avatar Feb 06 '25 11:02 mhsmith

200 updates a second would also point pretty hard at the 5ms delay in the event loop (1/200th of a second is 5ms).

freakboy3742 avatar Feb 06 '25 22:02 freakboy3742

Thanks for your answers, I understand that toga isn't made for the usage I was willing to make out of it. Next on my list was the fact that I was facing problem with the event loop running at 25-30ms from exiting cycle to new cycle. I raise these points because I was trying to build a rudimentary gaming interface with a 25Hz refresh rate, but I understand Toga isn't the right animal for that.

For this issue, my hope was that triggering a Label.text change involved an immediate rendering which I would have wanted to disable, and then proceed to rendering all Labels in one go.

I still think Toga is amazing and the project needs to be continued!

em-oaken avatar Feb 08 '25 10:02 em-oaken

For this issue, my hope was that triggering a Label.text change involved an immediate rendering which I would have wanted to disable, and then proceed to rendering all Labels in one go.

I'm not entirely sure how the rendering, specifically, is handled on the backend. As I understand it, Windows is the only backend that even supports updating the rendered display mid-loop. It might be possible for things like calls down to native property setters to be deferred to once per event loop, but this would presumably require yet another intermediary layer to cache and return such values when accessed. This is something that runs the risk of becoming desynced, needs to be tested, and might or might not actually make anything faster because of the increased overhead.

#2938 is a related concept; Toga's handling of layout calculations is currently synchronous and may recalculate layouts many times per event loop. (#3063 is a (currently not working) stab at reducing this to once per loop.)

HalfWhitt avatar Feb 09 '25 19:02 HalfWhitt

As I understand it, Windows is the only backend that even supports updating the rendered display mid-loop. It might be possible for things like calls down to native property setters to be deferred to once per event loop, but this would presumably require yet another intermediary layer to cache and return such values when accessed.

Winforms provides a SuspendLayout API that could be used to batch updates programatically. We're already using it for some very specific cases (e.g., getting an image rendition canvas).

#2938 is a related concept; Toga's handling of layout calculations is currently synchronous and may recalculate layouts many times per event loop. (#3063 is a (currently not working) stab at reducing this to once per loop.)

Agreed this is related; and in the case of Winforms, that's almost exactly the sort of place where we'd need to put the SuspendLayout call.

freakboy3742 avatar Feb 10 '25 03:02 freakboy3742