fix: bind performance evidence to production resources

This commit is contained in:
2026-09-02 08:53:00 +03:00
parent 5b7e47c2a2
commit e3ac3a4db4
23 changed files with 642 additions and 60 deletions

View File

@@ -24,6 +24,31 @@ export interface SceneResourceSnapshot {
rafCount: 0 | 1;
}
export interface GpuTextureAllocation {
id: string;
width: number;
height: number;
bytesPerPixel: number;
layers: number;
mipmapped: boolean;
}
export interface GpuRenderTargetAllocation {
id: string;
width: number;
height: number;
bytesPerPixel: number;
attachments: number;
samples: number;
}
export interface SceneGpuBudgetSnapshot {
tier: 'low' | 'high';
textures: readonly GpuTextureAllocation[];
geometryBytes: number;
renderTargets: readonly GpuRenderTargetAllocation[];
}
const resourceKinds: readonly SceneResourceKind[] = [
'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
];
@@ -127,4 +152,67 @@ export class SceneResourceBag {
rafCount: this.raf ? 1 : 0,
};
}
gpuBudgetSnapshot(input: {
tier: 'low' | 'high';
defaultFramebuffer: Omit<GpuRenderTargetAllocation, 'id'>;
}): SceneGpuBudgetSnapshot {
let geometryBytes = 0;
const seenBuffers = new Set<ArrayBufferLike>();
const textures: GpuTextureAllocation[] = [];
const renderTargets: GpuRenderTargetAllocation[] = [{ id: 'default-framebuffer', ...input.defaultFramebuffer }];
let textureIndex = 0;
let targetIndex = 0;
for (const [resource, kind] of this.resources) {
if (kind === 'geometry') {
const geometry = resource as DisposableResource & {
attributes?: Record<string, { array?: ArrayBufferView; data?: { array?: ArrayBufferView } }>;
index?: { array?: ArrayBufferView } | null;
};
const arrays = [
...Object.values(geometry.attributes ?? {}).map((attribute) => attribute.array ?? attribute.data?.array),
geometry.index?.array,
].filter((array): array is ArrayBufferView => array !== undefined);
for (const array of arrays) {
if (seenBuffers.has(array.buffer)) continue;
seenBuffers.add(array.buffer);
geometryBytes += array.byteLength;
}
}
if (kind === 'texture') {
const texture = resource as DisposableResource & {
image?: { width?: number; height?: number; data?: ArrayBufferView };
generateMipmaps?: boolean;
};
const width = Number(texture.image?.width);
const height = Number(texture.image?.height);
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
throw new Error('Owned production texture is missing measurable dimensions.');
}
const dataBytes = texture.image?.data?.byteLength;
const bytesPerPixel = dataBytes === undefined ? 4 : dataBytes / (width * height);
if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0) throw new Error('Owned production texture has an unsupported pixel layout.');
textures.push({ id: `owned-texture-${textureIndex}`, width, height, bytesPerPixel, layers: 1, mipmapped: texture.generateMipmaps !== false });
textureIndex += 1;
}
if (kind === 'target') {
const target = resource as DisposableResource & { width?: number; height?: number; samples?: number; depthBuffer?: boolean };
const width = Number(target.width);
const height = Number(target.height);
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
throw new Error('Owned production render target is missing measurable dimensions.');
}
renderTargets.push({
id: `owned-render-target-${targetIndex}`,
width,
height,
bytesPerPixel: 4,
attachments: target.depthBuffer === false ? 1 : 2,
samples: Math.max(1, Number(target.samples) || 1),
});
targetIndex += 1;
}
}
return { tier: input.tier, textures, geometryBytes, renderTargets };
}
}

View File

@@ -136,6 +136,18 @@ export async function createProceduralSceneGeneration(
Math.max(1, Math.floor(width * quality.pixelRatio)),
Math.max(1, Math.floor(height * quality.pixelRatio)),
);
if (TEST_BUILD && context.quality !== 'medium') {
window.__PORTFOLIO_PERFORMANCE__?.recordRuntimeAllocation(context.resources.gpuBudgetSnapshot({
tier: context.quality,
defaultFramebuffer: {
width: Math.max(1, canvas.width),
height: Math.max(1, canvas.height),
bytesPerPixel: 4,
attachments: 2,
samples: context.quality === 'low' ? 1 : 4,
},
}));
}
};
if (context.signal.aborted) {

View File

@@ -1,3 +1,5 @@
import type { SceneGpuBudgetSnapshot } from '@/scene/resources';
declare const __PORTFOLIO_TEST_BUILD__: boolean;
declare const __PORTFOLIO_TEST_TOKEN_DIGEST__: string;
@@ -37,6 +39,7 @@ export interface ScriptedActionSample {
export interface SceneInterval { start: number; end: number }
export interface SceneFrameSample { start: number; end: number }
export type RuntimeAllocationSample = SceneGpuBudgetSnapshot;
export const nearestRankP95 = (samples: readonly number[]): number => {
if (samples.length === 0) throw new Error('At least one sample is required.');
@@ -115,7 +118,7 @@ export const evaluateEventToNextPaint = (
if (entry.interactionId === 0) continue;
groups.set(entry.interactionId, Math.max(groups.get(entry.interactionId) ?? 0, entry.duration));
}
if (groups.size !== 1) throw new Error('Event Timing groups are ambiguous.');
if (groups.size !== 1) throw new Error(`Event Timing groups are ambiguous for ${action.id}: ${JSON.stringify(matching)}`);
return [...groups.values()][0]!;
});
return { values, p95: nearestRankP95(values) };
@@ -192,6 +195,7 @@ interface PerformanceSnapshot {
cutoff: number;
navigationBarrier: { armed: boolean; pendingEpoch: number | null; cancelledEpoch: number | null };
actions: readonly ScriptedActionSample[];
runtimeAllocation: RuntimeAllocationSample | null;
}
interface PerformanceHooks {
@@ -200,6 +204,7 @@ interface PerformanceHooks {
endTrace(): void;
recordSceneFrame(start: number, end: number): void;
recordSceneState(state: string): void;
recordRuntimeAllocation(allocation: RuntimeAllocationSample): void;
armNavigationSettlementBarrier(): void;
registerNavigationSettlement(epoch: number): void;
holdNavigationSettlement(epoch: number): boolean;
@@ -234,6 +239,7 @@ export const installAuthorizedPerformanceInstrumentation = async (): Promise<voi
const sceneFrames: SceneFrameSample[] = [];
const sceneIntervals: SceneInterval[] = [];
let firstSceneFrame: number | null = null;
let runtimeAllocation: RuntimeAllocationSample | null = null;
let traceStart: number | null = null;
let traceEnd: number | null = null;
let runningIntervalStart: number | null = null;
@@ -325,6 +331,13 @@ export const installAuthorizedPerformanceInstrumentation = async (): Promise<voi
runningIntervalStart = null;
}
},
recordRuntimeAllocation(allocation) {
runtimeAllocation = Object.freeze({
...allocation,
textures: Object.freeze(allocation.textures.map((item) => Object.freeze({ ...item }))),
renderTargets: Object.freeze(allocation.renderTargets.map((item) => Object.freeze({ ...item }))),
});
},
armNavigationSettlementBarrier() {
barrierArmed = true;
barrierPendingEpoch = null;
@@ -383,6 +396,7 @@ export const installAuthorizedPerformanceInstrumentation = async (): Promise<voi
...record,
eventEntries: Object.freeze([...record.eventEntries]),
}))),
runtimeAllocation,
}),
};
Object.defineProperty(window, '__PORTFOLIO_PERFORMANCE__', {