feat(image-pipeline): add backend capability and fallback diagnostics
This commit is contained in:
@@ -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);
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user