fyne-io / fyne

Cross platform GUI toolkit in Go inspired by Material Design
https://fyne.io/
Other
24.97k stars 1.38k forks source link

An idle application with no visible windows still uses noticeable CPU time #2506

Open nullst opened 3 years ago

nullst commented 3 years ago

Describe the bug:

I am writing a desktop GUI application that would be usually running in the background, with minimized window that is opened from time to time. I've discovered that any fyne application, including fyne_demo or even a simple window with a single label in it, puts some consistent CPU load even if the application window is not visible and no work is being done. It's not a large amount of CPU usage, but over time it adds up to a battery drain, making even the most basic fyne application the 8th largest energy consumer on my laptop if left running for 20 hours (assuming that a large portion of those 20 hours are spent in the web browser). That's more demanding than, e.g., Telegram chat client even though that program is actually being used actively for some portion of those 20 hours, not just stalling in the background.

On my computer, the background load (of any fyne application that does no work in the background) is 3-4% CPU usage, as reported by "Activity Monitor" of my OS. This can be confirmed with standard golang CPU profiling: it reports 1.27s of CPU time during 30.03s of running in the background:

Profiling idle fyne application

As one can see from this not very informative graphic, a small amount of work (~0.17s out of 1.27s CPU usage) is being done in glfw.PollEvents as invoked in runGL, but the majority of time is spent by golang's scheduling mechanism (runtime.schedule and its friends). I don't know what exactly is this scheduling mechanism working on. It is my impression that channel communication, for example, performingselect over several channels, may be part of this.

Ticker tuning helps, but not completely

Fyne's runGL from internal/driver/glfw/loop.go is an event loop which, in particular, polls GLFW events 60 times a second, triggered by the messages on a channel created by time.Ticker. This frequency is constant, regardless of whether the application window is visible or not. Even though profiling does not indicate that runGL is a significant CPU consumer, it is possible that some part of runtime.schedule CPU usage is the overhead for the select statement reading from several channels in runGL loop, since that select is performed 60 times per second. This is in fact the case.

As an experiment, I reduced the ticker frequency (by editing loop.go) to a rather extreme "1 event per second". This reduced background CPU load by a factor of two. Only 0.63s of CPU time were used during 30s of running in the background:

Ticker tuning

Thus I would suggest some dynamic tuning of the ticker frequency as an enhancement of runGL: if the application window is not visible, maybe run the ticker only 10 times per second, or something like that. This would significantly improve the energy usage for fyne applications running in the background.

However, a consistent 1-2% CPU load is still not the best experience for a background application that does absolutely nothing. This is worse than the majority of background applications my computer is currently running. I'm not sure what can be done to reduce the scheduler load since I don't know what causes it. I haven't checked whether the channel funcQueue in runGL wakes often -- if it is, maybe the same select loop in runGL is responsible for the entire background load.

Another option that I didn't try is to update Go: maybe new versions of golang have a more efficient runtime scheduler. But probably something can be done on the fyne level to reduce the load anyway.

To Reproduce:

Steps to reproduce the behaviour:

  1. Run any fyne application, e.g., fyne_demo.
  2. Keep it in the background.
  3. Check your CPU load with whatever process monitor you have.

Device (please complete the following information):

changkun commented 3 years ago

I don't think this is an issue with the frequent (but actually not) runGL call.

An easy verification is to replace the big for loop inside runGL via select{}. As one will see the application remains to eat 2-3% of CPUs even the entire application is not rendering anything.

nullst commented 3 years ago

I agree that the issue may not be just the runGL loop, but making the ticker in the loop run less often is a (presumably not hard to implement) small modification that, at least on my machine, reduces the idle CPU load by a factor of two, which already seems valuable.

changkun commented 3 years ago

The issue is because of the heavy Cgo call to glfwPollEvent. Each Cgo call needs to involve the runtime scheduler work. One can to verify in a minimum glfw window:

package main

import (
    "runtime"

    "github.com/go-gl/glfw/v3.3/glfw"
)

func init() {
    runtime.LockOSThread()
}

func main() {
    err := glfw.Init()
    if err != nil {
        panic(err)
    }
    defer glfw.Terminate()

    window, err := glfw.CreateWindow(640, 480, "Testing", nil, nil)
    if err != nil {
        panic(err)
    }

    window.MakeContextCurrent()
    for !window.ShouldClose() {
        window.SwapBuffers()
        glfw.PollEvents()
    }
}

