Skip to content

Commit ca9886c

Browse files
authored
[68] Add construction cost benchmark (#69)
* add construction cost benchmark
1 parent d075df8 commit ca9886c

4 files changed

Lines changed: 80 additions & 10 deletions

File tree

benchmarking/README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ npm run bench --workspace=benchmarking
1515

1616
Isolates the cost of `PartialMatchRegExp`'s `exec()` override. All three candidates run the same underlying partial pattern against the same input — the only variable is whether a JavaScript wrapper sits in the call chain:
1717

18-
| Candidate | Notes |
19-
|---|---|
20-
| Native `RegExp.exec` | Baseline — no partial transform, no override |
18+
| Candidate | Notes |
19+
| ------------------------- | -------------------------------------------------------------- |
20+
| Native `RegExp.exec` | Baseline — no partial transform, no override |
2121
| `compilePartial()` result | Partial source baked into a plain `RegExp` — no class overhead |
22-
| `PartialMatchRegExp.exec` | Partial source via the class override |
22+
| `PartialMatchRegExp.exec` | Partial source via the class override |
2323

2424
Two input cases are measured: a full match, and a partial input that returns `null` on the native regex.
2525

@@ -38,10 +38,10 @@ Models a user typing character-by-character into a validated input field. Each p
3838

3939
Two patterns are exercised:
4040

41-
| Pattern | Example input | Length |
42-
|---|---|---|
41+
| Pattern | Example input | Length |
42+
| ------------------------ | ------------------- | -------- |
4343
| E.164-style phone number | `+1 (555) 123-4567` | 18 chars |
44-
| ISO 8601 date | `2024-12-31` | 10 chars |
44+
| ISO 8601 date | `2024-12-31` | 10 chars |
4545

4646
Each group compares native `test` (always returns `false` for incomplete input), a plain partial `RegExp`, and `PartialMatchRegExp` on the fast path.
4747

@@ -51,13 +51,31 @@ When `exec()` is called on a partial input that contains backreferences, the pat
5151

5252
Two patterns are used to cover different positions within a backreference:
5353

54-
| Pattern | Example |
55-
|---|---|
56-
| Repeated word (`/^(\w+) \1$/`) | `"foo foo"` |
54+
| Pattern | Example |
55+
| ------------------------------------------------- | -------------------- |
56+
| Repeated word (`/^(\w+) \1$/`) | `"foo foo"` |
5757
| HTML open/close tag (`/^<([a-z]+)>[^<]+<\/\1>$/`) | `"<div>hello</div>"` |
5858

5959
Each pattern is measured at three stages — full match (native fast path), partial input before the backreference atom is reached, and partial input mid-backreference — plus an accumulated keystroke simulation that sums the cost over all prefixes.
6060

61+
### 5. Construction cost (`construction-cost.bench.ts`)
62+
63+
Scenarios 1-4 build every candidate once outside the timed loop, so they never see the cost of `compilePartial()`'s walk()/render() pass — the one-time parsing work done per `new PartialMatchRegExp()`. This scenario isolates that cost so walk additions can be tracked independently of the exec-time scenarios above.
64+
65+
| Candidate | Notes |
66+
| -------------------------- | --------------------------------------------- |
67+
| Native `new RegExp()` | Baseline — no parsing beyond V8's own compile |
68+
| `compilePartial()` | Walk + render, no class overhead |
69+
| `new PartialMatchRegExp()` | `compilePartial()` plus class construction |
70+
71+
Three patterns span the complexity range the walker branches on:
72+
73+
| Pattern | Notes |
74+
| --------------------- | ---------------------------------------------------------------- |
75+
| Simple (`/^hello+$/`) | No groups, no character classes, no backreferences |
76+
| Phone number | Several character classes and optional groups, no backreferences |
77+
| HTML tag | Capturing group + backreference — exercises the dynamic path |
78+
6179
## 🤖 CI integration
6280

6381
The workflow at [`.github/workflows/benchmark.yml`](../.github/workflows/benchmark.yml) runs on every push to `main` and on pull requests targeting `main`.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Scenario 5: construction cost
3+
*
4+
* The other scenarios measure exec()/test() on regexes built once outside the
5+
* timed loop, so they never see the cost of compilePartial()'s walk()/render()
6+
* pass. That pass runs once per `new PartialMatchRegExp()` (or per direct
7+
* `compilePartial()` call) and is where per-atom bookkeeping added for new
8+
* features (e.g. the feature-flag scan) actually lands. This scenario isolates
9+
* that one-time cost so it can be tracked independently of the exec-time
10+
* scenarios above.
11+
*
12+
* Baselines:
13+
* - native `new RegExp()` — no parsing beyond V8's own compile
14+
* - `compilePartial()` — walk() + render(), no class wrapper
15+
* - `new PartialMatchRegExp()` — compilePartial() plus class construction
16+
*
17+
* Three patterns span the complexity range the walker branches on:
18+
* - simple: no groups, no character classes, no backreferences
19+
* - phone: several character classes and optional groups, no backreferences
20+
* - HTML tag: capturing group + backreference, exercises the dynamic path
21+
*/
22+
23+
import { bench, group } from "mitata";
24+
import { compilePartial } from "../../src/compilePartial.ts";
25+
import PartialMatchRegExp from "../../src/partialMatchRegExp.ts";
26+
27+
const simplePattern = /^hello+$/;
28+
const phonePattern = /^\+?1?\s?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/;
29+
const htmlTagPattern = /^<([a-zA-Z][\w-]*)(?:\s[^<>]*)?>[^<]+<\/\1>$/;
30+
31+
group("construction — simple pattern (no groups, no backreferences)", () => {
32+
bench("native new RegExp()", () => new RegExp(simplePattern));
33+
bench("compilePartial()", () => compilePartial(simplePattern));
34+
bench("new PartialMatchRegExp()", () => new PartialMatchRegExp(simplePattern));
35+
});
36+
37+
group("construction — phone pattern (character classes, optional groups)", () => {
38+
bench("native new RegExp()", () => new RegExp(phonePattern));
39+
bench("compilePartial()", () => compilePartial(phonePattern));
40+
bench("new PartialMatchRegExp()", () => new PartialMatchRegExp(phonePattern));
41+
});
42+
43+
group("construction — HTML tag pattern (capturing group + backreference)", () => {
44+
bench("native new RegExp()", () => new RegExp(htmlTagPattern));
45+
bench("compilePartial()", () => compilePartial(htmlTagPattern));
46+
bench("new PartialMatchRegExp()", () => new PartialMatchRegExp(htmlTagPattern));
47+
});

benchmarking/src/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import "./dispatch-overhead.bench.ts";
1818
import "./hot-loop.bench.ts";
1919
import "./keystroke.bench.ts";
2020
import "./backref-slow-path.bench.ts";
21+
import "./construction-cost.bench.ts";
2122
import { run } from "mitata";
2223

2324
const isJson = process.argv.includes("--json");

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Benchmark for construction cost
13+
1014
## [1.0.0] - 2026-07-22
1115

1216
### Fixed

0 commit comments

Comments
 (0)