perf: harden realtime localization pipeline

This commit is contained in:
Codex
2026-08-28 18:58:48 +09:00
parent 35c33e360e
commit 256558e601
13 changed files with 765 additions and 84 deletions
+125 -28
View File
@@ -1,6 +1,11 @@
import { dbToLinear, createScanPoints, distance3, findPeak, scanIndexToPosition, TAU } from "./math.js";
import { interpolateFrequencyResponse } from "./frequency-response.js";
import { binsForBand, extractStftBins, getFrameCount } from "./stft.js";
import {
interpolateFrequencyResponse,
inverseResponseGainDb,
requireMaxCompensationDb,
} from "./frequency-response.js";
import { cooperativeYield } from "./cooperative-yield.js";
import { binsForBand, createStftWorkspace, extractStftBins, getFrameCount } from "./stft.js";
function throwIfAborted(signal) {
if (signal?.aborted) {
@@ -10,10 +15,6 @@ function throwIfAborted(signal) {
}
}
function yieldToEventLoop() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
export const DEFAULT_MAXIMUM_CPU_STEERING_CACHE_BYTES = 64 * 1024 * 1024;
export const DEFAULT_MAXIMUM_CPU_ANALYSIS_PLAN_BYTES = 512 * 1024 * 1024;
const DAS_ANALYSIS_PLAN_KIND = "micarrsim.das-analysis-plan/1";
@@ -47,12 +48,20 @@ function createBandIndexLookup(bandBins) {
return lookup;
}
function createBinCalibration(arrayDefinition, bin, sampleRate, fftSize, compensateFrequencyResponse) {
function createBinCalibration(
arrayDefinition,
bin,
sampleRate,
fftSize,
compensateFrequencyResponse,
maximumCompensationDb,
) {
const microphoneCount = arrayDefinition.microphones.length;
const gains = new Float64Array(microphoneCount);
const phases = new Float64Array(microphoneCount);
const frequencyHz = (bin * sampleRate) / fftSize;
const response = {};
const maxCompensationDb = requireMaxCompensationDb(maximumCompensationDb);
for (let microphoneIndex = 0; microphoneIndex < microphoneCount; microphoneIndex += 1) {
const microphone = arrayDefinition.microphones[microphoneIndex];
let responseGainDb = 0;
@@ -62,8 +71,11 @@ function createBinCalibration(arrayDefinition, bin, sampleRate, fftSize, compens
responseGainDb = response.gainDb;
responsePhaseDeg = response.phaseDeg;
}
const responseCompensationDb = compensateFrequencyResponse
? inverseResponseGainDb(responseGainDb, maxCompensationDb)
: 0;
gains[microphoneIndex] = dbToLinear(
-(microphone.sensitivityDb ?? 0) - responseGainDb,
-(microphone.sensitivityDb ?? 0) + responseCompensationDb,
);
phases[microphoneIndex] =
((microphone.phaseOffsetDeg ?? 0) + responsePhaseDeg) * Math.PI / 180;
@@ -119,7 +131,9 @@ async function createSteeringMatrix(options) {
outputImaginary: imaginary,
outputOffset: offset,
});
if ((pointIndex + 1) % pointBatchSize === 0) await yieldToEventLoop();
if ((pointIndex + 1) % pointBatchSize === 0 && pointIndex + 1 < pointCount) {
await cooperativeYield();
}
}
return { real, imaginary };
}
@@ -136,6 +150,7 @@ export async function computeBinMap(options) {
fftSize,
speedOfSound,
compensateFrequencyResponse = true,
maxCompensationDb,
calibration: providedCalibration,
steeringMatrix,
output: providedOutput,
@@ -156,6 +171,7 @@ export async function computeBinMap(options) {
sampleRate,
fftSize,
compensateFrequencyResponse,
maxCompensationDb,
);
const phaseRealValues = steeringMatrix?.real ?? steeringReal;
const phaseImaginaryValues = steeringMatrix?.imaginary ?? steeringImaginary;
@@ -212,7 +228,9 @@ export async function computeBinMap(options) {
accumulatedPower += (sumReal * sumReal + sumImaginary * sumImaginary) * microphoneScale * microphoneScale;
}
output[pointIndex] = accumulatedPower / frameCount;
if ((pointIndex + 1) % pointBatchSize === 0) await yieldToEventLoop();
if ((pointIndex + 1) % pointBatchSize === 0 && pointIndex + 1 < pointCount) {
await cooperativeYield();
}
}
return output;
}
@@ -255,32 +273,45 @@ export function createDasAnalysisPlan(options) {
const pointCount = experiment.scan.columns * experiment.scan.rows;
const pointMicrophonePairs = pointCount * microphoneCount;
const distanceMatrixBytes = pointMicrophonePairs * Float32Array.BYTES_PER_ELEMENT;
const stftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 3;
const calibrationBytes =
selectedBins.length * microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
const retainedBaseBytes =
distanceMatrixBytes +
pointCount * Float32Array.BYTES_PER_ELEMENT +
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2 +
stftWorkspaceBytes;
const retainedNonSteeringBytes = retainedBaseBytes + calibrationBytes;
if (
!Number.isSafeInteger(pointMicrophonePairs) ||
!Number.isSafeInteger(retainedBaseBytes) ||
retainedBaseBytes > maximumPlanBytes
!Number.isSafeInteger(calibrationBytes) ||
!Number.isSafeInteger(retainedNonSteeringBytes) ||
retainedNonSteeringBytes > maximumPlanBytes
) {
throw new RangeError(
"CPU analysis plan requires " + retainedBaseBytes.toLocaleString() +
" base bytes, above maximumPlanBytes " + maximumPlanBytes.toLocaleString() + ".",
"CPU analysis plan requires " + retainedNonSteeringBytes.toLocaleString() +
" non-steering bytes including " + calibrationBytes.toLocaleString() +
" calibration bytes, above maximumPlanBytes " +
maximumPlanBytes.toLocaleString() + ".",
);
}
const distances = createDistanceMatrix(arrayDefinition, experiment.scan);
const binMap = new Float32Array(pointCount);
const steeringReal = new Float64Array(microphoneCount);
const steeringImaginary = new Float64Array(microphoneCount);
const stftWorkspace = createStftWorkspace(fftSize);
const steeringBytesPerBin =
pointCount * microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
const requestedSteeringCacheBytes = steeringBytesPerBin * selectedBins.length;
const steeringCacheEnabled =
Number.isSafeInteger(requestedSteeringCacheBytes) &&
requestedSteeringCacheBytes > 0 &&
requestedSteeringCacheBytes <= maximumSteeringCacheBytes &&
retainedBaseBytes + requestedSteeringCacheBytes <= maximumPlanBytes;
const steeringCacheBinCapacity = Math.min(
selectedBins.length,
Math.floor(maximumSteeringCacheBytes / steeringBytesPerBin),
Math.floor((maximumPlanBytes - retainedNonSteeringBytes) / steeringBytesPerBin),
);
const steeringCacheBytes = steeringCacheBinCapacity * steeringBytesPerBin;
const steeringCacheEnabled = steeringCacheBinCapacity > 0;
const steeringCachedBins = new Set(selectedBins.slice(0, steeringCacheBinCapacity));
return {
kind: DAS_ANALYSIS_PLAN_KIND,
@@ -297,13 +328,20 @@ export function createDasAnalysisPlan(options) {
calibrations: new Map(),
steeringMatrices: new Map(),
steeringCacheEnabled,
steeringCacheBinCapacity,
steeringCachedBins,
steeringBytesPerBin,
steeringCacheBytes,
requestedSteeringCacheBytes,
retainedBaseBytes,
calibrationBytes,
retainedNonSteeringBytes,
maximumSteeringCacheBytes,
maximumPlanBytes,
binMap,
steeringReal,
steeringImaginary,
stftWorkspace,
busy: false,
statistics: {
analysisAttempts: 0,
@@ -315,8 +353,20 @@ export function createDasAnalysisPlan(options) {
calibrationCacheHits: 0,
steeringMatrixBuilds: 0,
steeringCacheHits: 0,
uncachedSteeringUses: 0,
steeringPointMicrophonePairs: 0,
uncachedSteeringPointMicrophonePairs: 0,
avoidedSteeringPointMicrophonePairs: 0,
stftWindowBuilds: 1,
stftWorkspaceAllocations: 1,
stftWorkspaceUses: 0,
avoidedStftWindowBuilds: 0,
avoidedStftWorkspaceAllocations: 0,
stftTransforms: 0,
fftButterflies: 0,
beamformingComplexMacs: 0,
binMapAllocations: 1,
steeringWorkspaceAllocations: 1,
binMapUses: 0,
avoidedBinMapAllocations: 0,
retainedBytes: retainedBaseBytes,
@@ -350,6 +400,7 @@ function planCalibration(plan, bin) {
plan.sampleRate,
plan.experiment.stft.fftSize,
plan.experiment.beamforming.compensateFrequencyResponse !== false,
plan.experiment.beamforming.maxCompensationDb,
);
plan.calibrations.set(bin, calibration);
plan.statistics.calibrationBuilds += 1;
@@ -358,7 +409,12 @@ function planCalibration(plan, bin) {
}
async function planSteeringMatrix(plan, bin, calibration, signal, pointBatchSize) {
if (!plan.steeringCacheEnabled) return undefined;
if (!plan.steeringCachedBins.has(bin)) {
plan.statistics.uncachedSteeringUses += 1;
plan.statistics.uncachedSteeringPointMicrophonePairs +=
plan.pointCount * plan.microphoneCount;
return undefined;
}
let matrix = plan.steeringMatrices.get(bin);
if (matrix) {
plan.statistics.steeringCacheHits += 1;
@@ -393,12 +449,31 @@ function analysisCacheSummary(plan) {
avoidedDistanceEvaluations: plan.statistics.avoidedDistanceEvaluations,
calibrationBuilds: plan.statistics.calibrationBuilds,
calibrationCacheHits: plan.statistics.calibrationCacheHits,
calibrationBytes: plan.calibrationBytes,
steeringCacheEnabled: plan.steeringCacheEnabled,
steeringCacheBinCapacity: plan.steeringCacheBinCapacity,
steeringCachedBinCount: plan.steeringMatrices.size,
steeringUncachedBinCount: plan.selectedBins.length - plan.steeringCacheBinCapacity,
steeringCacheBytes: plan.steeringCacheBytes,
requestedSteeringCacheBytes: plan.requestedSteeringCacheBytes,
steeringMatrixBuilds: plan.statistics.steeringMatrixBuilds,
steeringCacheHits: plan.statistics.steeringCacheHits,
uncachedSteeringUses: plan.statistics.uncachedSteeringUses,
steeringPointMicrophonePairs: plan.statistics.steeringPointMicrophonePairs,
uncachedSteeringPointMicrophonePairs:
plan.statistics.uncachedSteeringPointMicrophonePairs,
avoidedSteeringPointMicrophonePairs:
plan.statistics.avoidedSteeringPointMicrophonePairs,
stftWindowBuilds: plan.statistics.stftWindowBuilds,
stftWorkspaceAllocations: plan.statistics.stftWorkspaceAllocations,
stftWorkspaceUses: plan.statistics.stftWorkspaceUses,
avoidedStftWindowBuilds: plan.statistics.avoidedStftWindowBuilds,
avoidedStftWorkspaceAllocations: plan.statistics.avoidedStftWorkspaceAllocations,
stftTransforms: plan.statistics.stftTransforms,
fftButterflies: plan.statistics.fftButterflies,
beamformingComplexMacs: plan.statistics.beamformingComplexMacs,
binMapAllocations: plan.statistics.binMapAllocations,
steeringWorkspaceAllocations: plan.statistics.steeringWorkspaceAllocations,
avoidedBinMapAllocations: plan.statistics.avoidedBinMapAllocations,
retainedBytes: plan.statistics.retainedBytes,
};
@@ -419,12 +494,13 @@ export function estimateDasWorkload(options) {
const outputBytes = bandCount * pointCount * Float32Array.BYTES_PER_ELEMENT * 2;
const binMapBytes = pointCount * Float32Array.BYTES_PER_ELEMENT;
const spectrumBytesPerBin = frameCount * microphoneCount * Float32Array.BYTES_PER_ELEMENT * 2;
const fftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 2;
const steeringCacheBytes = analysisPlan?.steeringCacheEnabled
? analysisPlan.requestedSteeringCacheBytes
: 0;
const fftWorkspaceBytes = fftSize * Float64Array.BYTES_PER_ELEMENT * 3;
const steeringCacheBytes = analysisPlan?.steeringCacheBytes ?? 0;
const calibrationBytes = analysisPlan?.calibrationBytes ??
microphoneCount * Float64Array.BYTES_PER_ELEMENT * 2;
const baseWorkingBytes =
distanceBytes + outputBytes + binMapBytes + fftWorkspaceBytes + steeringCacheBytes;
distanceBytes + outputBytes + binMapBytes + fftWorkspaceBytes +
steeringCacheBytes + calibrationBytes;
const complexMacCount = pointCount * microphoneCount * frameCount * selectedBins.length;
return {
bandBins,
@@ -434,7 +510,10 @@ export function estimateDasWorkload(options) {
pointCount,
distanceBytes,
outputBytes,
fftWorkspaceBytes,
steeringCacheBytes,
calibrationBytes,
steeringCacheBinCapacity: analysisPlan?.steeringCacheBinCapacity ?? 0,
spectrumBytesPerBin,
baseWorkingBytes,
complexMacCount,
@@ -537,16 +616,21 @@ async function computeDasMapInternal(options, analysisPlan) {
let completedBins = 0;
for (let batchStart = 0; batchStart < workload.selectedBins.length; batchStart += batchSize) {
throwIfAborted(signal);
await yieldToEventLoop();
throwIfAborted(signal);
const batchBins = workload.selectedBins.slice(batchStart, batchStart + batchSize);
if (analysisPlan) {
analysisPlan.statistics.stftWorkspaceUses += 1;
const repeatedUses = Math.max(0, analysisPlan.statistics.stftWorkspaceUses - 1);
analysisPlan.statistics.avoidedStftWindowBuilds = repeatedUses;
analysisPlan.statistics.avoidedStftWorkspaceAllocations = repeatedUses;
}
const stft = await extractStftBins(channels, {
fftSize: experiment.stft.fftSize,
hopSize: experiment.stft.hopSize,
bins: batchBins,
signal,
transformBatchSize: stftTransformBatchSize,
workspace: analysisPlan?.stftWorkspace,
onProgress: ({ completedTransforms, totalTransforms }) =>
onProgress?.({
stage: "stft",
@@ -558,7 +642,15 @@ async function computeDasMapInternal(options, analysisPlan) {
spectrumBatchCount,
}),
});
await yieldToEventLoop();
if (analysisPlan) {
const completedTransformCount = stft.frameCount * stft.microphoneCount;
analysisPlan.statistics.stftTransforms += completedTransformCount;
analysisPlan.statistics.fftButterflies +=
completedTransformCount *
(experiment.stft.fftSize / 2) *
Math.log2(experiment.stft.fftSize);
}
await cooperativeYield();
throwIfAborted(signal);
for (const bin of batchBins) {
@@ -585,6 +677,7 @@ async function computeDasMapInternal(options, analysisPlan) {
speedOfSound: scene.speedOfSound,
compensateFrequencyResponse:
experiment.beamforming.compensateFrequencyResponse !== false,
maxCompensationDb: experiment.beamforming.maxCompensationDb,
calibration,
steeringMatrix,
output: analysisPlan?.binMap,
@@ -593,6 +686,10 @@ async function computeDasMapInternal(options, analysisPlan) {
signal,
pointBatchSize,
});
if (analysisPlan) {
analysisPlan.statistics.beamformingComplexMacs +=
workload.pointCount * workload.microphoneCount * stft.frameCount;
}
for (const bandIndex of bandIndexesByBin.get(bin)) {
const rawMap = rawMaps[bandIndex];
for (let pointIndex = 0; pointIndex < workload.pointCount; pointIndex += 1) {