Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
nullst opened this issue Sep 27, 2021 · 77 comments
Open
Labels
bug Something isn't working optimization Tickets that could help Fyne apps run faster

Comments

@nullst
Copy link
Contributor

nullst commented Sep 27, 2021

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):

  • OS: MacOS
  • Version: 10.12 Sierra
  • Go version: go1.15.3 darwin/amd64
  • Fyne version: 6e90820
@nullst nullst added the unverified A bug that has been reported but not verified label Sep 27, 2021
@changkun
Copy link
Member

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
Copy link
Contributor Author

nullst commented Sep 27, 2021

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
Copy link
Member

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
Copy link
Member

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
Copy link
Contributor Author

nullst commented Sep 27, 2021

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
Copy link
Member

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
Copy link
Contributor Author

nullst commented Sep 27, 2021

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
Copy link
Member

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
Copy link
Contributor

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
Copy link
Member

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
Copy link
Contributor Author

nullst commented Sep 27, 2021

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
Copy link
Member

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
Copy link
Member

Jacalz commented Sep 28, 2021

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: 829a94a.

@changkun
Copy link
Member

@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
Copy link
Member

Jacalz commented Sep 28, 2021

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
Copy link
Member

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.

@Jacalz Jacalz added bug Something isn't working optimization Tickets that could help Fyne apps run faster and removed unverified A bug that has been reported but not verified labels Oct 6, 2021
@changkun
Copy link
Member

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
Copy link
Member

Jacalz commented Oct 13, 2021

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
Copy link
Member

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

@changkun
Copy link
Member

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

@Jacalz
Copy link
Member

Jacalz commented Oct 14, 2021

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
Copy link
Member

andydotxyz commented Oct 17, 2021

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
Copy link

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
Copy link
Member

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
Copy link

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.

Screen.Recording.2022-01-11.at.21.43.52.mov

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

@changkun
Copy link
Member

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?

@Jacalz
Copy link
Member

Jacalz commented Aug 19, 2023

