Add stop-leak detection. Stoppable nodes that are not stopped

This commit is contained in:
jeromew 2025-11-30 17:50:56 +00:00
parent 24b69ad1bc
commit 05ed16e158

View file

@ -21,6 +21,7 @@ const initCache = JSON.stringify({
where: [], where: [],
disconnectAll: 0, disconnectAll: 0,
disconnectOne: 0, disconnectOne: 0,
hasStop: false,
stopCount: 0, stopCount: 0,
ac: null, ac: null,
creation: null, creation: null,
@ -56,6 +57,7 @@ const lazyRegister = (o) => {
this._audioid = ++audioid; this._audioid = ++audioid;
const s = JSON.parse(initCache); const s = JSON.parse(initCache);
s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name; s.type = this.constructor.name === 'AudiographNode' ? this.constructor._parentClassName : this.constructor.name;
s.hasStop = window[s.type].prototype instanceof AudioScheduledSourceNode;
s.ac = this.context?.constructor.name || 'AudioParam'; s.ac = this.context?.constructor.name || 'AudioParam';
s.creation = s.creation || stackTrace(); s.creation = s.creation || stackTrace();
cache.set(this._audioid, s); cache.set(this._audioid, s);
@ -103,7 +105,6 @@ const drawMessage = async function (message) {
const drawDiagram = async function () { const drawDiagram = async function () {
const element = document.querySelector('.strudel-mermaid'); const element = document.querySelector('.strudel-mermaid');
let code = window.strudelMirror.code; let code = window.strudelMirror.code;
code = code.replace(/^await debugAudiograph.*\n?/gm, ''); code = code.replace(/^await debugAudiograph.*\n?/gm, '');
code = '// date: ' + new Date().toISOString() + '\n\n' + code; code = '// date: ' + new Date().toISOString() + '\n\n' + code;
@ -140,13 +141,16 @@ const drawDiagram = async function () {
if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']'; if (s.ac === 'OfflineAudioContext') lb = '[' + lb + ']';
return lb; return lb;
}; };
const isLeak = (s) => { const isConnectLeak = (s) => {
return ( return (
['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 && ['AudioDestinationNode', 'AudioParam'].indexOf(s.type) === -1 &&
s.disconnectAll === 0 && s.disconnectAll === 0 &&
s.connect.length > s.disconnectOne s.connect.length > s.disconnectOne
); );
}; };
const isStopLeak = (s) => {
return s.hasStop && s.stopCount === 0;
};
do { do {
curRelations = relations.length; curRelations = relations.length;
lookup.slice().forEach((n) => { lookup.slice().forEach((n) => {
@ -197,6 +201,13 @@ const drawDiagram = async function () {
}); });
}); });
} while (relations.length > curRelations /*&& lookup.length < 100*/); } while (relations.length > curRelations /*&& lookup.length < 100*/);
// add orphan nodes
const inRelation = '-' + relations.join('-') + '-';
cache.forEach((v, k) => {
if (!inRelation.includes('-' + k + '-')) {
gd += '\t\tnode' + k + label(v) + '\n';
}
});
const codePlaceholder = 'm'.repeat(maxLineLength); const codePlaceholder = 'm'.repeat(maxLineLength);
gd += '\tsubgraph LEGEND\n'; gd += '\tsubgraph LEGEND\n';
@ -205,6 +216,7 @@ const drawDiagram = async function () {
gd += '\t\tlegend3[not disconnected]\n'; gd += '\t\tlegend3[not disconnected]\n';
gd += '\t\tlegend4[AudioParam]\n'; gd += '\t\tlegend4[AudioParam]\n';
gd += '\t\tlegend5[AudioDestinationNode]\n'; gd += '\t\tlegend5[AudioDestinationNode]\n';
gd += '\t\tlegend6[not stopped]\n';
gd += '\tend\n'; gd += '\tend\n';
gd += '\tsubgraph CODE[Strudel Code]\n'; gd += '\tsubgraph CODE[Strudel Code]\n';
// we use a codePlaceholder to // we use a codePlaceholder to
@ -216,13 +228,17 @@ const drawDiagram = async function () {
gd += '\tend\n'; gd += '\tend\n';
gd += '\tclassDef audioparam fill:#6f6;\n'; gd += '\tclassDef audioparam fill:#6f6;\n';
gd += '\tclassDef destination fill:#99f;\n'; gd += '\tclassDef destination fill:#99f;\n';
gd += '\tclassDef suspect fill:#f96,stroke:#f00,stroke-width:2px;\n'; gd += '\tclassDef connectleak fill:#f96,stroke:#f00,stroke-width:2px;\n';
gd += '\tclass legend3 suspect;\n'; gd += '\tclassDef stopleak fill:#f55,stroke:#f00,stroke-width:2px;\n';
gd += '\tclass legend3 connectleak;\n';
gd += '\tclass legend4 audioparam;\n'; gd += '\tclass legend4 audioparam;\n';
gd += '\tclass legend5 destination;\n'; gd += '\tclass legend5 destination;\n';
gd += '\tclass legend6 stopleak;\n';
cache.forEach((v, k) => { cache.forEach((v, k) => {
if (isLeak(v)) { if (isConnectLeak(v)) {
gd += '\tclass node' + k + ' suspect;\n'; gd += '\tclass node' + k + ' connectleak;\n';
} else if (isStopLeak(v)) {
gd += '\tclass node' + k + ' stopleak;\n';
} }
if (v.type === 'AudioParam') { if (v.type === 'AudioParam') {
gd += '\tclass node' + k + ' audioparam;\n'; gd += '\tclass node' + k + ' audioparam;\n';
@ -470,7 +486,19 @@ export const debugAudiograph = async (argOptions = {}) => {
StereoPannerNode, StereoPannerNode,
WaveShaperNode, WaveShaperNode,
]; ];
audioNodes.map((n) => audioNodeHook(n)); audioNodes.map((n) => {
if (n.prototype instanceof AudioScheduledSourceNode) {
const stopOrig = n.prototype.stop;
n.prototype.stop = function (...args) {
// stop called
const result = stopOrig.call(this, ...args);
const s = cache.get(this.audioid);
s.stopCount++;
return result;
};
}
audioNodeHook(n);
});
// patch BaseAudioContext factory methods // patch BaseAudioContext factory methods
// to capture the source reference // to capture the source reference