fix gc-related flaky test failures (#2402)

This commit is contained in:
Alex Lam S.L
2023-03-16 03:51:22 +02:00
committed by GitHub
parent 480567a5af
commit 47865fe82a
9 changed files with 111 additions and 115 deletions

View File

@@ -2694,7 +2694,7 @@ declare module "bun" {
*/
builder: PluginBuilder,
): void | Promise<void>;
}): ReturnType<typeof options["setup"]>;
}): ReturnType<(typeof options)["setup"]>;
/**
* Deactivate all plugins

View File

@@ -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`.
*

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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);
});
});

View File

@@ -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);
});

View File

@@ -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);
});
});
}

View File

@@ -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);
});