diff --git a/.github/scripts/heap-snapshot-util.mts b/.github/scripts/heap-snapshot-util.mts index 00f8f0cb26..d6bb6ee3c7 100644 --- a/.github/scripts/heap-snapshot-util.mts +++ b/.github/scripts/heap-snapshot-util.mts @@ -7,43 +7,21 @@ import * as util from './utility.mts'; -export const heapSnapshotCategories = [ - 'Total', - 'Code', - 'Strings', - 'JS arrays', - 'Typed arrays', - 'System objects', - 'Other JS objects', - 'Other non-JS objects', -] as const; - -const heapSnapshotCategoriesColors = { - 'Total': 'gray', - 'Code': 'orange', - 'Strings': 'red', - 'JS arrays': 'cyan', - 'Typed arrays': 'green', - 'System objects': 'yellow', - 'Other JS objects': 'violet', - 'Other non-JS objects': 'pink', -} as const satisfies Record; - -const heapSnapshotCategoriesColorsHex = { - 'Total': '#888888', - 'Code': '#f28e2c', - 'Strings': '#e15759', - 'JS arrays': '#76b7b2', - 'Typed arrays': '#59a14f', - 'System objects': '#edc949', - 'Other JS objects': '#af7aa1', - 'Other non-JS objects': '#ff9da7', -} as const satisfies Record; +export const heapSnapshotCategory = { + total: { label: 'Total', color: 'gray', colorHex: '#888888' }, + code: { label: 'Code', color: 'orange', colorHex: '#f28e2c' }, + strings: { label: 'Strings', color: 'red', colorHex: '#e15759' }, + jsArrays: { label: 'JS arrays', color: 'cyan', colorHex: '#76b7b2' }, + typedArrays: { label: 'Typed arrays', color: 'green', colorHex: '#59a14f' }, + systemObjects: { label: 'System objects', color: 'yellow', colorHex: '#edc949' }, + otherJsObjects: { label: 'Other JS objs', color: 'violet', colorHex: '#af7aa1' }, + otherNonJsObjects: { label: 'Other non-JS objs', color: 'pink', colorHex: '#ff9da7' }, +} as const satisfies Record; export type HeapSnapshotData = { - categories: Record; - nodeCounts: Record; - breakdowns?: Record>; + categories: Record; + nodeCounts: Record; + breakdowns?: Record>; }; export type HeapSnapshotReport = { @@ -54,7 +32,7 @@ export type HeapSnapshotReport = { }[]; }; -function getHeapSnapshotCategoryValue(report: HeapSnapshotReport, category: typeof heapSnapshotCategories[number]) { +function getHeapSnapshotCategoryValue(report: HeapSnapshotReport, category: keyof typeof heapSnapshotCategory) { return report.summary.categories[category]; } @@ -63,10 +41,10 @@ export function renderHeapSnapshotTable(base: HeapSnapshotReport, head: HeapSnap '| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |', '| --- | ---: | ---: | ---: | ---: | ---: | ---: |', ]; - const baseTotal = getHeapSnapshotCategoryValue(base, 'Total'); - const headTotal = getHeapSnapshotCategoryValue(head, 'Total'); + const baseTotal = getHeapSnapshotCategoryValue(base, 'total'); + const headTotal = getHeapSnapshotCategoryValue(head, 'total'); - function getHeapSnapshotSampleSpread(report: HeapSnapshotReport, category: typeof heapSnapshotCategories[number]) { + function getHeapSnapshotSampleSpread(report: HeapSnapshotReport, category: keyof typeof heapSnapshotCategory) { const values = report.samples .map(sample => sample.data.categories[category]) .filter(value => Number.isFinite(value)) as number[]; @@ -76,25 +54,29 @@ export function renderHeapSnapshotTable(base: HeapSnapshotReport, head: HeapSnap return util.median(values.map(value => Math.abs(value - center))); } - for (const category of heapSnapshotCategories) { + for (const category of Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[]) { const baseValue = getHeapSnapshotCategoryValue(base, category); const headValue = getHeapSnapshotCategoryValue(head, category); const baseSpread = getHeapSnapshotSampleSpread(base, category); const headSpread = getHeapSnapshotSampleSpread(head, category); const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => sample.data.categories[category]); const percent = summary.median * 100 / baseValue; - const deltaMedian = category === 'Total' ? `${util.formatDeltaBytes(summary.median)}
${util.formatDeltaPercent(percent).replaceAll('\\%', '\\\\%')}` : util.formatDeltaBytes(summary.median); - const baseText = category === 'Total' ? `${util.formatBytes(baseValue)}
± ${util.formatBytes(baseSpread)}` : util.formatBytes(baseValue); - const headText = category === 'Total' ? `${util.formatBytes(headValue)}
± ${util.formatBytes(headSpread)}` : util.formatBytes(headValue); - const basePercent = util.formatPercent((baseValue * 100) / baseTotal); - const headPercent = util.formatPercent((headValue * 100) / headTotal); - const metricText = category === 'Total' - ? `$\\color{${heapSnapshotCategoriesColors[category]}}{\\rule{8pt}{8pt}}$ **${category}**` - : `
$\\color{${heapSnapshotCategoriesColors[category]}}{\\rule{8pt}{8pt}}$ **${category}**${basePercent} → ${headPercent}
`; - lines.push(`| ${metricText} | ${baseText} | ${headText} | ${deltaMedian} | ${util.formatBytes(summary.mad)} | ${util.formatDeltaBytes(summary.min)} | ${util.formatDeltaBytes(summary.max)} |`); - if (category === 'Total') { + if (category === 'total') { + const deltaMedian = `${util.formatDeltaBytes(summary.median)}
${util.formatDeltaPercent(percent).replaceAll('\\%', '\\\\%')}`; + const baseText = `${util.formatBytes(baseValue)}
± ${util.formatBytes(baseSpread)}`; + const headText = `${util.formatBytes(headValue)}
± ${util.formatBytes(headSpread)}`; + const metricText = `$\\color{${heapSnapshotCategory[category].color}}{\\rule{8pt}{8pt}}$ **${heapSnapshotCategory[category].label}**`; + lines.push(`| ${metricText} | ${baseText} | ${headText} | ${deltaMedian} | ${util.formatBytes(summary.mad)} | ${util.formatDeltaBytes(summary.min)} | ${util.formatDeltaBytes(summary.max)} |`); lines.push('| | | | | | | |'); + } else { + const deltaMedian = util.formatDeltaBytes(summary.median); + const baseText = util.formatBytes(baseValue); + const headText = util.formatBytes(headValue); + const basePercent = util.formatPercent((baseValue * 100) / baseTotal); + const headPercent = util.formatPercent((headValue * 100) / headTotal); + const metricText = `
$\\color{${heapSnapshotCategory[category].color}}{\\rule{8pt}{8pt}}$ **${heapSnapshotCategory[category].label}**${basePercent} → ${headPercent}
`; + lines.push(`| ${metricText} | ${baseText} | ${headText} | ${deltaMedian} | ${util.formatBytes(summary.mad)} | ${util.formatDeltaBytes(summary.min)} | ${util.formatDeltaBytes(summary.max)} |`); } } @@ -110,10 +92,10 @@ function escapeCsvValue(value: string) { } export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: string) { - const total = getHeapSnapshotCategoryValue(report, 'Total'); + const total = getHeapSnapshotCategoryValue(report, 'total'); if (total == null || total <= 0) return null; - function getHeapSnapshotBreakdownEntries(category: typeof heapSnapshotCategories[number]) { + function getHeapSnapshotBreakdownEntries(category: keyof typeof heapSnapshotCategory) { const breakdown = report.summary.breakdowns?.[category]; if (breakdown == null || typeof breakdown !== 'object') return []; @@ -133,8 +115,8 @@ export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: stri return rounded.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); } - const categories = heapSnapshotCategories - .filter(category => category !== 'Total') + const categories = (Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[]) + .filter(category => category !== 'total') .map(category => { const value = getHeapSnapshotCategoryValue(report, category); if (value == null || value <= 0) return null; @@ -171,10 +153,10 @@ export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: stri if (categories.length === 0) return null; const nodeColors = { - [title]: heapSnapshotCategoriesColorsHex.Total, + [title]: heapSnapshotCategory.total.colorHex, } as Record; for (const { category, childEntries } of categories) { - const categoryColor = heapSnapshotCategoriesColorsHex[category] ?? heapSnapshotCategoriesColorsHex.Total; + const categoryColor = heapSnapshotCategory[category].colorHex; nodeColors[category] = categoryColor; for (const [childName] of childEntries) { @@ -203,10 +185,10 @@ export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: stri ]; for (const { category, percent, childEntries } of categories) { - lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${formatSankeyPercentValue(percent)}`); + lines.push(`${escapeCsvValue(title)},${escapeCsvValue(heapSnapshotCategory[category].label)},${formatSankeyPercentValue(percent)}`); for (const [childName, childPercent] of childEntries) { - lines.push(`${escapeCsvValue(category)},${escapeCsvValue(childName)},${formatSankeyPercentValue(childPercent)}`); + lines.push(`${escapeCsvValue(heapSnapshotCategory[category].label)},${escapeCsvValue(childName)},${formatSankeyPercentValue(childPercent)}`); } } diff --git a/.github/scripts/measure-backend-memory-comparison.mts b/.github/scripts/measure-backend-memory-comparison.mts index 23f02c717e..99ba4f64ae 100644 --- a/.github/scripts/measure-backend-memory-comparison.mts +++ b/.github/scripts/measure-backend-memory-comparison.mts @@ -69,10 +69,10 @@ async function resetState(repoDir: string) { } function summarizeHeapSnapshotBreakdowns(samples: MemoryReport['samples'], phase: typeof phases[number]) { - const breakdowns = {} as Record>; + const breakdowns = {} as Record>; - for (const category of heapSnapshotUtil.heapSnapshotCategories) { - if (category === 'Total') continue; + for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) { + if (category === 'total') continue; const childKeys = new Set(); for (const sample of samples) { @@ -133,8 +133,8 @@ function summarizeSamples(samples: MemoryReport['samples']) { summary[phase].memoryUsage[key] = util.median(values); } - const heapSnapshotCategoryValues = {} as Record; - for (const category of heapSnapshotUtil.heapSnapshotCategories) { + const heapSnapshotCategoryValues = {} as Record; + for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) { const values = samples .map(sample => sample.phases[phase].heapSnapshot?.categories?.[category]) .filter(value => Number.isFinite(value)) as number[]; @@ -142,8 +142,8 @@ function summarizeSamples(samples: MemoryReport['samples']) { if (values.length > 0) heapSnapshotCategoryValues[category] = util.median(values); } - const heapSnapshotNodeCountValues = {} as Record; - for (const category of heapSnapshotUtil.heapSnapshotCategories) { + const heapSnapshotNodeCountValues = {} as Record; + for (const category of Object.keys(heapSnapshotUtil.heapSnapshotCategory) as (keyof typeof heapSnapshotUtil.heapSnapshotCategory)[]) { const values = samples .map(sample => sample.phases[phase].heapSnapshot?.nodeCounts?.[category]) .filter(value => Number.isFinite(value)) as number[]; diff --git a/.github/scripts/utility.mts b/.github/scripts/utility.mts index a65da25a6d..bb3b1d9401 100644 --- a/.github/scripts/utility.mts +++ b/.github/scripts/utility.mts @@ -17,7 +17,7 @@ export function median(values: number[]) { } export function mad(values: number[]) { - if (values.length < 2) return null; + if (values.length < 2) throw new Error('Not enough samples to calculate MAD'); const center = median(values); return median(values.map(value => Math.abs(value - center))); @@ -111,7 +111,7 @@ export function formatNumber(value: number) { export function formatBytes(value: number) { if (value === 0) return '0 B'; - const units = ['B', 'KiB', 'MiB', 'GiB']; + const units = ['B', 'KB', 'MB', 'GB']; let unitIndex = 0; let size = value; while (size >= 1024 && unitIndex < units.length - 1) { diff --git a/packages/backend/scripts/measure-memory.mts b/packages/backend/scripts/measure-memory.mts index 51abc20ece..3da4ec2f60 100644 --- a/packages/backend/scripts/measure-memory.mts +++ b/packages/backend/scripts/measure-memory.mts @@ -10,7 +10,7 @@ import { dirname, join } from 'node:path'; import { tmpdir } from 'node:os'; //import * as http from 'node:http'; import * as fs from 'node:fs/promises'; -import { heapSnapshotCategories, type HeapSnapshotData } from '../../../.github/scripts/heap-snapshot-util.mts'; +import { heapSnapshotCategory, type HeapSnapshotData } from '../../../.github/scripts/heap-snapshot-util.mts'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -104,14 +104,14 @@ function isSystemNode(type, name) { name.startsWith('(system '); } -function classifyHeapSnapshotNode(type, name) { - if (type === 'code') return 'Code'; - if (type === 'string' || type === 'concatenated string' || type === 'sliced string') return 'Strings'; - if (isTypedArrayNode(type, name)) return 'Typed arrays'; - if (type === 'array' || (type === 'object' && name === 'Array')) return 'JS arrays'; - if (isSystemNode(type, name)) return 'System objects'; - if (otherJsNodeTypes.has(type)) return 'Other JS objects'; - return 'Other non-JS objects'; +function classifyHeapSnapshotNode(type, name): keyof typeof heapSnapshotCategory { + if (type === 'code') return 'code'; + if (type === 'string' || type === 'concatenated string' || type === 'sliced string') return 'strings'; + if (isTypedArrayNode(type, name)) return 'typedArrays'; + if (type === 'array' || (type === 'object' && name === 'Array')) return 'jsArrays'; + if (isSystemNode(type, name)) return 'systemObjects'; + if (otherJsNodeTypes.has(type)) return 'otherJsObjects'; + return 'otherNonJsObjects'; } function sanitizeHeapSnapshotBreakdownLabel(value, fallback = 'unknown') { @@ -210,15 +210,15 @@ function analyzeHeapSnapshot(snapshot) { if (!Array.isArray(nodeTypeNames)) throw new Error('Invalid heap snapshot node types'); function createEmptyHeapSnapshotCategoryMap() { - return Object.fromEntries(heapSnapshotCategories.map(category => [category, 0])) as Record; + return Object.fromEntries(Object.keys(heapSnapshotCategory).map(category => [category, 0])) as Record; } const fieldCount = nodeFields.length; const categories = createEmptyHeapSnapshotCategoryMap(); const nodeCounts = createEmptyHeapSnapshotCategoryMap(); const breakdowns = Object.fromEntries( - heapSnapshotCategories - .filter(category => category !== 'Total') + (Object.keys(heapSnapshotCategory) as (keyof typeof heapSnapshotCategory)[]) + .filter(category => category !== 'total') .map(category => [category, {}]), ); @@ -233,9 +233,9 @@ function analyzeHeapSnapshot(snapshot) { const category = classifyHeapSnapshotNode(type, name); categories[category] += selfSize; - categories.Total += selfSize; + categories.total += selfSize; nodeCounts[category]++; - nodeCounts.Total++; + nodeCounts.total++; addValue(breakdowns[category], classifyHeapSnapshotBreakdown(category, type, name), selfSize); }