Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ node_modules/
yalc.lock
coverage/
demo/
bench/results/
.lighthouseci/

# Security: prevent accidental commit of secrets and credentials
.env
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ Captured 2026-04-20 via `yarn test:coverage` (v8 instrumentation, 29 tests acros
| Functions | 13.19% |
| Lines | 9.66% |

## Performance benchmarks

A `bench/` workflow captures repeatable performance baselines for the demo across three layers: library bundle size, Lighthouse CI against a fixed set of UniProt scenarios, and DOM-observed custom milestones (`fetch-and-parse`, `render`, `total`). Run `yarn bench` to produce `bench/results/summary.md`. Reference snapshots live under `bench/baselines/` and are committed; per-run output is gitignored.

See [`bench/README.md`](./bench/README.md) for scenarios, capture procedure, and methodology notes.

## Configuration

You can pass your own configuration to the component using the `config` attribute/property.
Expand Down
87 changes: 87 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Performance benchmarks

1. **Library bundle size** — raw + gzipped bytes from `yarn build`'s `dist/` output. Catches accidental dependency bloat in shippable code.
2. **Lighthouse CI** — runs the demo (`yarn build:demo`, served by `vite preview`) against a fixed list of UniProt accessions. Captures LCP, TBT, CLS, Speed Index, and the overall Performance score.
3. **Custom milestones** — `<protvista-uniprot>` emits three `performance.mark()` calls at lifecycle transitions (`script-start` in `connectedCallback`, `data-loaded` after fetch resolves, `first-render` after Lit commits the manager to the DOM) plus three `performance.measure()` calls between them. Lighthouse's user-timings audit captures these automatically, so they appear next to the headline metrics in `summary.md`.

`fetch-and-parse` (script-start → data-loaded), `render` (data-loaded → first-render), and `total` (script-start → first-render) are the durations surfaced in the report.

## Stability contract

The four mark/measure names — `protvista:script-start`, `protvista:data-loaded`, `protvista:first-render`, plus the three measures derived from them — are part of the component's public observable surface. **Renaming them, moving them to a different lifecycle point, or removing them is a breaking change for performance comparison.** A refactor that changes the conceptual meaning of any mark must update the corresponding baseline.

The marks fire unconditionally (every demo run, every consumer page) — they're cheap (~150 bytes shipped, no work when nobody is observing) and useful for any consumer that wants to profile.

## Run

```bash
yarn bench
```

This builds, measures, and writes:

- `bench/results/bundle-size.json` — file-by-file sizes plus totals
- `bench/results/lighthouse/` — raw LHCI reports + `manifest.json`
- `bench/results/summary.md` — top-line markdown table

`bench/results/` is gitignored. Only `bench/baselines/` is tracked.

You can also run each layer on its own:

```bash
yarn bench:bundle # library only
yarn bench:lighthouse # demo only
yarn bench:summary # re-render summary.md from existing results
```

## Capturing a baseline

Lighthouse numbers are sensitive to machine state. To make a snapshot worth committing:

- Run on a quiet machine, plugged in, no other heavy apps.
- Same Chrome version on every run (LHCI uses the system Chrome).
- 5 runs per URL by default; LHCI picks the representative (median) run.

To pin a snapshot to a known commit:

```bash
yarn bench
SHA=$(git rev-parse --short HEAD)
cp bench/results/summary.md bench/baselines/summary-${SHA}.md
cp bench/results/bundle-size.json bench/baselines/bundle-size-${SHA}.json
git add bench/baselines/summary-${SHA}.md bench/baselines/bundle-size-${SHA}.json
git commit -m "Benchmarks: baseline at ${SHA}"
```

To capture a baseline against an **older** commit (e.g., `main` immediately before a merge), use a worktree so your working checkout stays untouched:

```bash
git worktree add ../protvista-baseline <commit-sha>
cd ../protvista-baseline
yarn install --frozen-lockfile
yarn bench
# copy the snapshot back into the main checkout's bench/baselines/
```

## Comparing

Eyeballing two `summary.md` tables — current run vs. a committed baseline under `bench/baselines/` — is enough most of the time. For raw numbers, `jq` over `bench/results/lighthouse/manifest.json` pulls per-run metrics out of the latest run; `lhci open` will pop the current run's HTML reports in a browser if you want to see Lighthouse's full breakdown for one scenario.

Treat any single-metric delta under ~5% as noise unless it's consistent across all scenarios.

## Editing scenarios

Scenarios are defined in `bench/lighthouserc.cjs` under `ci.collect.url`. Each query string is a UniProt accession — `index.html` reads `?accession=` and renders that protein. Add or remove URLs there.

## Files

| File | Purpose |
| ------------------ | ----------------------------------------------------- |
| `lighthouserc.cjs` | LHCI config: scenarios, run count, throttling preset |
| `bundle-size.mjs` | Walks `dist/`, writes raw + gzip sizes per file |
| `summarize.mjs` | Reads results, writes `summary.md` |
| `run.mjs` | One-shot driver (`yarn bench`) |
| `baselines/` | Committed snapshots — reference points for comparison |
| `results/` | Gitignored — output of the latest run |

The custom marks themselves live in `src/protvista-uniprot.ts`, not in this directory.
Empty file added bench/baselines/.gitkeep
Empty file.
16 changes: 16 additions & 0 deletions bench/baselines/bundle-size-14632a3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"commit": "14632a30376b321fe98b2f33160a094e7dc55c08",
"shortSha": "14632a3",
"capturedAt": "2026-04-29T13:45:49.709Z",
"files": [
{
"file": "protvista-uniprot.mjs",
"raw": 4655449,
"gzip": 1162894
}
],
"total": {
"raw": 4655449,
"gzip": 1162894
}
}
28 changes: 28 additions & 0 deletions bench/baselines/summary-14632a3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Bench results

