1import { HttpRouter, HttpServerResponse } from "effect/http"
2import * as inspector from "node:inspector/promises"
3
4const WARMUP = 5_000
5const REQUESTS = 50_000
6const RETAINED_LIMIT = 64
7const CHURN_LIMIT = 17_000
8const DEFERRED_REQUESTS = 20_000
9const DEFERRED_CONCURRENCY = 32
10// main deferred a per-request span-end task via setImmediate (~4k bytes/request
11// held until the loop turns); the native-tracer fast path ends the span inline.
12const DEFERRED_LIMIT = 512
13
14const gc = (globalThis as any).gc as undefined | (() => void)
15if (gc === undefined) {
16 console.error("run with --expose-gc")
17 process.exit(1)
18}
19
20const session = new inspector.Session()
21session.connect()
22
23const { dispose, handler } = HttpRouter.toWebHandler(
24 HttpRouter.add("GET", "/ping", HttpServerResponse.text("pong")),
25 { disableLogger: true }
26)
27
28const run = async (count: number) => {
29 for (let i = 0; i < count; i++) {
30 const response = await handler(new Request("http://localhost/ping"))
31 await response.arrayBuffer()
32 }
33}
34
35// a promise-chained load never yields to the event loop, so any per-request
36// macrotask (setImmediate) the request path schedules accumulates unrun
37const runConcurrent = async (count: number) => {
38 let remaining = count
39 const worker = async () => {
40 while (remaining > 0) {
41 remaining--
42 const response = await handler(new Request("http://localhost/ping"))
43 await response.arrayBuffer()
44 }
45 }
46 await Promise.all(Array.from({ length: DEFERRED_CONCURRENCY }, worker))
47}
48
49interface ProfileNode {
50 callFrame: { functionName: string; url: string; lineNumber: number }
51 selfSize: number
52 children?: Array<ProfileNode>
53}
54
55const topFrames = (head: ProfileNode) => {
56 const byFrame = new Map<string, number>()
57 let total = 0
58 const visit = (node: ProfileNode) => {
59 if (node.selfSize > 0) {
60 const f = node.callFrame
61 const url = f.url.replace(/^.*\/(packages|node_modules)\//, "$1/")
62 const key = `${f.functionName || "(anonymous)"} @ ${url}:${f.lineNumber + 1}`
63 byFrame.set(key, (byFrame.get(key) ?? 0) + node.selfSize)
64 total += node.selfSize
65 }
66 node.children?.forEach(visit)
67 }
68 visit(head)
69 return {
70 total,
71 frames: [...byFrame.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
72 }
73}
74
75// Allow FinalizationRegistry callbacks to run before measuring retained heap.
76const heapUsed = async () => {
77 for (let i = 0; i < 5; i++) {
78 gc()
79 await new Promise((resolve) => setImmediate(resolve))
80 }
81 return process.memoryUsage().heapUsed
82}
83
84await run(WARMUP)
85
86const heapBefore = await heapUsed()
87await run(REQUESTS)
88const heapAfter = await heapUsed()
89
90// A promise-chained burst never yields to the event loop, so synchronous-GC
91// heap deltas expose per-request work deferred into macrotask queues.
92await runConcurrent(WARMUP)
93gc()
94gc()
95const deferredBefore = process.memoryUsage().heapUsed
96await runConcurrent(DEFERRED_REQUESTS)
97gc()
98gc()
99const deferredAfter = process.memoryUsage().heapUsed
100
101await session.post("HeapProfiler.startSampling", {
102 samplingInterval: 16384,
103 includeObjectsCollectedByMajorGC: true,
104 includeObjectsCollectedByMinorGC: true
105})
106await run(REQUESTS)
107const { profile } = await session.post("HeapProfiler.stopSampling")
108
109await dispose()
110
111const { frames, total } = topFrames(profile.head as ProfileNode)
112const churnPerRequest = Math.round(total / REQUESTS)
113const retainedPerRequest = Math.round((heapAfter - heapBefore) / REQUESTS)
114const deferredPerRequest = Math.round((deferredAfter - deferredBefore) / DEFERRED_REQUESTS)
115
116console.log(`requests: ${REQUESTS}`)
117console.log(`allocated/request: ${churnPerRequest} bytes (limit ${CHURN_LIMIT})`)
118console.log(`retained/request: ${retainedPerRequest} bytes (limit ${RETAINED_LIMIT})`)
119console.log(`deferred/request: ${deferredPerRequest} bytes (limit ${DEFERRED_LIMIT})`)
120console.log("top allocation sites:")
121for (const [key, size] of frames) {
122 const pct = ((size / total) * 100).toFixed(1).padStart(5)
123 console.log(` ${pct}% ${(size / 1024 / 1024).toFixed(2).padStart(7)} MiB ${key}`)
124}
125
126if (retainedPerRequest > RETAINED_LIMIT) {
127 console.error(`FAIL: retained heap grows ${retainedPerRequest} bytes/request`)
128 process.exit(1)
129}
130if (deferredPerRequest > DEFERRED_LIMIT) {
131 console.error(`FAIL: microtask-only burst holds ${deferredPerRequest} bytes/request exceeds ${DEFERRED_LIMIT}`)
132 process.exit(1)
133}
134if (churnPerRequest > CHURN_LIMIT) {
135 console.error(`FAIL: allocation churn ${churnPerRequest} bytes/request exceeds ${CHURN_LIMIT}`)
136 process.exit(1)
137}
138console.log("PASS")