mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
fix gc-related flaky test failures (#2402)
This commit is contained in:
2
packages/bun-types/bun.d.ts
vendored
2
packages/bun-types/bun.d.ts
vendored
@@ -2694,7 +2694,7 @@ declare module "bun" {
|
||||
*/
|
||||
builder: PluginBuilder,
|
||||
): void | Promise<void>;
|
||||
}): ReturnType<typeof options["setup"]>;
|
||||
}): ReturnType<(typeof options)["setup"]>;
|
||||
|
||||
/**
|
||||
* Deactivate all plugins
|
||||
|
||||
2
packages/bun-types/events.d.ts
vendored
2
packages/bun-types/events.d.ts
vendored
@@ -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`.
|
||||
*
|
||||
|
||||
8
packages/bun-types/jsc.d.ts
vendored
8
packages/bun-types/jsc.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,32 +221,28 @@ describe("ReactDOM", () => {
|
||||
for (let [inputString, reactElement] of fixtures) {
|
||||
describe(`${renderToReadableStream.name}(${inputString})`, () => {
|
||||
it("http server, 1 request", async () => {
|
||||
await (async function () {
|
||||
let server;
|
||||
try {
|
||||
server = serve({
|
||||
port: 0,
|
||||
async fetch(req) {
|
||||
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;
|
||||
} finally {
|
||||
server?.stop();
|
||||
gc();
|
||||
}
|
||||
})();
|
||||
gc();
|
||||
expect(heapStats().objectTypeCounts.ReadableHTTPResponseSinkController ?? 0).toBeLessThan(4);
|
||||
});
|
||||
const count = 4;
|
||||
it(`http server, ${count} requests`, async () => {
|
||||
var remain = count;
|
||||
await (async function () {
|
||||
await (async () => {
|
||||
var server;
|
||||
try {
|
||||
server = serve({
|
||||
port: 0,
|
||||
async fetch(req) {
|
||||
return new Response(await renderToReadableStream(reactElement));
|
||||
},
|
||||
});
|
||||
const response = await fetch("http://localhost:" + server.port + "/");
|
||||
const result = await response.text();
|
||||
expect(result.replaceAll("<!-- -->", "")).toBe(inputString);
|
||||
} finally {
|
||||
server?.stop();
|
||||
}
|
||||
})();
|
||||
await expectObjectTypeCount("ReadableHTTPResponseSinkController", 1);
|
||||
});
|
||||
const count = 4;
|
||||
it(`http server, ${count} requests`, async () => {
|
||||
var remain = count;
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user