diff options
author | 2023-03-16 03:51:22 +0200 | |
---|---|---|
committer | 2023-03-15 18:51:22 -0700 | |
commit | 47865fe82a1495da910f6f7fa5bc515d1dd3bc96 (patch) | |
tree | 1ad24e2cdab372afcde159278ae7938c19816a39 | |
parent | 480567a5af0c84f667a027c6c0d9c1f004c27d8a (diff) | |
download | bun-47865fe82a1495da910f6f7fa5bc515d1dd3bc96.tar.gz bun-47865fe82a1495da910f6f7fa5bc515d1dd3bc96.tar.zst bun-47865fe82a1495da910f6f7fa5bc515d1dd3bc96.zip |
fix gc-related flaky test failures (#2402)
-rw-r--r-- | packages/bun-types/bun.d.ts | 2 | ||||
-rw-r--r-- | packages/bun-types/events.d.ts | 2 | ||||
-rw-r--r-- | packages/bun-types/jsc.d.ts | 8 | ||||
-rw-r--r-- | test/harness.ts | 30 | ||||
-rw-r--r-- | test/js/bun/http/serve.leak.ts | 6 | ||||
-rw-r--r-- | test/js/bun/jsc/bun-jsc.test.ts (renamed from test/js/bun/jsc/bun-jsc.test.js) | 39 | ||||
-rw-r--r-- | test/js/bun/net/tcp-server.test.ts | 42 | ||||
-rw-r--r-- | test/js/bun/stream/direct-readable-stream.test.tsx | 35 | ||||
-rw-r--r-- | test/js/node/events/event-emitter.test.ts | 32 |
9 files changed, 96 insertions, 100 deletions
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index bcfa0bf51..3773d3ebb 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2694,7 +2694,7 @@ declare module "bun" { */ builder: PluginBuilder, ): void | Promise<void>; - }): ReturnType<typeof options["setup"]>; + }): ReturnType<(typeof options)["setup"]>; /** * Deactivate all plugins diff --git a/packages/bun-types/events.d.ts b/packages/bun-types/events.d.ts index 2e9056618..96c97db32 100644 --- a/packages/bun-types/events.d.ts +++ b/packages/bun-types/events.d.ts @@ -121,7 +121,7 @@ declare module "events" { * @param eventName The name of the event. * @param listener The callback function */ - once(eventName: string | symbol, listener: (...args: any[]) => void): this; + once(eventName: string | symbol, listener: (this: this, ...args: any[]) => void): this; /** * Removes the specified `listener` from the listener array for the event named`eventName`. * diff --git a/packages/bun-types/jsc.d.ts b/packages/bun-types/jsc.d.ts index b909d0348..df33db1ec 100644 --- a/packages/bun-types/jsc.d.ts +++ b/packages/bun-types/jsc.d.ts @@ -1,9 +1,9 @@ declare module "bun:jsc" { export function describe(value: any): string; export function describeArray(args: any[]): string; - export function gcAndSweep(): void; - export function fullGC(): void; - export function edenGC(): void; + export function gcAndSweep(): number; + export function fullGC(): number; + export function edenGC(): number; export function heapSize(): number; export function heapStats(): { heapSize: number; @@ -29,7 +29,7 @@ declare module "bun:jsc" { export function callerSourceOrigin(): string; export function noFTL(func: Function): Function; export function noOSRExitFuzzing(func: Function): Function; - export function optimizeNextInvocation(func: Function): Function; + export function optimizeNextInvocation(func: Function): void; export function numberOfDFGCompiles(func: Function): number; export function releaseWeakRefs(): void; export function totalCompileTime(func: Function): number; diff --git a/test/harness.ts b/test/harness.ts index 9c4ce8e56..9470d24ee 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -1,3 +1,7 @@ +import { gc as bunGC, unsafe } from "bun"; +import { heapStats } from "bun:jsc"; +import { expect } from "bun:test"; + export const bunEnv: any = { ...process.env, BUN_DEBUG_QUIET_LOGS: "1", @@ -9,8 +13,18 @@ export function bunExe() { return process.execPath; } -export function gc(force: boolean = true) { - Bun.gc(force); +export function gc(force = true) { + bunGC(force); +} + +export async function expectObjectTypeCount(type: string, count: number, maxWait = 10000) { + gc(); + for (const wait = 20; maxWait > 0; maxWait -= wait) { + if (heapStats().objectTypeCounts[type] === count) break; + await new Promise(resolve => setTimeout(resolve, wait)); + gc(); + } + expect(heapStats().objectTypeCounts[type]).toBe(count); } // we must ensure that finalizers are run @@ -19,20 +33,18 @@ export function gcTick(trace = false) { trace && console.trace(""); // console.trace("hello"); gc(); - return new Promise(resolve => { - setTimeout(resolve, 0); - }); + return new Promise(resolve => setTimeout(resolve, 0)); } export function withoutAggressiveGC(block: () => unknown) { - if (!Bun.unsafe.gcAggressionLevel) return block(); + if (!unsafe.gcAggressionLevel) return block(); - const origGC = Bun.unsafe.gcAggressionLevel(); - Bun.unsafe.gcAggressionLevel(0); + const origGC = unsafe.gcAggressionLevel(); + unsafe.gcAggressionLevel(0); try { return block(); } finally { - Bun.unsafe.gcAggressionLevel(origGC); + unsafe.gcAggressionLevel(origGC); } } diff --git a/test/js/bun/http/serve.leak.ts b/test/js/bun/http/serve.leak.ts index 09935d24d..a7b8a44f6 100644 --- a/test/js/bun/http/serve.leak.ts +++ b/test/js/bun/http/serve.leak.ts @@ -1,8 +1,8 @@ import { heapStats } from "bun:jsc"; -var prevCounts; +var prevCounts: Record<string, number>; export default { - fetch(req) { - const out = {}; + fetch(req: Request) { + const out: Record<string, number> = {}; const counts = heapStats().objectTypeCounts; for (const key in counts) { if (prevCounts) { diff --git a/test/js/bun/jsc/bun-jsc.test.js b/test/js/bun/jsc/bun-jsc.test.ts index 6e6897eb3..aa93ce90a 100644 --- a/test/js/bun/jsc/bun-jsc.test.js +++ b/test/js/bun/jsc/bun-jsc.test.ts @@ -35,34 +35,39 @@ describe("bun:jsc", () => { } it("describe", () => { - jscDescribe([]); + expect(jscDescribe([])).toBeDefined(); }); it("describeArray", () => { - describeArray([1, 2, 3]); + expect(describeArray([1, 2, 3])).toBeDefined(); }); it("gcAndSweep", () => { - gcAndSweep(); + expect(gcAndSweep()).toBeGreaterThan(0); }); it("fullGC", () => { - fullGC(); + expect(fullGC()).toBeGreaterThan(0); }); it("edenGC", () => { - edenGC(); + expect(edenGC()).toBeGreaterThan(0); }); it("heapSize", () => { - expect(heapSize() > 0).toBe(true); + expect(heapSize()).toBeGreaterThan(0); }); it("heapStats", () => { - heapStats(); + const stats = heapStats(); + expect(stats.heapCapacity).toBeGreaterThan(0); + expect(stats.heapSize).toBeGreaterThan(0); + expect(stats.objectCount).toBeGreaterThan(0); }); it("memoryUsage", () => { - memoryUsage(); + const usage = memoryUsage(); + expect(usage.current).toBeGreaterThan(0); + expect(usage.peak).toBeGreaterThan(0); }); it("getRandomSeed", () => { - getRandomSeed(2); + expect(getRandomSeed()).toBeDefined(); }); it("setRandomSeed", () => { - setRandomSeed(2); + expect(setRandomSeed(2)).toBeUndefined(); }); it("isRope", () => { expect(isRope("a" + 123 + "b")).toBe(true); @@ -75,23 +80,23 @@ describe("bun:jsc", () => { it("noOSRExitFuzzing", () => {}); it("optimizeNextInvocation", () => { count(); - optimizeNextInvocation(count); + expect(optimizeNextInvocation(count)).toBeUndefined(); count(); }); it("numberOfDFGCompiles", () => { - expect(numberOfDFGCompiles(count) > 0).toBe(true); + expect(numberOfDFGCompiles(count)).toBeGreaterThan(0); }); it("releaseWeakRefs", () => { - releaseWeakRefs(); + expect(releaseWeakRefs()).toBeUndefined(); }); it("totalCompileTime", () => { - totalCompileTime(count); + expect(totalCompileTime(count)).toBeGreaterThanOrEqual(0); }); it("reoptimizationRetryCount", () => { - reoptimizationRetryCount(count); + expect(reoptimizationRetryCount(count)).toBeGreaterThanOrEqual(0); }); it("drainMicrotasks", () => { - drainMicrotasks(); + expect(drainMicrotasks()).toBeUndefined(); }); it("startRemoteDebugger", () => { // try { @@ -103,6 +108,6 @@ describe("bun:jsc", () => { // } }); it("getProtectedObjects", () => { - expect(getProtectedObjects().length > 0).toBe(true); + expect(getProtectedObjects().length).toBeGreaterThan(0); }); }); diff --git a/test/js/bun/net/tcp-server.test.ts b/test/js/bun/net/tcp-server.test.ts index ed5d04086..17f7df46b 100644 --- a/test/js/bun/net/tcp-server.test.ts +++ b/test/js/bun/net/tcp-server.test.ts @@ -1,11 +1,13 @@ import { listen, connect, TCPSocketListener, SocketHandler } from "bun"; import { describe, expect, it } from "bun:test"; -import * as JSC from "bun:jsc"; +import { expectObjectTypeCount } from "harness"; -var decoder = new TextDecoder(); +type Resolve = (value?: unknown) => void; +type Reject = (reason?: any) => void; +const decoder = new TextDecoder(); it("remoteAddress works", async () => { - var resolve: () => void, reject: (e: any) => void; + var resolve: Resolve, reject: Reject; var remaining = 2; var prom = new Promise<void>((resolve1, reject1) => { resolve = () => { @@ -60,17 +62,17 @@ it("echo server 1 on 1", async () => { // wrap it in a separate closure so the GC knows to clean it up // the sockets & listener don't escape the closure await (async function () { - var resolve, reject, serverResolve, serverReject; - var prom = new Promise((resolve1, reject1) => { + let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject; + const prom = new Promise((resolve1, reject1) => { resolve = resolve1; reject = reject1; }); - var serverProm = new Promise((resolve1, reject1) => { + const serverProm = new Promise((resolve1, reject1) => { serverResolve = resolve1; serverReject = reject1; }); - var serverData, clientData; + let serverData: any, clientData: any; const handlers = { open(socket) { socket.data.counter = 1; @@ -129,7 +131,7 @@ it("echo server 1 on 1", async () => { var server: TCPSocketListener<any> | undefined = listen({ socket: handlers, hostname: "localhost", - port: 8084, + port: 0, data: { isServer: true, @@ -139,7 +141,7 @@ it("echo server 1 on 1", async () => { const clientProm = connect({ socket: handlers, hostname: "localhost", - port: 8084, + port: server.port, data: { counter: 0, }, @@ -151,24 +153,23 @@ it("echo server 1 on 1", async () => { }); describe("tcp socket binaryType", () => { - var port = 8085; const binaryType = ["arraybuffer", "uint8array", "buffer"] as const; for (const type of binaryType) { it(type, async () => { // wrap it in a separate closure so the GC knows to clean it up // the sockets & listener don't escape the closure await (async function () { - var resolve, reject, serverResolve, serverReject; - var prom = new Promise((resolve1, reject1) => { + let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject; + const prom = new Promise((resolve1, reject1) => { resolve = resolve1; reject = reject1; }); - var serverProm = new Promise((resolve1, reject1) => { + const serverProm = new Promise((resolve1, reject1) => { serverResolve = resolve1; serverReject = reject1; }); - var serverData, clientData; + let serverData: any, clientData: any; const handlers = { open(socket) { socket.data.counter = 1; @@ -239,7 +240,7 @@ describe("tcp socket binaryType", () => { var server: TCPSocketListener<any> | undefined = listen({ socket: handlers, hostname: "localhost", - port, + port: 0, data: { isServer: true, counter: 0, @@ -249,12 +250,11 @@ describe("tcp socket binaryType", () => { const clientProm = connect({ socket: handlers, hostname: "localhost", - port, + port: server.port, data: { counter: 0, }, }); - port++; await Promise.all([prom, clientProm, serverProm]); server.stop(true); @@ -264,11 +264,9 @@ describe("tcp socket binaryType", () => { } }); -it("should not leak memory", () => { - // Tell the garbage collector for sure that we're done with the sockets - Bun.gc(true); +it("should not leak memory", async () => { // assert we don't leak the sockets // we expect 1 because that's the prototype / structure - expect(JSC.heapStats().objectTypeCounts.TCPSocket).toBe(1); - expect(JSC.heapStats().objectTypeCounts.Listener).toBe(1); + await expectObjectTypeCount("Listener", 1); + await expectObjectTypeCount("TCPSocket", 1); }); diff --git a/test/js/bun/stream/direct-readable-stream.test.tsx b/test/js/bun/stream/direct-readable-stream.test.tsx index 6ef4014c4..f685bd634 100644 --- a/test/js/bun/stream/direct-readable-stream.test.tsx +++ b/test/js/bun/stream/direct-readable-stream.test.tsx @@ -6,10 +6,9 @@ import { readableStreamToText, serve, } from "bun"; -import { heapStats } from "bun:jsc"; import { describe, expect, it } from "bun:test"; +import { expectObjectTypeCount, gc } from "harness"; import { renderToReadableStream as renderToReadableStreamBrowser } from "react-dom/server.browser"; -import { gc } from "harness"; import { renderToReadableStream as renderToReadableStreamBun } from "react-dom/server"; import React from "react"; @@ -222,8 +221,8 @@ describe("ReactDOM", () => { for (let [inputString, reactElement] of fixtures) { describe(`${renderToReadableStream.name}(${inputString})`, () => { it("http server, 1 request", async () => { - await (async function () { - let server; + await (async () => { + var server; try { server = serve({ port: 0, @@ -231,23 +230,19 @@ describe("ReactDOM", () => { return new Response(await renderToReadableStream(reactElement)); }, }); - const resp = await fetch("http://localhost:" + server.port + "/"); - expect((await resp.text()).replaceAll("<!-- -->", "")).toBe(inputString); - gc(); - } catch (e) { - throw e; + const response = await fetch("http://localhost:" + server.port + "/"); + const result = await response.text(); + expect(result.replaceAll("<!-- -->", "")).toBe(inputString); } finally { server?.stop(); - gc(); } })(); - gc(); - expect(heapStats().objectTypeCounts.ReadableHTTPResponseSinkController ?? 0).toBeLessThan(4); + await expectObjectTypeCount("ReadableHTTPResponseSinkController", 1); }); const count = 4; it(`http server, ${count} requests`, async () => { var remain = count; - await (async function () { + await (async () => { var server; try { server = serve({ @@ -256,11 +251,9 @@ describe("ReactDOM", () => { return new Response(await renderToReadableStream(reactElement)); }, }); - gc(); while (remain--) { var attempt = remain + 1; const response = await fetch("http://localhost:" + server.port + "/"); - gc(); const result = await response.text(); try { expect(result.replaceAll("<!-- -->", "")).toBe(inputString); @@ -268,19 +261,13 @@ describe("ReactDOM", () => { e.message += "\nAttempt: " + attempt; throw e; } - - gc(); } - } catch (e) { - throw e; } finally { - server.stop(); + server?.stop(); } })(); - - const { ReadableHTTPResponseSinkController = 0 } = heapStats().objectTypeCounts; - expect(ReadableHTTPResponseSinkController).toBeLessThan(4); - expect(remain + 1).toBe(0); + expect(remain).toBe(-1); + await expectObjectTypeCount("ReadableHTTPResponseSinkController", 1); }); }); } diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index 2bb891778..0ffc5574e 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -1,10 +1,9 @@ import { test, describe, expect, it } from "bun:test"; -import fs from "node:fs"; - +import { heapStats } from "bun:jsc"; +import { expectObjectTypeCount, gc } from "harness"; // this is also testing that imports with default and named imports in the same statement work // our transpiler transform changes this to a var with import.meta.require import EventEmitter, { getEventListeners, captureRejectionSymbol } from "node:events"; -import { heapStats } from "bun:jsc"; describe("EventEmitter", () => { it("captureRejectionSymbol", () => { @@ -91,15 +90,15 @@ const waysOfCreating = [ return foo; }, () => { - const FakeEmitter = function FakeEmitter() { + function FakeEmitter(this: any) { return EventEmitter.call(this); - }; + } Object.setPrototypeOf(FakeEmitter.prototype, EventEmitter.prototype); Object.setPrototypeOf(FakeEmitter, EventEmitter); - return new FakeEmitter(); + return new (FakeEmitter as any)(); }, () => { - const FakeEmitter = function FakeEmitter() { + const FakeEmitter: any = function FakeEmitter(this: any) { EventEmitter.call(this); }; Object.assign(FakeEmitter.prototype, EventEmitter.prototype); @@ -117,9 +116,9 @@ for (let create of waysOfCreating) { it(`${create.toString().slice(10, 40).replaceAll("\n", "\\n").trim()} should work`, () => { var myEmitter = create(); var called = false; - myEmitter.once("event", function () { + (myEmitter as EventEmitter).once("event", function () { called = true; - expect(this as any).toBe(myEmitter); + expect(this).toBe(myEmitter); }); var firstEvents = myEmitter._events; expect(myEmitter.listenerCount("event")).toBe(1); @@ -143,13 +142,11 @@ test("EventEmitter.off", () => { }); // Internally, EventEmitter has a JSC::Weak with the thisValue of the listener -test("EventEmitter GCs", () => { - Bun.gc(true); +test("EventEmitter GCs", async () => { + gc(); - const startCount = heapStats().objectTypeCounts["EventEmitter"] || 0; + const startCount = heapStats().objectTypeCounts["EventEmitter"] ?? 0; (function () { - Bun.gc(true); - function EventEmitterSubclass(this: any) { EventEmitter.call(this); } @@ -157,13 +154,10 @@ test("EventEmitter GCs", () => { Object.setPrototypeOf(EventEmitterSubclass.prototype, EventEmitter.prototype); Object.setPrototypeOf(EventEmitterSubclass, EventEmitter); - var myEmitter = new EventEmitterSubclass(); + var myEmitter = new (EventEmitterSubclass as any)(); myEmitter.on("foo", () => {}); myEmitter.emit("foo"); - Bun.gc(true); })(); - Bun.gc(true); - const endCount = heapStats().objectTypeCounts["EventEmitter"] || 0; - expect(endCount).toBe(startCount); + await expectObjectTypeCount("EventEmitter", startCount); }); |