mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-04 02:57:46 +00:00
runtime,syscall,internal/poll,os: wasip1 poll_oneoff scheduler integration + net.FileListener (#5386)
* runtime,syscall,internal/poll,os: wasip1 poll_oneoff scheduler integration + net.FileListener On wasip1 today every syscall.Read/Write blocks the entire wasm module — the cooperative scheduler invokes poll_oneoff only for sleep/timer wakeups, and there's no path from the net package to a working TCP server. This change fixes both: it threads poll_oneoff through the scheduler's idle path so a goroutine doing FD I/O parks instead of blocking the module, and it provides enough internal/poll / os / syscall surface that upstream Go's net.FileListener / net.FileConn works on a host-pre-opened TCP socket. * runtime: keep scheduler_cooperative idle-wait calls direct TestBinarySize/hifive1b/examples/echo regressed by 32 bytes after the previous commit routed the scheduler's idle wait through a schedulerIdleWait helper. The extra call frame + branch landed on every non-wasip1 cooperative target, where the original direct sleepTicks / waitForEvents calls compile to a single inlined call.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
//go:build wasip1 && (scheduler.tasks || scheduler.asyncify)
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"internal/task"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// pollMode identifies the I/O direction a goroutine is waiting on.
|
||||
// Zero is intentionally invalid so an uninitialized pollDesc cannot
|
||||
// silently look like a read waiter.
|
||||
type pollMode uint8
|
||||
|
||||
const (
|
||||
pollRead pollMode = 1
|
||||
pollWrite pollMode = 2
|
||||
)
|
||||
|
||||
// pollDesc tracks one parked goroutine waiting for an FD to become ready.
|
||||
// It is created by netpollAddWait, kept alive by activePolls, and freed
|
||||
// (eventually GC'd) once unlinked.
|
||||
type pollDesc struct {
|
||||
fd uint32
|
||||
mode pollMode
|
||||
fired bool // set by pollIO when the wait is satisfied; netpollDone uses this for idempotency
|
||||
task *task.Task
|
||||
bnxt *pollDesc // chain in activePolls
|
||||
}
|
||||
|
||||
var (
|
||||
// activePolls is the singly-linked list of all currently-parked FD
|
||||
// waiters. wasip1 is single-threaded — every mutation happens from
|
||||
// the running goroutine or the scheduler loop, never both.
|
||||
activePolls *pollDesc
|
||||
pollCount int
|
||||
|
||||
// Scratch buffers for poll_oneoff. Grown on demand, never shrunk —
|
||||
// the working set settles on a stable max.
|
||||
pollSubs []__wasi_subscription_t
|
||||
pollEvents []__wasi_event_t
|
||||
)
|
||||
|
||||
// netpollAddWait registers the calling goroutine's interest in fd / mode
|
||||
// and returns a descriptor identifying the wait. The caller must:
|
||||
//
|
||||
// 1. call task.Pause() to suspend until the FD is ready (or the task is
|
||||
// woken for some other reason — timer, manual scheduleTask), and
|
||||
// 2. call netpollDone(pd) after Pause returns to deregister.
|
||||
//
|
||||
// Multiple waiters on the same (fd, mode) pair are supported; each gets
|
||||
// its own pollDesc and its own subscription in the next poll_oneoff call.
|
||||
func netpollAddWait(fd uint32, mode pollMode) *pollDesc {
|
||||
pd := &pollDesc{
|
||||
fd: fd,
|
||||
mode: mode,
|
||||
task: task.Current(),
|
||||
bnxt: activePolls,
|
||||
}
|
||||
activePolls = pd
|
||||
pollCount++
|
||||
return pd
|
||||
}
|
||||
|
||||
// netpollDone removes pd from activePolls if it is still registered.
|
||||
// Idempotent — if pollIO has already woken the waiter, pd.fired is true
|
||||
// and this is a no-op.
|
||||
func netpollDone(pd *pollDesc) {
|
||||
if pd.fired {
|
||||
return
|
||||
}
|
||||
pp := &activePolls
|
||||
for *pp != nil {
|
||||
if *pp == pd {
|
||||
*pp = pd.bnxt
|
||||
pd.bnxt = nil
|
||||
pollCount--
|
||||
return
|
||||
}
|
||||
pp = &(*pp).bnxt
|
||||
}
|
||||
}
|
||||
|
||||
// pollIO is the cooperative scheduler's blocking wait on wasip1. It
|
||||
// invokes poll_oneoff with one subscription per pollDesc currently in
|
||||
// activePolls, plus optionally a clock subscription.
|
||||
//
|
||||
// timeoutNs > 0 : add a clock subscription with this nanosecond timeout.
|
||||
// timeoutNs == 0 : non-blocking poll, no clock sub. (Forward-looking; the
|
||||
// v1 scheduler does not invoke this path.)
|
||||
// timeoutNs < 0 : block until any FD is ready, no clock sub. Caller must
|
||||
// ensure pollCount > 0 — calling poll_oneoff with zero
|
||||
// subscriptions returns EINVAL.
|
||||
//
|
||||
// Tasks whose subscriptions fire are pushed onto runqueue. The caller
|
||||
// (the scheduler) re-walks the sleep / timer queues on its next loop
|
||||
// iteration to handle clock fires.
|
||||
func pollIO(timeoutNs int64) {
|
||||
addClock := timeoutNs > 0
|
||||
nsubs := pollCount
|
||||
if addClock {
|
||||
nsubs++
|
||||
}
|
||||
if nsubs == 0 {
|
||||
// Caller is responsible for not invoking pollIO with nothing to
|
||||
// wait on; bail out rather than calling poll_oneoff with zero
|
||||
// subscriptions.
|
||||
return
|
||||
}
|
||||
if cap(pollSubs) < nsubs {
|
||||
pollSubs = make([]__wasi_subscription_t, nsubs)
|
||||
pollEvents = make([]__wasi_event_t, nsubs)
|
||||
} else {
|
||||
pollSubs = pollSubs[:nsubs]
|
||||
pollEvents = pollEvents[:nsubs]
|
||||
}
|
||||
|
||||
i := 0
|
||||
for pd := activePolls; pd != nil; pd = pd.bnxt {
|
||||
var et __wasi_eventtype_t
|
||||
if pd.mode == pollRead {
|
||||
et = __wasi_eventtype_t_fd_read
|
||||
} else {
|
||||
et = __wasi_eventtype_t_fd_write
|
||||
}
|
||||
pollSubs[i].userData = uint64(uintptr(unsafe.Pointer(pd)))
|
||||
pollSubs[i].u.setFDReadWrite(et, pd.fd)
|
||||
i++
|
||||
}
|
||||
|
||||
if addClock {
|
||||
pollSubs[i].userData = 0
|
||||
pollSubs[i].u.setClock(0, uint64(timeoutNs), timePrecisionNanoseconds, 0)
|
||||
i++
|
||||
}
|
||||
|
||||
var nevents uint32
|
||||
poll_oneoff(&pollSubs[0], &pollEvents[0], uint32(nsubs), &nevents)
|
||||
|
||||
for k := uint32(0); k < nevents; k++ {
|
||||
ev := &pollEvents[k]
|
||||
if ev.userData == 0 {
|
||||
continue
|
||||
}
|
||||
pd := (*pollDesc)(unsafe.Pointer(uintptr(ev.userData)))
|
||||
if pd.fired {
|
||||
continue
|
||||
}
|
||||
pd.fired = true
|
||||
pp := &activePolls
|
||||
for *pp != nil {
|
||||
if *pp == pd {
|
||||
*pp = pd.bnxt
|
||||
pd.bnxt = nil
|
||||
pollCount--
|
||||
break
|
||||
}
|
||||
pp = &(*pp).bnxt
|
||||
}
|
||||
runqueue.Push(pd.task)
|
||||
}
|
||||
}
|
||||
|
||||
// runtime_netpoll_addwait is the linkname target used by package syscall
|
||||
// (and any future package using //go:linkname into runtime) to register
|
||||
// a wait on an FD without sharing the runtime's pollDesc / pollMode
|
||||
// types. The returned uintptr is an opaque pollDesc pointer; callers
|
||||
// must pass it back to runtime_netpoll_done.
|
||||
//
|
||||
// mode must be one of pollRead (1) or pollWrite (2).
|
||||
//
|
||||
//go:linkname runtime_netpoll_addwait
|
||||
func runtime_netpoll_addwait(fd uint32, mode uint8) uintptr {
|
||||
return uintptr(unsafe.Pointer(netpollAddWait(fd, pollMode(mode))))
|
||||
}
|
||||
|
||||
// runtime_netpoll_done is the linkname target used by package syscall to
|
||||
// release a pollDesc previously returned by runtime_netpoll_addwait.
|
||||
// Idempotent; safe to call whether or not pollIO has already woken the
|
||||
// waiter.
|
||||
//
|
||||
//go:linkname runtime_netpoll_done
|
||||
func runtime_netpoll_done(pd uintptr) {
|
||||
if pd == 0 {
|
||||
return
|
||||
}
|
||||
netpollDone((*pollDesc)(unsafe.Pointer(pd)))
|
||||
}
|
||||
|
||||
// runtime_netpoll_pdfired reports whether the given pollDesc has already
|
||||
// been woken (either by a poll_oneoff event or by a manual wake). Used
|
||||
// by deadline-driven cancellation paths to avoid double-waking a task.
|
||||
//
|
||||
//go:linkname runtime_netpoll_pdfired
|
||||
func runtime_netpoll_pdfired(pd uintptr) bool {
|
||||
if pd == 0 {
|
||||
return true
|
||||
}
|
||||
return (*pollDesc)(unsafe.Pointer(pd)).fired
|
||||
}
|
||||
|
||||
// runtime_netpoll_wake wakes the task parked on pd from outside the
|
||||
// poll_oneoff event loop — for example, from a deadline timer's
|
||||
// callback. Idempotent: a second call (or a race with pollIO firing
|
||||
// the same pd) is a no-op thanks to the pd.fired flag.
|
||||
//
|
||||
// wasip1 is single-threaded so we don't need atomic ops here.
|
||||
//
|
||||
//go:linkname runtime_netpoll_wake
|
||||
func runtime_netpoll_wake(pd uintptr) {
|
||||
if pd == 0 {
|
||||
return
|
||||
}
|
||||
p := (*pollDesc)(unsafe.Pointer(pd))
|
||||
if p.fired {
|
||||
return
|
||||
}
|
||||
p.fired = true
|
||||
pp := &activePolls
|
||||
for *pp != nil {
|
||||
if *pp == p {
|
||||
*pp = p.bnxt
|
||||
p.bnxt = nil
|
||||
pollCount--
|
||||
break
|
||||
}
|
||||
pp = &(*pp).bnxt
|
||||
}
|
||||
runqueue.Push(p.task)
|
||||
}
|
||||
@@ -78,11 +78,6 @@ var (
|
||||
sleepTicksNEvents uint32
|
||||
)
|
||||
|
||||
func sleepTicks(d timeUnit) {
|
||||
sleepTicksSubscription.u.u.timeout = uint64(d)
|
||||
poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents)
|
||||
}
|
||||
|
||||
func ticks() timeUnit {
|
||||
var nano uint64
|
||||
clock_time_get(0, timePrecisionNanoseconds, &nano)
|
||||
@@ -106,9 +101,9 @@ func poll_oneoff(in *__wasi_subscription_t, out *__wasi_event_t, nsubscriptions
|
||||
type __wasi_eventtype_t = uint8
|
||||
|
||||
const (
|
||||
__wasi_eventtype_t_clock __wasi_eventtype_t = 0
|
||||
// TODO: __wasi_eventtype_t_fd_read __wasi_eventtype_t = 1
|
||||
// TODO: __wasi_eventtype_t_fd_write __wasi_eventtype_t = 2
|
||||
__wasi_eventtype_t_clock __wasi_eventtype_t = iota
|
||||
__wasi_eventtype_t_fd_read
|
||||
__wasi_eventtype_t_fd_write
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -118,10 +113,12 @@ type (
|
||||
u __wasi_subscription_u_t
|
||||
}
|
||||
|
||||
// The union payload is sized by the largest variant (clock, 32 bytes after
|
||||
// the tag and its 7-byte alignment pad). FD read/write subscriptions reuse
|
||||
// the same memory via setFDReadWrite.
|
||||
__wasi_subscription_u_t struct {
|
||||
tag __wasi_eventtype_t
|
||||
|
||||
// TODO: support fd_read/fd_write event
|
||||
u __wasi_subscription_clock_t
|
||||
}
|
||||
|
||||
@@ -134,6 +131,28 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// __wasi_subscription_fd_readwrite_t is the FD variant of the subscription
|
||||
// union payload. It overlays the first 4 bytes of the clock variant.
|
||||
type __wasi_subscription_fd_readwrite_t struct {
|
||||
fd uint32
|
||||
}
|
||||
|
||||
func (s *__wasi_subscription_u_t) setClock(id uint32, timeoutNs, precision uint64, flags uint16) {
|
||||
s.tag = __wasi_eventtype_t_clock
|
||||
s.u = __wasi_subscription_clock_t{
|
||||
id: id,
|
||||
timeout: timeoutNs,
|
||||
precision: precision,
|
||||
flags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *__wasi_subscription_u_t) setFDReadWrite(eventType __wasi_eventtype_t, fd uint32) {
|
||||
s.tag = eventType
|
||||
s.u = __wasi_subscription_clock_t{}
|
||||
(*__wasi_subscription_fd_readwrite_t)(unsafe.Pointer(&s.u)).fd = fd
|
||||
}
|
||||
|
||||
type (
|
||||
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#-event-record
|
||||
__wasi_event_t struct {
|
||||
@@ -141,11 +160,18 @@ type (
|
||||
errno uint16
|
||||
eventType __wasi_eventtype_t
|
||||
|
||||
// only used for fd_read or fd_write events
|
||||
// TODO: support fd_read/fd_write event
|
||||
_ struct {
|
||||
// fdReadWrite is populated by poll_oneoff for fd_read / fd_write events.
|
||||
// For clock events the field is zero. Reading nBytes/flags after a
|
||||
// clock event is meaningless but not unsafe.
|
||||
fdReadWrite struct {
|
||||
nBytes uint64
|
||||
flags uint16
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Compile-time size assertions for the wasip1 ABI. If these fail to compile
|
||||
// the struct layout drifted from the spec and poll_oneoff would corrupt
|
||||
// memory.
|
||||
var _ [0]byte = [48 - unsafe.Sizeof(__wasi_subscription_t{})]byte{}
|
||||
var _ [0]byte = [32 - unsafe.Sizeof(__wasi_event_t{})]byte{}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build wasip1 && (scheduler.tasks || scheduler.asyncify)
|
||||
|
||||
package runtime
|
||||
|
||||
// sleepTicks is the cooperative scheduler's "wait until the next deadline"
|
||||
// primitive on wasip1. It is only called by the scheduler when the run queue
|
||||
// is empty and there's a sleeping task or pending timer due in d ticks.
|
||||
//
|
||||
// If any FD waiters are registered via netpollAddWait, this routes through
|
||||
// pollIO so the same poll_oneoff call observes both the clock subscription
|
||||
// and the FD subscriptions. With no FD waiters it falls back to the cheap
|
||||
// single-clock-subscription path.
|
||||
func sleepTicks(d timeUnit) {
|
||||
if pollCount > 0 {
|
||||
pollIO(ticksToNanoseconds(d))
|
||||
return
|
||||
}
|
||||
sleepTicksSubscription.u.u.timeout = uint64(d)
|
||||
poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents)
|
||||
}
|
||||
|
||||
// waitForEvents is the cooperative scheduler's "wait until something external
|
||||
// happens" primitive. It is only called when both the run queue and the
|
||||
// timer/sleep queues are empty. With no FD waiters this is a genuine
|
||||
// deadlock; with FD waiters we block until any of them is ready.
|
||||
func waitForEvents() {
|
||||
if pollCount > 0 {
|
||||
pollIO(-1)
|
||||
return
|
||||
}
|
||||
runtimePanic("deadlocked: no event source")
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build wasip1 && !scheduler.tasks && !scheduler.asyncify
|
||||
|
||||
package runtime
|
||||
|
||||
// sleepTicks blocks the current execution context for d ticks. This is the
|
||||
// fallback used when no cooperative scheduler is configured (-scheduler=none
|
||||
// or -scheduler=threads on wasip1) and it has no FD-polling integration —
|
||||
// see scheduler_idle_wasip1.go for the cooperative variant.
|
||||
func sleepTicks(d timeUnit) {
|
||||
sleepTicksSubscription.u.u.timeout = uint64(d)
|
||||
poll_oneoff(&sleepTicksSubscription, &sleepTicksResult, 1, &sleepTicksNEvents)
|
||||
}
|
||||
|
||||
// waitForEvents is only meaningful when there's an event source available.
|
||||
// Without the cooperative scheduler running poll_oneoff on FDs, wasip1 has
|
||||
// nothing to wake on, so this is a hard deadlock.
|
||||
func waitForEvents() {
|
||||
runtimePanic("deadlocked: no event source")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin
|
||||
//go:build !tinygo.riscv && !cortexm && !(linux && !baremetal && !tinygo.wasm && !nintendoswitch) && !darwin && !wasip1
|
||||
|
||||
package runtime
|
||||
|
||||
|
||||
Reference in New Issue
Block a user