I did a quick test where I just replaced glfw.PollEvents() with glfw.WaitEvents() (at loop_desktop.go#39) with no other changes and it almost works perfectly with reduced CPU usage. Animations and drawing on Linux works fine, even when the application is in the background, as they happen on another thread (I suppose that there will be trouble for M1/M2 laptops).

The downside is that it causes the main thread to hang when no events occur (not moving the mouse over it, not resizing etc.) meaning that any window events that occur in the function queue have to wait until the next time it receives events from glfw. For example, pressing ctrl+c in the terminal doesn't close the window until the window is focused again and I assume that the same applies for developer code trying to close the window when it isn't focused. If there is some sensible way to fix that problem (running functions on the main thread while we are waiting for events, waiting for events on a different thread, etc.), we could get a hugely more efficient runloop.

Examples

A simple log printout:
image

Before (always runs)

Screencast.from.2023-08-19.14-29-01.webm

After (only runs when events are posted)

Screencast.from.2023-08-19.14-29-57.webm

@dweymouth
Copy link
Contributor

dweymouth commented Aug 19, 2023

@Jacalz This is awesome! This is pretty much the last bug that is keeping me building Supersonic against a fork of Fyne (I've slowed down the main loop to 10 fps when no user input is occurring to reduce, but not eliminate, the background CPU problem).

You could look into glfwPostEmptyEvent as a way to wake up the main thread when needed. For M1 macs I feel we'd need a way to switch temporarily back to polling whenever an animation is running since the animations also share the main thread. But otherwise just waking up the main thread when user code calls a Refresh, or we receive a Ctrl+C or other signal, just might work!

Seems like it might be tight to get it in 2.4.0 but maybe we can figure out a way to solve this in 2.4.1

@Jacalz
Copy link
Member

Jacalz commented Aug 19, 2023

I'm glad you liked it @dweymouth. Are you sure about animations needing the main thread? The videos above are using the animation tab with the checkbox moving using an animation.

@dweymouth
Copy link
Contributor

dweymouth commented Aug 19, 2023

On M1/M2 Macs only animation needs the main thread, unless it's changed recently (I don't think so). I remember that the only thing I'm losing in Supersonic with the 10fps main thread is loss of button tap animation smoothness on Apple silicon macs.

@Jacalz
Copy link
Member

Jacalz commented Aug 19, 2023

Ah, I see. I thought you were talking about non M1/M2 Macs given that the drawing happens on the main thread (as far as I know) and that seems like a bigger problem. Anyway, I put together a messy POC (I know that it doesn't compile for WASM, the CTRL+C is support is glued on, etc.) in Jacalz@6b2a237 using your glfw.PostEmptyEvent() and it seems to work quite well here on Linux :)

@Jacalz
Copy link
Member

Jacalz commented Aug 19, 2023

You might get that running on M1/M2 by adding a glfw.PostEmptyEvent() to runOnDraw() like I did for runOnMain() but I can't guarantee anything as I don't have access to the hardware ;)

@dweymouth dweymouth added this to the D fixes (v2.4.x) milestone Aug 19, 2023
@andydotxyz
Copy link
Member

You should - we have an M1 device on the cloud - DM if you don't have login details.

@Jacalz
Copy link
Member

Jacalz commented Aug 20, 2023

Absolutely. I know about the M1 device but mostly meant that I hadn't tested it on the hardware. I'll send you a DM as I realize that I haven't used it before. I'm still not sure if my approach is a good one so we'll have to see what comes from it :)

@Jacalz
Copy link
Member

Jacalz commented Aug 20, 2023

The good news are that my proof of concept runs about just as good on M1 (without modifications) as it does on my Linux box. The bad news are that the implementation is buggy on both platforms. One window seems mostly fine but opening, hiding and closing another window seems slow, buggy and flickery. I'm afraid that my solution for the problem isn't the best one but it does show that it at least might be possible to solve this using glfw.WaitEvents().

All of my work can be found here: https://github.com/Jacalz/fyne/tree/poc-glfw-waitevents

@Jacalz
Copy link
Member

Jacalz commented Aug 20, 2023

Hmm. I am seeing the same flickery windows on develop now. Will have to track it down tomorrow but I suspect that there might be another bug at play there.

EDIT: Yes, it seems like a bug on latest develop.

@Jacalz
Copy link
Member

Jacalz commented Aug 21, 2023

I have opened #4173 as a draftnow. It is what I would consider as good of a solution as I can think of. There are some problems to sort out but most of the quirks with my POC have been rectified with the slight complication that the CI tests crash and Wayland support is broken for some reason but that's a different story. It seems to be working fine during local testing on X11 :)

Jacalz added a commit to Jacalz/fyne that referenced this issue Aug 23, 2023
We were allocating and appending to a slice 60 times per second to modify the window list if windows were supposed to close. This updates the runloop to keep track of how many windows need to be closed and only allocate a slice when we need to.

Updates fyne-io#2506
@0-issue
Copy link

0-issue commented Sep 30, 2023

The simple hello world application in fyne's README.md: https://github.com/fyne-io/fyne#getting-started executes 11 million instructions per second when user is not doing anything. My system is macOS Sonoma, and have installed and built using the steps mentioned on the README.md page. Similar hello world example on gio executes 0 instructions per second while idling: https://gioui.org/doc/learn/get-started. At the moment I am considering whether to invest building UIs in fyne or gio. Is there an inherent tradeoff? Like, will fyne consume more than 0 instructions per second by design?

@dweymouth
Copy link
Contributor

There is progress being made on this issue - see #4173 by @Jacalz above.

@andydotxyz
Copy link
Member

executes 11 million instructions per second

Are you able to be more specific about what you have found so it can feed into the work we are doing? I don't understand how any tool can operate 0 operations per second unless suspended - but the Fyne loop should (60 or 120 times per second) check if anything has changed (simple Boolean operation) and do nothing further if idle...

@0-issue
Copy link

0-issue commented Sep 30, 2023

@andydotxyz I am new to go. But afaik system signals/system events can be mapped on channels. Perhaps some main event loop in fyne can have a select statement waiting for events on channels instead of polling 60/120 a second. That is what gio seems to be doing. On maOS, top command gives you instructions executed per second. You can try running something like top -pid <pid_of_program_being_observed>. On linux, you can find similar information from procfs, as you would know. Similar change would work equally well in other situations too where you could be busy waiting.

On C side of things, select syscall has similar behavior... The application waiting for an i/o event registered in select does not consume any CPU time. That's the difference between polling vs event driven.

EDIT: If wanting to wait on N events, where N is unknown at compile time, or N is large, or some elements need to be masked/unmasked every iteration, or N changes from iteration to iteration, use https://pkg.go.dev/reflect#Select instead of select-case-structure. It currently supports registering and waiting on up to 65536 events at a time.

@Jacalz
Copy link
Member

Jacalz commented Sep 30, 2023

Like @dweymouth said above, that is basically what the change I was working on (took a break but will resume it sometime soon) was doing.

@andydotxyz
Copy link
Member

Yup work is ongoing. We can't rely solely on events like you say Gio does because we are stateful and we have to watch for state changes internal to the GUI structure. To avoid the developer having to fire events all the time we monitor. But like I say it should be closer to 60 operations a second not millions.

@0-issue
Copy link

0-issue commented Sep 30, 2023

@andydotxyz

we have to watch for state changes internal to the GUI structure

What would be an example? Sorry, I am new to fyne and do not know much about its architecture. In my mental model of how GUI framework could work, it seems possible to entirely quiescent the CPU activity of the application's process when nothing is happening.

To avoid the developer having to fire events all the time we monitor.

Some developers might actually appreciate an API where they interact with fyne over channels in an event-driven manner, perhaps saving power.

60 operations a second not millions

My comment above mentioned 11 million instructions per second, not operations.

@dweymouth
Copy link
Contributor

dweymouth commented Sep 30, 2023

but the Fyne loop should (60 or 120 times per second)

I actually don't think we even need to do this, since the only reason things would ever need updating is if

  1. there is user input event from the OS
  2. there is a running animation (we can track this internally)
  3. the developer calls a Refresh() on a CanvasObject or a Start on an animation

Am I missing anything? Couldn't # 3 send a value on a channel and the main loop can literally be asleep in a select statement until an event from any of the channels arrives?

@andydotxyz
Copy link
Member

Internally to Fyne it would be quite reasonable to move the dirty flag to a channel so we don't have to check a Boolean state each frame (though I'm not sure it's that slow?)

We do need to be careful about main thread though - depending on the OS / GLFW mode of the main thread stops polling then events may not be seen. Of course this could become a circular definition and perhaps solving one allows solving the other?

andydotxyz pushed a commit that referenced this issue Dec 18, 2023
We were allocating and appending to a slice 60 times per second to modify the window list if windows were supposed to close. This updates the runloop to keep track of how many windows need to be closed and only allocate a slice when we need to.

Updates #2506
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working optimization Tickets that could help Fyne apps run faster
Projects
None yet
Development

No branches or pull requests