The browser build gives the page time to run¶
Status: Done (GitHub issue 25)
Test: tests/regression/integration/test_js_browser_yield.py
Problem¶
The generated main() ran without a break, except while it waited at INPUT,
LINE INPUT or INPUT$. In a browser this caused two faults:
- A long loop froze the page. The page could not repaint, and the output of PRINT did not show until the program ended.
- A loop that polls
INKEY$never saw a key. Thekeydownhandler is a separate task, and it cannot run whilemain()runs.
Under Node, INKEY$ did not read stdin at all, so a polling loop never ended.
What the compiler does now¶
Programs that can loop¶
A program "can loop" when it has FOR, NEXT, WHILE, WEND, ON ERROR or RESUME, or a jump (GOTO, GOSUB, ON GOTO, ON GOSUB, IF ... THEN line) to the same line or an earlier line. The code generator writes the program twice:
main()is the copy that Node runs. It has no check, so its speed is the speed of the program before this change._run(pc)is the copy for the browser. Before each jump back it counts down a budget of 2000 jumps. When the budget is empty it reads the clock. If 12 ms have gone by since the last break,_runreturns the pc of the jump. The driver_main_page()waits for the page withsetTimeout(0)and calls_runagain at that pc.
The runtime chooses the copy with _browser, which is true when window
exists.
The check is not an await inside the loop. An await point inside the hot
loop made the loop 10 to 20 percent slower under Node, also when the await
never ran. The Node copy has no check.
WEND is a jump back, as NEXT is, and has the same check.
A program that cannot loop has one main() as before. It runs from the
first line to the last line and cannot freeze the page.
INKEY$¶
_inkey() is async. When the key buffer is empty:
- Browser: it waits for the page with
setTimeout(0), so thekeydownhandler can put a key in the buffer. - Node: it reads what is waiting on stdin. It does not wait. A file on stdin
is read from file descriptor 0. A pipe or a terminal is opened again as
/dev/stdinwithO_NONBLOCK, so a read with no data givesEAGAIN. At end of inputINKEY$gives an empty string for ever, as a keyboard with no key pressed does.
A program that uses INKEY$ has an async main() (and _run). The call is
(await _inkey()).
DEF FN¶
A DEF FN is async only when its body calls INPUT$ or INKEY$, or calls
another async DEF FN. Before this change every DEF FN was async in a
program with INPUT. Each call of an async function costs a microtask, so
now plain arithmetic functions stay plain functions.
Also changed¶
- The scan for INPUT, LINE INPUT, WRITE, GOSUB, ON GOSUB and the other
features now looks inside
IF ... THEN ... ELSE. Before this change anIF X THEN GOSUB 100left out the GOSUB stack and anIF X THEN INPUT Amade invalid JavaScript.
Speed under Node¶
The test program counts 30 million passes through a FOR loop in a FOR
loop (about 1.6 s), and 30 million jumps of an IF ... THEN line loop
(about 0.29 s). Both differ from the code before this change by 5 percent
or less. The variation between runs is about 3 percent.
Differences that remain¶
- The page gets time only at a jump back. One statement that takes long (for
example
SPACE$(255)in a very large string expression) still runs without a break. - The unit of time is a jump back, not a statement. The budget and the 12 ms
limit are in
JS_YIELD_BUDGETandJS_YIELD_MS. - In the vm stub the same program runs about 15 times slower than under plain Node (V8 handles globals of a new context slowly). A real browser does not have this limit.