|
|
@@ -1,127 +0,0 @@
|
|
|
-import { describe, test, expect } from "bun:test"
|
|
|
-import path from "path"
|
|
|
-
|
|
|
-const projectRoot = path.join(import.meta.dir, "../..")
|
|
|
-const worker = path.join(import.meta.dir, "abort-leak-webfetch.ts")
|
|
|
-
|
|
|
-const MB = 1024 * 1024
|
|
|
-const ITERATIONS = 50
|
|
|
-
|
|
|
-const getHeapMB = () => {
|
|
|
- Bun.gc(true)
|
|
|
- return process.memoryUsage().heapUsed / MB
|
|
|
-}
|
|
|
-
|
|
|
-describe("memory: abort controller leak", () => {
|
|
|
- test("webfetch does not leak memory over many invocations", async () => {
|
|
|
- // Measure the abort-timed fetch path in a fresh process so shared tool
|
|
|
- // runtime state does not dominate the heap signal.
|
|
|
- const proc = Bun.spawn({
|
|
|
- cmd: [process.execPath, worker],
|
|
|
- cwd: projectRoot,
|
|
|
- stdout: "pipe",
|
|
|
- stderr: "pipe",
|
|
|
- env: process.env,
|
|
|
- })
|
|
|
-
|
|
|
- const [code, stdout, stderr] = await Promise.all([
|
|
|
- proc.exited,
|
|
|
- new Response(proc.stdout).text(),
|
|
|
- new Response(proc.stderr).text(),
|
|
|
- ])
|
|
|
-
|
|
|
- if (code !== 0) {
|
|
|
- throw new Error(stderr.trim() || stdout.trim() || `worker exited with code ${code}`)
|
|
|
- }
|
|
|
-
|
|
|
- const result = JSON.parse(stdout.trim()) as {
|
|
|
- baseline: number
|
|
|
- after: number
|
|
|
- growth: number
|
|
|
- }
|
|
|
-
|
|
|
- console.log(`Baseline: ${result.baseline.toFixed(2)} MB`)
|
|
|
- console.log(`After ${ITERATIONS} fetches: ${result.after.toFixed(2)} MB`)
|
|
|
- console.log(`Growth: ${result.growth.toFixed(2)} MB`)
|
|
|
-
|
|
|
- // Memory growth should be minimal - less than 1MB per 10 requests.
|
|
|
- expect(result.growth).toBeLessThan(ITERATIONS / 10)
|
|
|
- }, 60000)
|
|
|
-
|
|
|
- test("compare closure vs bind pattern directly", async () => {
|
|
|
- const ITERATIONS = 500
|
|
|
-
|
|
|
- // Test OLD pattern: arrow function closure
|
|
|
- // Store closures in a map keyed by content to force retention
|
|
|
- const closureMap = new Map<string, () => void>()
|
|
|
- const timers: Timer[] = []
|
|
|
- const controllers: AbortController[] = []
|
|
|
-
|
|
|
- Bun.gc(true)
|
|
|
- Bun.sleepSync(100)
|
|
|
- const baseline = getHeapMB()
|
|
|
-
|
|
|
- for (let i = 0; i < ITERATIONS; i++) {
|
|
|
- // Simulate large response body like webfetch would have
|
|
|
- const content = `${i}:${"x".repeat(50 * 1024)}` // 50KB unique per iteration
|
|
|
- const controller = new AbortController()
|
|
|
- controllers.push(controller)
|
|
|
-
|
|
|
- // OLD pattern - closure captures `content`
|
|
|
- const handler = () => {
|
|
|
- // Actually use content so it can't be optimized away
|
|
|
- if (content.length > 1000000000) controller.abort()
|
|
|
- }
|
|
|
- closureMap.set(content, handler)
|
|
|
- const timeoutId = setTimeout(handler, 30000)
|
|
|
- timers.push(timeoutId)
|
|
|
- }
|
|
|
-
|
|
|
- Bun.gc(true)
|
|
|
- Bun.sleepSync(100)
|
|
|
- const after = getHeapMB()
|
|
|
- const oldGrowth = after - baseline
|
|
|
-
|
|
|
- console.log(`OLD pattern (closure): ${oldGrowth.toFixed(2)} MB growth (${closureMap.size} closures)`)
|
|
|
-
|
|
|
- // Cleanup after measuring
|
|
|
- timers.forEach(clearTimeout)
|
|
|
- controllers.forEach((c) => c.abort())
|
|
|
- closureMap.clear()
|
|
|
-
|
|
|
- // Test NEW pattern: bind
|
|
|
- Bun.gc(true)
|
|
|
- Bun.sleepSync(100)
|
|
|
- const baseline2 = getHeapMB()
|
|
|
- const handlers2: (() => void)[] = []
|
|
|
- const timers2: Timer[] = []
|
|
|
- const controllers2: AbortController[] = []
|
|
|
-
|
|
|
- for (let i = 0; i < ITERATIONS; i++) {
|
|
|
- const _content = `${i}:${"x".repeat(50 * 1024)}` // 50KB - won't be captured
|
|
|
- const controller = new AbortController()
|
|
|
- controllers2.push(controller)
|
|
|
-
|
|
|
- // NEW pattern - bind doesn't capture surrounding scope
|
|
|
- const handler = controller.abort.bind(controller)
|
|
|
- handlers2.push(handler)
|
|
|
- const timeoutId = setTimeout(handler, 30000)
|
|
|
- timers2.push(timeoutId)
|
|
|
- }
|
|
|
-
|
|
|
- Bun.gc(true)
|
|
|
- Bun.sleepSync(100)
|
|
|
- const after2 = getHeapMB()
|
|
|
- const newGrowth = after2 - baseline2
|
|
|
-
|
|
|
- // Cleanup after measuring
|
|
|
- timers2.forEach(clearTimeout)
|
|
|
- controllers2.forEach((c) => c.abort())
|
|
|
- handlers2.length = 0
|
|
|
-
|
|
|
- console.log(`NEW pattern (bind): ${newGrowth.toFixed(2)} MB growth`)
|
|
|
- console.log(`Improvement: ${(oldGrowth - newGrowth).toFixed(2)} MB saved`)
|
|
|
-
|
|
|
- expect(newGrowth).toBeLessThanOrEqual(oldGrowth)
|
|
|
- })
|
|
|
-})
|