first commmit
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { Logger, MetricAggregate } from './Logger';
|
||||
import {
|
||||
PERF_TOUCH_RESPONSE_MAX_MS,
|
||||
PERF_JUMP_STATE_TOGGLE_MAX_MS,
|
||||
PERF_COMBO_RECOGNITION_MAX_MS,
|
||||
PERF_PARABOLIC_ANGLE_ACCURACY_TARGET,
|
||||
PERF_AIR_JUMP_BLOCK_RATE_TARGET,
|
||||
MAX_FIRST_PACKAGE_BYTES,
|
||||
MAX_AUDIO_BUNDLE_BYTES,
|
||||
MAX_MEMORY_PEAK_BYTES,
|
||||
} from './Constants';
|
||||
|
||||
/**
|
||||
* Performance monitor (task 10.2, req 18 & 20).
|
||||
*
|
||||
* Aggregates all the KPI samples recorded through `Logger.metric(...)` and
|
||||
* reports pass/fail against every threshold listed in the requirements doc.
|
||||
* CI can run `collectReport()` and assert that `allPassing === true`.
|
||||
*/
|
||||
|
||||
export interface IPerfThreshold {
|
||||
metric: string;
|
||||
/** Budget target (max for latency, min for rates). */
|
||||
limit: number;
|
||||
comparator: '<=' | '>=';
|
||||
requirementId: string;
|
||||
}
|
||||
|
||||
export const CORE_PERF_THRESHOLDS: ReadonlyArray<IPerfThreshold> = [
|
||||
{ metric: 'input/touchStart', limit: PERF_TOUCH_RESPONSE_MAX_MS, comparator: '<=', requirementId: 'req 20.1' },
|
||||
{ metric: 'jump/state_toggle_ms', limit: PERF_JUMP_STATE_TOGGLE_MAX_MS, comparator: '<=', requirementId: 'req 20.2' },
|
||||
{ metric: 'input/parabolic_accuracy', limit: PERF_PARABOLIC_ANGLE_ACCURACY_TARGET, comparator: '>=', requirementId: 'req 20.3' },
|
||||
{ metric: 'input/combo_recognition_ms', limit: PERF_COMBO_RECOGNITION_MAX_MS, comparator: '<=', requirementId: 'req 20.4' },
|
||||
{ metric: 'jump/air_jump_block_rate', limit: PERF_AIR_JUMP_BLOCK_RATE_TARGET, comparator: '>=', requirementId: 'req 20.5' },
|
||||
];
|
||||
|
||||
export interface IPerfCheckResult {
|
||||
threshold: IPerfThreshold;
|
||||
aggregate?: MetricAggregate;
|
||||
passing: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface IPerfReport {
|
||||
allPassing: boolean;
|
||||
checks: IPerfCheckResult[];
|
||||
/** Optional build/runtime sizes filled in by CI (bytes). */
|
||||
firstPackageBytes?: number;
|
||||
audioBundleBytes?: number;
|
||||
memoryPeakBytes?: number;
|
||||
/** Top-level pass/fail for the size budgets. */
|
||||
sizeBudgetPassing?: boolean;
|
||||
}
|
||||
|
||||
export class PerfMonitor {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly thresholds: ReadonlyArray<IPerfThreshold> = CORE_PERF_THRESHOLDS
|
||||
) {}
|
||||
|
||||
public collectReport(buildSizes?: {
|
||||
firstPackageBytes?: number;
|
||||
audioBundleBytes?: number;
|
||||
memoryPeakBytes?: number;
|
||||
}): IPerfReport {
|
||||
const checks: IPerfCheckResult[] = this.thresholds.map((t) => this.check(t));
|
||||
let sizeBudgetPassing: boolean | undefined;
|
||||
if (buildSizes) {
|
||||
sizeBudgetPassing =
|
||||
(buildSizes.firstPackageBytes ?? 0) <= MAX_FIRST_PACKAGE_BYTES &&
|
||||
(buildSizes.audioBundleBytes ?? 0) <= MAX_AUDIO_BUNDLE_BYTES &&
|
||||
(buildSizes.memoryPeakBytes ?? 0) <= MAX_MEMORY_PEAK_BYTES;
|
||||
}
|
||||
const allPassing = checks.every((c) => c.passing) && (sizeBudgetPassing ?? true);
|
||||
return {
|
||||
allPassing,
|
||||
checks,
|
||||
firstPackageBytes: buildSizes?.firstPackageBytes,
|
||||
audioBundleBytes: buildSizes?.audioBundleBytes,
|
||||
memoryPeakBytes: buildSizes?.memoryPeakBytes,
|
||||
sizeBudgetPassing,
|
||||
};
|
||||
}
|
||||
|
||||
private check(t: IPerfThreshold): IPerfCheckResult {
|
||||
const agg = this.logger.aggregate(t.metric);
|
||||
if (!agg) {
|
||||
return {
|
||||
threshold: t,
|
||||
passing: false,
|
||||
reason: `no samples recorded for "${t.metric}"`,
|
||||
};
|
||||
}
|
||||
// For latency, use p95. For rate metrics, use avg.
|
||||
const isRate = t.comparator === '>=';
|
||||
const observed = isRate ? agg.avg : agg.p95;
|
||||
const passing = isRate ? observed >= t.limit : observed <= t.limit;
|
||||
return {
|
||||
threshold: t,
|
||||
aggregate: agg,
|
||||
passing,
|
||||
reason: `${t.metric} ${isRate ? 'avg' : 'p95'}=${observed.toFixed(2)} vs limit ${t.limit} (${t.comparator})`,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user