mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-14 08:26:16 -08:00
test: [ENG-08] add verified TS coverage and paired performance reports
This commit is contained in:
1 parent
8ed0a5228c
commit
4a9a26d4fc
38 files changed
+24335
-61
No files matched your search
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const hash = value => createHash('sha256').update(value).digest('hex')
|
||||
function inside(directory, file) {
|
||||
const relative = path.relative(directory, file)
|
||||
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
// Keep the original V8 files intact. c8's remap filter expects filesystem paths,
|
||||
// while Node's source-map cache contains file URLs (including on Windows).
|
||||
export function normalizeCoverageReports(rawDirectory, mappedDirectory, { root, moduleDirectory, inventory }) {
|
||||
assert(!fs.existsSync(mappedDirectory), 'Normalized coverage must use a fresh directory')
|
||||
fs.mkdirSync(mappedDirectory)
|
||||
const sources = new Map([...inventory.files, ...inventory.excluded].map(item => [path.resolve(root, item.file), item]))
|
||||
const reports = []
|
||||
let moduleCount = 0
|
||||
for (const file of fs.readdirSync(rawDirectory).filter(file => file.endsWith('.json')).sort()) {
|
||||
const raw = fs.readFileSync(path.join(rawDirectory, file))
|
||||
const report = JSON.parse(raw)
|
||||
assert(Array.isArray(report.result), `Invalid V8 report: ${file}`)
|
||||
const cache = report['source-map-cache'] || {}
|
||||
const modules = report.result.filter(item => item.url.startsWith('file:') && inside(moduleDirectory, fileURLToPath(item.url)))
|
||||
for (const module of modules) {
|
||||
const map = cache[module.url]?.data
|
||||
assert(map?.version === 3 && typeof map.mappings === 'string' && map.mappings.length > 0 && Array.isArray(map.sources) && map.sources.length > 0, `Missing source map: ${module.url}`)
|
||||
assert.equal(map.sourcesContent?.length, map.sources.length, `Missing source contents: ${module.url}`)
|
||||
for (const [index, source] of map.sources.entries()) {
|
||||
const absolute = source.startsWith('file:') ? fileURLToPath(source) : source
|
||||
assert(path.isAbsolute(absolute), `Unresolved source map path: ${source}`)
|
||||
const enrolled = sources.get(path.normalize(absolute))
|
||||
if (enrolled) {
|
||||
assert.equal(typeof map.sourcesContent[index], 'string', `Missing mapped source: ${source}`)
|
||||
assert.equal(hash(map.sourcesContent[index].replaceAll('\r\n', '\n')), enrolled.sha256, `Stale mapped source: ${source}`)
|
||||
}
|
||||
}
|
||||
moduleCount++
|
||||
}
|
||||
let normalizedSources = 0
|
||||
for (const entry of Object.values(cache)) {
|
||||
if (!entry.data?.sources)
|
||||
continue
|
||||
entry.data.sources = entry.data.sources.map((source) => {
|
||||
if (!source.startsWith('file:'))
|
||||
return source
|
||||
normalizedSources++
|
||||
return fileURLToPath(source)
|
||||
})
|
||||
}
|
||||
const normalized = `${JSON.stringify(report)}\n`
|
||||
fs.writeFileSync(path.join(mappedDirectory, file), normalized)
|
||||
reports.push({ file, rawSha256: hash(raw), normalizedSha256: hash(normalized), modules: modules.length, normalizedSources })
|
||||
}
|
||||
assert(moduleCount > 0, 'No source-mapped test modules were executed')
|
||||
return { modules: moduleCount, reports }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"packages": ["artplayer", "artplayer-plugin-chapter"],
|
||||
"exclude": {
|
||||
"packages/artplayer/src/libs/screenfull.js": "Vendored dependency with separate provenance and browser compatibility tests."
|
||||
},
|
||||
"minimum": {
|
||||
"packages/artplayer/src/lifecycle/scope.ts": { "lines": 95, "branches": 90, "functions": 95 },
|
||||
"packages/artplayer/src/lifecycle/resources.ts": { "lines": 95, "branches": 90, "functions": 95 },
|
||||
"packages/artplayer/src/lifecycle/instance.ts": { "lines": 95, "branches": 90, "functions": 95 },
|
||||
"packages/artplayer/src/source/operation.ts": { "lines": 90, "branches": 85, "functions": 90 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
|
||||
const relative = (root, file) => path.relative(root, file).split(path.sep).join('/')
|
||||
|
||||
export function sourceInventory(root, policy) {
|
||||
assert.equal(policy.schemaVersion, 1)
|
||||
assert(policy.packages.length > 0)
|
||||
const files = []
|
||||
const excluded = []
|
||||
for (const name of policy.packages) {
|
||||
assert(/^artplayer(?:-[a-z-]+)?$/.test(name), `Invalid coverage package: ${name}`)
|
||||
for (const file of walk(path.join(root, 'packages', name, 'src')).filter(file => /\.[jt]s$/.test(file))) {
|
||||
const name = relative(root, file)
|
||||
const source = fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
|
||||
const sha256 = createHash('sha256').update(source).digest('hex')
|
||||
const emitted = file.endsWith('.d.ts') ? '' : ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.ESNext } }).outputText.trim()
|
||||
const reason = policy.exclude[name] || (/^(?:export\s*\{\s*\}\s*;?)?$/.test(emitted) ? 'Type-only module; no emitted runtime code.' : undefined)
|
||||
if (reason) {
|
||||
excluded.push({ file: name, sha256, reason })
|
||||
}
|
||||
else {
|
||||
assert(!/\b(?:c8|v8|istanbul)\s+ignore\b/i.test(source), `Undocumented coverage suppression: ${name}`)
|
||||
files.push({ file: name, sha256 })
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const name of Object.keys(policy.exclude))
|
||||
assert(excluded.some(item => item.file === name), `Stale coverage exclusion: ${name}`)
|
||||
for (const name of Object.keys(policy.minimum))
|
||||
assert(files.some(item => item.file === name), `Critical coverage file is absent or excluded: ${name}`)
|
||||
return { files: files.sort((a, b) => a.file.localeCompare(b.file)), excluded }
|
||||
}
|
||||
|
||||
export function analyzeCoverage(root, summary, inventory, policy) {
|
||||
const normalized = new Map(Object.entries(summary).filter(([file]) => file !== 'total').map(([file, value]) => [relative(root, file), value]))
|
||||
assert.deepEqual([...normalized.keys()].sort(), inventory.files.map(item => item.file).sort(), 'Coverage source inventory differs; a source map or zero-coverage file is missing')
|
||||
const violations = []
|
||||
const files = inventory.files.map((source) => {
|
||||
const metrics = normalized.get(source.file)
|
||||
const values = {}
|
||||
for (const metric of ['lines', 'branches', 'functions', 'statements']) {
|
||||
const { covered, total, skipped } = metrics[metric]
|
||||
assert(Number.isInteger(total) && total >= 0 && Number.isInteger(covered) && covered >= 0 && covered <= total && skipped === 0, `Invalid or ignored ${metric}: ${source.file}`)
|
||||
const percent = total ? covered * 100 / total : 100
|
||||
values[metric] = { covered, total, percent }
|
||||
const minimum = policy.minimum[source.file]?.[metric]
|
||||
if (minimum !== undefined && (total === 0 || percent < minimum))
|
||||
violations.push({ file: source.file, metric, minimum, actual: percent, total })
|
||||
}
|
||||
return { ...source, metrics: values }
|
||||
})
|
||||
return { files, excluded: inventory.excluded, minimum: policy.minimum, violations }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { normalizeCoverageReports } from './coverage-maps.mjs'
|
||||
import { analyzeCoverage, sourceInventory } from './coverage-report.mjs'
|
||||
|
||||
const root = fileURLToPath(new URL('../', import.meta.url))
|
||||
const require = createRequire(import.meta.url)
|
||||
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
const write = (file, data) => fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
|
||||
|
||||
export function runCoverage() {
|
||||
assert.equal(process.env.npm_config_user_agent?.split(' ')[0], 'yarn/1.22.22', 'Run yarn test:coverage with the pinned Yarn')
|
||||
const policy = read(path.join(root, 'scripts/coverage-policy.json'))
|
||||
const inventory = sourceInventory(root, policy)
|
||||
const unit = read(path.join(root, 'package.json')).scripts['test:unit']
|
||||
assert(unit.startsWith('node --test '), 'Coverage expects the shared explicit Node unit test list')
|
||||
const tests = unit.slice('node --test '.length).split(' ')
|
||||
assert(tests.length > 0 && tests.every(file => /^test\/[a-z-]+\.test\.js$/.test(file)), 'Unsupported unit test command; update the coverage runner with it')
|
||||
const cache = path.join(root, 'refactor/.cache/coverage')
|
||||
fs.mkdirSync(cache, { recursive: true })
|
||||
const output = fs.mkdtempSync(path.join(cache, 'run-'))
|
||||
const configFile = path.join(output, 'c8.json')
|
||||
const config = {
|
||||
'all': true,
|
||||
'exclude-after-remap': true,
|
||||
'include': policy.packages.map(name => `packages/${name}/src/**`),
|
||||
'exclude': inventory.excluded.map(item => item.file),
|
||||
'src': policy.packages.map(name => `packages/${name}/src`),
|
||||
'extension': ['.js', '.ts'],
|
||||
'reporter': ['json', 'json-summary', 'text-summary', 'html', 'lcovonly'],
|
||||
'reports-dir': path.join(output, 'report'),
|
||||
'temp-directory': path.join(output, 'normalized'),
|
||||
}
|
||||
write(configFile, config)
|
||||
write(path.join(output, 'inventory.json'), inventory)
|
||||
const started = new Date().toISOString()
|
||||
const moduleDirectory = path.join(output, 'modules')
|
||||
const rawDirectory = path.join(output, 'raw')
|
||||
const result = spawnSync(process.execPath, ['--test', ...tests], {
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, NODE_V8_COVERAGE: rawDirectory, ARTPLAYER_COVERAGE_DIR: moduleDirectory },
|
||||
})
|
||||
const report = { task: 'ENG-08', started, node: process.version, c8: read(require.resolve('c8/package.json')).version, output: path.relative(root, output).replaceAll('\\', '/'), tests, testExitCode: result.status, status: 'failed' }
|
||||
write(path.join(cache, 'latest.json'), { output: report.output })
|
||||
write(path.join(output, 'report.json'), report)
|
||||
if (result.error || result.status !== 0) {
|
||||
throw result.error || new Error(`Coverage unit tests failed: ${result.status}`)
|
||||
}
|
||||
report.maps = normalizeCoverageReports(rawDirectory, config['temp-directory'], { root, moduleDirectory, inventory })
|
||||
write(path.join(output, 'report.json'), report)
|
||||
const rendered = spawnSync(process.execPath, [require.resolve('c8/bin/c8.js'), 'report', '--config', configFile], { cwd: root, stdio: 'inherit' })
|
||||
assert(!rendered.error && rendered.status === 0, 'Coverage report generation failed')
|
||||
Object.assign(report, analyzeCoverage(root, read(path.join(output, 'report/coverage-summary.json')), inventory, policy))
|
||||
report.status = report.violations.length ? 'failed' : 'passed'
|
||||
write(path.join(output, 'report.json'), report)
|
||||
console.log(`Coverage: ${report.files.length} runtime files, ${report.excluded.length} documented/type-only exclusions; ${report.violations.length} gate violations. Report: ${report.output}/report/index.html`)
|
||||
assert.deepEqual(report.violations, [], 'Critical lifecycle coverage is below the configured gate')
|
||||
return report
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
try {
|
||||
assert.equal(process.argv.length, 2, 'Use yarn test:coverage')
|
||||
runCoverage()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { ensureArchive, hash, readMember } from '../refactor/scripts/releases.mjs'
|
||||
import { measureBytes } from '../refactor/scripts/sizes.mjs'
|
||||
|
||||
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
const walk = directory => fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)])
|
||||
function digest(file) {
|
||||
const bytes = fs.readFileSync(file)
|
||||
return hash(/\.(?:[cm]?[jt]s|json|css|less|html|svg|txt)$/.test(file) ? bytes.toString('utf8').replaceAll('\r\n', '\n') : bytes)
|
||||
}
|
||||
|
||||
export function verifyPerformanceArtifacts(root, mapFile) {
|
||||
assert(mapFile, 'Run yarn test:package and set ARTPLAYER_BROWSER_ARTIFACTS to its browser-artifacts.json')
|
||||
const directory = path.dirname(path.resolve(mapFile))
|
||||
const artifacts = read(mapFile)
|
||||
const report = read(path.join(directory, 'report.json'))
|
||||
assert.equal(report.task, 'ENG-07', 'Performance requires isolated installation evidence')
|
||||
assert.equal(report.knownTypeBlockers, 0)
|
||||
assert.equal(report.node, process.versions.node, 'Build and benchmark Node versions differ')
|
||||
const toolchain = {}
|
||||
const currentManifest = read(path.join(root, 'package.json'))
|
||||
const buildManifest = read(path.join(directory, 'build/package.json'))
|
||||
for (const key of ['packageManager', 'devDependencies', 'resolutions'])
|
||||
assert.deepEqual(buildManifest[key], currentManifest[key], `Build toolchain changed: ${key}`)
|
||||
for (const file of ['yarn.lock', 'scripts/build.js', 'scripts/utils.js', 'scripts/projects.js']) {
|
||||
toolchain[file] = digest(path.join(root, file))
|
||||
assert.equal(digest(path.join(directory, 'build', file)), toolchain[file], `Stale build input: ${file}`)
|
||||
}
|
||||
const inputs = []
|
||||
for (const name of ['artplayer', 'artplayer-plugin-chapter']) {
|
||||
const pkg = report.packages.find(pkg => pkg.name === name)
|
||||
assert(pkg && artifacts[name], `Missing installed package: ${name}`)
|
||||
const file = path.resolve(directory, artifacts[name])
|
||||
const member = `package/dist/${path.basename(file)}`
|
||||
assert.equal(hash(fs.readFileSync(file)), pkg.files[member], `Installed performance artifact changed: ${name}`)
|
||||
const sourceHashes = {}
|
||||
for (const folder of ['src', 'public']) {
|
||||
const current = path.join(root, 'packages', name, folder)
|
||||
const snapshot = path.join(directory, 'build/packages', name, folder)
|
||||
if (!fs.existsSync(current)) {
|
||||
assert(!fs.existsSync(snapshot), `Source directory was removed: ${name}/${folder}`)
|
||||
continue
|
||||
}
|
||||
const inventory = location => Object.fromEntries(walk(location).map(file => [path.relative(location, file).replaceAll('\\', '/'), digest(file)]))
|
||||
const expected = inventory(current)
|
||||
assert.deepEqual(inventory(snapshot), expected, `Stale package source: ${name}/${folder}; rerun yarn test:package`)
|
||||
Object.assign(sourceHashes, Object.fromEntries(Object.entries(expected).map(([file, digest]) => [`${folder}/${file}`, digest])))
|
||||
}
|
||||
for (const file of ['package.json', 'THIRD_PARTY_NOTICES', 'tsconfig.json']) {
|
||||
const current = path.join(root, 'packages', name, file)
|
||||
const snapshot = path.join(directory, 'build/packages', name, file)
|
||||
assert.equal(fs.existsSync(current), fs.existsSync(snapshot), `Package input removed or added: ${name}/${file}`)
|
||||
if (fs.existsSync(current)) {
|
||||
sourceHashes[file] = digest(current)
|
||||
assert.equal(digest(snapshot), sourceHashes[file], `Stale package input: ${name}/${file}`)
|
||||
}
|
||||
}
|
||||
inputs.push({ name, file, member, sha256: pkg.files[member], archiveSha256: pkg.sha256, sourceHashes })
|
||||
}
|
||||
return { directory, inputs, toolchain, packages: report.packages }
|
||||
}
|
||||
|
||||
export async function compareArtifactSizes(root, artifacts) {
|
||||
const releases = read(path.join(root, 'refactor/baselines/releases.json')).releases
|
||||
const comparisons = []
|
||||
for (const pkg of artifacts.packages) {
|
||||
const release = releases.find(release => release.name === pkg.name)
|
||||
const archive = await ensureArchive(release)
|
||||
for (const extension of ['js', 'legacy.js', 'mjs']) {
|
||||
const member = `package/dist/${pkg.name}.${extension}`
|
||||
assert(release.files[member] && pkg.files[member], `Missing matching size entry: ${member}`)
|
||||
const before = measureBytes(readMember(archive, member))
|
||||
const after = measureBytes(fs.readFileSync(path.join(artifacts.directory, 'artifacts', pkg.name, member.slice('package/'.length))))
|
||||
assert.equal(before.sha256, release.files[member])
|
||||
assert.equal(after.sha256, pkg.files[member])
|
||||
const reviewSignals = ['raw', 'gzip9', 'brotli6'].flatMap((metric) => {
|
||||
const deltaBytes = after[metric] - before[metric]
|
||||
const allowanceBytes = Math.max(before[metric] * 0.05, 1024)
|
||||
return deltaBytes > allowanceBytes ? [{ metric, deltaBytes, allowanceBytes }] : []
|
||||
})
|
||||
comparisons.push({ name: pkg.name, member, before, after, reviewSignals })
|
||||
}
|
||||
}
|
||||
return comparisons
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// Reuse the frozen BASE-06 measurement body; only adapt transport and bundle URLs.
|
||||
const directory = fileURLToPath(new URL('../refactor/fixtures/', import.meta.url))
|
||||
function replaceOnce(source, before, after) {
|
||||
assert.equal(source.split(before).length, 2, `Performance fixture adapter drift: ${before}`)
|
||||
return source.replace(before, after)
|
||||
}
|
||||
|
||||
export function performanceScript() {
|
||||
const source = fs.readFileSync(`${directory}/performance.js`, 'utf8')
|
||||
return replaceOnce(source, 'const response = await fetch(\'/reports/performance\', { method: \'POST\', headers: { \'Content-Type\': \'application/json\' }, body: JSON.stringify(report) })', 'window.artplayerPerformanceReport = report; const response = { ok: true }')
|
||||
}
|
||||
|
||||
export function performanceHtml(variant) {
|
||||
assert(['published', 'candidate'].includes(variant), 'Invalid performance variant')
|
||||
let source = fs.readFileSync(`${directory}/performance.html`, 'utf8')
|
||||
source = replaceOnce(source, '/releases/artplayer/dist/artplayer.js', `/${variant}/artplayer.js`)
|
||||
source = replaceOnce(source, '/releases/artplayer-plugin-chapter/dist/artplayer-plugin-chapter.js', `/${variant}/artplayer-plugin-chapter.js`)
|
||||
source = replaceOnce(source, '/fixtures/performance.js', '/test/performance.js')
|
||||
return source.replaceAll('published performance baseline', `${variant} performance comparison`).replaceAll('Published performance and resource baseline', `${variant}: performance and resource comparison`)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { comparePerformance, validatePerformanceMeasurements } from '../refactor/scripts/performance.mjs'
|
||||
|
||||
export function validatePairedPerformance(report) {
|
||||
assert.equal(report.schemaVersion, 1)
|
||||
assert(report.environment.browser && report.environment.version && report.environment.platform, 'Missing benchmark environment')
|
||||
assert.deepEqual(report.runs.map(({ group, variant }) => ({ group, variant })), Array.from({ length: 3 }, (_, group) => (group % 2 ? ['candidate', 'published'] : ['published', 'candidate']).map(variant => ({ group, variant }))).flat(), 'Require three alternating paired groups')
|
||||
for (const run of report.runs) {
|
||||
validatePerformanceMeasurements(run.measurements)
|
||||
assert.deepEqual(run.measurements.environment, report.runs[0].measurements.environment, 'Different browser/viewport/hardware cohort')
|
||||
assert.deepEqual(run.measurements.scripts, [`/${run.variant}/artplayer.js`, `/${run.variant}/artplayer-plugin-chapter.js`, '/test/performance.js'], 'Unexpected benchmark scripts')
|
||||
if (run.variant === 'candidate') {
|
||||
for (const resource of run.measurements.resources) {
|
||||
assert.deepEqual(resource.immediatelyAfter, [], 'Candidate left active timers at destroy')
|
||||
assert.deepEqual(resource.after.timers, [], 'Candidate retained timers after destroy')
|
||||
assert.deepEqual(resource.after.lateCallbacks, [], 'Candidate executed callbacks after destroy')
|
||||
assert.equal(resource.after.lateResizeEvents, 0, 'Candidate dispatched late resize')
|
||||
}
|
||||
}
|
||||
}
|
||||
const groups = Array.from({ length: 3 }, (_, group) => {
|
||||
const pair = report.runs.filter(run => run.group === group)
|
||||
const before = pair.find(run => run.variant === 'published').measurements
|
||||
const after = pair.find(run => run.variant === 'candidate').measurements
|
||||
return { group, ...comparePerformance(before, after) }
|
||||
})
|
||||
return { groups, reviewRequired: groups.some(group => group.reviewSignals.length > 0) }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { summarizePerformance } from '../refactor/scripts/performance.mjs'
|
||||
|
||||
export function performanceSummary(report) {
|
||||
const lines = [
|
||||
`## ArtPlayer performance: ${report.environment.browser} ${report.environment.version}`,
|
||||
'',
|
||||
`Status: **${report.status}**. Timing and size signals require review; lifecycle checks must pass independently.`,
|
||||
'',
|
||||
'| Group | Configuration | Metric | Published median (ms) | Candidate median (ms) | Review |',
|
||||
'| --- | --- | --- | ---: | ---: | --- |',
|
||||
]
|
||||
for (const group of report.groups) {
|
||||
const before = summarizePerformance(report.runs.find(run => run.group === group.group && run.variant === 'published').measurements)
|
||||
for (const [configuration, metrics] of Object.entries(group.summary)) {
|
||||
for (const [metric, result] of Object.entries(metrics)) {
|
||||
const review = group.reviewSignals.some(signal => signal.configuration === configuration && signal.metric === metric)
|
||||
lines.push(`| ${group.group + 1} | ${configuration} | ${metric} | ${before[configuration][metric].median.toFixed(2)} | ${result.median.toFixed(2)} | ${review ? 'Required' : '-'} |`)
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push('', '| Package entry | Raw bytes (old → new) | gzip 9 | Brotli 6 | Review |', '| --- | ---: | ---: | ---: | --- |')
|
||||
for (const size of report.sizes)
|
||||
lines.push(`| ${size.member} | ${size.before.raw} → ${size.after.raw} | ${size.before.gzip9} → ${size.after.gzip9} | ${size.before.brotli6} → ${size.after.brotli6} | ${size.reviewSignals.length ? 'Required' : '-'} |`)
|
||||
const resources = report.runs.filter(run => run.variant === 'candidate').reduce((count, run) => count + run.measurements.resources.length, 0)
|
||||
lines.push('', `${resources} candidate resource probes passed: no retained timers, late callbacks, RAF, proxy entries, instances or DOM after destruction. This does not measure native heap or GPU memory.`, '', 'One warm-up and five measured samples per configuration, per group; three alternating paired groups. Compression is per file, not HTTP transfer size.', '')
|
||||
return lines.join('\n')
|
||||
}
|
||||
Reference in new issue
Block a user