Skip to content

Commit 78abedc

Browse files
committed
Add README perf chart
1 parent cad1d18 commit 78abedc

File tree

3 files changed

+230
-2
lines changed

3 files changed

+230
-2
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ Warning: performance numbers are not conformance claims. This parser is intentio
1616
- `bench/results/latest.md`
1717
- `bench/results/latest.json`
1818

19+
<!-- README_AUTO_SUMMARY:START -->
20+
21+
Source: `bench/results/latest.json` (`stable` profile).
22+
23+
### Parse Throughput (Average Across Fixtures)
24+
25+
| Parser | Avg Throughput (MB/s) | % of leader | Relative chart |
26+
|---|---:|---:|---|
27+
| `ours-fastest` | 1430.30 | 100.00% | `####################` |
28+
| `ours-strictest` | 1387.91 | 97.04% | `###################` |
29+
| `lol-html` | 1145.97 | 80.12% | `################` |
30+
| `lexbor` | 262.85 | 18.38% | `####` |
31+
32+
### Conformance Snapshot
33+
34+
| Profile | nwmatcher | qwery_contextual | html5lib subset |
35+
|---|---:|---:|---:|
36+
| `strictest/fastest` | 20/20 (0 failed) | 54/54 (0 failed) | 539/600 (61 failed) |
37+
38+
Source: `bench/results/external_suite_report.json`
39+
<!-- README_AUTO_SUMMARY:END -->
40+
1941
## Quick Start
2042

2143
```zig

bench/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ zig build tools -- run-benchmarks --profile quick
4646
zig build tools -- run-benchmarks --profile stable
4747
# optionally write a baseline snapshot file for manual comparisons
4848
zig build tools -- run-benchmarks --profile stable --write-baseline
49-
# refresh DOCUMENTATION benchmark snapshot from existing latest.json
49+
# refresh DOCUMENTATION + README benchmark summary from existing latest.json/report files
5050
zig build tools -- sync-docs-bench
5151
```
5252

@@ -61,7 +61,9 @@ Results are written to:
6161
- `bench/results/latest.json`
6262
- `bench/results/latest.md`
6363

64-
`run-benchmarks` also updates the `DOCUMENTATION.md` benchmark snapshot block from `bench/results/latest.json`.
64+
`run-benchmarks` also updates:
65+
- `DOCUMENTATION.md` benchmark snapshot block from `bench/results/latest.json`
66+
- `README.md` auto-summary block from `bench/results/latest.json` and `bench/results/external_suite_report.json` (if present)
6567

6668
The benchmark output also includes a hard gate table:
6769

src/tools/scripts.zig

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const SUITE_RUNNER_BIN = "bench/build/bin/suite_runner";
1515
const repeats: usize = 5;
1616
const DocumentationBenchmarkStartMarker = "<!-- BENCHMARK_SNAPSHOT:START -->";
1717
const DocumentationBenchmarkEndMarker = "<!-- BENCHMARK_SNAPSHOT:END -->";
18+
const ReadmeSummaryStartMarker = "<!-- README_AUTO_SUMMARY:START -->";
19+
const ReadmeSummaryEndMarker = "<!-- README_AUTO_SUMMARY:END -->";
1820

