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

Draft
wants to merge 28 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d50c1c3
Implement more efficient run loop
Jacalz Jan 16, 2025
90bbd9b
Look at posting events from runOnMain
Jacalz Jan 16, 2025
057f79c
Update loop_desktop.go - add waitEvents
dweymouth Jan 17, 2025
370fbcf
Update loop_goxjs.go - add waitEvents
dweymouth Jan 17, 2025
cf2f497
Update runner.go
dweymouth Jan 17, 2025
25a636f
Update loop.go - switch between waitEvents and pollEvents at 60fps wi…
dweymouth Jan 17, 2025
651577b
Update animation_test.go - RWMutex -> Mutex
dweymouth Jan 17, 2025
bf1eff6
PostEmptyEvent from Canvas.SetDirty() as part of threading transition…
dweymouth Jan 17, 2025
9c5351c
Factor out window removal to function
Jacalz Jan 17, 2025
f337943
move done to atomic flag instead of channel; run all available queued…
dweymouth Jan 17, 2025
1e98760
undo accidentally committed fyne_demo change
dweymouth Jan 17, 2025
521efc2
add missed t.Stop()
dweymouth Jan 17, 2025
7563ff9
remove redundant done flag to just use running
dweymouth Jan 17, 2025
57c982f
refactor runAnimation loop into its own func
dweymouth Jan 17, 2025
e66b5dd
undo fyne_demo changes (again)
dweymouth Jan 17, 2025
3779dff
setup test app for animation tests now that we need driver
dweymouth Jan 18, 2025
e654a6c
don't post empty event if called before GL started
dweymouth Jan 18, 2025
fb2d93c
use func callback for settings listener so we can synchronously post …
dweymouth Jan 18, 2025
751a44a
Use the more efficient window destroy again
Jacalz Jan 23, 2025
8457212
Nicer naming for waking up driver
Jacalz Jan 23, 2025
63c11cf
fix bad merge of DoAndWait PR
dweymouth Jan 24, 2025
1e08629
remove now-unneeded settingsToApplly
dweymouth Feb 2, 2025
e658a0b
fix bad merge
dweymouth Feb 6, 2025
4b55266
fix merge again - remove extra unused func
dweymouth Feb 6, 2025
350cb1d
use ring buffer func queue that lets us only wake up driver on first …
dweymouth Feb 7, 2025
a117e7c
queue GLFW events to be processed in main loop instead of directly in…
dweymouth Feb 7, 2025
6441f03
update arg types in window_test
dweymouth Feb 7, 2025
2f61ff4
move window resize event handling to synchronous
dweymouth Feb 7, 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
12 changes: 8 additions & 4 deletions internal/animation/animation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/test"
)

func tick(run *Runner) {
Expand All @@ -18,6 +19,7 @@ func tick(run *Runner) {
}

func TestGLDriver_StartAnimation(t *testing.T) {
test.NewTempApp(t)
done := make(chan float32)
run := &Runner{}
a := &fyne.Animation{
Expand All @@ -37,6 +39,7 @@ func TestGLDriver_StartAnimation(t *testing.T) {
}

func TestGLDriver_StopAnimation(t *testing.T) {
test.NewTempApp(t)
done := make(chan float32)
run := &Runner{}
a := &fyne.Animation{
Expand All @@ -54,12 +57,13 @@ 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) {
test.NewTempApp(t)
var wg sync.WaitGroup
run := &Runner{}

Expand Down Expand Up @@ -107,7 +111,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()
}
26 changes: 21 additions & 5 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{ WakeUp() }); ok {
drv.WakeUp()
}
}
}

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
return true
}

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{ WakeUp() }); ok {
drv.WakeUp()
}
}

// SetMenuTreeAndFocusMgr sets menu tree and focus manager.
Expand Down
81 changes: 81 additions & 0 deletions internal/driver/common/ringbuffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package common

// RingBuffer is a growable ring buffer supporting
// enqueue and dequeue operations. It is not thread safe.
type RingBuffer[T any] struct {
buf []T
head int
len int
}

// NewRingBuffer initializes and returns a new RingBuffer.
func NewRingBuffer[T any](initialCap int) RingBuffer[T] {
return RingBuffer[T]{
buf: make([]T, initialCap),
}
}

// Len returns the number of elements in the buffer.
func (r *RingBuffer[T]) Len() int {
return r.len
}

// Push adds the value to the end of the buffer.
func (r *RingBuffer[T]) Push(value T) {
r.checkGrow()

pos := (r.head + r.len) % len(r.buf)
r.buf[pos] = value
r.len++
}

// Pull removes the first item from the buffer, if any.
func (r *RingBuffer[T]) Pull() (value T, ok bool) {
if r.len == 0 {
return value, false
}
return r.pullOne(), true
}

// PullN removes up to len(buf) items from the queue,
// copying them into the supplied buffer and returning
// the number of elements copied.
func (r *RingBuffer[T]) PullN(buf []T) int {
l := len(buf)
if r.len < l {
l = r.len
}
if l == 0 {
return 0
}
for i := 0; i < l; i++ {
buf[i] = r.pullOne()
}
return l
}

func (r *RingBuffer[T]) pullOne() (value T) {
emptyT := value
value = r.buf[r.head]
r.buf[r.head] = emptyT

if r.len == 1 {
r.head = 0
} else {
r.head = (r.head + 1) % len(r.buf)
}
r.len--

return value
}

func (r *RingBuffer[T]) checkGrow() {
if l := len(r.buf); r.len == l {
newBuf := make([]T, l*2)
for i := 0; i < r.len; i++ {
newBuf[i] = r.buf[(r.head+i)%l]
}
r.head = 0
r.buf = newBuf
}
}
46 changes: 46 additions & 0 deletions internal/driver/common/ringbuffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package common

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_RingBuffer(t *testing.T) {
buf := NewRingBuffer[int](8)

assertDequeue := func(expect int) {
t.Helper()
got, valid := buf.Pull()
assert.True(t, valid, "got invalid pull")
assert.Equal(t, expect, got, "invalid pull result")
}

buf.Push(1)
buf.Push(2)
buf.Push(3)
buf.Push(4)
buf.Push(5)
buf.Push(6)
buf.Push(7)
buf.Push(8)
buf.Push(9)
buf.Push(10)

assertDequeue(1)
assertDequeue(2)

buf.Push(11)
buf.Push(12)
buf.Push(13)
buf.Push(14)
buf.Push(15)
buf.Push(16)
buf.Push(17)
buf.Push(18)
buf.Push(19)

for i := 3; i <= 19; i++ {
assertDequeue(i)
}
}
7 changes: 2 additions & 5 deletions internal/driver/glfw/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ var _ fyne.Driver = (*gLDriver)(nil)

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

animation animation.Runner

Expand Down Expand Up @@ -101,7 +100,7 @@ func (d *gLDriver) Quit() {

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

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

return &gLDriver{
done: make(chan struct{}),
}
return &gLDriver{}
}
Loading
Loading