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

app/internal/window: [wasm] fix animation framerate #7

Closed
wants to merge 1 commit into from

Conversation

inkeliz
Copy link
Sponsor Contributor

@inkeliz inkeliz commented Jan 3, 2021

Before this change, Gio could spawn multiple w.requestAnimationFrame.Invoke,
which generates multiples w.draw, faster than the monitor refresh-rate.

This patch aims to call w.requestAnimationFrame.Invoke everyframe, but it
will only draw (w.draw) when animating.

Before this change, Gio could spawn multiple `w.requestAnimationFrame.Invoke`,
which generates multiples `w.draw`, faster than the monitor frame-rate.

This patch aims to call `w.requestAnimationFrame.Invoke` everyframe, but it
will only draw (`w.draw`) when animating.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
w.animating = anim
w.mu.Unlock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defer the Unlock.

}

func (w *window) SetAnimating(anim bool) {
w.mu.Lock()
defer w.mu.Unlock()
if anim && !w.animating {
w.requestAnimationFrame.Invoke(w.redraw)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without requestAnimationFrame here, how will animation start? With your change, SetAnimating only sets w.animating, and that's it.

Copy link
Sponsor Contributor Author

@inkeliz inkeliz Jan 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It starts inside NewWindow, after the w.draw(true).

w.mu.Unlock()
if anim {
w.draw(false)
}
w.requestAnimationFrame.Invoke(w.redraw)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called unconditionally now. When will animation stop?

Copy link
Sponsor Contributor Author

@inkeliz inkeliz Jan 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if anim {} is responsable to prevent the redraw. The "animation callback" will never stop. The callback will be triggred each ~16ms (the refresh-rate of the monitor). However, it will not draw a new frame if it's not animating. If you are not animating it's ignored. :)

The old way of calling requestAnimationFrame: triggers the callback faster than the refresh-rate. I think it happens because SetAnimation can be true and false in one frame. Or, it can change one frame to another, which might call requestAnimationFrame inside the SetAnimation AND calls requestAnimationFrame in the current animCallback, so there's two calls in the same frame (or more).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea to always have requestAnimationFrame running, even if it is nearly a no-op. There must be some other way of fixing the piled up requests. How about tracking whether there is a pending requestAnimationFrame, and not submit another if so?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I did some:

t := time.Now()
   for e := range w.Events() {
   	switch e := e.(type) {
   	case system.FrameEvent:
   		gtx := layout.NewContext(&ops, e)
   		e.Frame(gtx.Ops)
   		fmt.Println("FRAME", t.Sub(time.Now()))
                       t = time.Now()
   		w.Invalidate()

The fmt.Println prints the time betwwen each frame. On Windows/Android it's 16ms. On WASM it's 6ms (or 2ms with #4 applied). Anyway, the WASM is creating frame faster than intended!

I found that the SetAnimating calls the w.requestAnimationFrame and also it's called inside the callback, so I guess... It's calling twice. I insert some fmt.Println, the w.SetAnimating sometimes is false and them true (I don't know if it's one the same frame, or not), but... Since it's false > true, it could call w.requestAnimationFrame again, and again.

If you test the same code (above) with that PR, the timing will be ~16ms.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure the PR fixes what it sets out to do (16 ms) frame, but I'm saying it's not worth the cost: always having requestAnimationFrame callbacks, even if the program is otherwise idle.

I suggested tracking whether a requestAnimationFrame has been called, and only submitting a new one if not. I believe that will achieve the same effect: reducing the updates to once every ~16ms, but without callbacks while idle (not animating).

Copy link
Sponsor Contributor Author

@inkeliz inkeliz Jan 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to better fix that. In theory the old code was suppose to prevent multiples w.requestAnimationFrame.Invoke:

if anim && !w.animating {	
		w.requestAnimationFrame.Invoke(w.redraw)

w.mu.Unlock()
if anim {
w.draw(false)
}
w.requestAnimationFrame.Invoke(w.redraw)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure the PR fixes what it sets out to do (16 ms) frame, but I'm saying it's not worth the cost: always having requestAnimationFrame callbacks, even if the program is otherwise idle.

I suggested tracking whether a requestAnimationFrame has been called, and only submitting a new one if not. I believe that will achieve the same effect: reducing the updates to once every ~16ms, but without callbacks while idle (not animating).

@eliasnaur
Copy link
Contributor

I can't reproduce the < 16 ms frame times (on macOS Firefox), but I suspect what's fixing your case is the move of requestAnimationFrame after draw. Does

diff --git a/app/internal/window/os_js.go b/app/internal/window/os_js.go
index 6a35ae9..dc1f07b 100644
--- a/app/internal/window/os_js.go
+++ b/app/internal/window/os_js.go
@@ -417,12 +417,10 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
 func (w *window) animCallback() {
        w.mu.Lock()
        anim := w.animating
-       if anim {
-               w.requestAnimationFrame.Invoke(w.redraw)
-       }
        w.mu.Unlock()
        if anim {
                w.draw(false)
+               w.requestAnimationFrame.Invoke(w.redraw)
        }
 }

fix your < 16 ms frame times?

@inkeliz
Copy link
Sponsor Contributor Author

inkeliz commented Jan 6, 2021

The suggestion of moving w.requestAnimationFrame.Invoke(w.redraw) doesn't fix. I tried it again (Chrome and Opera), and it doesn't work. I'm getting frames < 16ms.


I insert one fmt.Println() inside the SetAnimation, that is the result:

wasm.js:57 FRAME -6.610176ms
wasm.js:57 2021-01-06 13:24:08.449 +0000 UTC+0 m=+11.827149825 true
wasm.js:57 2021-01-06 13:24:08.456 +0000 UTC+0 m=+11.833569793 false
wasm.js:57 FRAME -6.834944ms
wasm.js:57 2021-01-06 13:24:08.457 +0000 UTC+0 m=+11.834279681 true
wasm.js:57 2021-01-06 13:24:08.463 +0000 UTC+0 m=+11.840424705 false
wasm.js:57 FRAME -6.56512ms
wasm.js:57 2021-01-06 13:24:08.463 +0000 UTC+0 m=+11.841059841 true
wasm.js:57 2021-01-06 13:24:08.47 +0000 UTC+0 m=+11.847494913 false

I think it's switching between true and false and causing to invoke again and again. I test only on Chrome/Opera.

@eliasnaur
Copy link
Contributor

The suggestion of moving w.requestAnimationFrame.Invoke(w.redraw) doesn't fix. I tried it again (Chrome and Opera), and it doesn't work. I'm getting frames < 16ms.

I insert one fmt.Println() inside the SetAnimation, that is the result:

wasm.js:57 FRAME -6.610176ms
wasm.js:57 2021-01-06 13:24:08.449 +0000 UTC+0 m=+11.827149825 true
wasm.js:57 2021-01-06 13:24:08.456 +0000 UTC+0 m=+11.833569793 false
wasm.js:57 FRAME -6.834944ms
wasm.js:57 2021-01-06 13:24:08.457 +0000 UTC+0 m=+11.834279681 true
wasm.js:57 2021-01-06 13:24:08.463 +0000 UTC+0 m=+11.840424705 false
wasm.js:57 FRAME -6.56512ms
wasm.js:57 2021-01-06 13:24:08.463 +0000 UTC+0 m=+11.841059841 true
wasm.js:57 2021-01-06 13:24:08.47 +0000 UTC+0 m=+11.847494913 false

I think it's switching between true and false and causing to invoke again and again. I test only on Chrome/Opera.

Indeed, SetAnimation may be called multiple times per frame with some efficiency cost. However, the important problem is whether drawing happens more than once per monitor refresh. Even on my somewhat fast Linux machine running either Firefox or Chromium I get frame times ~16ms. I'm using the hello example to minimize drawing overhead, patched with

diff --git hello/hello.go hello/hello.go
index 7a328e7..75896d9 100644
--- hello/hello.go
+++ hello/hello.go
@@ -8,6 +8,7 @@ import (
        "image/color"
        "log"
        "os"
+       "time"
 
        "gioui.org/app"
        "gioui.org/io/system"
@@ -33,18 +34,23 @@ func main() {
 func loop(w *app.Window) error {
        th := material.NewTheme(gofont.Collection())
        var ops op.Ops
+       last := time.Now()
        for {
                e := <-w.Events()
                switch e := e.(type) {
                case system.DestroyEvent:
                        return e.Err
                case system.FrameEvent:
+                       now := time.Now()
+                       log.Println("elapsed", now.Sub(last))
+                       last = now
                        gtx := layout.NewContext(&ops, e)
                        l := material.H1(th, "Hello, Gio")
                        maroon := color.NRGBA{R: 127, G: 0, B: 0, A: 255}
                        l.Color = maroon
                        l.Alignment = text.Middle
                        l.Layout(gtx)
+                       op.InvalidateOp{}.Add(gtx.Ops)
                        e.Frame(gtx.Ops)
                }
        }

Output:

wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.920064ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.909824ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.499968ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.64512ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.934912ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.125184ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 17.32992ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.32512ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 17.11488ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.395008ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.014848ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 17.110272ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 17.149952ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 17.529856ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 15.195136ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 18.204928ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.300032ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 15.219968ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.90496ms
wasm_exec.js:51 2021/01/07 11:32:47 elapsed 16.630016ms

Writing this, I note that you're using Window.Invalidate to trigger a re-draw. Invalidate is only meant for externally triggered events, such as receiving a response to a network request, and not for animation. Use InvalidateOp for animation. If you'd like to make Window.Invalidate more robust against misuse, I think one way is to cancel any outstanding animation frame callback with cancelAnimationFrame. Or better yet, understand why Window.Invalidate can avoid calling SetAnimating multiple times.

@inkeliz
Copy link
Sponsor Contributor Author

inkeliz commented Jan 7, 2021

Using op.InvalidateOp{}.Add(gtx.Ops) seems to "fix". But, not so much.... If I leave the page (switch the tabs) and go back, it will be ~8ms. So, it starts as ~16ms... Switching the page/tab it drops to ~8ms.


bandicam.2021-01-07.14-23-00-192.mp4

@eliasnaur
Copy link
Contributor

I still can't reproduce the issue, but it sounds like multiple requestAnimationCallbacks are triggered for each frame. Does https://gioui.org/commit/5f6fa25 help?

whereswaldon pushed a commit that referenced this pull request Jan 8, 2021
…ight

Track whether requestAnimationCallback has been called when SetAnimating
changes the animated state of the window. Multiple callbacks result in wasteful
redraws.

Without this change, my browser becomes unresponsive when Window.Invalidate
is called every frame. Calling Invalidate every frame is a misuse (InvalidateOp
should be used for animation), but it's nice to have reasonable behaviour.

This change might also fix the issues described in
#7.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
@inkeliz
Copy link
Sponsor Contributor Author

inkeliz commented Jan 8, 2021

Yes, it fixes the issue.

@inkeliz inkeliz closed this Jan 8, 2021
@theclapp
Copy link
Contributor

theclapp commented Jan 8, 2021

Elias:

I note that you're using Window.Invalidate to trigger a re-draw. Invalidate is only meant for externally triggered events, such as receiving a response to a network request, and not for animation. Use InvalidateOp for animation

Can you elaborate on why this is? I might be using it wrong.

@eliasnaur
Copy link
Contributor

@theclapp see https://gioui.org/commit/759b796 where I clarify the difference between Invalidate/InvalidateOp, and add an opportunistic check for Invalidate to make it almost as good as an InvalidateOp.

@theclapp
Copy link
Contributor

theclapp commented Jan 8, 2021

👍 Thanks, that helps.

theclapp added a commit to theclapp/gio that referenced this pull request Jan 16, 2023
- This is the 1st commit message:

ScrollTo, PagePrev/Next, etc

This is a combination of 5 commits.

- layout: add List.ScrollTo, ScrollPages, and 2 more

Also add PagePrev & PageNext.

Include tests for all.

- app,app/internal/window,io/system: macOS menus

A strawman interface to the macOS menuing system.

- Make menus compile on other architectures

- app/internal/window: Add modifiers to macOS menu event

- Update to latest Gio main

- This is the commit message gioui#2:

Delete app/internal/wm/window.go

No longer used in main branch.

- This is the commit message gioui#3:

Move menu changes into their own files.

- This is the commit message gioui#4:

io/key,widget: Make editor hotkey filter more specific

Only look for up/down/pageup/pagedown if the editor is multiline.

Only look for enter/return if the editor is Submit == true.

Fix modifier for delete-backward & forward.

Implement some key.Set builder functions and use them in the above code.

Fixes: https://todo.sr.ht/~eliasnaur/gio/399

- This is the commit message gioui#5:

widget: Tweak editor hotkey event listener

For delete-forward and backward, only listen for ShortAlt.

Only listen for Short-[C,X] (copy selection and cut selection) when
there's a selection.

- This is the commit message gioui#6:

Add system.MenuEvent to Window.processEvent

- This is the commit message gioui#7:

key: rename BuildKeySet and BuildKeyGroup

to BuildKeyset and BuildKeygroup.

- This is the commit message gioui#8:

Sleep 2ms after onClose.

Signed-off-by: Larry Clapp <larry@theclapp.org>
@inkeliz inkeliz deleted the inkeliz-jsfps branch February 24, 2023 14:37
@inkeliz inkeliz restored the inkeliz-jsfps branch February 24, 2023 20:52
theclapp added a commit to theclapp/gio that referenced this pull request Jan 12, 2024
- This is the 1st commit message:

ScrollTo, PagePrev/Next, etc

This is a combination of 5 commits.

- layout: add List.ScrollTo, ScrollPages, and 2 more

Also add PagePrev & PageNext.

Include tests for all.

- app,app/internal/window,io/system: macOS menus

A strawman interface to the macOS menuing system.

- Make menus compile on other architectures

- app/internal/window: Add modifiers to macOS menu event

- Update to latest Gio main

- This is the commit message gioui#2:

Delete app/internal/wm/window.go

No longer used in main branch.

- This is the commit message gioui#3:

Move menu changes into their own files.

- This is the commit message gioui#4:

io/key,widget: Make editor hotkey filter more specific

Only look for up/down/pageup/pagedown if the editor is multiline.

Only look for enter/return if the editor is Submit == true.

Fix modifier for delete-backward & forward.

Implement some key.Set builder functions and use them in the above code.

Fixes: https://todo.sr.ht/~eliasnaur/gio/399

- This is the commit message gioui#5:

widget: Tweak editor hotkey event listener

For delete-forward and backward, only listen for ShortAlt.

Only listen for Short-[C,X] (copy selection and cut selection) when
there's a selection.

- This is the commit message gioui#6:

Add system.MenuEvent to Window.processEvent

- This is the commit message gioui#7:

key: rename BuildKeySet and BuildKeyGroup

to BuildKeyset and BuildKeygroup.

- This is the commit message gioui#8:

Sleep 2ms after onClose.

Signed-off-by: Larry Clapp <larry@theclapp.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants