perf(lab): retain spatial scenes during replay
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type {
|
||||
RecordedEvidenceSemanticClass,
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
type E47SemanticSlamResult,
|
||||
} from "../../core/laboratory/e47SemanticSlam";
|
||||
import {
|
||||
fetchM49TgsFullShadowSpatialChunk,
|
||||
fetchM49TgsFullShadowPlaybackPack,
|
||||
type M49TgsFullShadowResult,
|
||||
type M49TgsFullShadowPlaybackPack,
|
||||
type M49TgsFullShadowPlaybackProgress,
|
||||
type M49TgsFullShadowSpatial,
|
||||
type M49TgsFullShadowSpatialChunk,
|
||||
type M49TgsFullShadowStateCode,
|
||||
} from "../../core/laboratory/m49TgsFullShadow";
|
||||
import {
|
||||
@@ -32,9 +33,6 @@ const PALETTE: readonly RecordedEvidenceSemanticPaletteEntry[] = [
|
||||
{ classId: 3, color: { kind: "token", token: "--nodedc-warning-rgb" } },
|
||||
];
|
||||
|
||||
const CHUNK_FRAMES = 24;
|
||||
const RETAINED_CHUNKS = 4;
|
||||
|
||||
function cellState(code: M49TgsFullShadowStateCode): M4ReplayClassifiedSpatialFrame["cellsMapGravityLocal"][number]["state"] {
|
||||
if (code === 1) return "ground-support";
|
||||
if (code === 2) return "nonground-occupied";
|
||||
@@ -52,16 +50,14 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
const [activeSequence, setActiveSequence] = useState<number | null>(null);
|
||||
const [semantic, setSemantic] = useState<E47SemanticSlamResult | null>(null);
|
||||
const [semanticError, setSemanticError] = useState<string | null>(null);
|
||||
const [chunks, setChunks] = useState<ReadonlyMap<number, M49TgsFullShadowSpatialChunk>>(
|
||||
() => new Map(),
|
||||
);
|
||||
const [playbackPack, setPlaybackPack] = useState<M49TgsFullShadowPlaybackPack | null>(null);
|
||||
const [playbackProgress, setPlaybackProgress] = useState<M49TgsFullShadowPlaybackProgress>({
|
||||
phase: "manifest",
|
||||
trackId: null,
|
||||
loadedBytes: 0,
|
||||
totalBytes: 0,
|
||||
});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inFlightRef = useRef(new Map<number, AbortController>());
|
||||
const activeChunkStart = activeSequence === null
|
||||
? null
|
||||
: Math.floor(activeSequence / CHUNK_FRAMES) * CHUNK_FRAMES;
|
||||
const activeChunkStartRef = useRef(activeChunkStart);
|
||||
activeChunkStartRef.current = activeChunkStart;
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
@@ -85,65 +81,64 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
}, [result.source.linkedSemanticResultId, result.source.linkedVisualResultId]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||
inFlightRef.current.clear();
|
||||
setChunks(new Map());
|
||||
const controller = new AbortController();
|
||||
setPlaybackPack(null);
|
||||
setPlaybackProgress({ phase: "manifest", trackId: null, loadedBytes: 0, totalBytes: 0 });
|
||||
setError(null);
|
||||
return () => {
|
||||
for (const controller of inFlightRef.current.values()) controller.abort();
|
||||
inFlightRef.current.clear();
|
||||
};
|
||||
void fetchM49TgsFullShadowPlaybackPack(result.resultId, {
|
||||
signal: controller.signal,
|
||||
onProgress: setPlaybackProgress,
|
||||
}).then((pack) => {
|
||||
if (!controller.signal.aborted) setPlaybackPack(pack);
|
||||
}).catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) setError(message(caught));
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [result.resultId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeChunkStart === null) return;
|
||||
const desiredStarts = [activeChunkStart, activeChunkStart + CHUNK_FRAMES]
|
||||
.filter((start) => start < result.timeline.frameCount);
|
||||
const desired = new Set(desiredStarts);
|
||||
for (const [start, controller] of inFlightRef.current) {
|
||||
if (desired.has(start)) continue;
|
||||
controller.abort();
|
||||
inFlightRef.current.delete(start);
|
||||
}
|
||||
for (const start of desiredStarts) {
|
||||
if (chunks.has(start) || inFlightRef.current.has(start)) continue;
|
||||
const controller = new AbortController();
|
||||
inFlightRef.current.set(start, controller);
|
||||
void fetchM49TgsFullShadowSpatialChunk(result.resultId, start, CHUNK_FRAMES, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setChunks((current) => {
|
||||
const next = new Map(current);
|
||||
next.set(start, chunk);
|
||||
const center = activeChunkStartRef.current ?? start;
|
||||
const retained = [...next.keys()]
|
||||
.sort((left, right) => Math.abs(left - center) - Math.abs(right - center))
|
||||
.slice(0, RETAINED_CHUNKS);
|
||||
return new Map(retained.map((key) => [key, next.get(key)!]));
|
||||
});
|
||||
if (start === activeChunkStartRef.current) setError(null);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted && start === activeChunkStartRef.current) {
|
||||
setError(message(caught));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (inFlightRef.current.get(start) === controller) inFlightRef.current.delete(start);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [activeChunkStart, chunks, result.resultId, result.timeline.frameCount]);
|
||||
|
||||
const spatial = useMemo<M49TgsFullShadowSpatial | null>(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
return chunks.get(activeChunkStart)?.frames.find(
|
||||
(frame) => frame.sourceSequence === activeSequence,
|
||||
) ?? null;
|
||||
}, [activeChunkStart, activeSequence, chunks]);
|
||||
if (activeSequence === null || !playbackPack) return null;
|
||||
const frame = playbackPack.frames[activeSequence];
|
||||
if (!frame) return null;
|
||||
const cellOffset = activeSequence * playbackPack.cellCount;
|
||||
const zOffset = cellOffset * 2;
|
||||
const states = Array.from(
|
||||
playbackPack.states.subarray(cellOffset, cellOffset + playbackPack.cellCount),
|
||||
(value) => value as M49TgsFullShadowStateCode,
|
||||
);
|
||||
const zBoundsM = Array.from({ length: playbackPack.cellCount }, (_, index) => {
|
||||
const minimum = playbackPack.zBoundsM[zOffset + index * 2]!;
|
||||
const maximum = playbackPack.zBoundsM[zOffset + index * 2 + 1]!;
|
||||
return [
|
||||
Number.isFinite(minimum) ? minimum : null,
|
||||
Number.isFinite(maximum) ? maximum : null,
|
||||
] as const;
|
||||
});
|
||||
return {
|
||||
resultId: playbackPack.resultId,
|
||||
sourceSequence: activeSequence,
|
||||
sourceFrameIndex: frame.sourceFrameIndex,
|
||||
sessionSeconds: frame.sessionSeconds,
|
||||
sampleAvailable: frame.sampleAvailable,
|
||||
costmap: {
|
||||
cellSizeM: result.configuration.cellSizeM,
|
||||
radiusM: result.configuration.radiusM,
|
||||
centersXyM: playbackPack.centersXyM,
|
||||
states,
|
||||
zBoundsM,
|
||||
},
|
||||
metrics: frame.metrics,
|
||||
};
|
||||
}, [activeSequence, playbackPack, result.configuration.cellSizeM, result.configuration.radiusM]);
|
||||
const loading = activeSequence !== null && !spatial && !error;
|
||||
const progressPercent = playbackProgress.totalBytes > 0
|
||||
? Math.min(100, Math.round((playbackProgress.loadedBytes / playbackProgress.totalBytes) * 100))
|
||||
: 0;
|
||||
const loadingLabel = playbackProgress.phase === "manifest"
|
||||
? "Проверяем playback manifest"
|
||||
: playbackProgress.phase === "ready"
|
||||
? "TGS playback готов"
|
||||
: `Подготавливаем TGS playback · ${progressPercent}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`;
|
||||
|
||||
const classifiedFrame = useMemo<M4ReplayClassifiedSpatialFrame | null>(() => {
|
||||
if (!spatial) return null;
|
||||
@@ -188,6 +183,7 @@ export function M49TgsFullShadowEvidence({ result }: { result: M49TgsFullShadowR
|
||||
expectedAtSequence: true,
|
||||
frame: classifiedFrame,
|
||||
loading,
|
||||
loadingLabel,
|
||||
error,
|
||||
replacePointCloud: false,
|
||||
}}
|
||||
|
||||
@@ -130,6 +130,7 @@ export interface M4ReplayClassifiedSpatialLayer {
|
||||
expectedAtSequence: boolean;
|
||||
frame: M4ReplayClassifiedSpatialFrame | null;
|
||||
loading: boolean;
|
||||
loadingLabel?: string;
|
||||
error: string | null;
|
||||
replacePointCloud?: boolean;
|
||||
}
|
||||
@@ -850,7 +851,9 @@ export function M4ReplayThreatVisual({
|
||||
: activeSpatialFrame
|
||||
? "map-gravity-local · all eligible points accounted · causal rolling 1 s"
|
||||
: "TGS рассчитан · linked source cloud недоступен для этого кадра"
|
||||
: classifiedSpatialLayer.error ?? `Открываем ${classifiedSpatialLayer.label}`
|
||||
: classifiedSpatialLayer.error
|
||||
?? classifiedSpatialLayer.loadingLabel
|
||||
?? `Открываем ${classifiedSpatialLayer.label}`
|
||||
: (
|
||||
<>
|
||||
{spatialFrame
|
||||
@@ -1022,7 +1025,7 @@ export function M4ReplayThreatVisual({
|
||||
: <Icon name="alert" size={18} />}
|
||||
<span>{classifiedSpatialLayer.error
|
||||
?? (classifiedSpatialLayer.loading || displayingBufferedFrame
|
||||
? `Открываем ${classifiedSpatialLayer.label}`
|
||||
? classifiedSpatialLayer.loadingLabel ?? `Открываем ${classifiedSpatialLayer.label}`
|
||||
: classifiedSpatialLayer.expectedAtSequence
|
||||
? `Открываем ${classifiedSpatialLayer.label}`
|
||||
: `${classifiedSpatialLayer.label} рассчитан только на 10 контрольных кадров.`)}</span>
|
||||
@@ -1071,7 +1074,7 @@ export function M4ReplayThreatVisual({
|
||||
{timelineFrame.loading || displayingBufferedFrame ? (
|
||||
<div className="m4-replay-threat-visual__buffering" role="status">
|
||||
<span className="busy-indicator" aria-hidden="true" />
|
||||
<span>Догружаем следующий spatial-буфер без сброса сцены</span>
|
||||
<span>{timelineFrame.loadingLabel ?? "Догружаем следующий spatial-буфер без сброса сцены"}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{timelineFrame.error ? (
|
||||
|
||||
@@ -2,10 +2,14 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
fetchM4ThreatCameraPointOverlay,
|
||||
fetchM4ThreatPlaybackPointPack,
|
||||
fetchM4ThreatTimeline,
|
||||
fetchM4ThreatTimelineChunk,
|
||||
M4_THREAT_TIMELINE_ENDPOINT_ROOT,
|
||||
selectM4ThreatTimelineSequence,
|
||||
type M4ThreatCameraPointOverlay,
|
||||
type M4ThreatPlaybackPointPack,
|
||||
type M4ThreatPlaybackProgress,
|
||||
type M4ThreatTimeline,
|
||||
type M4ThreatTimelineChunk,
|
||||
type M4ThreatTimelineFrame,
|
||||
@@ -82,11 +86,48 @@ export function useM4ThreatTimelineFrame({
|
||||
() => new Map(),
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pointPack, setPointPack] = useState<M4ThreatPlaybackPointPack | null>(null);
|
||||
const [playbackProgress, setPlaybackProgress] = useState<M4ThreatPlaybackProgress>({
|
||||
phase: "manifest",
|
||||
loadedBytes: 0,
|
||||
totalBytes: 0,
|
||||
});
|
||||
const [playbackError, setPlaybackError] = useState<string | null>(null);
|
||||
const binaryPlayback = endpointRoot === undefined
|
||||
|| endpointRoot === M4_THREAT_TIMELINE_ENDPOINT_ROOT;
|
||||
const inFlight = useRef(new Map<number, AbortController>());
|
||||
const chunksRef = useRef(chunks);
|
||||
const activeChunkStartRef = useRef<number | null>(null);
|
||||
chunksRef.current = chunks;
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setPointPack(null);
|
||||
setPlaybackError(null);
|
||||
setPlaybackProgress({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
|
||||
if (!timeline) return () => controller.abort();
|
||||
if (!binaryPlayback) {
|
||||
setPlaybackProgress({ phase: "ready", loadedBytes: 0, totalBytes: 0 });
|
||||
return () => controller.abort();
|
||||
}
|
||||
void fetchM4ThreatPlaybackPointPack(resultId, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
onProgress: (progress) => {
|
||||
if (!controller.signal.aborted) setPlaybackProgress(progress);
|
||||
},
|
||||
})
|
||||
.then((pack) => {
|
||||
if (!controller.signal.aborted) setPointPack(pack);
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
setPlaybackError(errorMessage(caught, "Бинарный spatial playback M4.6 недоступен."));
|
||||
}
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [binaryPlayback, endpointRoot, resultId, timeline]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const controller of inFlight.current.values()) controller.abort();
|
||||
inFlight.current.clear();
|
||||
@@ -116,7 +157,7 @@ export function useM4ThreatTimelineFrame({
|
||||
activeChunkStartRef.current = activeChunkStart;
|
||||
|
||||
useEffect(() => {
|
||||
if (!timeline || activeChunkStart === null) return;
|
||||
if (!timeline || activeChunkStart === null || (binaryPlayback && !pointPack)) return;
|
||||
const starts = m4ThreatChunkWindowStarts(
|
||||
activeChunkStart,
|
||||
chunkSize,
|
||||
@@ -131,6 +172,7 @@ export function useM4ThreatTimelineFrame({
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
|
||||
playbackPointPack: binaryPlayback ? pointPack ?? undefined : undefined,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
@@ -161,7 +203,7 @@ export function useM4ThreatTimelineFrame({
|
||||
// loaded first, then the next chunk is prefetched on the following render.
|
||||
break;
|
||||
}
|
||||
}, [activeChunkStart, chunkSize, chunks, endpointRoot, resultId, timeline]);
|
||||
}, [activeChunkStart, binaryPlayback, chunkSize, chunks, endpointRoot, pointPack, resultId, timeline]);
|
||||
|
||||
const activeFrame: M4ThreatTimelineFrame | null = useMemo(() => {
|
||||
if (activeSequence === null || activeChunkStart === null) return null;
|
||||
@@ -181,8 +223,17 @@ export function useM4ThreatTimelineFrame({
|
||||
activeSequence,
|
||||
activeFrame,
|
||||
availableFrames,
|
||||
loading: error === null && Boolean(timeline) && !activeFrame,
|
||||
error,
|
||||
loading: error === null && playbackError === null && Boolean(timeline) && !activeFrame,
|
||||
loadingLabel: !binaryPlayback
|
||||
? "Догружаем следующий spatial-буфер без сброса сцены"
|
||||
: playbackProgress.phase === "manifest"
|
||||
? "Проверяем M4 playback manifest"
|
||||
: playbackProgress.phase === "ready"
|
||||
? "Подготавливаем текущий spatial-кадр"
|
||||
: `${playbackProgress.phase === "verify" ? "Проверяем" : "Загружаем"} M4 spatial playback · ${playbackProgress.totalBytes > 0
|
||||
? `${Math.min(100, Math.round(playbackProgress.loadedBytes / playbackProgress.totalBytes * 100))}% · ${(playbackProgress.loadedBytes / 1_048_576).toFixed(1)}/${(playbackProgress.totalBytes / 1_048_576).toFixed(1)} МБ`
|
||||
: "0%"}`,
|
||||
error: playbackError ?? error,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user