@@ -16,6 +16,8 @@ const repeats: usize = 5;
1616const StableParseMinRatio : f64 = 0.99 ;
1717const StableQueryMinRatio : f64 = 0.99 ;
1818const RegressionConfirmRuns : usize = 3 ;
19+ const ReadmeBenchmarkStartMarker = "<!-- BENCHMARK_SNAPSHOT:START -->" ;
20+ const ReadmeBenchmarkEndMarker = "<!-- BENCHMARK_SNAPSHOT:END -->" ;
1921
2022const ParserCapability = struct {
2123 parser : []const u8 ,
@@ -288,6 +290,27 @@ const GateRow = struct {
288290 pass : bool ,
289291};
290292
293+ const ReadmeParseResult = struct {
294+ parser : []const u8 ,
295+ fixture : []const u8 ,
296+ throughput_mb_s : f64 ,
297+ };
298+
299+ const ReadmeQueryResult = struct {
300+ parser : []const u8 ,
301+ case : []const u8 ,
302+ ops_s : f64 ,
303+ ns_per_op : f64 ,
304+ };
305+
306+ const ReadmeBenchSnapshot = struct {
307+ profile : []const u8 ,
308+ parse_results : []const ReadmeParseResult ,
309+ query_parse_results : []const ReadmeQueryResult ,
310+ query_match_results : []const ReadmeQueryResult ,
311+ query_compiled_results : []const ReadmeQueryResult ,
312+ };
313+
291314fn runnerCmdParse (alloc : std.mem.Allocator , parser_name : []const u8 , fixture : []const u8 , iterations : usize ) ! []const []const u8 {
292315 const iter_s = try std .fmt .allocPrint (alloc , "{d}" , .{iterations });
293316 if (std .mem .eql (u8 , parser_name , "ours-strictest" )) {
@@ -477,6 +500,172 @@ fn findParseThroughput(rows: []const ParseResult, parser_name: []const u8, fixtu
477500 return null ;
478501}
479502
503+ fn findReadmeParseThroughput (rows : []const ReadmeParseResult , parser_name : []const u8 , fixture_name : []const u8 ) ? f64 {
504+ for (rows ) | row | {
505+ if (std .mem .eql (u8 , row .parser , parser_name ) and std .mem .eql (u8 , row .fixture , fixture_name )) {
506+ return row .throughput_mb_s ;
507+ }
508+ }
509+ return null ;
510+ }
511+
512+ fn findReadmeQuery (rows : []const ReadmeQueryResult , parser_name : []const u8 , case_name : []const u8 ) ? ReadmeQueryResult {
513+ for (rows ) | row | {
514+ if (std .mem .eql (u8 , row .parser , parser_name ) and std .mem .eql (u8 , row .case , case_name )) return row ;
515+ }
516+ return null ;
517+ }
518+
519+ fn appendUniqueString (list : * std .ArrayList ([]const u8 ), alloc : std .mem .Allocator , value : []const u8 ) ! void {
520+ for (list .items ) | it | {
521+ if (std .mem .eql (u8 , it , value )) return ;
522+ }
523+ try list .append (alloc , value );
524+ }
525+
526+ fn writeMaybeF64 (w : anytype , value : ? f64 ) ! void {
527+ if (value ) | v | {
528+ try w .print ("{d:.2}" , .{v });
529+ } else {
530+ try w .writeAll ("-" );
531+ }
532+ }
533+
534+ fn renderReadmeBenchmarkSection (alloc : std.mem.Allocator , snap : ReadmeBenchSnapshot ) ! []u8 {
535+ var out = std .ArrayList (u8 ).empty ;
536+ errdefer out .deinit (alloc );
537+ const w = out .writer (alloc );
538+
539+ var fixtures = std .ArrayList ([]const u8 ).empty ;
540+ defer fixtures .deinit (alloc );
541+ for (snap .parse_results ) | row | {
542+ try appendUniqueString (& fixtures , alloc , row .fixture );
543+ }
544+
545+ var query_match_cases = std .ArrayList ([]const u8 ).empty ;
546+ defer query_match_cases .deinit (alloc );
547+ for (snap .query_match_results ) | row | {
548+ try appendUniqueString (& query_match_cases , alloc , row .case );
549+ }
550+
551+ var query_parse_cases = std .ArrayList ([]const u8 ).empty ;
552+ defer query_parse_cases .deinit (alloc );
553+ for (snap .query_parse_results ) | row | {
554+ try appendUniqueString (& query_parse_cases , alloc , row .case );
555+ }
556+
557+ try w .print ("Source: `bench/results/latest.json` (`{s}` profile).\n\n " , .{snap .profile });
558+
559+ try w .writeAll ("#### Parse Throughput Comparison (MB/s)\n\n " );
560+ try w .writeAll ("| Fixture | ours-fastest | ours-strictest | lol-html | lexbor |\n " );
561+ try w .writeAll ("|---|---:|---:|---:|---:|\n " );
562+ for (fixtures .items ) | fixture | {
563+ try w .print ("| `{s}` | " , .{fixture });
564+ try writeMaybeF64 (w , findReadmeParseThroughput (snap .parse_results , "ours-fastest" , fixture ));
565+ try w .writeAll (" | " );
566+ try writeMaybeF64 (w , findReadmeParseThroughput (snap .parse_results , "ours-strictest" , fixture ));
567+ try w .writeAll (" | " );
568+ try writeMaybeF64 (w , findReadmeParseThroughput (snap .parse_results , "lol-html" , fixture ));
569+ try w .writeAll (" | " );
570+ try writeMaybeF64 (w , findReadmeParseThroughput (snap .parse_results , "lexbor" , fixture ));
571+ try w .writeAll (" |\n " );
572+ }
573+
574+ try w .writeAll ("\n #### Query Match Throughput (ours)\n\n " );
575+ try w .writeAll ("| Case | strictest ops/s | strictest ns/op | fastest ops/s | fastest ns/op |\n " );
576+ try w .writeAll ("|---|---:|---:|---:|---:|\n " );
577+ for (query_match_cases .items ) | case_name | {
578+ const strictest = findReadmeQuery (snap .query_match_results , "ours-strictest" , case_name );
579+ const fastest = findReadmeQuery (snap .query_match_results , "ours-fastest" , case_name );
580+ try w .print ("| `{s}` | " , .{case_name });
581+ try writeMaybeF64 (w , if (strictest ) | s | s .ops_s else null );
582+ try w .writeAll (" | " );
583+ try writeMaybeF64 (w , if (strictest ) | s | s .ns_per_op else null );
584+ try w .writeAll (" | " );
585+ try writeMaybeF64 (w , if (fastest ) | s | s .ops_s else null );
586+ try w .writeAll (" | " );
587+ try writeMaybeF64 (w , if (fastest ) | s | s .ns_per_op else null );
588+ try w .writeAll (" |\n " );
589+ }
590+
591+ try w .writeAll ("\n #### Cached Query Throughput (ours)\n\n " );
592+ try w .writeAll ("| Case | strictest ops/s | strictest ns/op | fastest ops/s | fastest ns/op |\n " );
593+ try w .writeAll ("|---|---:|---:|---:|---:|\n " );
594+ for (query_match_cases .items ) | case_name | {
595+ const strictest = findReadmeQuery (snap .query_compiled_results , "ours-strictest" , case_name );
596+ const fastest = findReadmeQuery (snap .query_compiled_results , "ours-fastest" , case_name );
597+ try w .print ("| `{s}` | " , .{case_name });
598+ try writeMaybeF64 (w , if (strictest ) | s | s .ops_s else null );
599+ try w .writeAll (" | " );
600+ try writeMaybeF64 (w , if (strictest ) | s | s .ns_per_op else null );
601+ try w .writeAll (" | " );
602+ try writeMaybeF64 (w , if (fastest ) | s | s .ops_s else null );
603+ try w .writeAll (" | " );
604+ try writeMaybeF64 (w , if (fastest ) | s | s .ns_per_op else null );
605+ try w .writeAll (" |\n " );
606+ }
607+
608+ try w .writeAll ("\n #### Query Parse Throughput (ours)\n\n " );
609+ try w .writeAll ("| Selector case | Ops/s | ns/op |\n " );
610+ try w .writeAll ("|---|---:|---:|\n " );
611+ for (query_parse_cases .items ) | case_name | {
612+ const ours = findReadmeQuery (snap .query_parse_results , "ours" , case_name ) orelse
613+ findReadmeQuery (snap .query_parse_results , "ours-strictest" , case_name ) orelse
614+ findReadmeQuery (snap .query_parse_results , "ours-fastest" , case_name );
615+ try w .print ("| `{s}` | " , .{case_name });
616+ try writeMaybeF64 (w , if (ours ) | r | r .ops_s else null );
617+ try w .writeAll (" | " );
618+ try writeMaybeF64 (w , if (ours ) | r | r .ns_per_op else null );
619+ try w .writeAll (" |\n " );
620+ }
621+
622+ try w .writeAll ("\n For full per-parser, per-fixture tables and gate output:\n " );
623+ try w .writeAll ("- `bench/results/latest.md`\n " );
624+ try w .writeAll ("- `bench/results/latest.json`\n " );
625+
626+ return out .toOwnedSlice (alloc );
627+ }
628+
629+ fn updateReadmeBenchmarkSnapshot (alloc : std.mem.Allocator ) ! void {
630+ const latest_json = try common .readFileAlloc (alloc , "bench/results/latest.json" );
631+ defer alloc .free (latest_json );
632+
633+ const parsed = try std .json .parseFromSlice (ReadmeBenchSnapshot , alloc , latest_json , .{
634+ .ignore_unknown_fields = true ,
635+ });
636+ defer parsed .deinit ();
637+
638+ const replacement = try renderReadmeBenchmarkSection (alloc , parsed .value );
639+ defer alloc .free (replacement );
640+
641+ const readme = try common .readFileAlloc (alloc , "README.md" );
642+ defer alloc .free (readme );
643+
644+ const start = std .mem .indexOf (u8 , readme , ReadmeBenchmarkStartMarker ) orelse return error .ReadmeBenchMarkersMissing ;
645+ const after_start = start + ReadmeBenchmarkStartMarker .len ;
646+ const end = std .mem .indexOfPos (u8 , readme , after_start , ReadmeBenchmarkEndMarker ) orelse return error .ReadmeBenchMarkersMissing ;
647+
648+ var out = std .ArrayList (u8 ).empty ;
649+ defer out .deinit (alloc );
650+ try out .appendSlice (alloc , readme [0.. after_start ]);
651+ try out .appendSlice (alloc , "\n\n " );
652+ try out .appendSlice (alloc , replacement );
653+ if (replacement .len == 0 or replacement [replacement .len - 1 ] != '\n ' ) {
654+ try out .append (alloc , '\n ' );
655+ }
656+ if (readme [end - 1 ] != '\n ' ) {
657+ try out .append (alloc , '\n ' );
658+ }
659+ try out .appendSlice (alloc , readme [end .. ]);
660+
661+ if (! std .mem .eql (u8 , out .items , readme )) {
662+ try common .writeFile ("README.md" , out .items );
663+ std .debug .print ("wrote README.md benchmark snapshot\n " , .{});
664+ } else {
665+ std .debug .print ("README.md benchmark snapshot already up-to-date\n " , .{});
666+ }
667+ }
668+
480669fn writeMarkdown (
481670 alloc : std.mem.Allocator ,
482671 profile_name : []const u8 ,
@@ -1015,6 +1204,7 @@ fn runBenchmarks(alloc: std.mem.Allocator, args: []const []const u8) !void {
10151204 const md = try writeMarkdown (alloc , profile .name , parse_results .items , query_parse_results .items , query_match_results .items , query_compiled_results .items , gate_rows );
10161205 defer alloc .free (md );
10171206 try common .writeFile ("bench/results/latest.md" , md );
1207+ try updateReadmeBenchmarkSnapshot (alloc );
10181208
10191209 // Optional baseline behavior.
10201210 const baseline_default = try std .fmt .allocPrint (alloc , "bench/results/baseline_{s}.json" , .{profile .name });
@@ -1948,6 +2138,7 @@ fn usage() void {
19482138 \\ htmlparser-tools setup-parsers
19492139 \\ htmlparser-tools setup-fixtures [--refresh]
19502140 \\ htmlparser-tools run-benchmarks [--profile quick|stable] [--baseline path] [--write-baseline]
2141+ \\ htmlparser-tools sync-readme-bench
19512142 \\ htmlparser-tools run-external-suites [--mode strictest|fastest|both] [--max-html5lib-cases N] [--json-out path]
19522143 \\ htmlparser-tools docs-check
19532144 \\ htmlparser-tools examples-check
@@ -1987,6 +2178,11 @@ pub fn main() !void {
19872178 try runBenchmarks (alloc , rest );
19882179 return ;
19892180 }
2181+ if (std .mem .eql (u8 , cmd , "sync-readme-bench" )) {
2182+ if (rest .len != 0 ) return error .InvalidArgument ;
2183+ try updateReadmeBenchmarkSnapshot (alloc );
2184+ return ;
2185+ }
19902186 if (std .mem .eql (u8 , cmd , "run-external-suites" )) {
19912187 try runExternalSuites (alloc , rest );
19922188 return ;
0 commit comments