1921
const ParserCapability = struct {
2022
parser: []const u8,
@@ -308,6 +310,27 @@ const ReadmeBenchSnapshot = struct {
308310
query_cached_results: []const ReadmeQueryResult,
309311
};
310312

313+
const ExternalSuiteCounts = struct {
314+
total: usize,
315+
passed: usize,
316+
failed: usize,
317+
};
318+
319+
const ExternalSuiteMode = struct {
320+
selector_suites: struct {
321+
nwmatcher: ExternalSuiteCounts,
322+
qwery_contextual: ExternalSuiteCounts,
323+
},
324+
parser_suite: ExternalSuiteCounts,
325+
};
326+
327+
const ExternalSuiteReport = struct {
328+
modes: struct {
329+
strictest: ?ExternalSuiteMode = null,
330+
fastest: ?ExternalSuiteMode = null,
331+
},
332+
};
333+
311334
fn runnerCmdParse(alloc: std.mem.Allocator, parser_name: []const u8, fixture: []const u8, iterations: usize) ![]const []const u8 {
312335
const iter_s = try std.fmt.allocPrint(alloc, "{d}", .{iterations});
313336
if (std.mem.eql(u8, parser_name, "ours-strictest")) {
@@ -663,6 +686,182 @@ fn updateDocumentationBenchmarkSnapshot(alloc: std.mem.Allocator) !void {
663686
}
664687
}
665688

689+
const ParseAverageRow = struct {
690+
parser: []const u8,
691+
avg_mb_s: f64,
692+
};
693+
694+
fn cmpParseAverageDesc(_: void, a: ParseAverageRow, b: ParseAverageRow) bool {
695+
return a.avg_mb_s > b.avg_mb_s;
696+
}
697+
698+
fn parseAverageRows(alloc: std.mem.Allocator, snap: ReadmeBenchSnapshot) ![]ParseAverageRow {
699+
const parser_names = [_][]const u8{ "ours-fastest", "ours-strictest", "lol-html", "lexbor" };
700+
var rows = std.ArrayList(ParseAverageRow).empty;
701+
errdefer rows.deinit(alloc);
702+
703+
for (parser_names) |parser_name| {
704+
var sum: f64 = 0.0;
705+
var count: usize = 0;
706+
for (snap.parse_results) |r| {
707+
if (!std.mem.eql(u8, r.parser, parser_name)) continue;
708+
sum += r.throughput_mb_s;
709+
count += 1;
710+
}
711+
if (count == 0) continue;
712+
try rows.append(alloc, .{
713+
.parser = parser_name,
714+
.avg_mb_s = sum / @as(f64, @floatFromInt(count)),
715+
});
716+
}
717+
718+
std.mem.sort(ParseAverageRow, rows.items, {}, cmpParseAverageDesc);
719+
return rows.toOwnedSlice(alloc);
720+
}
721+
722+
fn writeConformanceRow(
723+
w: anytype,
724+
profile: []const u8,
725+
nw: ExternalSuiteCounts,
726+
qw: ExternalSuiteCounts,
727+
parser: ExternalSuiteCounts,
728+
) !void {
729+
try w.print("| `{s}` | {d}/{d} ({d} failed) | {d}/{d} ({d} failed) | {d}/{d} ({d} failed) |\n", .{
730+
profile,
731+
nw.passed,
732+
nw.total,
733+
nw.failed,
734+
qw.passed,
735+
qw.total,
736+
qw.failed,
737+
parser.passed,
738+
parser.total,
739+
parser.failed,
740+
});
741+
}
742+
743+
fn sameExternalMode(a: ExternalSuiteMode, b: ExternalSuiteMode) bool {
744+
return a.selector_suites.nwmatcher.total == b.selector_suites.nwmatcher.total and
745+
a.selector_suites.nwmatcher.passed == b.selector_suites.nwmatcher.passed and
746+
a.selector_suites.nwmatcher.failed == b.selector_suites.nwmatcher.failed and
747+
a.selector_suites.qwery_contextual.total == b.selector_suites.qwery_contextual.total and
748+
a.selector_suites.qwery_contextual.passed == b.selector_suites.qwery_contextual.passed and
749+
a.selector_suites.qwery_contextual.failed == b.selector_suites.qwery_contextual.failed and
750+
a.parser_suite.total == b.parser_suite.total and
751+
a.parser_suite.passed == b.parser_suite.passed and
752+
a.parser_suite.failed == b.parser_suite.failed;
753+
}
754+
755+
fn renderReadmeAutoSummary(alloc: std.mem.Allocator) ![]u8 {
756+
var out = std.ArrayList(u8).empty;
757+
errdefer out.deinit(alloc);
758+
const w = out.writer(alloc);
759+
760+
const latest_exists = common.fileExists("bench/results/latest.json");
761+
if (latest_exists) {
762+
const latest_json = try common.readFileAlloc(alloc, "bench/results/latest.json");
763+
defer alloc.free(latest_json);
764+
const parsed = try std.json.parseFromSlice(ReadmeBenchSnapshot, alloc, latest_json, .{
765+
.ignore_unknown_fields = true,
766+
});
767+
defer parsed.deinit();
768+
const snap = parsed.value;
769+
770+
const avg_rows = try parseAverageRows(alloc, snap);
771+
defer alloc.free(avg_rows);
772+
773+
try w.print("Source: `bench/results/latest.json` (`{s}` profile).\n\n", .{snap.profile});
774+
try w.writeAll("### Parse Throughput (Average Across Fixtures)\n\n");
775+
try w.writeAll("| Parser | Avg Throughput (MB/s) | % of leader | Relative chart |\n");
776+
try w.writeAll("|---|---:|---:|---|\n");
777+
778+
var leader: f64 = 0.0;
779+
for (avg_rows) |r| leader = @max(leader, r.avg_mb_s);
780+
781+
for (avg_rows) |r| {
782+
const pct = if (leader > 0.0) (r.avg_mb_s / leader) * 100.0 else 0.0;
783+
const width: usize = 20;
784+
const filled = if (leader > 0.0)
785+
@min(width, @max(@as(usize, @intFromFloat(@round((r.avg_mb_s / leader) * @as(f64, @floatFromInt(width))))), @as(usize, 1)))
786+
else
787+
@as(usize, 0);
788+
const bar = try alloc.alloc(u8, filled);
789+
defer alloc.free(bar);
790+
@memset(bar, '#');
791+
try w.print("| `{s}` | {d:.2} | {d:.2}% | `{s}` |\n", .{
792+
r.parser,
793+
r.avg_mb_s,
794+
pct,
795+
bar,
796+
});
797+
}
798+
} else {
799+
try w.writeAll("Run `zig build bench-compare` to generate parse performance summary.\n");
800+
}
801+
802+
try w.writeAll("\n### Conformance Snapshot\n\n");
803+
if (common.fileExists("bench/results/external_suite_report.json")) {
804+
const ext_json = try common.readFileAlloc(alloc, "bench/results/external_suite_report.json");
805+
defer alloc.free(ext_json);
806+
const parsed_ext = try std.json.parseFromSlice(ExternalSuiteReport, alloc, ext_json, .{
807+
.ignore_unknown_fields = true,
808+
});
809+
defer parsed_ext.deinit();
810+
const modes = parsed_ext.value.modes;
811+
812+
try w.writeAll("| Profile | nwmatcher | qwery_contextual | html5lib subset |\n");
813+
try w.writeAll("|---|---:|---:|---:|\n");
814+
if (modes.strictest != null and modes.fastest != null and sameExternalMode(modes.strictest.?, modes.fastest.?)) {
815+
const m = modes.strictest.?;
816+
try writeConformanceRow(w, "strictest/fastest", m.selector_suites.nwmatcher, m.selector_suites.qwery_contextual, m.parser_suite);
817+
} else {
818+
if (modes.strictest) |m| {
819+
try writeConformanceRow(w, "strictest", m.selector_suites.nwmatcher, m.selector_suites.qwery_contextual, m.parser_suite);
820+
}
821+
if (modes.fastest) |m| {
822+
try writeConformanceRow(w, "fastest", m.selector_suites.nwmatcher, m.selector_suites.qwery_contextual, m.parser_suite);
823+
}
824+
}
825+
try w.writeAll("\nSource: `bench/results/external_suite_report.json`\n");
826+
} else {
827+
try w.writeAll("Run `zig build conformance` to generate conformance summary.\n");
828+
}
829+
830+
return out.toOwnedSlice(alloc);
831+
}
832+
833+
fn updateReadmeAutoSummary(alloc: std.mem.Allocator) !void {
834+
const replacement = try renderReadmeAutoSummary(alloc);
835+
defer alloc.free(replacement);
836+
837+
const readme = try common.readFileAlloc(alloc, "README.md");
838+
defer alloc.free(readme);
839+
840+
const start = std.mem.indexOf(u8, readme, ReadmeSummaryStartMarker) orelse return error.ReadmeBenchMarkersMissing;
841+
const after_start = start + ReadmeSummaryStartMarker.len;
842+
const end = std.mem.indexOfPos(u8, readme, after_start, ReadmeSummaryEndMarker) orelse return error.ReadmeBenchMarkersMissing;
843+
844+
var out = std.ArrayList(u8).empty;
845+
defer out.deinit(alloc);
846+
try out.appendSlice(alloc, readme[0..after_start]);
847+
try out.appendSlice(alloc, "\n\n");
848+
try out.appendSlice(alloc, replacement);
849+
if (replacement.len == 0 or replacement[replacement.len - 1] != '\n') {
850+
try out.append(alloc, '\n');
851+
}
852+
if (readme[end - 1] != '\n') {
853+
try out.append(alloc, '\n');
854+
}
855+
try out.appendSlice(alloc, readme[end..]);
856+
857+
if (!std.mem.eql(u8, out.items, readme)) {
858+
try common.writeFile("README.md", out.items);
859+
std.debug.print("wrote README.md auto summary\n", .{});
860+
} else {
861+
std.debug.print("README.md auto summary already up-to-date\n", .{});
862+
}
863+
}
864+
666865
fn writeMarkdown(
667866
alloc: std.mem.Allocator,
668867
profile_name: []const u8,
@@ -1148,6 +1347,7 @@ fn runBenchmarks(alloc: std.mem.Allocator, args: []const []const u8) !void {
11481347
defer alloc.free(md);
11491348
try common.writeFile("bench/results/latest.md", md);
11501349
try updateDocumentationBenchmarkSnapshot(alloc);
1350+
try updateReadmeAutoSummary(alloc);
11511351

11521352
// Optional baseline behavior.
11531353
const baseline_default = try std.fmt.allocPrint(alloc, "bench/results/baseline_{s}.json", .{profile.name});
@@ -1632,6 +1832,9 @@ fn runExternalSuites(alloc: std.mem.Allocator, args: []const []const u8) !void {
16321832
try jw.writeAll("}}");
16331833
try common.writeFile(json_out, json_buf.items);
16341834
std.debug.print("Wrote report: {s}\n", .{json_out});
1835+
if (std.mem.eql(u8, json_out, "bench/results/external_suite_report.json")) {
1836+
try updateReadmeAutoSummary(alloc);
1837+
}
16351838
}
16361839

16371840
fn cmpStringSlice(_: void, a: []const u8, b: []const u8) bool {
@@ -1975,6 +2178,7 @@ pub fn main() !void {
19752178
if (std.mem.eql(u8, cmd, "sync-docs-bench")) {
19762179
if (rest.len != 0) return error.InvalidArgument;
19772180
try updateDocumentationBenchmarkSnapshot(alloc);
2181+
try updateReadmeAutoSummary(alloc);
19782182
return;
19792183
}
19802184
if (std.mem.eql(u8, cmd, "run-external-suites")) {

0 commit comments

Comments
 (0)