111 lines
4.9 KiB
TypeScript
111 lines
4.9 KiB
TypeScript
import { Logger } from '@common/Logger';
|
|
import { CORE_PERF_THRESHOLDS, PerfMonitor } from '@common/PerfMonitor';
|
|
import {
|
|
PERF_TOUCH_RESPONSE_MAX_MS,
|
|
PERF_COMBO_RECOGNITION_MAX_MS,
|
|
MAX_FIRST_PACKAGE_BYTES,
|
|
MAX_AUDIO_BUNDLE_BYTES,
|
|
MAX_MEMORY_PEAK_BYTES,
|
|
} from '@common/Constants';
|
|
|
|
describe('PerfMonitor — threshold catalog (req 18 & 20)', () => {
|
|
it('includes every KPI threshold called out in requirements 20.1-20.5', () => {
|
|
const names = CORE_PERF_THRESHOLDS.map((t) => t.metric);
|
|
expect(names).toContain('input/touchStart');
|
|
expect(names).toContain('jump/state_toggle_ms');
|
|
expect(names).toContain('input/combo_recognition_ms');
|
|
expect(names).toContain('input/parabolic_accuracy');
|
|
expect(names).toContain('jump/air_jump_block_rate');
|
|
});
|
|
});
|
|
|
|
describe('PerfMonitor — pass/fail evaluation', () => {
|
|
function seedPassing(logger: Logger): void {
|
|
for (let i = 0; i < 100; i++) {
|
|
logger.metric({ name: 'input/touchStart', value: 20 });
|
|
logger.metric({ name: 'jump/state_toggle_ms', value: 25 });
|
|
logger.metric({ name: 'input/combo_recognition_ms', value: 40 });
|
|
}
|
|
logger.metric({ name: 'input/parabolic_accuracy', value: 0.97 });
|
|
logger.metric({ name: 'jump/air_jump_block_rate', value: 0.995 });
|
|
}
|
|
|
|
it('passes when all metrics are within budget', () => {
|
|
const logger = new Logger();
|
|
seedPassing(logger);
|
|
const monitor = new PerfMonitor(logger);
|
|
const report = monitor.collectReport();
|
|
expect(report.allPassing).toBe(true);
|
|
});
|
|
|
|
it('fails when no samples exist for a threshold', () => {
|
|
const monitor = new PerfMonitor(new Logger());
|
|
const report = monitor.collectReport();
|
|
expect(report.allPassing).toBe(false);
|
|
expect(report.checks[0].reason).toMatch(/no samples/);
|
|
});
|
|
|
|
it('fails when p95 latency exceeds the limit', () => {
|
|
const logger = new Logger();
|
|
seedPassing(logger);
|
|
// Push a huge batch of slow touches so p95 exceeds budget.
|
|
for (let i = 0; i < 200; i++) {
|
|
logger.metric({ name: 'input/touchStart', value: PERF_TOUCH_RESPONSE_MAX_MS + 50 });
|
|
}
|
|
const report = new PerfMonitor(logger).collectReport();
|
|
const touchCheck = report.checks.find((c) => c.threshold.metric === 'input/touchStart')!;
|
|
expect(touchCheck.passing).toBe(false);
|
|
});
|
|
|
|
it('fails when parabolic accuracy drops below 95%', () => {
|
|
const logger = new Logger();
|
|
seedPassing(logger);
|
|
logger.resetMetrics();
|
|
logger.metric({ name: 'input/touchStart', value: 20 });
|
|
logger.metric({ name: 'jump/state_toggle_ms', value: 20 });
|
|
logger.metric({ name: 'input/combo_recognition_ms', value: 20 });
|
|
logger.metric({ name: 'input/parabolic_accuracy', value: 0.8 });
|
|
logger.metric({ name: 'jump/air_jump_block_rate', value: 0.999 });
|
|
const report = new PerfMonitor(logger).collectReport();
|
|
expect(report.allPassing).toBe(false);
|
|
});
|
|
|
|
it('evaluates build-size budget when provided', () => {
|
|
const logger = new Logger();
|
|
logger.metric({ name: 'input/touchStart', value: 10 });
|
|
logger.metric({ name: 'jump/state_toggle_ms', value: 10 });
|
|
logger.metric({ name: 'input/combo_recognition_ms', value: 10 });
|
|
logger.metric({ name: 'input/parabolic_accuracy', value: 1 });
|
|
logger.metric({ name: 'jump/air_jump_block_rate', value: 1 });
|
|
const monitor = new PerfMonitor(logger);
|
|
|
|
const okReport = monitor.collectReport({
|
|
firstPackageBytes: MAX_FIRST_PACKAGE_BYTES - 1,
|
|
audioBundleBytes: MAX_AUDIO_BUNDLE_BYTES - 1,
|
|
memoryPeakBytes: MAX_MEMORY_PEAK_BYTES - 1,
|
|
});
|
|
expect(okReport.sizeBudgetPassing).toBe(true);
|
|
expect(okReport.allPassing).toBe(true);
|
|
|
|
const badReport = monitor.collectReport({
|
|
firstPackageBytes: MAX_FIRST_PACKAGE_BYTES + 1,
|
|
audioBundleBytes: MAX_AUDIO_BUNDLE_BYTES + 1,
|
|
memoryPeakBytes: MAX_MEMORY_PEAK_BYTES + 1,
|
|
});
|
|
expect(badReport.sizeBudgetPassing).toBe(false);
|
|
expect(badReport.allPassing).toBe(false);
|
|
});
|
|
|
|
it('honours the <= comparator at exactly the boundary', () => {
|
|
const logger = new Logger();
|
|
logger.metric({ name: 'input/touchStart', value: PERF_TOUCH_RESPONSE_MAX_MS });
|
|
logger.metric({ name: 'jump/state_toggle_ms', value: 10 });
|
|
logger.metric({ name: 'input/combo_recognition_ms', value: PERF_COMBO_RECOGNITION_MAX_MS });
|
|
logger.metric({ name: 'input/parabolic_accuracy', value: 1 });
|
|
logger.metric({ name: 'jump/air_jump_block_rate', value: 1 });
|
|
const report = new PerfMonitor(logger).collectReport();
|
|
const touch = report.checks.find((c) => c.threshold.metric === 'input/touchStart')!;
|
|
expect(touch.passing).toBe(true);
|
|
});
|
|
});
|