// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package tensor import ( "os" "path/filepath" "runtime" "testing" ) // The leak harness. A kernel that returns must leave nothing behind: // every parallel worker joined, every buffer handed back or // unreachable, no reference parked in a package global. Go garbage // collects, so a leak here means unbounded growth, not a lost block, // and the check is the same shape either way: run the heavy paths, // force collection, and require the goroutine count and the live heap // to come back to where they started. The buffers sync.Pool hoards are // released by the collections the check forces first. // leakOps is the representative load: parallel kernels across every // domain, the ones that spawn workers and the ones that allocate // scratch proportional to the input, plus the untrusted-input readers. // The data is sized so that a single retained buffer per round shows up // far above the tolerance below: the vectors are 2 MiB and the matrix // 32 KiB, while the tolerance is a fraction of a megabyte. func leakOps(t *testing.T) []func() { t.Helper() const bigN = 1 << 18 // 2 MiB of float64 big := randA(t, 31, bigN) med := randA(t, 32, 1<<16) sq := mustA(t, med.RawFloats()[:64*64], 64, 64) // Symmetric, diagonally dominant: both factorisations stay stable. mm := sq.RawFloats() for i := range 64 { for j := range 64 { mm[i*64+j] = (mm[i*64+j] + mm[j*64+i]) / 2 } mm[i*64+i] += 20 } sig := randA(t, 33, 1<<14) // Files the readers must refuse: a header that lies about its sizes // and a table header with no data behind it. Reading them allocates // nothing beyond the few dozen bytes they contain, and nothing may // stay behind either. dir := t.TempDir() hostileNC := filepath.Join(dir, "hostile.nc") if err := os.WriteFile(hostileNC, hostileOracleHeader(), 0o644); err != nil { t.Fatal(err) } hostileFITS := filepath.Join(dir, "hostile.fits") if err := os.WriteFile(hostileFITS, hostileOracleTable(), 0o644); err != nil { t.Fatal(err) } return []func(){ func() { _, _ = Add(big, big) }, func() { _, _ = Mul(big, big) }, func() { _, _ = MatMul2D(sq, sq) }, func() { _, _ = Einsum("ij,jk->ik", sq, sq) }, func() { _, _ = Sort(big) }, func() { _, _ = ArgSort(big) }, func() { _, _ = FFT(sig) }, func() { _, _, _, _ = SVD(sq) }, func() { _, _, _ = Eigen(sq) }, func() { _, _ = Inv(sq) }, func() { _, _ = Cholesky(sq) }, func() { _, _, _ = QR(sq) }, func() { _, _, _ = WelchPSD(sig, 1000, 256, 128, "hann") }, func() { _, _ = SavitzkyGolay(sig, 21, 3) }, func() { _, _ = DWT(sig, 4) }, func() { _, _ = CWT(sig, Morlet, []float64{1, 2, 4, 8, 16}, 1) }, func() { _, _ = SobolPoints(1<<12, 8, 0) }, func() { _, _ = HaltonPoints(1<<12, 8, 0) }, func() { _, _ = Gradient1D(sig, 1) }, func() { _, _ = Laplacian(sq, 1, 1) }, func() { _, _ = IntegrateODE(func(t float64, y *Array) (*Array, error) { return MulF(y, -1), nil }, 0, 5, mustA(t, []float64{1}, 1), ODEOptions{MaxSteps: 100000}) }, func() { _, _, _, _ = LoadNetCDF(hostileNC) }, func() { _, _ = LoadFITSTable(hostileFITS) }, func() { _, _ = LoadHDF5(filepath.Join("io", "testdata", "h5", "fixture.h5")) }, func() { _, _ = LinearRegression(mustA(t, randA(t, 34, 4096).RawFloats(), 4096, 1), randA(t, 35, 4096)) }, } } // liveHeap returns the live heap after forcing every reclaimable byte // to be collected twice: sync.Pool keeps one victim generation, so two // rounds are what it takes for pooled buffers to be unreachable. func liveHeap() (heap uint64, goroutines int) { runtime.GC() runtime.GC() var m runtime.MemStats runtime.ReadMemStats(&m) return m.HeapAlloc, runtime.NumGoroutine() } // leakBlock runs one measurement block over the given operations and // returns the live heap and goroutine count after it, so the leak // detector itself can be measured by the same instrument it uses. func leakBlock(ops []func(), rounds int) (heap uint64, goroutines int) { for range rounds { for _, op := range ops { op() } } return liveHeap() } // TestLeakHarnessDetectsLeaks is the harness's self-check: the same // block measurement run over an operation that deliberately retains // half a megabyte per round must fail, so a regression that empties // the comparison or flips its direction cannot pass silently. func TestLeakHarnessDetectsLeaks(t *testing.T) { var retained [][]float64 leaky := []func(){ func() { retained = append(retained, make([]float64, 64<<10)) }, } leakBlock(leaky, 3) // settle, as the real harness does first, _ := leakBlock(leaky, 10) second, _ := leakBlock(leaky, 10) // The closure must stay reachable through the second block's // collections: without a later use, the caller's slot is dead // during the call and the collector would free exactly the data // the self-check means to catch. runtime.KeepAlive(leaky) if second <= first+256<<10 { t.Fatalf("the leak harness measured %d then %d bytes: a 5 MiB retention over 10 rounds went undetected", first, second) } } func TestNoResourceLeaks(t *testing.T) { ops := leakOps(t) if len(ops) == 0 { t.Fatal("the leak harness has no operations: a refactor emptied leakOps and the measurement below would pass vacuously") } run := func(rounds int) { for range rounds { for _, op := range ops { op() } } } // Caches, pooled buffers and lazy tables settle in the first block. // The measurement then compares successive blocks rather than the // whole run against a baseline: a leak retains data per round and // grows steadily block over block, while the runtime's own // structures settle. Comparing endpoint to start once is what let // a leak of a few hundred kilobytes per block hide under the // settling curve. const ( block = 10 tolerance = 256 << 10 ) run(3) run(block) prev, g0 := liveHeap() for range 2 { run(block) heap, g := liveHeap() if heap > prev+tolerance { t.Errorf("live heap grew from %d to %d bytes over %d rounds of the facade load (tolerance %d): something retains data per round", prev, heap, block, tolerance) } if g > g0 { t.Errorf("goroutines grew from %d to %d over %d rounds of the facade load: a worker did not return", g0, g, block) } prev = heap } }