@progen-labs/spectra 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -4
- package/bin/spectra.js +87 -7
- package/dist/assets/{index-COnXcwA_.js → index-DY8VmG9y.js} +1 -1
- package/dist/assets/index-Dx1YSFIx.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +9 -6
- package/schema/config.schema.json +94 -0
- package/schema/result.schema.json +749 -0
- package/server/args.js +29 -0
- package/server/cli.js +143 -67
- package/server/compare-cli.js +126 -0
- package/server/explore/evaluate-cli.js +72 -40
- package/server/output.js +50 -0
- package/server/project-config.js +30 -0
- package/server/recorder.js +4 -1
- package/server/reports-cli.js +31 -0
- package/server/test-result.js +226 -0
- package/shared/compare.js +1 -0
- package/shared/result.js +291 -0
- package/src/styles.css +20 -3
- package/dist/assets/index-DxxJOPn9.css +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ and per-component render/change-detection time; source files too, where the fram
|
|
|
14
14
|
functions. A [Progen Labs](https://github.com/progen-labs) project — open source, non-profit, no
|
|
15
15
|
telemetry, no account required.
|
|
16
16
|
|
|
17
|
+
**Documentation:** the full guide, with annotated screenshots of every screen, is at
|
|
18
|
+
[progen-labs.github.io/website](https://progen-labs.github.io/website/#spectra/docs/introduction).
|
|
19
|
+
|
|
17
20
|
## Install
|
|
18
21
|
|
|
19
22
|
```bash
|
|
@@ -52,15 +55,23 @@ Then open the visual dashboard — ranked culprits, React re-renders, long tasks
|
|
|
52
55
|
**PDF export** and **commit-to-commit compare**:
|
|
53
56
|
|
|
54
57
|
```bash
|
|
55
|
-
npx spectra dashboard
|
|
58
|
+
npx spectra dashboard --open
|
|
56
59
|
```
|
|
57
60
|
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
A [tour of the dashboard](https://progen-labs.github.io/website/#spectra/docs/dashboard), with every part
|
|
64
|
+
labelled, is in the docs.
|
|
65
|
+
|
|
58
66
|
## Commands
|
|
59
67
|
|
|
60
68
|
```
|
|
61
|
-
spectra performance [options]
|
|
62
|
-
spectra
|
|
63
|
-
spectra
|
|
69
|
+
spectra performance [options] Golden-path evaluation (default)
|
|
70
|
+
spectra performance --quick One page-load profile (about 15 s)
|
|
71
|
+
spectra compare <before> <after> Did it get faster? Noise-aware before/after
|
|
72
|
+
spectra reports List saved reports
|
|
73
|
+
spectra dashboard [--port N] [--open] Open the dashboard UI
|
|
74
|
+
spectra --help Full option list
|
|
64
75
|
spectra --version
|
|
65
76
|
```
|
|
66
77
|
|
|
@@ -74,7 +85,70 @@ spectra --version
|
|
|
74
85
|
| `--boot-timeout <s>` | How long to wait for the dev server (default 60) |
|
|
75
86
|
| `--headed` | Show the browser window |
|
|
76
87
|
| `--sign-in <url>` | Sign-in URL template, e.g. `http://host/login/?redirect={url}` |
|
|
88
|
+
| `--quick` | One page-load profile instead of the golden-path suite: the fast loop |
|
|
89
|
+
| `--runs <n>` | Record n times and report the median (with `--quick` or `--mode`) |
|
|
77
90
|
| `--mode live\|load` | Classic single-page profile instead of the golden-path suite |
|
|
91
|
+
| `--json` | Print a machine-readable result on stdout (tables go to stderr) |
|
|
92
|
+
| `--out <file>` | Also write that result to a file (an input for `compare`) |
|
|
93
|
+
| `--config <file>` | Budgets file (default: `spectra.config.json` in your project) |
|
|
94
|
+
|
|
95
|
+
## Measure, change, verify
|
|
96
|
+
|
|
97
|
+
Performance numbers are noisy, so Spectra never trusts a single recording when you ask it whether a change
|
|
98
|
+
helped. Record a few runs before and after, then compare:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx spectra performance --quick --runs 5 --out before.json
|
|
102
|
+
# …make your change…
|
|
103
|
+
npx spectra performance --quick --runs 5 --out after.json
|
|
104
|
+
npx spectra compare before.json after.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Spectra compare — IMPROVED (confidence: medium)
|
|
109
|
+
✓ JavaScript CPU time: 1.52 s → 92.1 ms (−94%)
|
|
110
|
+
✓ Row (src/Row.jsx) — render time: 1.44 s → 7.4 ms (−99%)
|
|
111
|
+
|
|
112
|
+
Findings: 3 resolved · 2 new · 10 still present
|
|
113
|
+
resolved [critical] Row is slow to render: 1444ms total, 0.2ms per render (7200×)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Each side is reduced to its median, and a difference only counts when it is larger than the run-to-run
|
|
117
|
+
spread. A side can be a `--out` result file, a report id from `spectra reports`, a report file, or several
|
|
118
|
+
ids joined by commas.
|
|
119
|
+
|
|
120
|
+
## Budgets and CI
|
|
121
|
+
|
|
122
|
+
Put limits in `spectra.config.json` at your project root:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"$schema": "./node_modules/@progen-labs/spectra/schema/config.schema.json",
|
|
127
|
+
"budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every `spectra performance` run checks them: the median for `--quick`/`--mode`, every journey for the
|
|
132
|
+
golden-path suite. Metrics a recording did not measure are skipped, never failed. Available budgets:
|
|
133
|
+
`tbtMs`, `lcpMs`, `fcpMs`, `cls`, `inpMs`, `ttfbMs`, `longTaskCount`, `maxLongTaskMs`, `scriptingMs`,
|
|
134
|
+
`forcedLayoutCount`, `domNodes`, `heapUsedMb`, `transferKb`, `criticalFindings`, `findingCount`.
|
|
135
|
+
|
|
136
|
+
Exit codes make it a CI gate:
|
|
137
|
+
|
|
138
|
+
| Code | Meaning |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `0` | Done, and within budget |
|
|
141
|
+
| `1` | Could not run (no Chrome, server down, bad config) |
|
|
142
|
+
| `2` | Ran, but a budget was exceeded, or `compare --fail-on-regression` found a regression |
|
|
143
|
+
|
|
144
|
+
## Machine-readable output
|
|
145
|
+
|
|
146
|
+
`--json` prints a small, versioned result on stdout and sends every human-readable line to stderr, so it can
|
|
147
|
+
be piped into `jq`, an editor or an AI agent. Each result has `schemaVersion` and a `kind` (`profile`,
|
|
148
|
+
`evaluation`, `compare` or `reports`) and points at the full reports on disk rather than embedding them.
|
|
149
|
+
Findings carry the file, line, cost in ms and a suggested fix. The format is described by a JSON Schema
|
|
150
|
+
shipped with the package (`schema/result.schema.json`); additive changes keep `schemaVersion`, breaking
|
|
151
|
+
changes bump it. Saved reports carry their own `schemaVersion` too.
|
|
78
152
|
|
|
79
153
|
## Safe by default
|
|
80
154
|
|
|
@@ -97,6 +171,18 @@ that cost the most main-thread time. See the
|
|
|
97
171
|
[deep-dive docs](https://github.com/progen-labs/spectra/blob/main/DOCS.md) for how capture, correlation
|
|
98
172
|
and report generation work.
|
|
99
173
|
|
|
174
|
+
## Documentation
|
|
175
|
+
|
|
176
|
+
- **[The guide](https://progen-labs.github.io/website/#spectra/docs/introduction)**: every feature, with
|
|
177
|
+
annotated screenshots. Start with
|
|
178
|
+
[Quick start](https://progen-labs.github.io/website/#spectra/docs/quick-start),
|
|
179
|
+
[Compare before and after](https://progen-labs.github.io/website/#spectra/docs/compare),
|
|
180
|
+
[Budgets and CI](https://progen-labs.github.io/website/#spectra/docs/budgets),
|
|
181
|
+
[JSON output](https://progen-labs.github.io/website/#spectra/docs/json-output), the
|
|
182
|
+
[Findings reference](https://progen-labs.github.io/website/#spectra/docs/findings) and
|
|
183
|
+
[Troubleshooting](https://progen-labs.github.io/website/#spectra/docs/troubleshooting).
|
|
184
|
+
- **[DOCS.md](DOCS.md)**: the in-repo manual, the technical deep dive, and how to contribute.
|
|
185
|
+
|
|
100
186
|
## License
|
|
101
187
|
|
|
102
188
|
MIT © Progen Labs
|
package/bin/spectra.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Spectra CLI — a Progen Labs project.
|
|
4
4
|
*
|
|
5
5
|
* spectra performance [--url <page>] [profiler options] profile a frontend
|
|
6
|
+
* spectra compare <before> <after> before vs after, noise-aware
|
|
7
|
+
* spectra reports list saved reports
|
|
6
8
|
* spectra dashboard open the dashboard UI
|
|
7
9
|
* spectra --help show this help
|
|
8
10
|
* spectra --version print the version
|
|
@@ -12,7 +14,7 @@
|
|
|
12
14
|
* root is used for source lookup, unless the matching PERF_* env var overrides it.
|
|
13
15
|
*/
|
|
14
16
|
import { spawn } from 'node:child_process';
|
|
15
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
18
|
import path from 'node:path';
|
|
17
19
|
import { fileURLToPath } from 'node:url';
|
|
18
20
|
import { bootDevServer } from '../server/explore/boot.js';
|
|
@@ -33,6 +35,10 @@ Spectra ${pkg.version} — frontend performance profiler (Progen Labs)
|
|
|
33
35
|
|
|
34
36
|
Usage:
|
|
35
37
|
spectra performance [options] Profile a frontend build and report the culprits
|
|
38
|
+
spectra compare <before> <after> [options]
|
|
39
|
+
Compare two profiles; a change counts only when
|
|
40
|
+
it is bigger than the noise between runs
|
|
41
|
+
spectra reports [--json] List saved reports (ids for compare / dashboard)
|
|
36
42
|
spectra dashboard Open the Spectra dashboard UI
|
|
37
43
|
spectra --help, -h Show this help
|
|
38
44
|
spectra --version, -v Print the version
|
|
@@ -52,18 +58,40 @@ performance options:
|
|
|
52
58
|
--headed Show the browser window
|
|
53
59
|
--sign-in <url> Sign-in URL template, e.g. "http://host/login/?redirect={url}"
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
quick single-page profile (opt in with --quick, --mode or --scenario):
|
|
62
|
+
--quick One page-load profile (about 15 s): the fast loop
|
|
63
|
+
--runs <n> Record n times and report the median (with --quick/--mode)
|
|
56
64
|
--mode live|load Live session or page-load recording
|
|
57
65
|
--duration <seconds> Live recording length (default 8)
|
|
58
66
|
--throttle <n> CPU throttle multiplier (default 1)
|
|
59
67
|
--scenario <file.mjs> Script a live session: export default async page => {…}
|
|
60
68
|
--save-trace Keep the raw Chrome trace alongside the report
|
|
61
69
|
|
|
70
|
+
output and checks (any performance run):
|
|
71
|
+
--json Print a machine-readable result on stdout (progress and
|
|
72
|
+
tables go to stderr)
|
|
73
|
+
--out <file> Also write that result to a file (input for compare)
|
|
74
|
+
--config <file> Budgets file (default: spectra.config.json in the project)
|
|
75
|
+
|
|
76
|
+
compare options:
|
|
77
|
+
<before> <after> Each: a report id, a report file, a --out result file,
|
|
78
|
+
or several ids joined by commas
|
|
79
|
+
--json, --out <file> As above
|
|
80
|
+
--fail-on-regression Exit with code 2 when a key metric got worse
|
|
81
|
+
|
|
82
|
+
Budgets (spectra.config.json in your project root):
|
|
83
|
+
{ "budgets": { "tbtMs": 200, "lcpMs": 2500, "cls": 0.1, "criticalFindings": 0 } }
|
|
84
|
+
|
|
85
|
+
Exit codes: 0 done · 1 could not run · 2 over budget / regression
|
|
86
|
+
|
|
62
87
|
Examples:
|
|
63
88
|
spectra performance # self-boot + golden-path suite
|
|
64
89
|
spectra performance --paths 8 # cap the suite at 8 journeys
|
|
65
90
|
spectra performance --url http://localhost:3000/ # evaluate a running server
|
|
66
|
-
spectra performance --
|
|
91
|
+
spectra performance --quick --runs 5 --out before.json
|
|
92
|
+
spectra performance --quick --runs 5 --out after.json
|
|
93
|
+
spectra compare before.json after.json # did the change help?
|
|
94
|
+
spectra performance --json | jq .summary # pipe it anywhere
|
|
67
95
|
`;
|
|
68
96
|
|
|
69
97
|
function runNodeAsync(scriptRelPath, args, extraEnv = {}) {
|
|
@@ -91,6 +119,19 @@ function takeOption(args, name) {
|
|
|
91
119
|
return { value, rest: copy };
|
|
92
120
|
}
|
|
93
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The scripts run with the package as their working directory, so path
|
|
124
|
+
* options the user typed relative to their own directory are made absolute
|
|
125
|
+
* here, before they cross over.
|
|
126
|
+
*/
|
|
127
|
+
function absolutize(args, names) {
|
|
128
|
+
return args.map((a, i) =>
|
|
129
|
+
i > 0 && names.includes(args[i - 1].replace(/^--/, '')) && args[i - 1].startsWith('--') && !a.startsWith('--')
|
|
130
|
+
? path.resolve(process.cwd(), a)
|
|
131
|
+
: a,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
/**
|
|
95
136
|
* Default the profiled project and the reports location to the user's current
|
|
96
137
|
* directory, so an installed Spectra profiles the host project and writes
|
|
@@ -114,20 +155,38 @@ async function runProfile() {
|
|
|
114
155
|
return 0;
|
|
115
156
|
}
|
|
116
157
|
const env = projectEnv();
|
|
158
|
+
let args = absolutize(rest, ['scenario', 'out', 'config']);
|
|
159
|
+
|
|
160
|
+
// --quick is shorthand for a single page-load profile.
|
|
161
|
+
if (args.includes('--quick')) {
|
|
162
|
+
const mode = takeOption(args, 'mode').value;
|
|
163
|
+
if ((mode && mode !== 'load') || args.includes('--scenario')) {
|
|
164
|
+
console.error('spectra: --quick is a page-load profile; it cannot be combined with --mode live or --scenario.');
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
args = args.filter(a => a !== '--quick');
|
|
168
|
+
if (!mode) args.push('--mode', 'load');
|
|
169
|
+
}
|
|
117
170
|
|
|
118
171
|
// Golden-path evaluation is the default. --mode/--scenario opt into the
|
|
119
172
|
// classic single-page profile instead.
|
|
120
|
-
const classic =
|
|
173
|
+
const classic = args.includes('--mode') || args.includes('--scenario');
|
|
121
174
|
const script = classic ? 'server/cli.js' : 'server/explore/evaluate-cli.js';
|
|
175
|
+
if (!classic && args.includes('--runs')) {
|
|
176
|
+
console.error(
|
|
177
|
+
'spectra: --runs works with single-page profiles (--quick, --mode or --scenario); the golden-path suite records each journey once.',
|
|
178
|
+
);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
122
181
|
|
|
123
182
|
// An explicit --url means the server is already running: run against it.
|
|
124
|
-
if (
|
|
125
|
-
return runNodeAsync(script,
|
|
183
|
+
if (args.includes('--url')) {
|
|
184
|
+
return runNodeAsync(script, args, env);
|
|
126
185
|
}
|
|
127
186
|
|
|
128
187
|
// No --url: self-boot the target project's own dev server, run, stop.
|
|
129
188
|
const cwd = process.cwd();
|
|
130
|
-
const { value: command, rest: afterCmd } = takeOption(
|
|
189
|
+
const { value: command, rest: afterCmd } = takeOption(args, 'command');
|
|
131
190
|
const { value: bootTimeout, rest: profileArgs } = takeOption(afterCmd, 'boot-timeout');
|
|
132
191
|
const fw = detectFramework(cwd);
|
|
133
192
|
const label =
|
|
@@ -164,6 +223,16 @@ async function runProfile() {
|
|
|
164
223
|
return code;
|
|
165
224
|
}
|
|
166
225
|
|
|
226
|
+
function runCompare() {
|
|
227
|
+
// Sides may be files relative to the user's directory (ids pass through).
|
|
228
|
+
const fixToken = t =>
|
|
229
|
+
t.endsWith('.json') || existsSync(path.resolve(process.cwd(), t)) ? path.resolve(process.cwd(), t) : t;
|
|
230
|
+
const args = absolutize(rest, ['out']).map((a, i, all) =>
|
|
231
|
+
a.startsWith('--') || all[i - 1] === '--out' ? a : a.split(',').map(fixToken).join(','),
|
|
232
|
+
);
|
|
233
|
+
return runNodeAsync('server/compare-cli.js', args, projectEnv());
|
|
234
|
+
}
|
|
235
|
+
|
|
167
236
|
function runDashboard() {
|
|
168
237
|
// Standalone server: serves the prebuilt dashboard and the backend API, so an
|
|
169
238
|
// installed Spectra needs no dev toolchain. projectEnv() points it at this
|
|
@@ -194,6 +263,17 @@ switch (command) {
|
|
|
194
263
|
process.exit(1);
|
|
195
264
|
});
|
|
196
265
|
break;
|
|
266
|
+
case 'compare':
|
|
267
|
+
case 'reports': {
|
|
268
|
+
const run = command === 'compare' ? runCompare() : runNodeAsync('server/reports-cli.js', rest, projectEnv());
|
|
269
|
+
run
|
|
270
|
+
.then(code => process.exit(code ?? 0))
|
|
271
|
+
.catch(err => {
|
|
272
|
+
console.error(`spectra: ${err.message}`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
197
277
|
case 'dashboard':
|
|
198
278
|
case 'ui':
|
|
199
279
|
runDashboard();
|