Captured: 2026-04-29T13:33:27.855Z
Commit: `14632a3`

Numeric cells show `median (min–max)`.

## Lighthouse (5 runs)

| Scenario | Perf | LCP | TBT | CLS | Speed Index |
|---|---|---|---|---|---|
| `accession=P05067` | 69 (38–70) | 5.5 s (5.5–5.8) | 22 ms (18–910) | 0.00 (0.00–0.00) | 2.1 s (2.0–2.9) |
| `accession=P38398` | 52 (45–53) | 46.4 s (46.2–46.7) | 266 ms (260–403) | 0.00 (0.00–0.00) | 4.2 s (3.7–4.6) |
| `accession=A0A2K5ULD0` | 80 (78–82) | 2.4 s (2.2–2.6) | 35 ms (16–42) | 0.00 (0.00–0.00) | 2.2 s (2.0–2.2) |

### Custom milestones (5 runs)

| Scenario | fetch-and-parse | render | total |
|---|---|---|---|
| `accession=P05067` | 1.7 s (1.6–2.3) | 7 ms (7–8) | 1.7 s (1.6–2.3) |
| `accession=P38398` | 5.2 s (4.3–5.8) | 7 ms (6–9) | 5.2 s (4.3–5.8) |
| `accession=A0A2K5ULD0` | 1.8 s (1.4–1.9) | 11 ms (9–12) | 1.8 s (1.5–1.9) |

## Bundle size (library, `dist/`)

| Total raw | Total gzip | Files |
|---|---|---|
| 4546.3 KB | 1135.6 KB | 1 |
88 changes: 88 additions & 0 deletions bench/bundle-size.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Measure raw + gzipped size of the library build (dist/).
*
* Writes bench/results/bundle-size.json:
* { commit, shortSha, capturedAt, files: [{ file, raw, gzip }], total }
*
* Run after `yarn build`. The `bench:bundle` script in package.json
* does both in one go.
*/
import {
readdirSync,
readFileSync,
writeFileSync,
mkdirSync,
existsSync,
} from 'node:fs';
import { gzipSync } from 'node:zlib';
import { join, relative } from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const root = join(fileURLToPath(import.meta.url), '..', '..');
const distDir = join(root, 'dist');
const outDir = join(root, 'bench/results');

if (!existsSync(distDir)) {
console.error(
`error: ${relative(root, distDir)} does not exist. Run \`yarn build\` first.`
);
process.exit(1);
}

mkdirSync(outDir, { recursive: true });

