Complete V1 performance and OOM release gates

This commit is contained in:
mes123456
2026-08-14 22:32:09 -04:00
parent 3ea9974eee
commit a3f3071c03
45 changed files with 4206 additions and 276 deletions

View File

@@ -0,0 +1,323 @@
import {
SequencerValidationError,
parseSequencerTimeline,
sequencerSourceFrame,
type SequencerFrameStripIR,
type SequencerStripIR,
type SequencerTimelineIR,
} from "../../../protocol/sequencer";
export const LONG_MEDIA_SCHEMA = 1 as const;
export const LONG_MEDIA_INDEX_BUCKET_FRAMES = 1_024;
export const LONG_MEDIA_MAX_INDEX_REFERENCES = 2_000_000;
export const LONG_MEDIA_MAX_CACHE_BYTES = 256 * 1024 * 1024;
export interface LongMediaIndexStatsIR {
stripCount: number;
bucketCount: number;
referenceCount: number;
estimatedBytes: number;
}
export interface LongMediaCacheStatsIR {
entries: number;
bytes: number;
maxBytes: number;
hits: number;
misses: number;
evictions: number;
keys: string[];
}
export interface LongMediaSeekResultIR {
status: "COMPLETED" | "SUPERSEDED" | "CANCELLED";
frame: number;
strips: SequencerFrameStripIR[];
previewBytes: number;
cache: LongMediaCacheStatsIR;
}
export interface LongMediaSessionAssetIR {
sourceId: string;
sha256: string;
mimeType: "image/png" | "audio/wav";
}
export interface LongMediaSessionManifestIR {
schemaVersion: typeof LONG_MEDIA_SCHEMA;
timeline: SequencerTimelineIR;
currentFrame: number;
cacheMaxBytes: number;
assets: LongMediaSessionAssetIR[];
}
export interface LongMediaPreviewSource {
load(strip: SequencerStripIR, sourceFrame: number, signal: AbortSignal): Promise<ArrayBuffer>;
}
function cancelled(signal: AbortSignal, stage: string): void {
if (signal.aborted) throw new SequencerValidationError("SEQUENCER_CANCELLED", `Long media ${stage} was cancelled`);
}
function cacheBudget(value: number): number {
if (!Number.isSafeInteger(value) || value < 1 || value > LONG_MEDIA_MAX_CACHE_BYTES) {
throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", "Long media cache byte budget is invalid");
}
return value;
}
function frameInTimeline(timeline: SequencerTimelineIR, frame: number): number {
if (!Number.isSafeInteger(frame) || frame < timeline.frameStart || frame > timeline.frameEnd) {
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Long media frame ${frame} is outside the timeline`);
}
return frame;
}
export class LongMediaTimelineIndex {
readonly timeline: SequencerTimelineIR;
readonly stats: LongMediaIndexStatsIR;
private readonly buckets: ReadonlyMap<number, readonly number[]>;
private constructor(timeline: SequencerTimelineIR, buckets: Map<number, number[]>, referenceCount: number) {
this.timeline = timeline;
this.buckets = buckets;
this.stats = {
stripCount: timeline.strips.length,
bucketCount: buckets.size,
referenceCount,
estimatedBytes: referenceCount * Uint32Array.BYTES_PER_ELEMENT + buckets.size * 16,
};
}
static async build(value: unknown, signal: AbortSignal): Promise<LongMediaTimelineIndex> {
const timeline = parseSequencerTimeline(value);
const buckets = new Map<number, number[]>();
let referenceCount = 0;
for (let index = 0; index < timeline.strips.length; index++) {
if (index % 256 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
cancelled(signal, "index build");
}
const strip = timeline.strips[index];
const first = Math.floor(strip.frameStart / LONG_MEDIA_INDEX_BUCKET_FRAMES);
const last = Math.floor((strip.frameEnd - 1) / LONG_MEDIA_INDEX_BUCKET_FRAMES);
for (let bucket = first; bucket <= last; bucket++) {
referenceCount++;
if (referenceCount > LONG_MEDIA_MAX_INDEX_REFERENCES) {
throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", "Long media index reference budget exceeded");
}
const entries = buckets.get(bucket) ?? [];
entries.push(index);
buckets.set(bucket, entries);
}
}
cancelled(signal, "index build");
return new LongMediaTimelineIndex(timeline, buckets, referenceCount);
}
resolve(frameValue: number, signal: AbortSignal): SequencerFrameStripIR[] {
const frame = frameInTimeline(this.timeline, frameValue);
cancelled(signal, "seek");
const bucket = Math.floor(frame / LONG_MEDIA_INDEX_BUCKET_FRAMES);
const candidates = this.buckets.get(bucket) ?? [];
const active = candidates
.map((index) => this.timeline.strips[index])
.filter((strip) => !strip.muted && frame >= strip.frameStart && frame < strip.frameEnd);
const activeIds = new Set(active.map((strip) => strip.id));
const hiddenByMeta = new Set(active.filter((strip) => strip.type === "META").flatMap((strip) => strip.childStripIds ?? []));
const result = active.filter((strip) => !hiddenByMeta.has(strip.id)).map((strip): SequencerFrameStripIR => {
const dependencies = [...(strip.inputStripIds ?? []), ...(strip.childStripIds ?? [])];
if (dependencies.some((id) => !activeIds.has(id))) {
throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `${strip.id} has an inactive long-media dependency`);
}
return { stripId: strip.id, channel: strip.channel, sourceFrame: sequencerSourceFrame(strip, frame), dependencyStripIds: dependencies };
});
cancelled(signal, "seek");
return result.sort((left, right) => left.channel - right.channel || left.stripId.localeCompare(right.stripId));
}
}
export class LongMediaPreviewCache {
readonly maxBytes: number;
private readonly entries = new Map<string, ArrayBuffer>();
private currentBytes = 0;
private hitCount = 0;
private missCount = 0;
private evictionCount = 0;
constructor(maxBytes: number) {
this.maxBytes = cacheBudget(maxBytes);
}
get(key: string): ArrayBuffer | undefined {
const entry = this.entries.get(key);
if (!entry) { this.missCount++; return undefined; }
this.hitCount++;
this.entries.delete(key);
this.entries.set(key, entry);
return entry.slice(0);
}
set(key: string, data: ArrayBuffer): void {
if (!(data instanceof ArrayBuffer) || data.byteLength === 0 || data.byteLength > this.maxBytes) {
throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", "Long media preview exceeds the cache byte budget");
}
const copy = data.slice(0);
const previous = this.entries.get(key);
if (previous) { this.currentBytes -= previous.byteLength; this.entries.delete(key); }
while (this.currentBytes + copy.byteLength > this.maxBytes) {
const oldest = this.entries.entries().next().value as [string, ArrayBuffer] | undefined;
if (!oldest) break;
this.entries.delete(oldest[0]);
this.currentBytes -= oldest[1].byteLength;
this.evictionCount++;
}
this.entries.set(key, copy);
this.currentBytes += copy.byteLength;
}
clear(): void {
this.entries.clear();
this.currentBytes = 0;
}
stats(): LongMediaCacheStatsIR {
return {
entries: this.entries.size,
bytes: this.currentBytes,
maxBytes: this.maxBytes,
hits: this.hitCount,
misses: this.missCount,
evictions: this.evictionCount,
keys: [...this.entries.keys()],
};
}
}
export class LongMediaTimelineSession {
private generation = 0;
private controller: AbortController | null = null;
private explicitlyCancelledGeneration: number | null = null;
readonly cache: LongMediaPreviewCache;
constructor(
private readonly index: LongMediaTimelineIndex,
private readonly source: LongMediaPreviewSource,
maxCacheBytes: number,
private readonly publish: (result: LongMediaSeekResultIR) => void = () => undefined,
) {
this.cache = new LongMediaPreviewCache(maxCacheBytes);
}
cancel(): void {
if (!this.controller) return;
this.explicitlyCancelledGeneration = this.generation;
this.controller.abort();
}
async seek(frame: number): Promise<LongMediaSeekResultIR> {
this.controller?.abort();
const generation = ++this.generation;
this.explicitlyCancelledGeneration = null;
const controller = new AbortController();
this.controller = controller;
try {
const strips = this.index.resolve(frame, controller.signal);
let previewBytes = 0;
for (const resolved of strips) {
const strip = this.index.timeline.strips.find((candidate) => candidate.id === resolved.stripId)!;
if (!strip.sourceId || !["IMAGE", "SOUND", "MOVIE"].includes(strip.type)) continue;
const key = `${strip.sourceId}:${resolved.sourceFrame}`;
let preview = this.cache.get(key);
if (!preview) {
preview = await this.source.load(strip, resolved.sourceFrame, controller.signal);
if (!this.isCurrent(generation, controller)) return this.interrupted(frame, generation);
this.cache.set(key, preview);
}
previewBytes += preview.byteLength;
}
if (!this.isCurrent(generation, controller)) return this.interrupted(frame, generation);
const result: LongMediaSeekResultIR = { status: "COMPLETED", frame, strips, previewBytes, cache: this.cache.stats() };
this.publish(result);
return result;
}
catch (error) {
if (controller.signal.aborted || error instanceof SequencerValidationError && error.code === "SEQUENCER_CANCELLED") {
return this.interrupted(frame, generation);
}
throw error;
}
finally {
if (this.generation === generation) this.controller = null;
}
}
dispose(): void {
this.cancel();
this.cache.clear();
}
private isCurrent(generation: number, controller: AbortController): boolean {
return generation === this.generation && this.controller === controller && !controller.signal.aborted;
}
private interrupted(frame: number, generation: number): LongMediaSeekResultIR {
return {
status: this.explicitlyCancelledGeneration === generation ? "CANCELLED" : "SUPERSEDED",
frame,
strips: [],
previewBytes: 0,
cache: this.cache.stats(),
};
}
}
const SHA256 = /^[a-f0-9]{64}$/;
export function parseLongMediaSessionManifest(value: unknown): LongMediaSessionManifestIR {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Long media session manifest must be an object");
const input = value as Record<string, unknown>;
if (input.schemaVersion !== LONG_MEDIA_SCHEMA || !Array.isArray(input.assets)) throw new SequencerValidationError("PROTOCOL_MISMATCH", "Unsupported long media session schema");
const timeline = parseSequencerTimeline(input.timeline);
const currentFrame = frameInTimeline(timeline, input.currentFrame as number);
const cacheMaxBytes = cacheBudget(input.cacheMaxBytes as number);
const sourceIds = new Set<string>();
const assets = input.assets.map((value, index): LongMediaSessionAssetIR => {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Long media asset ${index} is invalid`);
const asset = value as Record<string, unknown>;
if (typeof asset.sourceId !== "string" || asset.sourceId.length === 0 || asset.sourceId.length > 128 || sourceIds.has(asset.sourceId) ||
typeof asset.sha256 !== "string" || !SHA256.test(asset.sha256) || (asset.mimeType !== "image/png" && asset.mimeType !== "audio/wav")) {
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Long media asset ${index} is invalid`);
}
sourceIds.add(asset.sourceId);
return { sourceId: asset.sourceId, sha256: asset.sha256, mimeType: asset.mimeType };
});
for (const strip of timeline.strips) {
if (["IMAGE", "SOUND"].includes(strip.type) && strip.sourceId && !sourceIds.has(strip.sourceId)) {
throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `${strip.id} has no persisted long media asset`);
}
}
return { schemaVersion: LONG_MEDIA_SCHEMA, timeline, currentFrame, cacheMaxBytes, assets };
}
export function serializeLongMediaSessionManifest(value: LongMediaSessionManifestIR): ArrayBuffer {
const parsed = parseLongMediaSessionManifest(value);
return new TextEncoder().encode(JSON.stringify(parsed)).buffer as ArrayBuffer;
}
export function deserializeLongMediaSessionManifest(data: ArrayBuffer): LongMediaSessionManifestIR {
if (!(data instanceof ArrayBuffer) || data.byteLength === 0 || data.byteLength > 64 * 1024 * 1024) {
throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", "Long media session manifest byte size is invalid");
}
try { return parseLongMediaSessionManifest(JSON.parse(new TextDecoder().decode(data))); }
catch (error) {
if (error instanceof SequencerValidationError) throw error;
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Long media session manifest JSON is invalid");
}
}
export async function buildLongMediaTimelineIndex(value: unknown, signal: AbortSignal): Promise<LongMediaTimelineIndex> {
return LongMediaTimelineIndex.build(value, signal);
}
export { gateSequencerCodec, sequencerRuntimeCapabilities } from "../../../protocol/sequencer";