feat(image-pipeline): add backend capability and fallback diagnostics

This commit is contained in:
Matthias
2026-04-04 21:17:32 +02:00
parent a6bec59866
commit 8fb5482550
4 changed files with 535 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ import { applyPipelineStep, applyPipelineSteps } from "@/lib/image-pipeline/rend
import {
CPU_BACKEND_ID,
type BackendExecutionOptions,
type BackendHint,
type BackendRouter,
type FullBackendRequest,
@@ -9,6 +10,20 @@ import {
type PreviewBackendRequest,
} from "@/lib/image-pipeline/backend/backend-types";
type BackendFallbackReason = "unsupported_api" | "flag_disabled" | "runtime_error";
type BackendFallbackEvent = {
reason: BackendFallbackReason;
requestedBackend: string;
fallbackBackend: string;
error?: Error;
};
type BackendAvailability = {
supported?: boolean;
enabled?: boolean;
};
const cpuBackend: ImagePipelineBackend = {
id: CPU_BACKEND_ID,
runPreviewStep(request) {
@@ -43,6 +58,8 @@ function normalizeBackendHint(value: BackendHint): string | null {
export function createBackendRouter(options?: {
backends?: readonly ImagePipelineBackend[];
defaultBackendId?: string;
backendAvailability?: Readonly<Record<string, BackendAvailability>>;
onFallback?: (event: BackendFallbackEvent) => void;
}): BackendRouter {
const configuredBackends = options?.backends?.length ? [...options.backends] : [cpuBackend];
const byId = new Map(configuredBackends.map((backend) => [backend.id.toLowerCase(), backend]));
@@ -51,23 +68,124 @@ export function createBackendRouter(options?: {
byId.get(CPU_BACKEND_ID) ??
configuredBackends[0] ??
cpuBackend;
const normalizedDefaultId = defaultBackend.id.toLowerCase();
function readAvailability(backendId: string): BackendAvailability | undefined {
return options?.backendAvailability?.[backendId.toLowerCase()];
}
function emitFallback(event: BackendFallbackEvent): void {
options?.onFallback?.(event);
}
function resolveBackendWithFallbackReason(backendHint: BackendHint): {
backend: ImagePipelineBackend;
fallbackReason: BackendFallbackReason | null;
requestedBackend: string | null;
} {
const normalizedHint = normalizeBackendHint(backendHint);
if (!normalizedHint) {
return {
backend: defaultBackend,
fallbackReason: null,
requestedBackend: null,
};
}
const hintedBackend = byId.get(normalizedHint);
if (!hintedBackend) {
return {
backend: defaultBackend,
fallbackReason: "unsupported_api",
requestedBackend: normalizedHint,
};
}
const availability = readAvailability(normalizedHint);
if (availability?.enabled === false) {
return {
backend: defaultBackend,
fallbackReason: "flag_disabled",
requestedBackend: normalizedHint,
};
}
if (availability?.supported === false) {
return {
backend: defaultBackend,
fallbackReason: "unsupported_api",
requestedBackend: normalizedHint,
};
}
return {
backend: hintedBackend,
fallbackReason: null,
requestedBackend: normalizedHint,
};
}
function runWithRuntimeFallback(args: {
backendHint: BackendHint;
runBackend: (backend: ImagePipelineBackend) => void;
executionOptions?: BackendExecutionOptions;
}): void {
const selection = resolveBackendWithFallbackReason(args.backendHint);
if (selection.fallbackReason && selection.requestedBackend) {
emitFallback({
reason: selection.fallbackReason,
requestedBackend: selection.requestedBackend,
fallbackBackend: defaultBackend.id,
});
}
try {
args.runBackend(selection.backend);
return;
} catch (error: unknown) {
const shouldAbort = args.executionOptions?.shouldAbort;
if (shouldAbort?.()) {
throw error;
}
if (selection.backend.id.toLowerCase() === normalizedDefaultId) {
throw error;
}
const normalizedError =
error instanceof Error ? error : new Error("Image pipeline backend execution failed.");
emitFallback({
reason: "runtime_error",
requestedBackend: selection.backend.id.toLowerCase(),
fallbackBackend: defaultBackend.id,
error: normalizedError,
});
args.runBackend(defaultBackend);
}
}
return {
resolveBackend(backendHint) {
const normalizedHint = normalizeBackendHint(backendHint);
if (!normalizedHint) {
return defaultBackend;
}
return byId.get(normalizedHint) ?? defaultBackend;
return resolveBackendWithFallbackReason(backendHint).backend;
},
runPreviewStep(request) {
const backend = this.resolveBackend(request.backendHint);
backend.runPreviewStep(request);
runWithRuntimeFallback({
backendHint: request.backendHint,
executionOptions: request.executionOptions,
runBackend: (backend) => {
backend.runPreviewStep(request);
},
});
},
runFullPipeline(request) {
const backend = this.resolveBackend(request.backendHint);
backend.runFullPipeline(request);
runWithRuntimeFallback({
backendHint: request.backendHint,
executionOptions: request.executionOptions,
runBackend: (backend) => {
backend.runFullPipeline(request);
},
});
},
};
}

View File

@@ -0,0 +1,93 @@
export type BackendCapabilities = {
webgl: boolean;
wasmSimd: boolean;
offscreenCanvas: boolean;
};
type CapabilityProbes = {
probeWebgl: () => boolean;
probeWasmSimd: () => boolean;
probeOffscreenCanvas: () => boolean;
};
const WASM_SIMD_PROBE_MODULE = new Uint8Array([
0x00,
0x61,
0x73,
0x6d,
0x01,
0x00,
0x00,
0x00,
0x01,
0x05,
0x01,
0x60,
0x00,
0x01,
0x7b,
0x03,
0x02,
0x01,
0x00,
0x0a,
0x0a,
0x01,
0x08,
0x00,
0x41,
0x00,
0xfd,
0x0f,
0x0b,
]);
function probeOffscreenCanvasAvailability(): boolean {
return typeof OffscreenCanvas !== "undefined";
}
function probeWebglAvailability(): boolean {
try {
if (typeof document !== "undefined") {
const canvas = document.createElement("canvas");
const context = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
if (context) {
return true;
}
}
if (typeof OffscreenCanvas !== "undefined") {
const offscreenCanvas = new OffscreenCanvas(1, 1);
const context = offscreenCanvas.getContext("webgl2") ?? offscreenCanvas.getContext("webgl");
return Boolean(context);
}
return false;
} catch {
return false;
}
}
function probeWasmSimdAvailability(): boolean {
if (typeof WebAssembly === "undefined" || typeof WebAssembly.validate !== "function") {
return false;
}
try {
return WebAssembly.validate(WASM_SIMD_PROBE_MODULE);
} catch {
return false;
}
}
export function detectBackendCapabilities(probes?: Partial<CapabilityProbes>): BackendCapabilities {
const probeWebgl = probes?.probeWebgl ?? probeWebglAvailability;
const probeWasmSimd = probes?.probeWasmSimd ?? probeWasmSimdAvailability;
const probeOffscreenCanvas = probes?.probeOffscreenCanvas ?? probeOffscreenCanvasAvailability;
return {
webgl: probeWebgl(),
wasmSimd: probeWasmSimd(),
offscreenCanvas: probeOffscreenCanvas(),
};
}