diff --git a/tests/scratch-cava-reinit.test.ts b/tests/scratch-cava-reinit.test.ts new file mode 100644 index 0000000..80921f4 --- /dev/null +++ b/tests/scratch-cava-reinit.test.ts @@ -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) diff --git a/tests/scratch-region-count.ts b/tests/scratch-region-count.ts new file mode 100644 index 0000000..79ce0d5 --- /dev/null +++ b/tests/scratch-region-count.ts @@ -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 } +} diff --git a/tests/scratch-render-leak.tsx b/tests/scratch-render-leak.tsx new file mode 100644 index 0000000..03f99fd --- /dev/null +++ b/tests/scratch-render-leak.tsx @@ -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(() => {s()}) +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) diff --git a/tests/scratch-stream-leak.test.ts b/tests/scratch-stream-leak.test.ts new file mode 100644 index 0000000..1931cb2 --- /dev/null +++ b/tests/scratch-stream-leak.test.ts @@ -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) diff --git a/tests/scratch-stream-run.ts b/tests/scratch-stream-run.ts new file mode 100644 index 0000000..40d0948 --- /dev/null +++ b/tests/scratch-stream-run.ts @@ -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")