test(scratch): leak-hunt probes for cava, render loop, decode stream
One-off diagnostics behind the FFTW leak fix: init/destroy x50 RSS bound, Mach VM region walker, render-churn region growth, ffmpeg decode-stream region/rss sampling.
This commit is contained in:
23
tests/scratch-cava-reinit.test.ts
Normal file
23
tests/scratch-cava-reinit.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/** Scratch: cava init/destroy cycles leak native fftw buffers? */
|
||||
import { test, expect } from "bun:test"
|
||||
import { loadCavaCore } from "../src/utils/cavacore"
|
||||
|
||||
const cava = loadCavaCore()
|
||||
const skip = !cava
|
||||
|
||||
test.skipIf(skip)("init/destroy x50: RSS bounded", () => {
|
||||
const cfg = { bars: 64, sampleRate: 22050, channels: 1, autosens: 0 }
|
||||
const samples = new Float64Array(8192)
|
||||
Bun.gc(true)
|
||||
const startRss = process.memoryUsage.rss()
|
||||
for (let i = 0; i < 50; i++) {
|
||||
cava!.init(cfg)
|
||||
cava!.execute(samples)
|
||||
cava!.destroy()
|
||||
}
|
||||
Bun.gc(true)
|
||||
const endRss = process.memoryUsage.rss()
|
||||
const grown = (endRss - startRss) / 1048576
|
||||
console.log(`init/destroy x50: rss delta=${grown.toFixed(1)}MB`)
|
||||
expect(grown).toBeLessThan(100)
|
||||
}, 60_000)
|
||||
34
tests/scratch-region-count.ts
Normal file
34
tests/scratch-region-count.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/** Mach VM region walker via FFI (self-process). */
|
||||
import { dlopen, FFIType, ptr } from "bun:ffi"
|
||||
|
||||
const k = dlopen("/usr/lib/system/libsystem_kernel.dylib", {
|
||||
mach_task_self: { args: [], returns: FFIType.u64 },
|
||||
mach_vm_region: {
|
||||
args: [FFIType.u64, FFIType.ptr, FFIType.ptr, FFIType.u32, FFIType.ptr, FFIType.ptr, FFIType.ptr],
|
||||
returns: FFIType.i32,
|
||||
},
|
||||
})
|
||||
|
||||
/** Walk own VM map; count total regions and ~128K ones. */
|
||||
export function countRegions(): { total: number; r128k: number } {
|
||||
const task = (k.symbols.mach_task_self as any)() as number
|
||||
const addr = new BigUint64Array(1)
|
||||
const size = new BigUint64Array(1)
|
||||
const info = new Uint32Array(16)
|
||||
const infoCnt = new Uint32Array(1)
|
||||
const objectName = new Uint32Array(1)
|
||||
let total = 0
|
||||
let r128k = 0
|
||||
addr[0] = 1n
|
||||
const walk = k.symbols.mach_vm_region as any
|
||||
for (;;) {
|
||||
infoCnt[0] = 16
|
||||
const kr = walk(BigInt(task), ptr(addr), ptr(size), 9, ptr(info), ptr(infoCnt), ptr(objectName))
|
||||
if (kr !== 0) break
|
||||
total++
|
||||
if (size[0] >= 131072n && size[0] <= 139264n) r128k++
|
||||
addr[0] = addr[0] + size[0]
|
||||
if (addr[0] === 0n || total > 500000) break
|
||||
}
|
||||
return { total, r128k }
|
||||
}
|
||||
27
tests/scratch-render-leak.tsx
Normal file
27
tests/scratch-render-leak.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/** Minimal opentui app: VM region growth from render loop and/or text churn. */
|
||||
import { appendFileSync } from "node:fs"
|
||||
import { createSignal } from "solid-js"
|
||||
import { render } from "@opentui/solid"
|
||||
import { countRegions } from "./scratch-region-count"
|
||||
|
||||
const CHURN = Bun.argv.includes("--churn")
|
||||
const OUT = Bun.argv.find((a) => a.startsWith("--out="))?.slice(6) ?? "/tmp/render-leak.log"
|
||||
const log = (m: string) => Bun.write(Bun.stderr, m + "\n") // stderr may be hijacked too; use fd via file:
|
||||
const append = (m: string) => appendFileSync(OUT, m + "\n")
|
||||
|
||||
const [s, setS] = createSignal("hello")
|
||||
if (CHURN) {
|
||||
let i = 0
|
||||
setInterval(() => setS(`hello ${++i} ${"x".repeat(i % 50)}`), 33)
|
||||
}
|
||||
|
||||
render(() => <text>{s()}</text>)
|
||||
await Bun.sleep(1000)
|
||||
const c0 = countRegions()
|
||||
append(`start churn=${CHURN}: total=${c0.total}`)
|
||||
for (let w = 1; w <= 4; w++) {
|
||||
await Bun.sleep(15_000)
|
||||
const c = countRegions()
|
||||
append(`t=${w * 15}s total=${c.total} (delta ${c.total - c0.total})`)
|
||||
}
|
||||
process.exit(0)
|
||||
36
tests/scratch-stream-leak.test.ts
Normal file
36
tests/scratch-stream-leak.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/** Scratch: does the ffmpeg decode stream leak VM regions per chunk? */
|
||||
import { test, expect } from "bun:test"
|
||||
import { EpisodePcmCache } from "../src/utils/audio-pcm-cache"
|
||||
import { spawnSync } from "child_process"
|
||||
|
||||
const wav = "/tmp/podtui-stream.wav"
|
||||
if (!(await Bun.file(wav).exists()) && (await Bun.$`which ffmpeg`.nothrow())) {
|
||||
await Bun.$`ffmpeg -f lavfi -i "sine=frequency=440:duration=600" -ar 22050 -ac 1 -sample_fmt s16 ${wav}`.quiet().nothrow()
|
||||
}
|
||||
|
||||
function regions(): number {
|
||||
const out = spawnSync("vmmap", [String(process.pid)], { timeout: 20000 }).stdout?.toString() ?? ""
|
||||
return out.split("\n").filter((l) => l.includes("VM_ALLOCATE")).length
|
||||
}
|
||||
function rss(): number {
|
||||
return Number(spawnSync("ps", ["-o", "rss=", "-p", String(process.pid)]).stdout?.toString().trim() || 0)
|
||||
}
|
||||
|
||||
test("decode stream 90s: regions and rss bounded", async () => {
|
||||
Bun.gc(true)
|
||||
await Bun.sleep(200)
|
||||
const r0 = regions(), m0 = rss()
|
||||
const pcm = new EpisodePcmCache({ url: wav })
|
||||
pcm.startDecode(0)
|
||||
const t0 = Date.now()
|
||||
while (Date.now() - t0 < 90_000) {
|
||||
await Bun.sleep(5_000)
|
||||
const pos = ((Date.now() - t0) / 1000) * 4
|
||||
pcm.readWindow(new Float64Array(512), pos)
|
||||
}
|
||||
Bun.gc(true)
|
||||
const r1 = regions(), m1 = rss()
|
||||
console.log(`stream 90s: regions ${r0}->${r1} (delta ${r1 - r0}), rss ${(m0 / 1048576) | 0}->${(m1 / 1048576) | 0}MB`)
|
||||
pcm.stop()
|
||||
expect(r1 - r0).toBeLessThan(100)
|
||||
}, 140_000)
|
||||
22
tests/scratch-stream-run.ts
Normal file
22
tests/scratch-stream-run.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/** Scratch runner: decode stream, self-measure VM regions over time. */
|
||||
import { EpisodePcmCache } from "../src/utils/audio-pcm-cache"
|
||||
import { countRegions } from "./scratch-region-count"
|
||||
const wav = "/tmp/podtui-stream.wav"
|
||||
if (!(await Bun.file(wav).exists())) {
|
||||
await Bun.$`ffmpeg -f lavfi -i "sine=frequency=440:duration=600" -ar 22050 -ac 1 -sample_fmt s16 ${wav}`.quiet().nothrow()
|
||||
}
|
||||
const pcm = new EpisodePcmCache({ url: wav })
|
||||
Bun.gc(true)
|
||||
console.log(`start: ${JSON.stringify(countRegions())}`)
|
||||
pcm.startDecode(0)
|
||||
const t0 = Date.now()
|
||||
const buf = new Float64Array(512)
|
||||
while (Date.now() - t0 < 90_000) {
|
||||
await Bun.sleep(15_000)
|
||||
const pos = ((Date.now() - t0) / 1000) * 4
|
||||
pcm.readWindow(buf, pos)
|
||||
const c = countRegions()
|
||||
console.log(`t=${((Date.now() - t0) / 1000) | 0}s total=${c.total} r128k=${c.r128k} rss=${(process.memoryUsage.rss() / 1048576) | 0}MB`)
|
||||
}
|
||||
pcm.stop()
|
||||
console.log("done")
|
||||
Reference in New Issue
Block a user