Keep small listener snapshots of readline and EventEmitter emits on the stack (#2483)

This commit is contained in:
Chris Tate
2026-10-28 15:51:22 -05:00
committed by GitHub
5 changed files with 61 additions and 4 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/emit-listener-snapshots.ts"
],
"diags": []
}
+4 -2
View File
@@ -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;
}
+4 -2
View File
@@ -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) {
+46
View File
@@ -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<void> {
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<void>((resolve) => rl.on("close", () => resolve()));
console.log(count, log.join(" "));
}
for (const count of [1, 3, 6]) await lines(count);
@@ -0,0 +1 @@
{ "plain": 5463, "san": 3430 }