hololake-platform/scripts/editor-performance-thresholds.mjs

114 lines
3.9 KiB
JavaScript

import console from 'node:console'
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
const thresholdDescription = 'Ratcheted editor performance budgets for synthetic small and large note opens. Lower is better; maxMs values should only move down unless intentionally rebaselined.'
export async function readThresholds(thresholdsPath) {
if (!existsSync(thresholdsPath)) {
return { scenarios: {}, version: 1 }
}
return JSON.parse(await readFile(thresholdsPath, 'utf8'))
}
export async function writeThresholds(thresholdsPath, thresholds) {
await writeFile(thresholdsPath, `${JSON.stringify(thresholds, null, 2)}\n`)
}
export function updateThresholds(thresholds, summaries) {
const next = {
...thresholds,
description: thresholdDescription,
scenarios: { ...thresholds.scenarios },
updatedAt: new Date().toISOString(),
version: 1,
}
for (const [scenarioName, summary] of Object.entries(summaries)) {
next.scenarios[scenarioName] = updatedScenarioThreshold(thresholds, scenarioName, summary)
}
return next
}
export function thresholdFailures(thresholds, summaries) {
return Object.entries(summaries).flatMap(([scenarioName, summary]) => (
scenarioThresholdFailures(thresholds, scenarioName, summary)
))
}
export function printSummary({ metricLabels, summaries, thresholds, writeLine = console.log }) {
for (const [scenarioName, summary] of Object.entries(summaries)) {
writeLine(`\n${scenarioName} (${summary.contentBytes} bytes, ${summary.sectionCount} sections)`)
for (const [metricName, value] of currentMetricEntries(summary)) {
writeLine(summaryMetricLine({
label: metricLabels[metricName] ?? metricName,
maxMs: thresholds.scenarios?.[scenarioName]?.metrics?.[metricName]?.maxMs,
value,
}))
}
}
}
export function printThresholdFailures({ failures, metricLabels, writeLine = console.error }) {
writeLine('\nEditor performance thresholds failed:')
for (const failure of failures) {
const label = metricLabels[failure.metricName] ?? failure.metricName
writeLine(` ${failure.scenarioName} ${label}: ${failure.value}ms > ${failure.maxMs}ms`)
}
}
function updatedScenarioThreshold(thresholds, scenarioName, summary) {
const previousScenario = thresholds.scenarios?.[scenarioName] ?? {}
return {
contentBytes: summary.contentBytes,
metrics: updatedMetricThresholds(previousScenario.metrics ?? {}, summary),
sectionCount: summary.sectionCount,
}
}
function updatedMetricThresholds(previousMetrics, summary) {
return Object.fromEntries(currentMetricEntries(summary).map(([metricName, value]) => [
metricName,
{
baselineMs: value,
maxMs: ratchetedMax(metricName, previousMetrics[metricName], value),
},
]))
}
function currentMetricEntries(summary) {
return Object.entries(summary.medians)
.filter(([, value]) => typeof value === 'number' && Number.isFinite(value))
}
function ratchetedMax(metricName, existingMetric, value) {
const observedBudget = metricName === 'editFrameMs'
? Math.ceil(Math.max(value * 2.5, value + 8, 16))
: Math.ceil(Math.max(value * 1.35, value + 25))
if (!existingMetric?.maxMs) return observedBudget
return Math.min(existingMetric.maxMs, observedBudget)
}
function scenarioThresholdFailures(thresholds, scenarioName, summary) {
return currentMetricEntries(summary)
.map(([metricName, value]) => metricFailure(thresholds, scenarioName, metricName, value))
.filter(Boolean)
}
function metricFailure(thresholds, scenarioName, metricName, value) {
const maxMs = thresholds.scenarios?.[scenarioName]?.metrics?.[metricName]?.maxMs
if (typeof maxMs !== 'number' || value <= maxMs) return null
return {
maxMs,
metricName,
scenarioName,
value,
}
}
function summaryMetricLine({ label, maxMs, value }) {
const suffix = typeof maxMs === 'number' ? ` / max ${maxMs}ms` : ''
return ` ${label.padEnd(24)} ${String(value).padStart(6)}ms${suffix}`
}