// Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build js && wasm package runtime import ( "internal/runtime/sys" _ "unsafe" // for go:linkname ) // js/wasm has no support for threads yet. There is no preemption. const ( mutex_unlocked = 0 mutex_locked = 1 note_cleared = 0 note_woken = 1 note_timeout = 2 active_spin = 4 active_spin_cnt = 30 passive_spin = 1 ) func mutexContended(l *mutex) bool { return false } func lock(l *mutex) { lockWithRank(l, getLockRank(l)) } func lock2(l *mutex) { if l.key == mutex_locked { // js/wasm is single-threaded so we should never // observe this. throw("self deadlock") } gp := getg() if gp.m.locks < 0 { throw("lock count") } gp.m.locks++ l.key = mutex_locked } func unlock(l *mutex) { unlockWithRank(l) } func unlock2(l *mutex) { if l.key == mutex_unlocked { throw("unlock of unlocked lock") } gp := getg() gp.m.locks-- if gp.m.locks < 0 { throw("lock count") } l.key = mutex_unlocked } // One-time notifications. // Linked list of notes with a deadline. var allDeadlineNotes *note func noteclear(n *note) { n.status = note_cleared } func notewakeup(n *note) { if n.status == note_woken { throw("notewakeup - double wakeup") } cleared := n.status == note_cleared n.status = note_woken if cleared { goready(n.gp, 1) } } func notesleep(n *note) { throw("notesleep not supported by js") } func notetsleep(n *note, ns int64) bool { throw("notetsleep not supported by js") return false } // same as runtimeĀ·notetsleep, but called on user g (not g0) func notetsleepg(n *note, ns int64) bool { gp := getg() if gp == gp.m.g0 { throw("notetsleepg on g0") } if ns >= 0 { deadline := nanotime() + ns delay := ns/1000000 + 1 // round up if delay > 1<<31-1 { delay = 1<<31 - 1 // cap to max int32 } id := scheduleTimeoutEvent(delay) n.gp = gp n.deadline = deadline if allDeadlineNotes != nil { allDeadlineNotes.allprev = n } n.allnext = allDeadlineNotes allDeadlineNotes = n gopark(nil, nil, waitReasonSleep, traceBlockSleep, 1) clearTimeoutEvent(id) // note might have woken early, clear timeout n.gp = nil n.deadline = 0 if n.allprev != nil { n.allprev.allnext = n.allnext } if allDeadlineNotes == n { allDeadlineNotes = n.allnext } n.allprev = nil n.allnext = nil return n.status == note_woken } for n.status != note_woken { n.gp = gp gopark(nil, nil, waitReasonZero, traceBlockGeneric, 1) n.gp = nil } return true } // checkTimeouts resumes goroutines that are waiting on a note which has reached its deadline. func checkTimeouts() { now := nanotime() for n := allDeadlineNotes; n != nil; n = n.allnext { if n.status == note_cleared && n.deadline != 0 && now >= n.deadline { n.status = note_timeout goready(n.gp, 1) } } } // events is a stack of calls from JavaScript into Go. var events []*event type event struct { // g was the active goroutine when the call from JavaScript occurred. // It needs to be active when returning to JavaScript. gp *g // returned reports whether the event handler has returned. // When all goroutines are idle and the event handler has returned, // then g gets resumed and returns the execution to JavaScript. 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 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. // If an event handler returned, we resume it and it will pause the execution. // beforeIdle either returns the specific goroutine to schedule next or // indicates with otherReady that some goroutine became ready. // TODO(drchase): need to understand if write barriers are really okay in this context. // //go:yeswritebarrierrec func beforeIdle(now, pollUntil int64) (gp *g, otherReady bool) { delay := int64(-1) if pollUntil != 0 { // 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 } } 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 { // TODO: this is the line that requires the yeswritebarrierrec go handleAsyncEvent() return nil, true } e := events[len(events)-1] if e.returned { return e.gp, false } return nil, false } var idleStart int64 func handleAsyncEvent() { idleStart = nanotime() pause(sys.GetCallerSP() - 16) } // clearIdleTimeout clears our record of the timeout started by beforeIdle. func clearIdleTimeout() { idleTimeout.clear() idleTimeout = nil } // scheduleTimeoutEvent tells the WebAssembly environment to trigger an event after ms milliseconds. // It returns a timer id that can be used with clearTimeoutEvent. // //go:wasmimport gojs runtime.scheduleTimeoutEvent func scheduleTimeoutEvent(ms int64) int32 // clearTimeoutEvent clears a timeout event scheduled by scheduleTimeoutEvent. // //go:wasmimport gojs runtime.clearTimeoutEvent func clearTimeoutEvent(id int32) // handleEvent gets invoked on a call from JavaScript into Go. It calls the event handler of the syscall/js package // and then parks the handler goroutine to allow other goroutines to run before giving execution back to JavaScript. // When no other goroutine is awake any more, beforeIdle resumes the handler goroutine. Now that the same goroutine // is running as was running when the call came in from JavaScript, execution can be safely passed back to JavaScript. func handleEvent() { sched.idleTime.Add(nanotime() - idleStart) e := &event{ gp: getg(), returned: false, } events = append(events, e) 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 gopark(nil, nil, waitReasonZero, traceBlockGeneric, 1) events[len(events)-1] = nil events = events[:len(events)-1] // return execution to JavaScript idleStart = nanotime() pause(sys.GetCallerSP() - 16) } // 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() bool) { eventHandler = fn }