|
| 1 | +# Benchmarks |
| 2 | + |
| 3 | +Performance benchmarks for `webpack-sources`, tracked over time via |
| 4 | +[CodSpeed](https://codspeed.io/). |
| 5 | + |
| 6 | +Runner stack: [tinybench](https://github.com/tinylibs/tinybench) + |
| 7 | +[`@codspeed/core`](https://www.npmjs.com/package/@codspeed/core) with a local |
| 8 | +`withCodSpeed()` wrapper ported from webpack's |
| 9 | +`test/BenchmarkTestCases.benchmark.mjs` (also used by `enhanced-resolve`). |
| 10 | +Locally it falls back to plain tinybench wall-clock measurements, and under |
| 11 | +`CodSpeedHQ/action` in CI it automatically switches to CodSpeed's |
| 12 | +instrumentation mode. |
| 13 | + |
| 14 | +## Running locally |
| 15 | + |
| 16 | +```sh |
| 17 | +npm run benchmark |
| 18 | +``` |
| 19 | + |
| 20 | +Optional substring filter to run only matching cases: |
| 21 | + |
| 22 | +```sh |
| 23 | +npm run benchmark -- replace-source |
| 24 | +BENCH_FILTER=source-map npm run benchmark |
| 25 | +``` |
| 26 | + |
| 27 | +Locally the runner uses tinybench's wall-clock measurements and prints a |
| 28 | +table of ops/s, mean, p99, and relative margin of error per task. Under CI, |
| 29 | +the wrapper detects the CodSpeed runner environment and switches to |
| 30 | +instruction-counting mode automatically. |
| 31 | + |
| 32 | +The V8 flags in `package.json` (`--no-opt --predictable --hash-seed=1` etc.) |
| 33 | +are required by CodSpeed's instrumentation mode for deterministic results — |
| 34 | +do not drop them. |
| 35 | + |
| 36 | +### Optional: running real instruction counts locally |
| 37 | + |
| 38 | +If you want to reproduce CI's exact instrument-count numbers on your own |
| 39 | +machine (Linux only — the underlying Valgrind tooling has no macOS backend), |
| 40 | +install the standalone CodSpeed CLI and wrap `npm run benchmark` with it: |
| 41 | + |
| 42 | +```sh |
| 43 | +curl -fsSL https://codspeed.io/install.sh | bash |
| 44 | +codspeed run npm run benchmark |
| 45 | +``` |
| 46 | + |
| 47 | +This is only useful if you want to debug an instruction-count regression |
| 48 | +outside CI. Day-to-day benchmark iteration should use `npm run benchmark` |
| 49 | +directly (wall-clock mode). |
| 50 | + |
| 51 | +## Layout |
| 52 | + |
| 53 | +``` |
| 54 | +benchmark/ |
| 55 | +├── run.mjs # entry point: discovers cases, runs bench |
| 56 | +├── with-codspeed.mjs # CodSpeed <-> tinybench bridge |
| 57 | +├── fixtures.mjs # shared fixture loaders |
| 58 | +└── cases/ |
| 59 | + └── <case-name>/ |
| 60 | + ├── index.bench.mjs # default export: register(bench, ctx) |
| 61 | + └── fixture/ # optional: per-case input files |
| 62 | +``` |
| 63 | + |
| 64 | +Each case directory must contain `index.bench.mjs` exporting a default |
| 65 | +function with the signature: |
| 66 | + |
| 67 | +```js |
| 68 | +export default function register(bench, { caseName, caseDir, fixtureDir }) { |
| 69 | + bench.add("my case: descriptive name", () => { |
| 70 | + // ... code to measure ... |
| 71 | + }); |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +`fixtureDir` is the absolute path to the case's `fixture/` subdirectory |
| 76 | +(which may or may not exist). `caseDir` is the parent directory containing |
| 77 | +`index.bench.mjs`. |
| 78 | + |
| 79 | +Each task body should loop over a small batch of operations rather than |
| 80 | +performing a single call — tinybench decides its own iteration count, so |
| 81 | +we want the measurement to reflect per-batch throughput, which is more |
| 82 | +stable than per-call timing for sub-microsecond work. |
| 83 | + |
| 84 | +## Existing cases |
| 85 | + |
| 86 | +### Per source class |
| 87 | + |
| 88 | +| Case | What it measures | |
| 89 | +| ------------------- | -------------------------------------------------------------------------------- | |
| 90 | +| `raw-source` | `RawSource` constructor, string/buffer accessors, streamChunks, updateHash | |
| 91 | +| `original-source` | `OriginalSource` map/sourceAndMap/streamChunks across columns on/off combos | |
| 92 | +| `replace-source` | `ReplaceSource` source/map/streamChunks for no, few, and many replacements | |
| 93 | +| `concat-source` | `ConcatSource` _optimize, source/buffer/map, nested flattening, hash | |
| 94 | +| `prefix-source` | `PrefixSource` delegation + newline prefix rewriting | |
| 95 | +| `source-map-source` | `SourceMapSource` full + lines-only streamChunks, including combined inner maps | |
| 96 | +| `cached-source` | `CachedSource` cold vs warm, plus `getCachedData()` round-trip | |
| 97 | +| `compat-source` | `CompatSource` delegation vs `Source.prototype` fallback paths | |
| 98 | +| `size-only-source` | `SizeOnlySource` constructor, `size()`, and the throw paths for other accessors | |
| 99 | + |
| 100 | +### Per helper module |
| 101 | + |
| 102 | +| Case | What it measures | |
| 103 | +| ------------------------------------- | -------------------------------------------------------------------------- | |
| 104 | +| `helpers-split-into-lines` | `splitIntoLines` scanner on fixture / big / long-line / empty inputs | |
| 105 | +| `helpers-split-into-potential-tokens` | `splitIntoPotentialTokens` scanner used by column-aware OriginalSource | |
| 106 | +| `helpers-get-generated-source-info` | `getGeneratedSourceInfo` final-line/column probe on various input shapes | |
| 107 | +| `helpers-read-mappings` | VLQ decoder used by every source-map aware streamChunks path | |
| 108 | +| `helpers-create-mappings-serializer` | VLQ encoder (full + lines-only) fed a representative event stream | |
| 109 | +| `helpers-string-buffer-utils` | `internString` and enter/exitStringInterningRange | |
| 110 | + |
| 111 | +### End-to-end |
| 112 | + |
| 113 | +| Case | What it measures | |
| 114 | +| --------------------------------- | ----------------------------------------------------------------------------- | |
| 115 | +| `realistic-source-map-pipeline` | OriginalSource -> ReplaceSource -> ConcatSource -> CachedSource (cold + warm) | |
| 116 | + |
| 117 | +## Adding a new case |
| 118 | + |
| 119 | +1. Create `benchmark/cases/<case-name>/index.bench.mjs`. |
| 120 | +2. Export a default `register(bench, ctx)` function. Call `bench.add(name, fn)` |
| 121 | + for each task. |
| 122 | +3. If the case needs input files, place them under |
| 123 | + `benchmark/cases/<case-name>/fixture/` and read them from `ctx.fixtureDir`. |
| 124 | +4. Run `npm run benchmark -- <case-name>` to verify locally. |
| 125 | + |
| 126 | +Each task name should start with the case directory name (e.g. |
| 127 | +`raw-source: source()`) so CodSpeed's report groups tasks by module. |
0 commit comments