Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions test/js/web/timers/setInterval-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 17 additions & 21 deletions test/js/web/timers/setInterval-leak-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

143 changes: 92 additions & 51 deletions test/js/web/timers/setInterval.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, it } from "bun:test";
import { isWindows } from "harness";
import { bunEnv, bunExe } from "harness";
import { join } from "path";

it("setInterval", async () => {
Expand Down Expand Up @@ -28,7 +28,7 @@ it("setInterval", async () => {
});

expect(result).toBe(10);
expect(performance.now() - start > 9).toBe(true);
expect(performance.now() - start).toBeGreaterThanOrEqual(9);
});

it("clearInterval", async () => {
Expand All @@ -38,8 +38,9 @@ it("clearInterval", async () => {
expect.unreachable();
}, 1);
clearInterval(id);
await new Promise((resolve, reject) => {
setInterval(() => {
await new Promise(resolve => {
const id2 = setInterval(() => {
clearInterval(id2);
resolve();
}, 10);
});
Expand All @@ -60,70 +61,110 @@ it("async setInterval", async () => {
}, 1);
});
});
expect(remaining).toBe(0);
});

it("refreshed setInterval should not reschedule again", async () => {
let relative = performance.now();
let runCount = 0;
let timer = setInterval(() => {
let end = performance.now();

// loop for 100
const spinloop = end;
while (performance.now() - spinloop < 100) {
end = performance.now();
}

timer.refresh();

const elapsed = Math.round(end - relative);
console.log("Time since last run", elapsed);

runCount++;
await new Promise((resolve, reject) => {
const timer = setInterval(() => {
let end = performance.now();

switch (runCount) {
case 1: {
if (elapsed < 180) {
throw new Error("Expected elapsed time to be greater than 180");
}
break;
// spin for 100ms so the next scheduled tick is already due by the time we return
const spinloop = end;
while (performance.now() - spinloop < 100) {
end = performance.now();
}
case 3:
case 2: {
if (elapsed > 180) {
throw new Error("Expected elapsed time to be less than 180");

timer.refresh();

const elapsed = Math.round(end - relative);
runCount++;

try {
switch (runCount) {
case 1:
// initial 100ms delay + 100ms spinloop
expect(elapsed).toBeGreaterThanOrEqual(180);
break;
case 2:
case 3:
// refresh() inside the callback must not push the next tick out: since the
// spinloop already consumed the interval, the next fire happens immediately
expect(elapsed).toBeLessThan(180);
break;
}
break;
} catch (err) {
clearInterval(timer);
reject(err);
return;
}
}

relative = end;
relative = end;

if (runCount === 3) {
clearInterval(timer);
}
}, 100);
if (runCount === 3) {
clearInterval(timer);
resolve();
}
}, 100);
});
expect(runCount).toBe(3);
});

it("setInterval runs with at least the delay time", () => {
expect([`run`, join(import.meta.dir, "setInterval-fixture.js")]).toRun();
async function runFixture(args) {
await using proc = Bun.spawn({
cmd: [bunExe(), ...args],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}

it.concurrent("setInterval runs with at least the delay time", async () => {
const { stdout, stderr, exitCode } = await runFixture(["run", join(import.meta.dir, "setInterval-fixture.js")]);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("PASS");
expect(exitCode).toBe(0);
});

it("setInterval canceling with unref, close, _idleTimeout, and _onTimeout", () => {
expect([join(import.meta.dir, "timers-fixture-unref.js"), "setInterval"]).toRun();
it.concurrent("setInterval doesn't run when cancelled after being scheduled", async () => {
const { stdout, stderr, exitCode } = await runFixture([
"run",
join(import.meta.dir, "setinterval-cancel-fixture.js"),
]);
expect(stderr).toBe("");
expect(stdout).toMatch(/^RSS: \d+ MB\n$/);
expect(exitCode).toBe(0);
});

it(
"setInterval doesn't leak memory",
() => {
expect([`run`, join(import.meta.dir, "setInterval-leak-fixture.js")]).toRun();
it.concurrent(
"setInterval canceling with unref, close, _idleTimeout, and _onTimeout",
async () => {
const { stdout, stderr, exitCode } = await runFixture([
join(import.meta.dir, "timers-fixture-unref.js"),
"setInterval",
]);
expect(stderr).toBe("");
expect(stdout).toBe("");
expect(exitCode).toBe(0);
},
!isWindows ? 30_000 : 90_000,
// the fixture spends ~2s under debug+ASAN just loading node/test/common; with the other
// fixtures running concurrently it can exceed the default 5s
30_000,
);
// ✓ setInterval doesn't leak memory [9930.00ms]
// ✓ setInterval doesn't leak memory [80188.00ms]
// TODO: investigate this discrepancy further

it("setInterval doesn't run when cancelled after being scheduled", () => {
expect([`run`, join(import.meta.dir, "setInterval-cancel-fixture.js")]).toRun();
}, 30_000);
it.concurrent(
"setInterval doesn't leak memory",
async () => {
const { stdout, stderr, exitCode } = await runFixture([
"run",
join(import.meta.dir, "setInterval-leak-fixture.js"),
]);
expect(stderr).toBe("");
expect(stdout).toMatch(/^RSS \d+ MB\nDelta -?\d+ MB\nTimeout object count: \d+\n$/);
expect(exitCode).toBe(0);
},
30_000,
);
5 changes: 2 additions & 3 deletions test/js/web/timers/setinterval-cancel-fixture.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading