From 1a9c341a1f43aef0fae3b376155bbcb068f27d8a Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Tue, 21 Jan 2025 21:45:12 +0000 Subject: [PATCH 1/5] Creating a version of that does not wait Naming and implementation open for discussion --- driver.go | 6 +++--- internal/app/lifecycle.go | 4 ++-- internal/driver/glfw/driver.go | 4 ++-- internal/driver/glfw/loop.go | 21 ++++++++++++++++----- internal/driver/glfw/window.go | 6 +----- internal/driver/mobile/canvas_test.go | 8 ++++---- internal/driver/mobile/driver.go | 21 ++++++++++++++------- test/app.go | 2 +- test/driver.go | 3 ++- thread.go | 12 +++++++++++- 10 files changed, 56 insertions(+), 31 deletions(-) diff --git a/driver.go b/driver.go index 49244f7a4c..4aa7c6272f 100644 --- a/driver.go +++ b/driver.go @@ -47,11 +47,11 @@ type Driver interface { // Since: 2.5 SetDisableScreenBlanking(bool) - // DoFromGoroutine provides a way to queue a function that is running on a goroutine back to - // the central thread for Fyne updates. + // DoFromGoroutine provides a way to queue a function `fn` that is running on a goroutine back to + // the central thread for Fyne updates, waiting for it to return if `wait` is true. // The driver provides the implementation normally accessed through [fyne.Do]. // This is required when background tasks want to execute code safely in the graphical context. // // Since: 2.6 - DoFromGoroutine(func()) + DoFromGoroutine(fn func(), wait bool) } diff --git a/internal/app/lifecycle.go b/internal/app/lifecycle.go index 64ac131961..2a7375f4b3 100644 --- a/internal/app/lifecycle.go +++ b/internal/app/lifecycle.go @@ -106,9 +106,9 @@ func (l *Lifecycle) QueueEvent(fn func()) { // RunEventQueue runs the event queue. This should called inside a go routine. // This function blocks. -func (l *Lifecycle) RunEventQueue(run func(func())) { +func (l *Lifecycle) RunEventQueue(run func(func(), bool)) { for fn := range l.eventQueue.Out() { - run(fn) + run(fn, true) } } diff --git a/internal/driver/glfw/driver.go b/internal/driver/glfw/driver.go index 29c48efccc..72b931bbc4 100644 --- a/internal/driver/glfw/driver.go +++ b/internal/driver/glfw/driver.go @@ -56,9 +56,9 @@ func toOSIcon(icon []byte) ([]byte, error) { return buf.Bytes(), nil } -func (d *gLDriver) DoFromGoroutine(f func()) { +func (d *gLDriver) DoFromGoroutine(f func(), wait bool) { async.EnsureNotMain(func() { - runOnMain(f) + runOnMainWithWait(f, wait) }) } diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go index 8340648692..aaa89bc4e8 100644 --- a/internal/driver/glfw/loop.go +++ b/internal/driver/glfw/loop.go @@ -31,6 +31,11 @@ func init() { // force a function f to run on the main thread func runOnMain(f func()) { + runOnMainWithWait(f, true) +} + +// force a function f to run on the main thread and specify if we should wait for it to return +func runOnMainWithWait(f func(), wait bool) { // If we are on main just execute - otherwise add it to the main queue and wait. // The "running" variable is normally false when we are on the main thread. if !running.Load() { @@ -38,12 +43,16 @@ func runOnMain(f func()) { return } - done := common.DonePool.Get() - defer common.DonePool.Put(done) + var done chan struct{} + if wait { + done = common.DonePool.Get() + defer common.DonePool.Put(done) + } funcQueue <- funcData{f: f, done: done} - - <-done + if wait { + <-done + } } // Preallocate to avoid allocations on every drawSingleFrame. @@ -110,7 +119,9 @@ func (d *gLDriver) runGL() { return case f := <-funcQueue: f.f() - f.done <- struct{}{} + if f.done != nil { + f.done <- struct{}{} + } case <-eventTick.C: d.pollEvents() for i := 0; i < len(d.windows); i++ { diff --git a/internal/driver/glfw/window.go b/internal/driver/glfw/window.go index 0bc165da03..92bea00d63 100644 --- a/internal/driver/glfw/window.go +++ b/internal/driver/glfw/window.go @@ -867,11 +867,7 @@ func (w *window) Context() any { func (w *window) runOnMainWhenCreated(fn func()) { if w.view() != nil { - if async.IsMainGoroutine() { - fn() - return - } - fyne.Do(fn) + async.EnsureMain(fn) return } diff --git a/internal/driver/mobile/canvas_test.go b/internal/driver/mobile/canvas_test.go index 17fd9b802c..5a1c450004 100644 --- a/internal/driver/mobile/canvas_test.go +++ b/internal/driver/mobile/canvas_test.go @@ -142,7 +142,7 @@ func Test_canvas_Focusable(t *testing.T) { c.tapUp(pos, 0, func(wid fyne.Tappable, ev *fyne.PointEvent) { wid.Tapped(ev) }, nil, nil, nil) - }) + }, true) waitAndCheck(tapDoubleDelay/time.Millisecond+150, func() { assert.Equal(t, 1, content.focusedTimes) @@ -154,7 +154,7 @@ func Test_canvas_Focusable(t *testing.T) { c.tapUp(pos, 1, func(wid fyne.Tappable, ev *fyne.PointEvent) { wid.Tapped(ev) }, nil, nil, nil) - }) + }, true) waitAndCheck(tapDoubleDelay/time.Millisecond+150, func() { assert.Equal(t, 1, content.focusedTimes) assert.Equal(t, 0, content.unfocusedTimes) @@ -177,7 +177,7 @@ func Test_canvas_Focusable(t *testing.T) { c.tapDown(fyne.NewPos(10, 10), 2) assert.Equal(t, 1, content.focusedTimes) assert.Equal(t, 1, content.unfocusedTimes) - }) + }, true) } func Test_canvas_InteractiveArea(t *testing.T) { @@ -534,7 +534,7 @@ func waitAndCheck(msWait time.Duration, fn func()) { fn() waitForCheck <- struct{}{} - }) + }, true) }() <-waitForCheck } diff --git a/internal/driver/mobile/driver.go b/internal/driver/mobile/driver.go index 7448f6030e..a2a96b78e4 100644 --- a/internal/driver/mobile/driver.go +++ b/internal/driver/mobile/driver.go @@ -72,17 +72,24 @@ func init() { runtime.LockOSThread() } -func (d *driver) DoFromGoroutine(fn func()) { +func (d *driver) DoFromGoroutine(fn func(), wait bool) { async.EnsureNotMain(func() { - done := common.DonePool.Get() - defer common.DonePool.Put(done) + var done chan struct{} + if wait { + done = common.DonePool.Get() + defer common.DonePool.Put(done) + } d.queuedFuncs.In() <- func() { fn() - done <- struct{}{} + if wait { + done <- struct{}{} + } } - <-done + if wait { + <-done + } }) } @@ -446,11 +453,11 @@ func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { d.DoFromGoroutine(func() { wid.Dragged(ev) - }) + }, true) time.Sleep(time.Millisecond * 16) } - d.DoFromGoroutine(wid.DragEnd) + d.DoFromGoroutine(wid.DragEnd, true) }() }) } diff --git a/test/app.go b/test/app.go index f1d885cfd1..84e1a98a81 100644 --- a/test/app.go +++ b/test/app.go @@ -253,7 +253,7 @@ func (s *testSettings) apply() { cache.ResetThemeCaches() intapp.ApplySettings(s, s.app) s.app.propertyLock.Unlock() - }) + }, false) s.app.propertyLock.Lock() s.app.appliedTheme = s.Theme() diff --git a/test/driver.go b/test/driver.go index 16df8cccef..b116955a06 100644 --- a/test/driver.go +++ b/test/driver.go @@ -50,7 +50,8 @@ func NewDriverWithPainter(painter SoftwarePainter) fyne.Driver { return &driver{painter: painter} } -func (d *driver) DoFromGoroutine(f func()) { +// DoFromGoroutine on a test driver ignores the wait flag as our threading is simple +func (d *driver) DoFromGoroutine(f func(), _ bool) { // Tests all run on a single (but potentially different per-test) thread async.EnsureNotMain(f) } diff --git a/thread.go b/thread.go index c96d692a91..cd6b17e554 100644 --- a/thread.go +++ b/thread.go @@ -6,5 +6,15 @@ package fyne // // Since: 2.6 func Do(fn func()) { - CurrentApp().Driver().DoFromGoroutine(fn) + CurrentApp().Driver().DoFromGoroutine(fn, true) +} + +// DoAsync is used to execute a specified function in the main Fyne runtime context without waiting. +// This is required when a background process wishes to adjust graphical elements of a running app. +// Developers should use this only from within goroutines they have created and when the result does not have to +// be waited for. +// +// Since: 2.6 +func DoAsync(fn func()) { + CurrentApp().Driver().DoFromGoroutine(fn, false) } From 9f4c72ca0798e8f586f2ab2011683527934ce0bd Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Tue, 21 Jan 2025 21:57:36 +0000 Subject: [PATCH 2/5] Fix lifecycle usage of wait parameter --- internal/app/lifecycle_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/lifecycle_test.go b/internal/app/lifecycle_test.go index aedb3254f7..d6776f5453 100644 --- a/internal/app/lifecycle_test.go +++ b/internal/app/lifecycle_test.go @@ -17,7 +17,7 @@ func TestLifecycle(t *testing.T) { var entered, exited, start, stop, hookedStop, called bool life.InitEventQueue() - go life.RunEventQueue(func(fn func()) { + go life.RunEventQueue(func(fn func(), _ bool) { fn() }) life.QueueEvent(func() { called = true }) From bb81fadd8b0d899edf0193563f15cc6ef249338b Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Tue, 21 Jan 2025 22:03:34 +0000 Subject: [PATCH 3/5] Missed one usage --- internal/driver/mobile/canvas.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/driver/mobile/canvas.go b/internal/driver/mobile/canvas.go index a62bc4040d..5423a56150 100644 --- a/internal/driver/mobile/canvas.go +++ b/internal/driver/mobile/canvas.go @@ -407,7 +407,7 @@ func (c *canvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tap c.touchCancelFunc = nil c.touchLastTapped = nil c.touchCancelLock.Unlock() - }) + }, true) } func (c *canvas) windowHeadIsDisplacing() bool { From f98bb13588934e169378b46f88c2afab4284a676 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 22 Jan 2025 16:08:26 +0000 Subject: [PATCH 4/5] Put performance through the roof. With the migration helper disabled I now get 1.2 to 1.5 million DoAsync operations per second... --- internal/driver/glfw/loop.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/driver/glfw/loop.go b/internal/driver/glfw/loop.go index aaa89bc4e8..085ad947b9 100644 --- a/internal/driver/glfw/loop.go +++ b/internal/driver/glfw/loop.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/driver/common" "fyne.io/fyne/v2/internal/painter" @@ -20,7 +21,7 @@ type funcData struct { } // channel for queuing functions on the main thread -var funcQueue = make(chan funcData) +var funcQueue = async.NewUnboundedChan[funcData]() var running atomic.Bool var initOnce = &sync.Once{} @@ -43,15 +44,14 @@ func runOnMainWithWait(f func(), wait bool) { return } - var done chan struct{} if wait { - done = common.DonePool.Get() + done := common.DonePool.Get() defer common.DonePool.Put(done) - } - funcQueue <- funcData{f: f, done: done} - if wait { + funcQueue.In() <- funcData{f: f, done: done} <-done + } else { + funcQueue.In() <- funcData{f: f} } } @@ -117,7 +117,7 @@ func (d *gLDriver) runGL() { l.QueueEvent(f) } return - case f := <-funcQueue: + case f := <-funcQueue.Out(): f.f() if f.done != nil { f.done <- struct{}{} From 3daa7ad1afb93dbb15a1663d492975a720fcb829 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Wed, 22 Jan 2025 16:28:02 +0000 Subject: [PATCH 5/5] Oops, we missed protection around SetFullscreen --- internal/driver/glfw/window_desktop.go | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/driver/glfw/window_desktop.go b/internal/driver/glfw/window_desktop.go index 481313ec1b..26f3c70a0f 100644 --- a/internal/driver/glfw/window_desktop.go +++ b/internal/driver/glfw/window_desktop.go @@ -114,23 +114,25 @@ type window struct { } func (w *window) SetFullScreen(full bool) { - w.fullScreen = full - if !w.visible { - return - } + w.runOnMainWhenCreated(func() { + w.fullScreen = full + if !w.visible { + return + } - monitor := w.getMonitorForWindow() - mode := monitor.GetVideoMode() + monitor := w.getMonitorForWindow() + mode := monitor.GetVideoMode() - if full { - w.viewport.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate) - } else { - if w.width == 0 && w.height == 0 { // if we were fullscreen on creation... - s := w.canvas.Size().Max(w.canvas.MinSize()) - w.width, w.height = w.screenSize(s) + if full { + w.viewport.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate) + } else { + if w.width == 0 && w.height == 0 { // if we were fullscreen on creation... + s := w.canvas.Size().Max(w.canvas.MinSize()) + w.width, w.height = w.screenSize(s) + } + w.viewport.SetMonitor(nil, w.xpos, w.ypos, w.width, w.height, 0) } - w.viewport.SetMonitor(nil, w.xpos, w.ypos, w.width, w.height, 0) - } + }) } func (w *window) CenterOnScreen() {