This is quite unfortunate, and I don't see anything we can do about this.

changkun commented 3 years ago

We can patch a small update to prevent emitting Gl calls if the window is not focused, but as being demonstrated before, it won't help too much:

diff --git a/cmd/fyne_demo/main.go b/cmd/fyne_demo/main.go
index c29de488..4b280250 100644
--- a/cmd/fyne_demo/main.go
+++ b/cmd/fyne_demo/main.go
@@ -5,6 +5,8 @@ import (
    "fmt"
    "log"
    "net/url"
+   "os"
+   "runtime/pprof"

    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
@@ -67,16 +69,24 @@ func main() {
 }

 func logLifecycle(a fyne.App) {
+   f, err := os.Create("x.pprof")
+   if err != nil {
+       panic(err)
+   }
    a.Lifecycle().SetOnStarted(func() {
        log.Println("Lifecycle: Started")
    })
    a.Lifecycle().SetOnStopped(func() {
        log.Println("Lifecycle: Stopped")
+       pprof.StopCPUProfile()
+       f.Close()
    })
+
    a.Lifecycle().SetOnEnteredForeground(func() {
        log.Println("Lifecycle: Entered Foreground")
    })
    a.Lifecycle().SetOnExitedForeground(func() {
+       pprof.StartCPUProfile(f)
        log.Println("Lifecycle: Exited Foreground")
    })
 }
diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go
index 2d2296e7..19b1990a 100644
--- a/internal/driver/glfw/loop.go
+++ b/internal/driver/glfw/loop.go
@@ -131,7 +134,11 @@ func (d *gLDriver) runGL() {
                w.viewLock.RLock()
                expand := w.shouldExpand
                fullScreen := w.fullScreen
+               focus := w.focus
                w.viewLock.RUnlock()
+               if !focus {
+                   continue
+               }

                if expand && !fullScreen {
                    w.fitContent()
@@ -220,8 +227,9 @@ func (d *gLDriver) startDrawThread() {
                    canvas := w.canvas
                    closing := w.closing
                    visible := w.visible
+                   focus := w.focus
                    w.viewLock.RUnlock()
-                   if closing || !canvas.IsDirty() || !visible {
+                   if closing || !canvas.IsDirty() || !visible || !focus {
                        continue
                    }
                    canvasRefreshed = true
diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go
index 56f61698..4694d190 100644
--- a/internal/driver/glfw/window.go
+++ b/internal/driver/glfw/window.go
@@ -73,6 +73,7 @@ type window struct {
    fullScreen bool
    centered   bool
    visible    bool
+   focus      bool

    mouseLock            sync.RWMutex
    mousePos             fyne.Position
@@ -1224,6 +1225,10 @@ func (w *window) charInput(_ *glfw.Window, char rune) {
 }

 func (w *window) focused(_ *glfw.Window, focus bool) {
+   w.viewLock.Lock()
+   w.focus = focus
+   w.viewLock.Unlock()
+
    if focus {
        if curWindow == nil {
            fyne.CurrentApp().Lifecycle().(*app.Lifecycle).TriggerEnteredForeground()

And we will have the following pprof data:

(pprof) top
Showing nodes accounting for 150ms, 100% of 150ms total
Showing top 10 nodes out of 30
      flat  flat%   sum%        cum   cum%
      60ms 40.00% 40.00%       60ms 40.00%  runtime.cgocall
      30ms 20.00% 60.00%       30ms 20.00%  <unknown>
      30ms 20.00% 80.00%       30ms 20.00%  runtime.pthread_cond_signal
      20ms 13.33% 93.33%       20ms 13.33%  runtime.pthread_cond_wait
      10ms  6.67%   100%       10ms  6.67%  runtime.kevent
         0     0%   100%       60ms 40.00%  fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).Run
         0     0%   100%       60ms 40.00%  fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).runGL
         0     0%   100%       60ms 40.00%  fyne.io/fyne/v2/internal/driver/glfw.(*gLDriver).tryPollEvents
         0     0%   100%       60ms 40.00%  fyne.io/fyne/v2/internal/driver/glfw.(*window).ShowAndRun
         0     0%   100%       60ms 40.00%  github.com/go-gl/glfw/v3.3/glfw.PollEvents
nullst commented 3 years ago

Sorry, I'm not sure I understand.

You are saying that cgo calls, like PollEvents, are the reason for the CPU load. I agree that they are CPU intensive. If I take the example of a minimal glfw window you suggested above and add time.Sleep(time.Second) into the main loop, thus polling the events at most once per second, the CPU load of that window decreases to basically zero. That's fine.

Compare this with fyne: I've tested what happens if I modify the ticker in runGL to run only once per second. Then the application will only call PollEvents once per second (am I right?). From the example above, with a minimal glfw window, I expect that the cgo overhead for running PollEvents once per second is very small, practically zero. However, this is not what happens: CPU load decreased by a factor of two, but it's still a noticeable amount, 630ms of CPU time during 30s run time.

In the modification you suggested, I think the logic is "poll events as usual, but if the window is not in focus, ignore the events". This does not decrease the number of calls to PollEvents so we shouldn't expect any performance improvements. My suggestion was to run ticker less frequently when the window is not visible, thus calling PollEvents less frequently. This does not solve the background CPU load issue completely, but it has a noticeable impact.

changkun commented 3 years ago

add time.Sleep(time.Second) into the main loop

This action will reduce the load to basically zero because everything is delayed by 1 second. PollEvents is not only about paint, handling keyboard and mouse events. There are more things to taking care of: Bluetooth connection, notifications, etc. Adding sleep may help in one case, but will also destroy other important events processing.

nullst commented 3 years ago

I guess I have two questions:

  1. Is calling PollEvents() less than 60 times per second unacceptable for a window that's not visible? If it's only called 10 times per second, a background window would take 100ms to process an event like a notification or a bluetooth connection, and this does not seem too bad from my point of view as a user. I mean, there are different applications with different needs, but if you envision something like a chat client or a note taking application, I think this trade-off may be worth it for decreasing background CPU load. I'm not suggesting decreasing throughput to one event polling per second, that was purely an experiment to see if changing the ticker frequency in runGL has any impact, and it seems like it does. This would not solve the issue completely, but it could improve CPU load of a background application.
  2. What is causing runtime schedule load during that experiment when I change PollEvents() to be called by runGL only once per second? If PollEvents() is the only cgo call that fyne main loop is performing regularly, than the rest of the load is not caused by cgo overhead. (Note that runtime.cgocall has only a very small role in the profiling data showed in my original post. I took a profile of 30s of background work, a couple of minutes after the application start.)
andydotxyz commented 3 years ago

I think it is important to define "not visible" at this time. Minimised windows are not necessarily hidden in the way you think. Some OS keep the apps running so their thumbnails can be updated etc. If you were to hide the window (Window.Hide()) the window will no longer be visible and so events will not run.

Additionally we can't really stop calling the event loop for windows not in the foreground as many OS support tapping, resizing and other interactions on background windows.

Bluebugs commented 3 years ago

I think that trying to use a polling interface is good for reactivity/low latency use case, but if energy matters it would be best to wait and block until we get an event. Maybe using glfwWaitEventsTimeout ( https://www.glfw.org/docs/3.3/group__window.html#ga605a178db92f1a7f1a925563ef3ea2cf ) would work to solve this problem.

andydotxyz commented 3 years ago

That's an interesting suggestion. It was not possible to use this before because there are certain situations where that can hang (resize I vaguely remember?) and then other events would not deliver. However now we have split draw from event "threads" it may be possible to review it's usage...

nullst commented 3 years ago

Minimised windows are not necessarily hidden in the way you think. Some OS keep the apps running so their thumbnails can be updated etc.

I see, that makes sense. I didn't realize that "window that's not visible" is not as precise concept as it seems. I meant the situation where the user does not see any part of the window, and hence can't tap/resize or do anything without first bringing window on screen somehow. If there is no way to test visibility, I have in mind something like "is minimized" or "is completely covered by a full-screen window". Or, as a random crazy idea, maybe runGL loop could track the number of events and switch to "low-latency mode" with slower ticker if there are less than 5 events per second for 30 consecutive seconds, or something like that. Maybe that would be acceptable, I don't know. 10 event polls/second could be good enough for the OS purpose of, e.g., showing a thumbnail of a minimized/hidden window. Though maybe a complicated mechanism is not worth implementing if there is no clear-cut way to test if user can observe any part of the window at the moment.

If you were to hide the window (Window.Hide()) the window will no longer be visible and so events will not run.

Hiding the window in this way does not seem to significantly change background CPU load. In terms of the profiling snapshots linked from the original post, it seems to only reduce time attributed by profiler to the runGL loop, but that's only 0.17s out of 1.27s CPU usage during 30 seconds, so the improvement here is not significant.

(Also, question (2) still stands: a basic glfw window polling events once per second consumes almost no CPU time while a fyne application modified to only poll events once per second in runGL loop has a consistent 1-2% CPU load even if no work is being done. I wasn't able to track what exactly causes the runtime scheduler to work, but maybe that part could be optimized when the application is not visible...)

changkun commented 3 years ago

BTW: the internal dependency of fsnotify is also a source of CPU eater, roughly 1% on my darwin/amd64. It is currently for the setting file. Maybe it is important if the file is changed from the disk, but there are ways to lock the file in order to prevent people from changing it. In this case, the watcher may be removed and give some help to this issue.

Jacalz commented 3 years ago

Removing fsnotify would be great in my opinion (assuming that we can do so in a reasonable way). It is barely maintained and is pretty buggy in some situations: https://github.com/fyne-io/fyne/commit/829a94a8259c609eac25f3bba1fc0193d3c3f022.

changkun commented 3 years ago

@Jacalz How would you define "reasonable way"? Why fsnotify was introduced? Do the previous comments "Maybe it is important if the file is changed from the disk" matches the reason? If so, does "lock the file in order to prevent people from changing it" a good alternative approach for that purpose?

Jacalz commented 3 years ago

I just wrote "in a reasonable way" because I didn't have enough understanding about why we use it and thus didn't want to speculate on the exact possibility of removing it cleanly.

andydotxyz commented 3 years ago

We watch the files (both Settings and Preference) so that if a user changes the file, or an external source (such as a cloud sync/rsync) it will be updated in the app. The primary use-case has always been so that theme changes/font size/primary colour update across all apps if updated. Preventing this file from being changed would remove functionality.

changkun commented 3 years ago

watch the files (both Settings and Preference) so that if a user changes the file, or an external source (such as a cloud sync/rsync) it will be updated in the app.

How much delay is tolerant here? Say if we run a loop that reads the settings every 5 seconds, the functionality can still be preserved, and the CPU consumption from fsnotify can also be eliminated.

Jacalz commented 3 years ago

How often is fsnotify updating to check for it? I wonder if there simply isn't a better solution to not have to go through the file system for this. I wonder how other toolkits achieve similar things.

andydotxyz commented 3 years ago

Typically they are owned by the OS and have a background daemon running using event pushes.

changkun commented 3 years ago

A known issue since 2018: https://github.com/fsnotify/fsnotify/issues/237 (darwin specific though)

Jacalz commented 3 years ago

Couldn't we use some kind of dbus communication instead of fsnotify on Linux and BSD? I'm thinking that there must be a better way than constantly monitoring a file for changes.

andydotxyz commented 3 years ago

Possibly, but this issue seems to relate to the macOS CPU issues, and macOS has no DBus.

Though doing so would remove support for the basic file system config (and user theme too)... So editing the file directly would no longer be supported.

A9G-Data-Droid commented 3 years ago

I have noticed that some applications only notice a setting change in a file when they are activated. So if it's inactive it should never check the settings. Then when you click on the window, it updates as it comes active. It's nice because you can see your change happen instead of it occurring when you aren't looking.

andydotxyz commented 3 years ago

I don't think this is desirable. The settings include info about how user likes theme to be set up. So if a user runs fyne_settings to change the theme it would not update any of the background apps - currently it updates them all, which is what I would expect.

ventsislav-georgiev commented 2 years ago

This is a pretty inconvenient issue. A fyne application without windows, without any code, uses more CPU than Spotify app playing music. This makes fyne unsuitable for long-running apps in the background for laptops as it drains too much energy.

https://user-images.githubusercontent.com/5616486/149010914-bab70b55-8e7b-4049-9f67-6d9cc5c326d4.mov

Isn't there something that can be done to alleviate this issue?

changkun commented 2 years ago

This makes fyne unsuitable for long-running apps in the background for laptops as it drains too much energy.

I fail to conclude this argument as the kernel task uses even more CPUs compare to the fyne application. Did you measured the actual energy consumption with/without opening the app? Is there a statistical significant difference?

Bluebugs commented 2 years ago

I wonder if there is a possible different way to do this on MacOS. The goal here is to get a notification when the setting are changed. On Linux, I could imagine using a property on the X root windows (to avoid having to create a server like dbus would). Is there anything related?

andydotxyz commented 2 years ago

The settings change we are looking for is Fyne settings... so if we use dbus or X signals we would need a service of some sort running to do the push though? I think if you only have 1 Fyne app running this does not help. If you had many running then it could lighten the load across the whole computer by a little.

A9G-Data-Droid commented 2 years ago

Application specific settings can be stored in the Mac Preferences. This could be done for Fyne itself and for Applications made in Fyne. Once you have stored your settings in the OS recommended location, you can subscribe to the change event NSUserDefaultsDidChangeNotification. Then you never have to check for changes you just react to the event:

https://developer.apple.com/documentation/foundation/nsuserdefaultsdidchangenotification

You can use key-value observing to register observers for specific keys of interest in order to be notified of all updates, regardless of whether changes are made within or outside the current process.

Bluebugs commented 2 years ago

The settings change we are looking for is Fyne settings... so if we use dbus or X signals we would need a service of some sort running to do the push though?

With X11, you always have a root window and you can add whatever property to it. Preference could be stored on it and when it change, every X11 application would know. If the preference is not there on startup, the app would read a file and set it on the root window. This mechanism is highly related to X11 and I don't think it would work on Mac. Investigating @A9G-Data-Droid idea might be a good path forward.

ventsislav-georgiev commented 2 years ago

I fail to conclude this argument as the kernel task uses even more CPUs compare to the fyne application. Did you measured the actual energy consumption with/without opening the app? Is there a statistical significant difference?

I am using the Energy tab of the Activity monitor and have noticed it from my other app showing in front of others there, when not being used at all just hidden in the background. For the post here I wanted to have the cleanest example and in order for the code above to show in the Energy tab I had to package it and run it as an app. Here is the video:

https://user-images.githubusercontent.com/5616486/149036806-ccbb8693-83b4-4d6d-afc4-c75be891dec8.mov

Again this as an app that does nothing, has no code at all. This will not be a real fyne app that actually brings value to the user by running something useful in the background. Spotify here is again playing/streaming and switching songs.

Will have to leave it running in order to see the overall impact.

ventsislav-georgiev commented 2 years ago

Had it running just for 30 mins and its energy jumped by a lot. Now it even consumes more than the Activity Monitor:

https://user-images.githubusercontent.com/5616486/149039117-f6791724-992d-42bb-832d-fe9935532d1d.mov

image
andydotxyz commented 2 years ago

Application specific settings can be stored in the Mac Preferences

Whilst this would work for some configuration the more challenging area is all-fyne app settings. When we update the theme, scaling or colour etc we expect all apps to update immediately. That is why they monitor the central settings file.

andydotxyz commented 2 years ago

One thing that we could do (related to the new lifecycle API) would be to stop monitoring if we know that no windows are visible? There may be a small impact because we would have to quickly check settings as a window appears... The trade-off may be worth it to stop using CPU in the background?

sunshine69 commented 2 years ago

Hi all,

I think there must be something inherently not right. Lets compare with a golang gtk3 which does not have the issues. Given that it might not use GL rendering,

I recently jump to this https://gioui.org/ which does all drawing by itself so not relying on gtk3 or anything. However they use Xwayland to draw, not GL . This does not have CPU problems,

They all use CGO to draw graphic,

Is it render using GL is main cause? forgive me I have not researched through careful yet but I will do more later on.

Jacalz commented 2 years ago

GL and Xwayland are entirely different things. We use GL and run through Xwayland when not compiling using -tags wayland. Something else is wrong here. It likely isn't the usage of GL that results in this.

Bluebugs commented 2 years ago

I totally agree that their is room for improvement on how the rendering goroutine logic is being triggered. The problem is actually a bit deeper and part of a general synchronization problem. We should only have an active goroutine going for rendering when the window is visible, but also all rendering request should not just be queued like they are, and instead they should just be discarded and a partial window redraw should be done. That would mean that a refresh shouldn't always end up in the rendering queue, but instead it should just get in for the next rendering and all computation should be delayed until the next rendering iteration (layout should not be done in advance for example). Also animation should be disabled when the window is hidden. Finally if we could instead of using a timer actually listen to the screen refresh, we would have a tick that actually match the real refresh rate instead of hoping to be close to it.

Anyway there is a lot here to address and it is a bigger picture problem. It might even require some api change as some of this problem are actually synchronization problem that are related to race condition problem/ solution.

dispensable commented 1 year ago

Hi, I am developing a app with a very simple interface (just a few buttons, a status indicator and some menu items). I replaced pullEvents with glfwWaitEventsTimeout and it works very well. The battery impact in macos reduced from 26 to 0.3 when window is not focused. I haven't found any block or resize freeze bugs. And this solution works well on linux/windows too.

Of course change this event loop need more tests but this seems solved my problem.

Thanks @Bluebugs

tenox7 commented 1 year ago

@dispensable Could you post a diff of your change?

Jacalz commented 1 year ago

Or perhaps open a PR and we can review the change? I suspect that waitEvents() might be useful as well

Jacalz commented 1 year ago

It seems like this documentation on even polling might be useful: https://www.glfw.org/docs/latest/input_guide.html#events

Jacalz commented 1 year ago

I did a quick local test and it seems like waitEvents makes the window not pop up as it is waiting for events initially before showing anything. We might want to either fix that (if it is an actual problem or undesired behaviour) or use waitEvents() when the window isn't focused.

dispensable commented 1 year ago

@Jacalz https://github.com/dispensable/fyne/commit/4735b27e1b63186af856b19900318b7f06617452

It works for my simple app which doesn't need much GUI work. I haven't tested it on Android or iOS platforms.

As for PR, I think this change needs a refactor on the rendering thread. We can send an empty window event combined with glfw waitevent api (wait until at least one event comes). So we can render other tasks waiting in the queue.

andydotxyz commented 1 year ago

I think the threading model we use causes challenges in this space. Perhaps we can switch to the alternative mode when the window is hidden? In fact perhaps we can just stop listening for events totally?

dweymouth commented 1 year ago

A known issue since 2018: fsnotify/fsnotify#237 (darwin specific though)

This issue was marked as fixed and the fix landed in the latest 1.6.0 release of fsnotify. Perhaps bumping the fsnotify version would at least partially mitigate the high background CPU issue?

Jacalz commented 1 year ago

A known issue since 2018: fsnotify/fsnotify#237 (darwin specific though)

This issue was marked as fixed and the fix landed in the latest 1.6.0 release of fsnotify. Perhaps bumping the fsnotify version would at least partially mitigate the high background CPU issue?

That seems like a good idea. It does unfortunately look like the new version requires Go 1.16 so we would have to wait until we update our minimum go version.

However, the versions they said needed Go 1.16 before did work fine on Go 1.14 so it is worth testing the new version as well

dweymouth commented 1 year ago

I did some playing around with the diff that @dispensable shared above. It does seem that, on Mac OS at least, both the preferences/settings watching and the GLFW loop are drivers of background CPU use, but the GLFW loop has the biggest impact. Switching to glfw.WaitEventsTimeout does indeed cut background CPU use by a lot, but the downside is that no rendering can take place while the thread is blocked on that call. I found that setting the timeout to 0.1 seconds keeps button push animations, etc looking smooth enough in my app that has no other animation needs. But I wonder if there is a way for Fyne to dynamically tune the timeout or switch back to glfw.PollEvents when it knows there is rendering that needs to happen (animations e.g. for button pushes and the animation framework, etc.), but then switch back to waiting for events with a long timeout if there is nothing that needs updating in the window.

Bluebugs commented 1 year ago

Did you try with a period of 1/60 seconds? That would match the current speed for animation. It isn't ideal as they would not be synchronized. I am thinking the best solution might be to have a glfw event triggered by the fyne animation code somehow (the animation timer would also be responsible to send the event if any change did happen since last tick to improve synchronization in updating). This would unblock the WaitEvents and would not require to wakeup on any timeout. In general, this might open a large and long discussion of how to do vsync properly, but we might be good with just the above at starting as it would likely be an improvement from what we currently have.

andydotxyz commented 1 year ago

Given the context of this issue would it not be most interesting to see if we can switch to the eventTimeout based loop when the window is not visible?

dweymouth commented 1 year ago

I am thinking the best solution might be to have a glfw event triggered by the fyne animation code

It seems that GLFW has a function glfwPostEmptyEvent designed exactly for this use case https://www.glfw.org/docs/3.3/group__window.html#gab5997a25187e9fd5c6f2ecbbc8dfd7e9

I think finding a solution that reduces background CPU use when the window is still visible, but idle, is highly desirable. The background use isn't that high, but it is constant and higher than the background CPU used by many apps when they're idling, even Electron apps. If a laptop user has a small handful of Fyne apps running, it would definitely be enough to start noticably degrading battery life.