]> Cypherpunks.ru repositories - gostls13.git/commitdiff
wasm: remove redundant calls to setTimeout and clearTimeout
authorGaret Halliday <me@garet.holiday>
Fri, 14 Oct 2022 00:35:18 +0000 (19:35 -0500)
committerMichael Knyszek <mknyszek@google.com>
Mon, 22 May 2023 17:47:47 +0000 (17:47 +0000)
The existing implementation clears and recreates Javascript
timeouts when Go is called from js, leading to excessive
load on the js scheduler. Instead, we should remove redundant
calls to clearTimeout and refrain from creating new timeouts
if the previous event's timestamp is within 1 millisecond of
our target (the js scheduler's max precision)

Fixes #56100

Change-Id: I42bbed4c2f1fa6579c1f3aa519b6ed8fc003a20c
Reviewed-on: https://go-review.googlesource.com/c/go/+/442995
Reviewed-by: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
Run-TryBot: Michael Knyszek <mknyszek@google.com>

misc/wasm/wasm_exec.js
src/runtime/lock_js.go
src/syscall/js/func.go

index 7f72bee005c1d2fb351a9f907dfbd458e0bc21ce..bc6f210242824c25b5ee619afa507767b6e587be 100644 (file)
                                                                        this._resume();
                                                                }
                                                        },
-                                                       getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early
+                                                       getInt64(sp + 8),
                                                ));
                                                this.mem.setInt32(sp + 16, id, true);
                                        },
index ae2bb3db4750674246ffcbbe9870fe842ae712ee..fd2abee7c448848e9dea9a99c132c278f7e982a7 100644 (file)
@@ -117,7 +117,6 @@ func notetsleepg(n *note, ns int64) bool {
                gopark(nil, nil, waitReasonSleep, traceBlockSleep, 1)
 
                clearTimeoutEvent(id) // note might have woken early, clear timeout
-               clearIdleID()
 
                mp = acquirem()
                delete(notes, n)
@@ -169,8 +168,36 @@ type event struct {
        returned bool
 }
 
+type timeoutEvent struct {
+       id int32
+       // The time when this timeout will be triggered.
+       time int64
+}
+
+// diff calculates the difference of the event's trigger time and x.
+func (e *timeoutEvent) diff(x int64) int64 {
+       if e == nil {
+               return 0
+       }
+
+       diff := x - idleTimeout.time
+       if diff < 0 {
+               diff = -diff
+       }
+       return diff
+}
+
+// clear cancels this timeout event.
+func (e *timeoutEvent) clear() {
+       if e == nil {
+               return
+       }
+
+       clearTimeoutEvent(e.id)
+}
+
 // The timeout event started by beforeIdle.
-var idleID int32
+var idleTimeout *timeoutEvent
 
 // beforeIdle gets called by the scheduler if no goroutine is awake.
 // If we are not already handling an event, then we pause for an async event.
@@ -183,21 +210,23 @@ var idleID int32
 func beforeIdle(now, pollUntil int64) (gp *g, otherReady bool) {
        delay := int64(-1)
        if pollUntil != 0 {
-               delay = pollUntil - now
-       }
-
-       if delay > 0 {
-               clearIdleID()
-               if delay < 1e6 {
-                       delay = 1
-               } else if delay < 1e15 {
-                       delay = delay / 1e6
-               } else {
+               // round up to prevent setTimeout being called early
+               delay = (pollUntil-now-1)/1e6 + 1
+               if delay > 1e9 {
                        // An arbitrary cap on how long to wait for a timer.
                        // 1e9 ms == ~11.5 days.
                        delay = 1e9
                }
-               idleID = scheduleTimeoutEvent(delay)
+       }
+
+       if delay > 0 && (idleTimeout == nil || idleTimeout.diff(pollUntil) > 1e6) {
+               // If the difference is larger than 1 ms, we should reschedule the timeout.
+               idleTimeout.clear()
+
+               idleTimeout = &timeoutEvent{
+                       id:   scheduleTimeoutEvent(delay),
+                       time: pollUntil,
+               }
        }
 
        if len(events) == 0 {
@@ -217,12 +246,10 @@ func handleAsyncEvent() {
        pause(getcallersp() - 16)
 }
 
-// clearIdleID clears our record of the timeout started by beforeIdle.
-func clearIdleID() {
-       if idleID != 0 {
-               clearTimeoutEvent(idleID)
-               idleID = 0
-       }
+// clearIdleTimeout clears our record of the timeout started by beforeIdle.
+func clearIdleTimeout() {
+       idleTimeout.clear()
+       idleTimeout = nil
 }
 
 // pause sets SP to newsp and pauses the execution of Go's WebAssembly code until an event is triggered.
@@ -250,9 +277,10 @@ func handleEvent() {
        }
        events = append(events, e)
 
-       eventHandler()
-
-       clearIdleID()
+       if !eventHandler() {
+               // If we did not handle a window event, the idle timeout was triggered, so we can clear it.
+               clearIdleTimeout()
+       }
 
        // wait until all goroutines are idle
        e.returned = true
@@ -265,9 +293,11 @@ func handleEvent() {
        pause(getcallersp() - 16)
 }
 
-var eventHandler func()
+// eventHandler retrieves and executes handlers for pending JavaScript events.
+// It returns true if an event was handled.
+var eventHandler func() bool
 
 //go:linkname setEventHandler syscall/js.setEventHandler
-func setEventHandler(fn func()) {
+func setEventHandler(fn func() bool) {
        eventHandler = fn
 }
index cc9497236450bb829f90dce982b577eef9b7fd54..53a4d79a95e322a96764187687e23e0bef9ed857 100644 (file)
@@ -60,16 +60,19 @@ func (c Func) Release() {
 }
 
 // setEventHandler is defined in the runtime package.
-func setEventHandler(fn func())
+func setEventHandler(fn func() bool)
 
 func init() {
        setEventHandler(handleEvent)
 }
 
-func handleEvent() {
+// handleEvent retrieves the pending event (window._pendingEvent) and calls the js.Func on it.
+// It returns true if an event was handled.
+func handleEvent() bool {
+       // Retrieve the event from js
        cb := jsGo.Get("_pendingEvent")
        if cb.IsNull() {
-               return
+               return false
        }
        jsGo.Set("_pendingEvent", Null())
 
@@ -77,14 +80,17 @@ func handleEvent() {
        if id == 0 { // zero indicates deadlock
                select {}
        }
+
+       // Retrieve the associated js.Func
        funcsMu.Lock()
        f, ok := funcs[id]
        funcsMu.Unlock()
        if !ok {
                Global().Get("console").Call("error", "call to released function")
-               return
+               return true
        }
 
+       // Call the js.Func with arguments
        this := cb.Get("this")
        argsObj := cb.Get("args")
        args := make([]Value, argsObj.Length())
@@ -92,5 +98,8 @@ func handleEvent() {
                args[i] = argsObj.Index(i)
        }
        result := f(this, args)
+
+       // Return the result to js
        cb.Set("result", result)
+       return true
 }