Skip to content

web target: setTimeout/setInterval callbacks never fire (top-level runs; deferred callbacks dropped) #1323

Description

@proggeramlug

Summary

On the --target web runtime (perry 0.5.1022), a compiled program's top-level code runs, but timer callbacks scheduled via setTimeout/setInterval never execute. Plain JS timers in the same page tick normally, so it's specific to the WASM→runtime timer bridge, not the browser.

Repro

// repro.ts
console.log('TOP_RAN');                                  // prints
setTimeout(() => console.log('TIMEOUT_FIRED'), 200);     // never prints
setInterval(() => console.log('INTERVAL_FIRED'), 150);   // never prints
PERRY_ALLOW_PERRY_FEATURES=1 perry compile repro.ts --target web -o repro.html
# serve + open in Chrome (headless=new, --disable-background-timer-throttling)

Console (captured via a console.log collector installed with Page.addScriptToEvaluateOnNewDocument, so capture timing isn't a factor):

["TOP_RAN"]

Expected: TOP_RAN, then TIMEOUT_FIRED, then repeated INTERVAL_FIRED.

A plain-JS control in the identical headless setup (setInterval(()=>el.textContent='ticks='+(++n),150)) reaches ticks=8 in 1.5s — so real timers fire; only the perry-compiled callbacks don't.

Likely area

crates/perry-codegen-wasm/src/wasm_runtime.js rt.set_timeout / rt.set_interval:

set_interval: (closureHandle, delay) => {
  const cb = getHandle(closureHandle);
  if (!cb || !wasmInstance) return 0;          // <- if cb is falsy, nothing is scheduled
  const id = setInterval(() => {
    const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0);
    if (fn) fn(...cb.captures);                 // <- or fn lookup yields nothing
  }, delay);
  return id;
},

Either the closure handle passed from compiled code no longer resolves via getHandle() (so cb is falsy and no real timer is scheduled), or cb.funcIdx/cb.captures don't match the current closure ABI. Given the recent spin-throttle / async-scheduler work (#1114, #1181), a closure-representation drift seems plausible.

Impact

This blocks any web app that defers work:

  • @honeide/editor drives its input/render poll loop with setInterval(_pollAllEditors, 8) — with timers dead, the editor renders once but is non-interactive (no typing, getText() never updates, queued setText() never drains). This is currently blocking playground.perryts.com.
  • Any user program using setTimeout/setInterval/timer-backed async in the playground run sandbox.

Top-level console.log, DOM construction, and synchronous compute all work — it's specifically deferred callbacks.

Ask

Confirm whether this reproduces in a non-headless build too (I can only test headless here), and restore timer-callback dispatch on the web target. Happy to test a branch against the editor poll loop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions