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

Implement more efficient run loop #5422

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ae83021
Implement more efficient run loop
Jacalz Jan 16, 2025
655cf74
Look at posting events from runOnMain
Jacalz Jan 16, 2025
62279a0
Update loop_desktop.go - add waitEvents
dweymouth Jan 17, 2025
5579769
Update loop_goxjs.go - add waitEvents
dweymouth Jan 17, 2025
a24ef66
Update runner.go
dweymouth Jan 17, 2025
58a8be1
Update loop.go - switch between waitEvents and pollEvents at 60fps wi…
dweymouth Jan 17, 2025
3a6b0ba
Update animation_test.go - RWMutex -> Mutex
dweymouth Jan 17, 2025
4a917f0
PostEmptyEvent from Canvas.SetDirty() as part of threading transition…
dweymouth Jan 17, 2025
9c98e2c
Merge remote-tracking branch 'upstream/develop' into efficient-runloop
Jacalz Jan 17, 2025
58f60b5
Factor out window removal to function
Jacalz Jan 17, 2025
2f22eb0
move done to atomic flag instead of channel; run all available queued…
dweymouth Jan 17, 2025
22db93c
undo accidentally committed fyne_demo change
dweymouth Jan 17, 2025
04c3d38
add missed t.Stop()
dweymouth Jan 17, 2025
b206b18
remove redundant done flag to just use running
dweymouth Jan 17, 2025
5849531
refactor runAnimation loop into its own func
dweymouth Jan 17, 2025
3042d6e
undo fyne_demo changes (again)
dweymouth Jan 17, 2025
6d4c780
setup test app for animation tests now that we need driver
dweymouth Jan 18, 2025
1589f25
post empty event from settings when invoking listeners
dweymouth Jan 18, 2025
6a0fe31
don't post empty event if called before GL started
dweymouth Jan 18, 2025
28a6afd
use func callback for settings listener so we can synchronously post …
dweymouth Jan 18, 2025
736694f
Try to fix a race in tests
Jacalz Jan 23, 2025
2999f3c
Use the more efficient window destroy again
Jacalz Jan 23, 2025
3174f9b
Nicer naming for waking up driver
Jacalz Jan 23, 2025
dfd8081
Merge branch 'develop' into efficient-runloop
dweymouth Jan 24, 2025
266175a
Merge remote-tracking branch 'upstream/develop' into efficient-runloop
Jacalz Jan 24, 2025
fc3f109
fix bad merge of DoAndWait PR
dweymouth Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions internal/animation/animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ func TestGLDriver_StopAnimation(t *testing.T) {
t.Error("animation was not ticked")
}
run.Stop(a)
run.animationMutex.RLock()
run.animationMutex.Lock()
assert.Zero(t, len(run.animations))
run.animationMutex.RUnlock()
run.animationMutex.Unlock()
}

