@@ -15,6 +15,8 @@ const SUITE_RUNNER_BIN = "bench/build/bin/suite_runner";
1515const repeats : usize = 5 ;
1616const DocumentationBenchmarkStartMarker = "<!-- BENCHMARK_SNAPSHOT:START -->" ;
1717const DocumentationBenchmarkEndMarker = "<!-- BENCHMARK_SNAPSHOT:END -->" ;
18+ const ReadmeSummaryStartMarker = "<!-- README_AUTO_SUMMARY:START -->" ;
19+ const ReadmeSummaryEndMarker = "<!-- README_AUTO_SUMMARY:END -->" ;
1820
1921const 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+
311334fn 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 ("\n Source: `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+
666865fn 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
16371840fn 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