Some checks failed
Auto-update PR branches / Update open PR branches (push) Has been cancelled
CI / Frontend Static Quality Checks (push) Has been cancelled
CI / Frontend Tests & Coverage (push) Has been cancelled
CI / Rust Tests & Quality Checks (push) Has been cancelled
CI / Linux build verification (push) Has been cancelled
Release (Alpha) / Compute alpha version (push) Has been cancelled
Release (Alpha) / Build release artifacts (push) Has been cancelled
Release (Alpha) / GitHub Release (alpha) (push) Has been cancelled
Release (Alpha) / Update docs and release pages (push) Has been cancelled
Deploy docs / Build VitePress site (push) Has been cancelled
Deploy docs / Deploy to GitHub Pages (push) Has been cancelled
114 lines
3.9 KiB
JavaScript
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}`
|
|
}
|