func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
Expand Down Expand Up @@ -107,7 +107,7 @@ func TestGLDriver_StopAnimationImmediatelyAndInsideTick(t *testing.T) {
wg.Wait()
// animations stopped inside tick are really stopped in the next runner cycle
time.Sleep(time.Second/60 + 100*time.Millisecond)
run.animationMutex.RLock()
run.animationMutex.Lock()
assert.Zero(t, len(run.animations))
run.animationMutex.RUnlock()
run.animationMutex.Unlock()
}
24 changes: 20 additions & 4 deletions internal/animation/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
type Runner struct {
// animationMutex synchronizes access to `animations` and `pendingAnimations`
// between the runner goroutine and calls to Start and Stop
animationMutex sync.RWMutex
animationMutex sync.Mutex

// animations is the list of animations that are being ticked in the current frame
animations []*anim
Expand All @@ -33,7 +33,7 @@ type Runner struct {
// Start will register the passed application and initiate its ticking.
func (r *Runner) Start(a *fyne.Animation) {
r.animationMutex.Lock()
defer r.animationMutex.Unlock()
hadAnimations := len(r.pendingAnimations) > 0 || len(r.animations) > 0

if !r.runnerStarted {
r.runnerStarted = true
Expand All @@ -51,6 +51,21 @@ func (r *Runner) Start(a *fyne.Animation) {
}
r.pendingAnimations = append(r.pendingAnimations, newAnim(a))
}
r.animationMutex.Unlock()

if !hadAnimations {
// wake up main thread if needed to begin running animations
if drv, ok := fyne.CurrentApp().Driver().(interface{ PostEmptyEvent() }); ok {
drv.PostEmptyEvent()
}
}
}

func (r *Runner) HasAnimations() bool {
r.animationMutex.Lock()
defer r.animationMutex.Unlock()

return len(r.pendingAnimations) > 0 || len(r.animations) > 0
}

// Stop causes an animation to stop ticking (if it was still running) and removes it from the runner.
Expand Down Expand Up @@ -86,18 +101,19 @@ func (r *Runner) Stop(a *fyne.Animation) {

// TickAnimations progresses all running animations by one tick.
// This will be called from the driver to update objects immediately before next paint.
func (r *Runner) TickAnimations() {
func (r *Runner) TickAnimations() (done bool) {
if !r.runnerStarted {
return
}

done := r.runOneFrame()
done = r.runOneFrame()

if done {
r.animationMutex.Lock()
r.runnerStarted = false
r.animationMutex.Unlock()
}
return done
}

func (r *Runner) runOneFrame() (done bool) {
Expand Down
7 changes: 7 additions & 0 deletions internal/driver/common/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,13 @@ func (c *Canvas) CheckDirtyAndClear() bool {
// SetDirty sets canvas dirty flag atomically.
func (c *Canvas) SetDirty() {
c.dirty = true

// wake up main thread in case we were called from goroutine
// TODO: hide this behind the migration flag introduced in
// https://github.com/fyne-io/fyne/pull/5425
if drv, ok := fyne.CurrentApp().Driver().(interface{ PostEmptyEvent() }); ok {
drv.PostEmptyEvent()
}
}

// SetMenuTreeAndFocusMgr sets menu tree and focus manager.
Expand Down
10 changes: 5 additions & 5 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"image"
"os"
"runtime"
"sync/atomic"

"fyne.io/fyne/v2/internal/async"
"github.com/fyne-io/image/ico"
Expand All @@ -28,7 +29,7 @@ var _ fyne.Driver = (*gLDriver)(nil)

type gLDriver struct {
windows []fyne.Window
done chan struct{}
done atomic.Bool

animation animation.Runner

Expand Down Expand Up @@ -95,7 +96,8 @@ func (d *gLDriver) Quit() {

// Only call close once to avoid panic.
if running.CompareAndSwap(true, false) {
close(d.done)
d.done.Store(true)
d.PostEmptyEvent()
}
}

Expand Down Expand Up @@ -161,7 +163,5 @@ func (d *gLDriver) SetDisableScreenBlanking(disable bool) {
func NewGLDriver() *gLDriver {
repository.Register("file", intRepo.NewFileRepository())

return &gLDriver{
done: make(chan struct{}),
}
return &gLDriver{}
}
190 changes: 118 additions & 72 deletions internal/driver/glfw/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func runOnMain(f func()) {
defer common.DonePool.Put(done)

funcQueue <- funcData{f: f, done: done}
postEmptyEvent()

<-done
}
Expand Down Expand Up @@ -97,93 +98,138 @@ func (d *gLDriver) runGL() {
settingsChange := make(chan fyne.Settings)
fyne.CurrentApp().Settings().AddChangeListener(settingsChange)

eventTick := time.NewTicker(time.Second / 60)
for {
select {
case <-d.done:
eventTick.Stop()
d.Terminate()
l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle)
if f := l.OnStopped(); f != nil {
l.QueueEvent(f)
d.waitEvents()

if d.animation.HasAnimations() {
// Switch to running at 60 fps with d.pollEvents
// until we have no more animations to tick
t := time.NewTicker(time.Second / 60)
dweymouth marked this conversation as resolved.
Show resolved Hide resolved
for range t.C {
d.pollEvents()
exit, animationsDone := d.runSingleFrame(settingsChange)
if exit {
t.Stop()
return
}
if animationsDone {
t.Stop()
break
}
}
} else {
// idle mode: run single frame and sleep on d.waitEvents() above
exit, _ := d.runSingleFrame(settingsChange)
if exit {
return
}
return
}
}
}

func (d *gLDriver) runSingleFrame(settingsChange <-chan fyne.Settings) (exit, animationsDone bool) {
// check if we're shutting down
if d.done.Load() {
dweymouth marked this conversation as resolved.
Show resolved Hide resolved
d.Terminate()
l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle)
if f := l.OnStopped(); f != nil {
l.QueueEvent(f)
}
return true, false
}

// run funcs queued to main
funcsDone := false
for !funcsDone {
select {
case f := <-funcQueue:
f.f()
f.done <- struct{}{}
case <-eventTick.C:
d.pollEvents()
windowsToRemove := 0
for _, win := range d.windowList() {
w := win.(*window)
if w.viewport == nil {
continue
}
default:
funcsDone = true
}
}

if w.viewport.ShouldClose() {
windowsToRemove++
continue
}
// apply settings change if any
select {
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*glCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
c.reloadScale()
})
default:
break
}

expand := w.shouldExpand
fullScreen := w.fullScreen
windowsToRemove := 0
for _, win := range d.windowList() {
w := win.(*window)
if w.viewport == nil {
continue
}

if expand && !fullScreen {
w.fitContent()
shouldExpand := w.shouldExpand
w.shouldExpand = false
view := w.viewport
if w.viewport.ShouldClose() {
windowsToRemove++
continue
}

if shouldExpand && runtime.GOOS != "js" {
view.SetSize(w.shouldWidth, w.shouldHeight)
}
}
expand := w.shouldExpand
fullScreen := w.fullScreen

if expand && !fullScreen {
w.fitContent()
shouldExpand := w.shouldExpand
w.shouldExpand = false
view := w.viewport

d.animation.TickAnimations()
d.drawSingleFrame()
if shouldExpand && runtime.GOOS != "js" {
view.SetSize(w.shouldWidth, w.shouldHeight)
}
if windowsToRemove > 0 {
oldWindows := d.windowList()
newWindows := make([]fyne.Window, 0, len(oldWindows)-windowsToRemove)

for _, win := range oldWindows {
w := win.(*window)
if w.viewport == nil {
continue
}

if w.viewport.ShouldClose() {
w.visible = false
v := w.viewport

// remove window from window list
v.Destroy()
w.destroy(d)
continue
}

newWindows = append(newWindows, win)
}
}
}
animationsDone = d.animation.TickAnimations()
d.drawSingleFrame()

d.windows = newWindows
if windowsToRemove > 0 {
d.removeWindows(windowsToRemove)
}

if len(newWindows) == 0 {
d.Quit()
}
}
case set := <-settingsChange:
painter.ClearFontCache()
cache.ResetThemeCaches()
app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) {
c, ok := w.Canvas().(*glCanvas)
if !ok {
return
}
c.applyThemeOutOfTreeObjects()
c.reloadScale()
})
return false, animationsDone
}

func (d *gLDriver) removeWindows(count int) {
oldWindows := d.windowList()
newWindows := make([]fyne.Window, 0, len(oldWindows)-count)

for _, win := range oldWindows {
w := win.(*window)
if w.viewport == nil {
continue
}

if w.viewport.ShouldClose() {
w.visible = false
v := w.viewport

// remove window from window list
v.Destroy()
w.destroy(d)
continue
}

newWindows = append(newWindows, win)
}

d.windows = newWindows

if len(newWindows) == 0 {
d.Quit()
}
}

Expand Down
12 changes: 12 additions & 0 deletions internal/driver/glfw/loop_desktop.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ func (d *gLDriver) pollEvents() {
glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called
}

func (d *gLDriver) waitEvents() {
glfw.WaitEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called
}

func (d *gLDriver) Terminate() {
glfw.Terminate()
}

func (d *gLDriver) PostEmptyEvent() {
glfw.PostEmptyEvent()
}

func postEmptyEvent() {
glfw.PostEmptyEvent()
}
12 changes: 12 additions & 0 deletions internal/driver/glfw/loop_wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ func (d *gLDriver) pollEvents() {
glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called
}

func (d *gLDriver) waitEvents() {
glfw.WaitEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called
}

func (d *gLDriver) Terminate() {
glfw.Terminate()
}

func (d *gLDriver) PostEmptyEvent() {
glfw.PostEmptyEvent()
}

func postEmptyEvent() {
glfw.PostEmptyEvent()
}
Loading