// Walk dist/ and collect anything a consumer would actually ship.
const SHIPPABLE = /\.(m?js|css)$/;
function walk(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = join(dir, entry.name);
if (entry.isDirectory()) return walk(full);
if (SHIPPABLE.test(entry.name)) return [full];
return [];
});
}

const files = walk(distDir).map((path) => {
const buf = readFileSync(path);
return {
file: relative(distDir, path),
raw: buf.length,
gzip: gzipSync(buf).length,
};
});

const total = files.reduce(
(acc, f) => ({ raw: acc.raw + f.raw, gzip: acc.gzip + f.gzip }),
{ raw: 0, gzip: 0 }
);

// Tag the snapshot with the commit when available — but don't crash if
// git is unavailable (tarball install, shallow CI clone, etc.).
const git = (cmd) => {
try {
return execSync(cmd, { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim();
} catch {
return 'unknown';
}
};
const result = {
commit: git('git rev-parse HEAD'),
shortSha: git('git rev-parse --short HEAD'),
capturedAt: new Date().toISOString(),
files,
total,
};

writeFileSync(
join(outDir, 'bundle-size.json'),
JSON.stringify(result, null, 2)
);

const kb = (b) => (b / 1024).toFixed(1) + ' KB';
console.log(
`bundle-size: ${kb(total.raw)} raw / ${kb(total.gzip)} gzip across ${files.length} files`
);
56 changes: 56 additions & 0 deletions bench/lighthouserc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Lighthouse CI config.
*
* `lhci autorun` will boot `vite preview` against the demo build, run
* Lighthouse N times against each URL, then write reports under
* bench/results/lighthouse/.
*
* Edit `collect.url` to change scenarios. Each query string is a UniProt
* accession; index.html reads `?accession=` and renders that protein.
*/
module.exports = {
ci: {
collect: {
// Built artefact lives in `demo/` (see vite.demo.config.mjs).
// --strictPort makes startup fail loudly if 4173 is busy instead of
// silently moving to another port that the URLs below won't match.
startServerCommand:
'npx vite preview --config vite.demo.config.mjs --port 4173 --strictPort',
startServerReadyPattern: 'Local:',
// The component emits `protvista:*` performance marks/measures
// unconditionally; Lighthouse captures them via its user-timings
// audit and `bench/summarize.mjs` surfaces them in summary.md.
url: [
// Well-annotated default — features, variants, structure.
'http://localhost:4173/?accession=P05067',
// Heavy entry — many variants, 3D Beacons.
'http://localhost:4173/?accession=P38398',
// Sparse entry — minimal feature load.
'http://localhost:4173/?accession=A0A2K5ULD0',
],
// 5 runs per URL — LHCI takes the median, this smooths out the
// noise floor more than the default 3 without doubling wall time.
numberOfRuns: 5,
settings: {
// Library demo, not a PWA — only the perf category is meaningful.
onlyCategories: ['performance'],
// Researchers use this on desktop; mobile throttling distorts the
// signal we care about.
preset: 'desktop',
// Be explicit so two machines on different Chrome versions still
// produce comparable numbers.
chromeFlags: '--headless=new --no-sandbox',
// Default is 45000 ms; the heavy variation payload on P38398
// sometimes runs right at that edge and Lighthouse marks the
// whole run as a page-load failure (Perf=0, all audits empty).
// 60 s gives those scenarios room to finish.
maxWaitForLoad: 60000,
},
},
upload: {
target: 'filesystem',
outputDir: './bench/results/lighthouse',
reportFilenamePattern: '%%PATHNAME%%-%%DATETIME%%-%%EXTENSION%%',
},
},
};
23 changes: 23 additions & 0 deletions bench/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node
/**
* One-shot driver: bundle size, then Lighthouse, then summary.
* Equivalent to `yarn bench:bundle && yarn bench:lighthouse && yarn bench:summary`,
* but keeps the orchestration in one place so CI can call a single script.
*/
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const root = join(fileURLToPath(import.meta.url), '..', '..');
const run = (cmd) => execSync(cmd, { cwd: root, stdio: 'inherit' });

console.log('▶ bundle size');
run('yarn bench:bundle');

console.log('▶ lighthouse');
run('yarn bench:lighthouse');

console.log('▶ summary');
run('yarn bench:summary');

console.log('\n✔ done — see bench/results/summary.md');
Loading
Loading