fix(image-pipeline): diagnose and stabilize webgl preview path

This commit is contained in:
2026-04-05 11:28:42 +02:00
parent 186a5b9f92
commit 451ab0b986
11 changed files with 401 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useMemo } from "react";
import { useEffect, useMemo, useRef } from "react";
import { useCanvasGraph } from "@/components/canvas/canvas-graph-context";
import { usePipelinePreview } from "@/hooks/use-pipeline-preview";
@@ -18,6 +18,30 @@ const PREVIEW_PIPELINE_TYPES = new Set([
"detail-adjust",
]);
type PreviewLatencyTrace = {
sequence: number;
changedAtMs: number;
nodeType: string;
origin: string;
};
function readPreviewLatencyTrace(): PreviewLatencyTrace | null {
if (process.env.NODE_ENV === "production") {
return null;
}
const debugGlobals = globalThis as typeof globalThis & {
__LEMONSPACE_DEBUG_PREVIEW_LATENCY__?: boolean;
__LEMONSPACE_LAST_PREVIEW_TRACE__?: PreviewLatencyTrace;
};
if (debugGlobals.__LEMONSPACE_DEBUG_PREVIEW_LATENCY__ !== true) {
return null;
}
return debugGlobals.__LEMONSPACE_LAST_PREVIEW_TRACE__ ?? null;
}
export default function AdjustmentPreview({
nodeId,
nodeWidth,
@@ -30,6 +54,7 @@ export default function AdjustmentPreview({
currentParams: unknown;
}) {
const graph = useCanvasGraph();
const lastLoggedTraceSequenceRef = useRef<number | null>(null);
const sourceUrl = useMemo(
() =>
@@ -68,6 +93,30 @@ export default function AdjustmentPreview({
});
}, [currentParams, currentType, graph, nodeId]);
useEffect(() => {
const trace = readPreviewLatencyTrace();
if (!trace) {
return;
}
if (lastLoggedTraceSequenceRef.current === trace.sequence) {
return;
}
lastLoggedTraceSequenceRef.current = trace.sequence;
console.info("[Preview latency] downstream-graph-visible", {
nodeId,
nodeType: currentType,
sourceNodeType: trace.nodeType,
sourceOrigin: trace.origin,
sinceChangeMs: performance.now() - trace.changedAtMs,
pipelineDepth: steps.length,
stepTypes: steps.map((step) => step.type),
hasSource: Boolean(sourceUrl),
});
}, [currentType, nodeId, sourceUrl, steps]);
const { canvasRef, histogram, isRendering, hasSource, previewAspectRatio, error } =
usePipelinePreview({
sourceUrl,

View File

@@ -21,6 +21,37 @@ function logNodeDataDebug(event: string, payload: Record<string, unknown>): void
console.info("[Canvas node debug]", event, payload);
}
type PreviewLatencyTrace = {
sequence: number;
changedAtMs: number;
nodeType: string;
origin: "applyLocalData" | "updateLocalData";
};
function writePreviewLatencyTrace(trace: Omit<PreviewLatencyTrace, "sequence">): void {
if (process.env.NODE_ENV === "production") {
return;
}
const debugGlobals = globalThis as typeof globalThis & {
__LEMONSPACE_DEBUG_PREVIEW_LATENCY__?: boolean;
__LEMONSPACE_LAST_PREVIEW_TRACE__?: PreviewLatencyTrace;
};
if (debugGlobals.__LEMONSPACE_DEBUG_PREVIEW_LATENCY__ !== true) {
return;
}
const nextTrace: PreviewLatencyTrace = {
...trace,
sequence: (debugGlobals.__LEMONSPACE_LAST_PREVIEW_TRACE__?.sequence ?? 0) + 1,
};
debugGlobals.__LEMONSPACE_LAST_PREVIEW_TRACE__ = nextTrace;
console.info("[Preview latency] node-local-change", nextTrace);
}
export function useNodeLocalData<T>({
data,
normalize,
@@ -78,11 +109,16 @@ export function useNodeLocalData<T>({
const applyLocalData = useCallback(
(next: T) => {
hasPendingLocalChangesRef.current = true;
writePreviewLatencyTrace({
changedAtMs: performance.now(),
nodeType: debugLabel,
origin: "applyLocalData",
});
localDataRef.current = next;
setLocalDataState(next);
queueSave();
},
[queueSave],
[debugLabel, queueSave],
);
const updateLocalData = useCallback(
@@ -90,12 +126,17 @@ export function useNodeLocalData<T>({
hasPendingLocalChangesRef.current = true;
setLocalDataState((current) => {
const next = updater(current);
writePreviewLatencyTrace({
changedAtMs: performance.now(),
nodeType: debugLabel,
origin: "updateLocalData",
});
localDataRef.current = next;
queueSave();
return next;
});
},
[queueSave],
[debugLabel, queueSave],
);
return {