diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emit-listener-snapshots.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emit-listener-snapshots.ts.json new file mode 100644 index 000000000..f99d7a9c1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/emit-listener-snapshots.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/emit-listener-snapshots.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_events_emitter.c b/packages/runtime/src/scr_events_emitter.c index 93a724ecb..002f16f5a 100644 --- a/packages/runtime/src/scr_events_emitter.c +++ b/packages/runtime/src/scr_events_emitter.c @@ -1066,7 +1066,9 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, ScrEeBucket *b = em->reg ? scr_ee_bucket_of(em->reg, name) : NULL; if (!b || b->n == 0) return false; size_t n = b->n; - ScrEeEntry **snap = malloc(n * sizeof *snap); + /* Most events have a listener or two: their snapshot stays on the stack. */ + ScrEeEntry *small[4]; + ScrEeEntry **snap = n <= 4 ? small : malloc(n * sizeof *snap); if (!snap) scr_ee_oom(); for (size_t i = 0; i < n; i++) { snap[i] = b->ls[i]; @@ -1130,7 +1132,7 @@ static bool scr_ee_emit_core(ScrEmitter *em, ScrStr *name, va_list *ap_template, } scr_ee_entry_unref(e); } - free(snap); + if (snap != small) free(snap); return true; } diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 178d620c6..8d0108649 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -105,7 +105,9 @@ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err) { RlList *l = &rl->ev[ev]; size_t n = l->n; if (n == 0) return; - RlEntry *snap = malloc(n * sizeof *snap); + /* Most events have a listener or two: their snapshot stays on the stack. */ + RlEntry small[4]; + RlEntry *snap = n <= 4 ? small : malloc(n * sizeof *snap); if (!snap) scr_rl_oom(); memcpy(snap, l->v, n * sizeof *snap); for (size_t i = 0; i < n; i++) { @@ -139,7 +141,7 @@ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err) { } if (!e.iter) scr_closure_release(e.cb); } - free(snap); + if (snap != small) free(snap); } ScrArr *scr_rl_history_of(ScrRl *rl) { diff --git a/tests/corpus/emit-listener-snapshots.ts b/tests/corpus/emit-listener-snapshots.ts new file mode 100644 index 000000000..3897e6479 --- /dev/null +++ b/tests/corpus/emit-listener-snapshots.ts @@ -0,0 +1,46 @@ +// Listeners run from a snapshot of the list taken when the event is +// emitted: a listener added during an emit runs from the next emit on, a +// listener removed during an emit still runs in that emit, and a once +// listener runs once. Covers readline 'line' events and EventEmitter +// events with fewer and more listeners than a few. +import { EventEmitter } from "node:events"; +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function emitter(count: number): void { + const em = new EventEmitter(); + const log: string[] = []; + const listeners: ((n: number) => void)[] = []; + for (let i = 0; i < count; i++) { + const listener = (n: number): void => { + log.push(`${i}:${n}`); + if (n === 1 && i === 0) em.on("tick", (m: number) => log.push(`late:${m}`)); + if (n === 1 && i === 1 && listeners[count - 1]) em.off("tick", listeners[count - 1]!); + }; + listeners.push(listener); + em.on("tick", listener); + } + em.once("tick", (n: number) => log.push(`once:${n}`)); + em.emit("tick", 1); + em.emit("tick", 2); + console.log(count, em.listenerCount("tick"), log.join(" ")); +} + +for (const count of [1, 2, 4, 5, 7]) emitter(count); + +async function lines(count: number): Promise { + const input = Readable.from(["alpha\nbeta\n", "gamma\ndelta\n"]); + const rl = createInterface({ input, terminal: false }); + const log: string[] = []; + for (let i = 0; i < count; i++) { + rl.on("line", (line: string) => { + log.push(`${i}:${line}`); + if (line === "beta" && i === 0) rl.on("line", (next: string) => log.push(`late:${next}`)); + }); + } + rl.once("line", (line: string) => log.push(`once:${line}`)); + await new Promise((resolve) => rl.on("close", () => resolve())); + console.log(count, log.join(" ")); +} + +for (const count of [1, 3, 6]) await lines(count); diff --git a/tests/harness/corpus-costs/emit-listener-snapshots.ts.json b/tests/harness/corpus-costs/emit-listener-snapshots.ts.json new file mode 100644 index 000000000..e77e24843 --- /dev/null +++ b/tests/harness/corpus-costs/emit-listener-snapshots.ts.json @@ -0,0 +1 @@ +{ "plain": 5463, "san": 3430 }