fix: bind performance evidence to production resources
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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__', {
|
||||
|
||||
Reference in New Issue
Block a user