fix(cava): identical init re-uses the live plan
cava_init/destroy churn leaks the old plan's FFTW work buffers — upstream frees only its own struct. init() now serializes the config and no-ops when unchanged, so pipeline restarts (focus/episode churn) keep the live plan instead of leaking a new one each cycle.
This commit is contained in:
@@ -95,6 +95,8 @@ export class CavaCore {
|
|||||||
private _bars = 0;
|
private _bars = 0;
|
||||||
private _channels = 1;
|
private _channels = 1;
|
||||||
private _destroyed = false;
|
private _destroyed = false;
|
||||||
|
/** Serialized last init config — identical init() calls are no-ops. */
|
||||||
|
private lastConfigKey = "";
|
||||||
|
|
||||||
/** Use loadCavaCore() instead of constructing directly. */
|
/** Use loadCavaCore() instead of constructing directly. */
|
||||||
constructor(lib: CavaLib) {
|
constructor(lib: CavaLib) {
|
||||||
@@ -112,15 +114,25 @@ export class CavaCore {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the cavacore engine with the given configuration.
|
* Initialize the cavacore engine with the given configuration.
|
||||||
* Must be called before execute(). Can be called again after destroy()
|
* Must be called before execute(). Identical configs are a no-op:
|
||||||
* to reinitialize with different parameters.
|
* cava_init/destroy churn leaks the old plan's FFTW work buffers
|
||||||
|
* (upstream frees only its own struct), so a pipeline restart with
|
||||||
|
* unchanged bars/rate/cutoffs must re-USE the live plan.
|
||||||
*/
|
*/
|
||||||
init(config: CavaCoreConfig = {}): void {
|
init(config: CavaCoreConfig = {}): void {
|
||||||
|
const cfg = { ...DEFAULTS, ...config };
|
||||||
|
if (
|
||||||
|
this.plan !== null &&
|
||||||
|
!this._destroyed &&
|
||||||
|
this.lastConfigKey === JSON.stringify(cfg)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastConfigKey = JSON.stringify(cfg);
|
||||||
if (this.plan) {
|
if (this.plan) {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const cfg = { ...DEFAULTS, ...config };
|
|
||||||
this._bars = cfg.bars;
|
this._bars = cfg.bars;
|
||||||
this._channels = cfg.channels;
|
this._channels = cfg.channels;
|
||||||
|
|
||||||
|
|||||||
58
tests/cavacore-init-reuse.test.ts
Normal file
58
tests/cavacore-init-reuse.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* CavaCore.init() must be a no-op for an identical config: re-initializing
|
||||||
|
* the same plan leaks the old plan's native FFTW work buffers, so pipeline
|
||||||
|
* restarts (focus/episode churn) have to re-USE the live plan.
|
||||||
|
*/
|
||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import { CavaCore } from "../src/utils/cavacore"
|
||||||
|
|
||||||
|
function stubLib() {
|
||||||
|
const calls = { init: 0, destroy: 0 }
|
||||||
|
let plans = 0
|
||||||
|
const lib = {
|
||||||
|
symbols: {
|
||||||
|
cava_init: () => {
|
||||||
|
calls.init++
|
||||||
|
return { p: ++plans }
|
||||||
|
},
|
||||||
|
cava_execute: () => {},
|
||||||
|
cava_destroy: () => {
|
||||||
|
calls.destroy++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
close: () => {},
|
||||||
|
}
|
||||||
|
return { lib, calls }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("identical init config reuses the plan", () => {
|
||||||
|
const { lib, calls } = stubLib()
|
||||||
|
const cava = new CavaCore(lib as never)
|
||||||
|
const cfg = { bars: 64, sampleRate: 22050, channels: 1, autosens: 0 }
|
||||||
|
cava.init(cfg)
|
||||||
|
cava.init(cfg)
|
||||||
|
cava.init(cfg)
|
||||||
|
expect(calls.init).toBe(1)
|
||||||
|
expect(cava.isReady).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("changed config re-inits, destroying the old plan", () => {
|
||||||
|
const { lib, calls } = stubLib()
|
||||||
|
const cava = new CavaCore(lib as never)
|
||||||
|
cava.init({ bars: 64, sampleRate: 22050, channels: 1, autosens: 0 })
|
||||||
|
cava.init({ bars: 32, sampleRate: 22050, channels: 1, autosens: 0 })
|
||||||
|
expect(calls.init).toBe(2)
|
||||||
|
expect(calls.destroy).toBe(1)
|
||||||
|
expect(cava.bars).toBe(32)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("init after destroy creates a fresh plan", () => {
|
||||||
|
const { lib, calls } = stubLib()
|
||||||
|
const cava = new CavaCore(lib as never)
|
||||||
|
const cfg = { bars: 64, sampleRate: 22050, channels: 1, autosens: 0 }
|
||||||
|
cava.init(cfg)
|
||||||
|
cava.destroy()
|
||||||
|
cava.init(cfg)
|
||||||
|
expect(calls.init).toBe(2)
|
||||||
|
expect(cava.isReady).toBe(true)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user