{{ message }}
Repository navigation
Expand file tree
/
Copy pathresults_site.html
More file actions
1034 lines (979 loc) · 70.6 KB
/
Copy pathresults_site.html
File metadata and controls
1034 lines (979 loc) · 70.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>fast-ruby results</title>
<meta name="description" content="How fast is each fast-ruby idiom on each Ruby, and does the advice hold on yours? Every benchmark, on every Ruby from 2.1 to head, with and without the JIT.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M2 11 8 4h16l6 7-14 17z' fill='%23ff0000'/%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Oxygen:wght@400;700&family=Source+Code+Pro:wght@400;500;700&display=swap">
<style>
/* One reading column in the fastruby.io look: white, ruby red mark, green
square buttons, and one subway-line station per Ruby. Light only, like
fastruby.io. Built by script/build_results_site.rb. */
:root {
color-scheme: light;
--bg: #ffffff;
--soft: #f8f8f8;
--ink: #262320;
--text: #333333;
--muted: #666666;
--faint: #999999;
--line: #dddddd;
--green: #00d242;
--green-dark: #00952f;
--red: #e00000;
--ruby: #ff0000;
--yellow: #fdca31;
--yellow-ink: #7a5c00;
--skip: #d0d0d0;
--grid: #eeeeee;
/* Chart series, claimed report first: up to 6 reports, checked for color blindness on neighbouring lines.
--s5 and --s6 are under 3:1 contrast, so every chart also has a legend and a table. */
--s1: #00952f;
--s2: #2a78d6;
--s3: #eb6834;
--s4: #4a3aa7;
--s5: #eda100;
--s6: #e87ba4;
--font: "Oxygen", system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: "Source Code Pro", ui-monospace, SFMono-Regular, Menlo, monospace;
}
* { box-sizing: border-box; }
html { background: var(--bg); }
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.6 var(--font); }
a { color: var(--text); }
:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
code { font-family: var(--mono); font-size: .88em; }
.col { max-width: 860px; margin: 0 auto; padding-inline: 16px; }
header .col { max-width: 1100px; display: flex; align-items: center; justify-content: space-between; gap: 12px 24px; flex-wrap: wrap; padding-block: 22px; }
.logo { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--ink); }
.logo svg { width: 60px; height: 34px; flex: none; }
.logo b { display: block; font-size: 22px; line-height: 1; letter-spacing: -.01em; }
.logo small { display: block; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; margin-top: 3px; color: var(--text); }
nav { display: flex; align-items: center; gap: 8px 22px; flex-wrap: wrap; font-size: 14px; }
nav a { text-decoration: underline; text-underline-offset: 3px; }
nav a.maint { text-decoration: none; color: var(--muted); }
nav a.maint b { color: var(--ink); }
nav a.maint i { font-style: normal; color: var(--green-dark); }
.btn { display: inline-block; background: var(--green); color: #fff; font-weight: 700; font-size: 13px; letter-spacing: .02em; text-transform: uppercase; text-decoration: none; padding: 11px 16px; border: 0; }
.btn:hover { background: var(--green-dark); }
.intro { padding-block: 34px 10px; text-align: center; }
h1 { font-size: clamp(36px, 7vw, 60px); line-height: 1.08; margin: 0 auto 14px; color: var(--text); letter-spacing: -.02em; max-width: 21ch; text-wrap: balance; }
.intro p { margin: 0 auto; color: var(--muted); max-width: 58ch; font-size: 15px; }
.intro p.run { margin-top: 10px; font-size: 13px; color: var(--faint); }
.intro p.run a { color: var(--muted); }
.intro p.note { margin: 14px auto 0; max-width: 58ch; padding: 10px 14px; border: 2px solid var(--red); color: var(--ink); font-size: 14px; font-weight: 700; }
.intro p.note[hidden] { display: none; }
.picker { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 5; background: var(--bg); padding-block: 16px; border-bottom: 3px solid var(--ink); display: grid; gap: 10px; }
.current { font-size: clamp(22px, 4vw, 30px); font-weight: 700; color: var(--ink); line-height: 1.2; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; }
.current .pre { font-size: 14px; font-weight: 400; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; }
.current .rb { background: var(--ink); color: #fff; padding: 2px 12px; font-family: var(--mono); }
.current .md { font-size: 15px; font-weight: 700; color: var(--green-dark); border: 2px solid var(--green); padding: 1px 8px; }
.prow { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; }
.lbl { font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); min-width: 84px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { font: 500 13px var(--mono); border: 1px solid var(--line); background: var(--bg); color: var(--text); padding: 4px 10px; cursor: pointer; }
.chips.big .chip { font-size: 15px; padding: 7px 13px; }
.chip:hover { border-color: var(--text); }
.chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: #fff; }
.chip:disabled { color: var(--skip); border-style: dashed; cursor: not-allowed; }
.chip .j { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-left: 5px; vertical-align: 2px; }
.jkey { font-size: 13px; color: var(--muted); display: inline-flex; align-items: center; gap: 6px; }
.jkey .j { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--green); }
.tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.tabs button { font: 700 16px var(--font); background: var(--bg); border: 2px solid var(--line); padding: 8px 16px; color: var(--text); cursor: pointer; }
.tabs button:hover { border-color: var(--text); }
.tabs button .c { font-family: var(--mono); font-weight: 500; font-size: 14px; color: var(--muted); margin-left: 6px; }
.tabs button[aria-pressed="true"] { background: var(--green); border-color: var(--green); color: #fff; }
.tabs button[aria-pressed="true"] .c { color: #fff; }
.tabs button:disabled { color: var(--skip); border-style: dashed; cursor: not-allowed; }
.tabs button:disabled .c { display: none; }
/* Skipped and Suspect are hidden when their count is 0. */
.tabs button[hidden] { display: none; }
/* (!) explanations: shown on hover, focus or tap. On phones they open as a
full-width bar at the bottom, so they never push the page wider. */
/* A small black badge raised like an exponent, so it reads as clickable.
Black, not yellow: yellow already means "tie" on this page. */
.tip { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; margin-left: 2px; padding: 0; border-radius: 50%; border: 0; background: var(--ink); color: #fff; font: 700 10px/1 var(--font); vertical-align: .55em; cursor: pointer; }
.tip:hover, .tip:focus-visible, .tip.open { background: #000; box-shadow: 0 0 0 2px rgba(38, 35, 32, .25); }
.tip::after { content: attr(data-tip); position: absolute; left: 50%; top: calc(100% + 8px); transform: translateX(-50%); width: max-content; max-width: min(280px, 70vw); background: var(--ink); color: #fff; font: 400 13px/1.45 var(--font); text-align: left; padding: 8px 10px; display: none; pointer-events: none; z-index: 30; }
.tip:hover::after, .tip:focus::after, .tip.open::after { display: block; }
h2 { font-size: 13px; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); margin: 32px 0 2px; }
h2 .on { float: right; text-transform: none; letter-spacing: 0; font-family: var(--mono); font-weight: 500; color: var(--ink); }
ul.list { list-style: none; margin: 0; padding: 0; }
li.item { border-bottom: 1px solid var(--line); }
li.item > button { all: unset; box-sizing: border-box; width: 100%; display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 6px 12px; align-items: start; padding: 14px 4px; cursor: pointer; }
li.item > button:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }
li.item > button:hover .t { text-decoration: underline; text-underline-offset: 3px; }
.ico { position: relative; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; margin-top: 2px; }
.ico.ok { background: var(--green); color: #fff; }
.ico.eq { background: #fff; color: var(--yellow-ink); box-shadow: inset 0 0 0 3px var(--yellow); }
.ico.no { background: var(--red); color: #fff; }
.ico.na { background: #fff; color: var(--faint); box-shadow: inset 0 0 0 3px var(--skip); }
.t { font-weight: 700; font-size: 15.5px; line-height: 1.35; color: var(--ink); min-width: 0; }
.t code { font-weight: 400; }
.meta { grid-column: 2 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; font-size: 13.5px; color: var(--muted); }
.meta > .line { margin-left: auto; }
.res { font-family: var(--mono); font-size: 13.5px; font-weight: 700; margin-left: 4px; }
.res.ok { color: var(--green-dark); }
.res.eq { color: var(--yellow-ink); }
.res.no { color: var(--red); }
.res.na { color: var(--faint); }
.res .sus { color: var(--ink); font-weight: 500; }
/* Subway line: one station per Ruby that has the selected build. */
.line { position: relative; display: flex; align-items: center; gap: 5px; padding: 0 2px; }
.line::before { content: ""; position: absolute; left: 6px; right: 6px; top: 50%; height: 3px; margin-top: -1.5px; background: var(--line); }
.line span { position: relative; width: 11px; height: 11px; border-radius: 50%; background: #fff; }
.line .ok { background: var(--green); }
.line .no { background: var(--red); }
.line .eq { box-shadow: inset 0 0 0 3px var(--yellow); }
.line .na { box-shadow: inset 0 0 0 2px var(--skip); }
.line .me { box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--ink); }
.line .me.eq { box-shadow: inset 0 0 0 3px var(--yellow), 0 0 0 2px #fff, 0 0 0 4px var(--ink); }
.line .me.na { box-shadow: inset 0 0 0 2px var(--skip), 0 0 0 2px #fff, 0 0 0 4px var(--ink); }
.detail { padding: 0 4px 20px 42px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.detail .tb { overflow-x: auto; border: 1px solid var(--line); }
.detail table { border-collapse: collapse; width: 100%; font-size: 13.5px; font-variant-numeric: tabular-nums; }
.detail th { text-align: right; font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 8px 10px; background: var(--soft); border-bottom: 1px solid var(--line); white-space: nowrap; }
.detail th:first-child, .detail td:first-child { text-align: left; }
.detail th .v { display: block; font-family: var(--mono); text-transform: none; letter-spacing: 0; font-weight: 700; margin-top: 2px; }
.detail th .v.ok { color: var(--green-dark); }
.detail th .v.eq { color: var(--yellow-ink); }
.detail th .v.no { color: var(--red); }
.detail th .v.na { color: var(--faint); }
.detail th .v.sus { color: var(--ink); font-weight: 500; }
.detail td { padding: 7px 10px; border-bottom: 1px solid var(--line); text-align: right; font-family: var(--mono); font-size: 12.5px; white-space: nowrap; }
.detail td:first-child { font-family: var(--font); font-size: 13.5px; white-space: normal; }
.detail tr:last-child td { border-bottom: 0; }
.detail td.fast { color: var(--green-dark); font-weight: 700; }
.detail td small.ips { display: block; font-size: 11px; font-weight: 400; color: var(--faint); margin-top: 1px; }
.detail tr.claimed td:first-child { font-weight: 700; }
.detail tr.env td { background: var(--soft); font-size: 12px; color: var(--muted); }
.detail .links { font-size: 13px; display: flex; gap: 18px; flex-wrap: wrap; }
.fine { font-size: 12.5px; color: var(--muted); margin: 0; }
.empty { padding: 24px 0; color: var(--muted); }
.legend { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 13px; color: var(--muted); padding-block: 20px 0; align-items: center; }
.legend .line { display: inline-flex; }
.legend .line::before { display: none; }
.band { background: #262320; color: #f1ebe1; margin-top: 56px; }
.bwrap { max-width: 1100px; margin: 0 auto; padding: 48px 16px; display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: 36px; align-items: start; }
.beyebrow { font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #b9b0a3; }
.bh { font-size: clamp(24px, 3vw, 32px); line-height: 1.15; margin: 6px 0 12px; color: #f1ebe1; text-transform: none; letter-spacing: -.01em; }
.bh span { color: var(--green); }
.band p { color: #b9b0a3; max-width: 58ch; margin: 0 0 12px; font-size: 15px; }
.band p b { color: #f1ebe1; }
.cta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.bbtn { display: inline-block; padding: 11px 16px; font-weight: 700; font-size: 13px; letter-spacing: .02em; text-transform: uppercase; text-decoration: none; }
.bbtn.primary { background: var(--green); color: #fff; }
.bbtn.primary:hover { background: var(--green-dark); }
.bbtn.ghost { border: 1px solid #57514a; color: #f1ebe1; }
.bbtn.ghost:hover { border-color: var(--green); }
.services { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.svc { border: 1px solid #3a352f; padding: 14px; text-decoration: none; color: #f1ebe1; display: grid; gap: 4px; }
.svc:hover { border-color: var(--green); }
.svc b { font-size: 15px; }
.svc span { font-size: 13px; color: #b9b0a3; line-height: 1.4; }
.proof { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 10px 28px; border-top: 1px solid #3a352f; padding-top: 18px; font-family: var(--mono); font-size: 13px; color: #b9b0a3; }
.proof b { color: var(--green); font-size: 18px; margin-right: 6px; }
.band a:focus-visible { outline-color: var(--green); }
footer { border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); }
footer .col { padding-block: 18px 30px; display: flex; flex-wrap: wrap; gap: 6px 20px; }
/* The two views, Across Rubies first. */
.views { display: flex; justify-content: center; padding-top: 18px; }
.views div { display: inline-flex; border: 2px solid var(--ink); }
.views button { font: 700 13px var(--font); letter-spacing: .04em; text-transform: uppercase; background: var(--bg); color: var(--ink); border: 0; padding: 9px 16px; cursor: pointer; }
.views button[aria-pressed="true"] { background: var(--ink); color: #fff; }
.views button:hover:not([aria-pressed="true"]) { background: var(--soft); }
/* Across Rubies: one card per benchmark, a chart with the Rubies on the x axis. */
.rcard { border-bottom: 1px solid var(--line); padding-block: 18px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
/* Card: the title says what is compared, the verdict what happened, the tiles the numbers behind it. */
.rcard h3 { margin: 0; font-size: 20px; line-height: 1.3; color: var(--ink); overflow-wrap: anywhere; }
/* One line when it fits: fitTitles() shrinks a long title down to 14px first, and only lets it wrap past that. */
.rcard h3.one { white-space: nowrap; overflow-wrap: normal; }
.rcard h3 code { font-weight: 700; font-size: .9em; }
.rcard .verdict { margin: 0; display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 8px; align-items: start; font-size: 15.5px; line-height: 1.45; color: var(--text); text-wrap: pretty; overflow-wrap: anywhere; }
.rcard .verdict code { font-weight: 700; color: var(--ink); }
.rcard .verdict b { color: var(--ink); }
.rcard .verdict .ico { width: 20px; height: 20px; font-size: 11px; margin-top: 1px; }
.ico.mix { background: #fff; color: var(--red); box-shadow: inset 0 0 0 3px var(--red); }
.stats { margin: 4px 0 2px; display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 8px; }
.stats > div { border: 1px solid var(--line); padding: 10px 12px; display: grid; align-content: start; gap: 2px; min-width: 0; }
.stats dt { font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.stats dd { margin: 0; }
.stats .v { font: 700 22px/1.2 var(--mono); color: var(--ink); }
.stats .v.up { color: var(--green-dark); }
.stats .v.down { color: var(--red); }
.stats .n { font-size: 12.5px; line-height: 1.4; color: var(--muted); overflow-wrap: anywhere; }
.stats .n code { color: var(--text); }
.rcard td .lose { color: var(--red); }
.rlegend { display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 13px; color: var(--text); }
.rlegend span { display: inline-flex; align-items: center; gap: 6px; }
.rlegend i { width: 14px; height: 3px; border-radius: 2px; display: inline-block; }
.rlegend .ghost i { height: 0; border-top: 2px dashed var(--faint); background: none; }
.chart { position: relative; width: 100%; min-width: 0; }
.chart svg { display: block; width: 100%; height: auto; overflow: visible; }
.chart text { font-family: var(--mono); font-size: 11px; fill: var(--muted); }
.chart text.eng { font-family: var(--font); fill: var(--faint); }
.rlegend .sus svg { width: 10px; height: 8px; fill: var(--bg); stroke: var(--muted); stroke-width: 1.5; overflow: visible; }
.chart .tick line { stroke: var(--grid); }
.chart .base { stroke: var(--line); }
.chart .cross { stroke: var(--faint); stroke-width: 1; opacity: 0; }
.chart .cross.on { opacity: 1; }
.chart .hit { fill: transparent; cursor: crosshair; }
.tipbox { position: absolute; pointer-events: none; background: var(--ink); color: #fff; font: 13px/1.45 var(--font); padding: 8px 10px; width: max-content; max-width: min(280px, 80%); z-index: 4; }
.tipbox b { display: block; font-family: var(--mono); font-size: 12.5px; margin-bottom: 3px; }
.tipbox .r { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 12px; }
.tipbox .r i { width: 8px; height: 8px; border-radius: 50%; flex: none; box-shadow: 0 0 0 1.5px #fff; }
.tipbox .n { color: #cfcfcf; font-size: 12px; margin-top: 3px; }
.rfoot { margin: 8px 0 0; font-size: 13px; color: var(--muted); }
.rfoot.noisy { margin: 0; color: var(--red); font-weight: 700; }
.rcard details summary { cursor: pointer; font-size: 13.5px; font-weight: 700; color: var(--ink); width: max-content; }
.rcard .tb { overflow-x: auto; border: 1px solid var(--line); margin-top: 8px; }
.rcard table { border-collapse: collapse; width: 100%; font-size: 13px; font-variant-numeric: tabular-nums; }
.rcard th { text-align: right; font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); padding: 7px 9px; background: var(--soft); border-bottom: 1px solid var(--line); white-space: nowrap; }
.rcard td { text-align: right; padding: 5px 9px; border-bottom: 1px solid var(--grid); font-family: var(--mono); font-size: 12.5px; white-space: nowrap; }
.rcard th:first-child, .rcard td:first-child { text-align: left; }
.rcard td:first-child { font-family: var(--font); font-size: 13px; }
.rcard td small { color: var(--faint); font-size: 11.5px; margin-left: 4px; }
.rcard td .sm { color: var(--yellow-ink); }
.rcard td.off { color: var(--faint); }
.rcard .links { font-size: 13px; }
/* Section headings: one per folder under code/, with how many benchmarks it shows. */
.rsec { font-size: 24px; letter-spacing: -.01em; text-transform: none; color: var(--ink); margin: 40px 0 0; padding-top: 14px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; scroll-margin-top: calc(var(--rpick, 0px) + 8px); }
.rsec small { font-size: 14px; font-weight: 400; color: var(--muted); letter-spacing: 0; }
.rsec .on { margin-left: auto; font: 500 13px var(--mono); color: var(--ink); float: none; }
.jump { font: 14px var(--font); color: var(--text); background: var(--bg); border: 1px solid var(--line); padding: 5px 9px; min-width: 0; max-width: 100%; }
.jump:focus-visible { outline: 2px solid var(--green); outline-offset: 0; border-color: var(--green); }
.rhow { padding-block: 22px 0; display: grid; gap: 8px; font-size: 14px; color: var(--muted); }
.rhow p { margin: 0; max-width: 75ch; }
@media (max-width: 820px) { .bwrap { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 640px) {
/* On a phone the pinned picker would cover most of the screen. */
.picker { position: static; border-bottom: 1px solid var(--line); }
.tip::after { position: fixed; left: 16px; right: 16px; top: auto; bottom: calc(16px + env(safe-area-inset-bottom, 0px)); transform: none; width: auto; max-width: none; font-size: 14px; padding: 12px 14px; }
}
@media (max-width: 560px) { .detail { padding-left: 4px; } .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .stats > div { padding: 8px 10px; } .stats .v { font-size: 16px; } }
@media (max-width: 480px) { .services { grid-template-columns: minmax(0, 1fr); } }
</style>
</head>
<body>
<header>
<div class="col">
<a class="logo" href="./" aria-label="fast-ruby results">
<svg viewBox="0 0 60 34" aria-hidden="true">
<g fill="var(--ruby)"><path d="M6 10 12 4h14l6 6-13 15z" opacity=".15"/><path d="M12 10 18 4h14l6 6-13 15z" opacity=".3"/><path d="M18 10 24 4h14l6 6-13 15z" opacity=".55"/><path d="M24 10 30 4h14l6 6-13 15z"/></g>
</svg>
<span><b>fast-ruby</b><small>results</small></span>
</a>
<nav>
<a href="https://github.com/fastruby/fast-ruby">GitHub</a>
<a href="https://www.fastruby.io/blog">Blog</a>
<a class="maint" href="#sponsor">Maintained by <b>FastRuby<i>.io</i></b></a>
<a class="btn" href="https://www.fastruby.io/contact">Contact us</a>
</nav>
</div>
</header>
<main class="col" id="top">
<nav class="views" aria-label="Views"><div id="views">
<button type="button" data-view="across-rubies">Across Rubies</button>
<button type="button" data-view="does-it-hold">Does it hold?</button>
</div></nav>
<div id="view-across-rubies" hidden>
<div class="intro">
<h1>How fast is each idiom on each Ruby?</h1>
<p>Every Ruby ran each benchmark on the same machine, one after the other, so here you can compare i/s across Rubies. A reference Ruby ran again and again between them, to show how steady the machine was during the run.</p>
<p class="run" id="rrun"></p>
<p class="run"><a href="#rhow">How to read these charts</a></p>
<p class="note" id="rnote" hidden></p>
</div>
<div class="picker" id="rpicker">
<div class="prow"><span class="lbl">Build</span><div class="chips" id="rmodes"></div><span class="jkey" id="rhint"></span></div>
<div class="prow"><span class="lbl">Show</span><div class="chips" id="rshow"></div></div>
<div class="prow"><label class="lbl" for="rjump">Jump to</label><select id="rjump" class="jump"></select></div>
</div>
<div id="rout"></div>
<div class="rhow" id="rhow">
<p><b>How to read a card.</b> The heading says what happened on the build you picked: ✓ the claimed idiom (the one the benchmark file says is fastest) wins on every Ruby, or wins on some and ties on the rest; = it ties on every Ruby; ~ it wins on some and another idiom wins on others; ✗ it never wins; – no Ruby could compare them. It compares the claimed idiom with the fastest of the others, and a gap within the measured error is a tie.</p>
<p><b>How to read a chart.</b> The y axis is logarithmic: each gridline step is the same ratio, so a 100× gap between two idioms and a 10% change between two Rubies both show. Under "Relative", every line is divided by the claimed idiom on the reference Ruby, so 1× is that idiom there and the higher line is still the faster one.</p>
<p><b>How this is measured.</b> Each shard of benchmark files ran on one GitHub runner: every build, in a random order, with benchmark-ips' default timing. The numbers are i/s as measured.</p>
<p><b>How steady the machine was.</b> The reference Ruby ran first, again after every few builds, and last. It is the same code every time, so how much its i/s varies is the machine, not Ruby. "Numbers per build" shows that variation (standard deviation over mean). Over 5% the benchmark is marked noisy under its chart, and when the typical benchmark of a run is over 5%, the page warns at the top. Two Rubies whose i/s differ by less than that variation are shown as same-ish.</p>
<p>Only Rubies of the same run are compared. Weeks are not compared with each other: each week can land on a different CPU. Results over a billion iterations per second (under 1 ns each) are marked suspect and kept off the chart: the JIT most likely removed the work.</p>
</div>
</div>
<div id="view-does-it-hold">
<div class="intro">
<h1>Which fast-ruby advice holds on your Ruby?</h1>
<p>Every benchmark names the idiom it claims is fastest. We run each one on every Ruby, with and without the JIT, and check whether the claim still wins on that Ruby.</p>
<p class="run" id="run"></p>
</div>
<div class="picker" id="picker">
<div class="current" id="current"></div>
<div class="prow"><span class="lbl">Your Ruby</span><div class="chips big" id="chips"></div><span class="jkey"><span class="j"></span>also built with YJIT or ZJIT</span></div>
<div class="prow"><span class="lbl">Build</span><div class="chips" id="modes"></div></div>
<div class="tabs" id="tabs">
<button type="button" data-f="all">All</button>
<button type="button" data-f="no">Does not hold</button>
<button type="button" data-f="eq">Tie</button>
<button type="button" data-f="ok">Holds</button>
<button type="button" data-f="na">Skipped</button>
<button type="button" data-f="sus">Suspect</button>
<button type="button" data-f="jit">JIT changes the result</button>
</div>
</div>
<div id="out"></div>
<div class="legend">
<span class="line"><span class="ok"></span></span>holds
<span class="line"><span class="eq"></span></span>tie, within error
<span class="line"><span class="no"></span></span>another idiom won
<span class="line"><span class="na"></span></span>skipped
<span>Each dot is one Ruby, from 2.1 to head, then JRuby and TruffleRuby, that has the build you picked. The dot with the dark ring is the Ruby you selected, <b id="legendme"></b>. Hover a dot to see which Ruby it is.</span>
</div>
</div>
</main>
<section class="band" id="sponsor" aria-label="Sponsor">
<div class="bwrap">
<div>
<div class="beyebrow">Maintained and sponsored by</div>
<h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
<p>FastRuby.io is the Rails upgrade team at OmbuLabs. We maintain this project and run every benchmark on <b id="rubycount"></b> Ruby builds.</p>
<p>If these results show your app is stuck on an old Ruby, that is the work we do every day: we upgrade Ruby on Rails applications without interrupting product development.</p>
<div class="cta">
<a class="bbtn primary" href="https://www.fastruby.io/contact">Talk to us about your upgrade</a>
<a class="bbtn ghost" href="https://www.fastruby.io/blog">Read the FastRuby.io blog</a>
</div>
</div>
<div class="services">
<a class="svc" href="https://www.fastruby.io/"><b>Rails upgrades</b><span>Upgrade your Rails application without interrupting product development.</span></a>
<a class="svc" href="https://www.fastruby.io/roadmap"><b>The Roadmap</b><span>A deep-dive audit with a step-by-step plan, effort estimates and risk flags.</span></a>
<a class="svc" href="https://www.fastruby.io/monthly-rails-maintenance"><b>Monthly maintenance</b><span>Get your Rails app current while your team keeps shipping.</span></a>
<a class="svc" href="https://www.fastruby.io/security-audit"><b>Security audit</b><span>A list of vulnerable dependencies and security issues in your code.</span></a>
</div>
<div class="proof"><span><b>100+</b>apps upgraded</span><span><b>2012</b>upgrading since</span><span><b>1 to 100</b>people on the teams we work with</span></div>
</div>
</section>
<footer>
<div class="col">
<span>fast-ruby, code under CC0.</span>
<a href="https://github.com/fastruby/fast-ruby">GitHub</a>
<a href="https://www.fastruby.io/">fastruby.io</a>
<a href="https://www.ombulabs.com/">OmbuLabs</a>
</div>
</footer>
<script>
/*DOES_IT_HOLD_DATA*/
/*ACROSS_RUBIES_DATA*/
</script>
<script>
// Two views, chosen by ?view=across-rubies (the default) or ?view=does-it-hold.
// Each view keeps its own choices in the URL and leaves the others' alone.
(function () {
const VIEWS = ["across-rubies", "does-it-hold"], preferred = "across-rubies";
const current = () => { const v = new URLSearchParams(location.search).get("view"); return VIEWS.includes(v) ? v : preferred; };
// Sets (or with null, removes) these URL parameters; push makes a history entry.
function setParams(values, push) {
const p = new URLSearchParams(location.search);
Object.entries(values).forEach(([k, v]) => (v == null || v === "" ? p.delete(k) : p.set(k, v)));
const q = p.toString();
const url = location.pathname + (q ? "?" + q : "") + location.hash;
if (url === location.pathname + location.search + location.hash) return;
try { history[push ? "pushState" : "replaceState"](null, "", url); } catch (e) { /* file:// in some browsers */ }
}
function show() {
const v = current();
VIEWS.forEach((x) => { document.getElementById("view-" + x).hidden = x !== v; });
document.querySelectorAll("#views button").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.view === v)));
document.dispatchEvent(new CustomEvent("viewchange", { detail: v }));
}
document.getElementById("views").addEventListener("click", (e) => {
const b = e.target.closest("button");
if (!b || b.dataset.view === current()) return;
setParams({ view: b.dataset.view === preferred ? null : b.dataset.view }, true);
window.scrollTo(0, 0);
show();
});
window.addEventListener("popstate", show);
document.getElementById("rubycount").textContent = window.ACROSS_RUBIES.meta.labels.length;
window.SiteViews = { current, setParams, show };
})();
</script>
<script>
(function () {
const $ = (id) => document.getElementById(id);
const D = window.DOES_IT_HOLD, M = D.meta, B = D.benchmarks;
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const md = (s) => esc(s).replace(/`([^`]+)`/g, "<code>$1</code>");
const x = (r) => r >= 1e6 ? (r / 1e6).toFixed(1) + "M×" : r >= 1e4 ? Math.round(r / 1e3) + "k×" : r >= 10 ? Math.round(r) + "×" : r < 1.1 ? r.toFixed(2) + "×" : r.toFixed(1) + "×";
const fmtIps = (v) => v >= 1e9 ? (v / 1e9).toFixed(2) + "B" : v >= 1e6 ? (v / 1e6).toFixed(2) + "M" : v >= 1e3 ? (v / 1e3).toFixed(1) + "k" : v.toFixed(1);
const cpuName = (c) => (c || "unknown").replace(/^AMD /, "").replace(/ \d+-Core Processor$/, "").replace(/^Intel\(R\) Xeon\(R\) Platinum (\S+) CPU.*$/i, "Xeon $1");
const bases = M.labels.filter((l) => !l.includes("+"));
const MODES = [["", "Interpreter"], ["yjit", "YJIT"], ["zjit", "ZJIT"]];
const FILTERS = ["all", "no", "eq", "ok", "na", "sus", "jit"];
const lab = (base, mode) => mode ? base + "+" + mode : base;
const has = (base, mode) => M.labels.includes(lab(base, mode));
const modeName = (mode) => MODES.find(([m]) => m === mode)[1];
const nice = (l) => { const [b, m] = l.split("+"); return b.replace(/^ruby_/, "Ruby ").replace(/^jruby_/, "JRuby ").replace(/^truffleruby_/, "TruffleRuby ") + (m ? " + " + m.toUpperCase() : ""); };
const shortLabel = (l) => l.replace(/^ruby_/, "");
const jitsOf = (base) => MODES.filter(([m]) => m && has(base, m)).map(([m]) => m);
// The builder computed every verdict; here each run only gets its reports
// by name and its fastest one.
B.forEach((b, i) => {
b.i = i;
b.v = {};
M.labels.forEach((l) => {
const r = b.runs[l];
if (!r) { b.v[l] = { s: "na" }; return; }
const es = r.entries.map((e, k) => e && { name: b.names[k], ips: e[0], err: e[1] }).filter(Boolean);
const top = es.reduce((a, c) => (c.ips > a.ips ? c : a), es[0]);
b.v[l] = { s: r.state, r: r.ratio, winner: r.winner == null ? null : b.names[r.winner], sus: r.suspect && r.state !== "na", es, top };
});
});
const jitDiff = (b, base) => jitsOf(base).some((m) => { const a = b.v[base].s, c = b.v[lab(base, m)].s; return a !== "na" && c !== "na" && a !== c; });
const bucket = (v) => (v.sus ? "sus" : v.s);
const TIPS = {
ok: "Holds: the idiom the file says is fastest won on this Ruby.",
no: "Does not hold: another idiom was faster, by more than the measured error.",
eq: "Tie: the gap is within the error, so neither is faster.",
na: "Skipped: the benchmark uses a method this Ruby does not have yet, so it did not run here.",
sus: "Suspect: a report ran over a billion times per second, under 1 ns per iteration. The JIT most likely removed the work, so these results are not counted as holds, ties or losses.",
jit: "JIT changes the result: with YJIT or ZJIT on this Ruby, the verdict is different from the interpreter.",
ips: "i/s is iterations per second: how many times benchmark-ips ran that code in one second. Higher is faster.",
same: "Every build of this benchmark (interpreter, YJIT, ZJIT, every Ruby) ran on the same machine, one after the other, so the columns can be compared: YJIT against the interpreter is a fair comparison here.",
cpuSame: "The CPU model of the machine every build of this benchmark ran on.",
};
const tip = (k) => `<button type="button" class="tip" aria-label="${esc(TIPS[k])}" data-tip="${esc(TIPS[k])}">!</button>`;
const icon = { ok: "✓", eq: "=", no: "✗", na: "–" };
const tabName = { all: "All", no: "Does not hold", eq: "Tie", ok: "Holds", na: "Skipped", sus: "Suspect", jit: "JIT changes the result" };
function resText(v) {
if (v.s === "ok") return `wins by ${x(v.r)}`;
if (v.s === "eq") return "tie";
if (v.s === "no") return `loses to ${esc(v.winner)} by ${x(v.r)}`;
return "skipped";
}
const short = (v) => v.s === "ok" ? "✓ wins by " + x(v.r) : v.s === "eq" ? "= tie" : v.s === "no" ? "✗ loses" : "skipped";
// State: Ruby, build and filter, kept in the URL (?ruby=3.3&build=yjit&filter=no).
let me, mode, filter;
const open = new Set();
function fromURL() {
const p = new URLSearchParams(location.search);
me = bases.find((l) => shortLabel(l) === p.get("ruby")) || M.default;
mode = MODES.some(([m]) => m && m === p.get("build")) ? p.get("build") : "";
filter = FILTERS.includes(p.get("filter")) ? p.get("filter") : "all";
}
function toURL(push) {
window.SiteViews.setParams({ ruby: me !== M.default ? shortLabel(me) : null, build: mode || null, filter: filter !== "all" ? filter : null }, push);
}
$("chips").innerHTML = bases.map((l) => `<button type="button" class="chip" data-l="${esc(l)}">${esc(shortLabel(l).replace("truffleruby_", "truffle ").replace("jruby_", "jruby "))}${jitsOf(l).length ? '<span class="j" title="also built with YJIT or ZJIT"></span>' : ""}</button>`).join("");
// Where the results come from: the weekly run on main, or a local run.
const runLink = M.run_id ? `<a href="${M.repo}/actions/runs/${esc(M.run_id)}">CI run ${esc(M.run_id)}</a>` : "a local run";
const at = M.commit ? ` @ <a href="${M.repo}/commit/${esc(M.commit)}">${esc(String(M.commit).slice(0, 7))}</a>` : "";
$("run").innerHTML = `Results from ${runLink}${at}, built ${esc(M.built_at)}.`;
function detail(b) {
const cols = ["", ...jitsOf(me)].map((m) => lab(me, m));
const vs = cols.map((l) => b.v[l]);
if (vs.every((v) => v.s === "na")) {
return `<div class="detail"><p class="fine">This benchmark needs a newer Ruby than ${esc(nice(me))}.</p></div>`;
}
const head = cols.map((l, i) => `<th>${esc(i ? l.split("+")[1].toUpperCase() : "Interpreter")}<span class="v ${vs[i].s}">${short(vs[i])}</span>${vs[i].sus ? '<span class="v sus">! suspect</span>' : ""}</th>`).join("");
const rows = b.names.map((nm) => `<tr class="${nm === b.names[0] ? "claimed" : ""}"><td><code>${esc(nm)}</code>${nm === b.names[0] ? " (claimed)" : ""}</td>` +
vs.map((v) => {
if (v.s === "na" && !v.es) return "<td>–</td>";
const e = v.es.find((z) => z.name === nm);
if (!e) return "<td>–</td>";
const ips = `<small class="ips">${fmtIps(e.ips)} i/s ±${((e.err / e.ips) * 100).toFixed(1)}%</small>`;
return e === v.top ? `<td class="fast">fastest${ips}</td>` : `<td>${x(v.top.ips / e.ips)} slower${ips}</td>`;
}).join("") + "</tr>").join("");
// Every build of a benchmark ran on one machine.
const envRow = `<tr class="env"><td>Ran on</td>${cols.map(() => `<td title="${esc(b.cpu || "")}">${esc(cpuName(b.cpu))}</td>`).join("")}</tr>`;
// How to read this table, next to it instead of in the pinned picker.
const used = new Set(vs.map((v) => (v.sus ? "sus" : v.s)));
const words = [["ok", "holds"], ["eq", "tie"], ["no", "does not hold"], ["na", "skipped"], ["sus", "suspect"]]
.filter(([k]) => ["ok", "eq", "no"].includes(k) || used.has(k))
.map(([k, w]) => `${w}${tip(k)}`).join(", ");
const howto = `<p class="fine">Reading the numbers: ${words} · i/s${tip("ips")} · one machine for every build${tip("same")} · Ran on${tip("cpuSame")}</p>`;
const built = bases.filter((l) => has(l, mode));
const fails = built.filter((l) => b.v[lab(l, mode)].s === "no");
const ran = built.filter((l) => b.v[lab(l, mode)].s !== "na").length;
const buildName = mode ? esc(mode.toUpperCase()) : "the interpreter";
const claim = `<code>${esc(b.names[0])}</code>`;
const across = fails.length
? `Across all Rubies with ${buildName}: ${claim} does not hold on ${fails.map(nice).map(esc).join(", ")}. It holds or ties on the other ${ran - fails.length} that ran it.`
: `Across all Rubies with ${buildName}: ${claim} holds or ties on all ${ran} that ran it.`;
return `<div class="detail">
<div class="tb"><table><thead><tr><th>${esc(nice(me))}</th>${head}</tr></thead><tbody>${rows}${envRow}</tbody></table></div>
${howto}
<p class="fine">${across}</p>
<div class="links"><a href="${M.repo}/blob/main/${encodeURI(b.file)}">View the code</a>${M.run_id ? `<a href="${M.repo}/actions/runs/${esc(M.run_id)}">CI run</a>` : ""}</div>
</div>`;
}
function render() {
if (!has(me, mode)) mode = "";
const cur = lab(me, mode);
const jits = jitsOf(me);
const cnt = { ok: 0, eq: 0, no: 0, na: 0, sus: 0 };
B.forEach((b) => cnt[bucket(b.v[cur])]++);
const nDiff = jits.length ? B.filter((b) => jitDiff(b, me)).length : 0;
if ((filter === "jit" && !jits.length) || (filter === "na" && !cnt.na) || (filter === "sus" && !cnt.sus)) filter = "all";
document.querySelectorAll("#chips .chip").forEach((c) => c.setAttribute("aria-pressed", String(c.dataset.l === me)));
$("modes").innerHTML = MODES.map(([m, name]) => `<button type="button" class="chip" data-m="${m}" aria-pressed="${m === mode}" ${has(me, m) ? "" : 'disabled title="Not built for this Ruby"'}>${name}</button>`).join("");
const tabCount = { all: B.length, no: cnt.no, eq: cnt.eq, ok: cnt.ok, na: cnt.na, sus: cnt.sus, jit: nDiff };
document.querySelectorAll("#tabs button").forEach((c) => {
const f = c.dataset.f;
c.innerHTML = `${tabName[f]}<span class="c">${tabCount[f]}</span>`;
if (TIPS[f]) c.title = TIPS[f];
c.setAttribute("aria-pressed", String(f === filter));
if (f === "jit") c.disabled = !jits.length;
if (f === "na" || f === "sus") c.hidden = !tabCount[f];
});
$("current").innerHTML = `<span class="pre">Results for</span><span class="rb">${esc(nice(me))}</span><span class="md">${esc(modeName(mode))}</span>`;
$("legendme").textContent = nice(me);
const shown = B.filter((b) => filter === "all" || (filter === "jit" ? jitDiff(b, me) : bucket(b.v[cur]) === filter));
const stations = bases.filter((l) => has(l, mode));
let h = "", sec = null;
shown.forEach((b) => {
if (b.section !== sec) {
if (sec !== null) h += "</ul>";
sec = b.section;
h += `<h2>${esc(sec)}<span class="on">on ${esc(nice(cur))}</span></h2><ul class="list">`;
}
const v = b.v[cur], isOpen = open.has(b.i);
h += `<li class="item"><button type="button" data-i="${b.i}" aria-expanded="${isOpen}">
<span class="ico ${v.s}" aria-hidden="true">${icon[v.s]}</span>
<span class="t">${md(b.title)}</span>
<span class="meta"><span>Use <code>${esc(b.names[0])}</code><span class="res ${v.s}">${resText(v)}${v.sus ? ' <span class="sus">(suspect)</span>' : ""}</span></span>
<span class="line" aria-label="Result on every Ruby with ${esc(modeName(mode))}">${stations.map((l) => { const w = b.v[lab(l, mode)]; return `<span class="${w.s}${l === me ? " me" : ""}" title="${esc(nice(lab(l, mode)))}: ${resText(w).replace(/<[^>]+>/g, "")}${w.sus ? " (suspect)" : ""}"></span>`; }).join("")}</span></span>
</button>${isOpen ? detail(b) : ""}</li>`;
});
if (sec !== null) h += "</ul>";
$("out").innerHTML = h || `<p class="empty">Nothing in this group on ${esc(nice(cur))}. Pick "All" to see every benchmark.</p>`;
}
// A change made while scrolled into the list:
// - on the All list, switching Ruby or build keeps the same benchmark at the
// same place on screen (the list is the same, so results can be compared);
// - otherwise the new list starts right under the pinned picker, instead of
// keeping the old scroll offset.
function change(fn, keepPlace) {
const picker = $("picker"), out = $("out");
const edge = () => picker.getBoundingClientRect().bottom;
const inList = getComputedStyle(picker).position === "sticky" && out.getBoundingClientRect().top < edge();
const wasAll = filter === "all";
const anchor = inList && keepPlace && wasAll
? [...out.querySelectorAll("li.item")].find((li) => li.getBoundingClientRect().bottom > edge())
: null;
const anchorId = anchor && anchor.querySelector("button").dataset.i;
const anchorGap = anchor && anchor.getBoundingClientRect().top - edge();
fn();
render();
toURL(true);
if (!inList) return;
const again = anchorId != null && filter === "all" && out.querySelector(`li.item > button[data-i="${anchorId}"]`);
if (again) window.scrollTo(0, window.scrollY + again.getBoundingClientRect().top - edge() - anchorGap);
else window.scrollTo(0, window.scrollY + out.getBoundingClientRect().top - picker.getBoundingClientRect().height);
}
$("chips").addEventListener("click", (e) => { const c = e.target.closest(".chip"); if (c) change(() => { me = c.dataset.l; }, true); });
$("modes").addEventListener("click", (e) => { const c = e.target.closest(".chip"); if (c && !c.disabled) change(() => { mode = c.dataset.m; }, true); });
$("tabs").addEventListener("click", (e) => { const c = e.target.closest("button"); if (c && !c.disabled) change(() => { filter = c.dataset.f; }); });
$("out").addEventListener("click", (e) => {
const c = e.target.closest("li.item > button");
if (!c) return;
const i = +c.dataset.i;
open.has(i) ? open.delete(i) : open.add(i);
render();
});
window.addEventListener("popstate", () => { fromURL(); render(); });
// The other view may have changed the build, which both views share.
document.addEventListener("viewchange", (e) => { if (e.detail === "does-it-hold") { fromURL(); render(); } });
// iPhone Safari does not focus a button on tap, so :focus alone never
// shows the text there: a tap toggles it, a tap anywhere else closes it.
document.addEventListener("click", (e) => {
const t = e.target.closest(".tip");
document.querySelectorAll(".tip.open").forEach((o) => { if (o !== t) o.classList.remove("open"); });
if (t) t.classList.toggle("open");
});
// No URL write here: on load, a build the selected Ruby lacks falls back to the interpreter on screen only, so a build chosen in Across Rubies survives a reload.
fromURL();
render();
})();
</script>
<script>
// Across Rubies. Each entry is [i/s, error, relative to the reference's median of the claimed report].
(function () {
const $ = (id) => document.getElementById(id);
const D = window.ACROSS_RUBIES;
const M = D.meta, B = D.benchmarks, REF = M.reference;
const COLORS = ["var(--s1)", "var(--s2)", "var(--s3)", "var(--s4)", "var(--s5)", "var(--s6)"];
// A seventh report (none today) would reuse a color, never fail to draw.
const color = (i) => COLORS[i % COLORS.length];
const MODES = [["", "Interpreter"], ["yjit", "YJIT"], ["zjit", "ZJIT"]];
// Without the reference (its image failed, say), there is nothing to be relative to.
const SHOWS = REF ? [["ips", "i/s"], ["rel", "Relative to " + nice(REF)]] : [["ips", "i/s"]];
let mode = "", showing = "ips";
function esc(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); }
function md(s) { return esc(s).replace(/`([^`]+)`/g, "<code>$1</code>"); }
function nice(l) { const [b, m] = l.split("+"); return b.replace(/^ruby_/, "Ruby ").replace(/^jruby_/, "JRuby ").replace(/^truffleruby_/, "TruffleRuby ") + (m ? " + " + m.toUpperCase() : ""); }
function fmt(v) { return v >= 1e9 ? (v / 1e9).toFixed(1) + "B" : v >= 1e6 ? (v / 1e6).toFixed(v >= 1e7 ? 1 : 2) + "M" : v >= 1e3 ? (v / 1e3).toFixed(v >= 1e5 ? 0 : 1) + "k" : v.toFixed(1); }
function pct(a, b) { const p = (a / b - 1) * 100; return (p >= 0 ? "+" : "") + p.toFixed(1) + "%"; }
function cpu(c) { return (c || "unknown CPU").replace(/^AMD /, "").replace(/ \d+-Core Processor$/, "").replace(/^Intel\(R\) Xeon\(R\) Platinum (\S+) CPU.*$/i, "Xeon $1"); }
const base = (l) => l.split("+")[0], variant = (l) => l.split("+")[1] || "", mri = (l) => /^ruby_/.test(l);
const has = (m) => M.labels.some((l) => variant(l) === m);
const sus = (e) => !!e && e[0] > M.suspect;
// A result that can be drawn and compared: it ran and is not suspect.
const ok = (e) => !!e && !sus(e) && e[0] > 0;
// Relative needs the reference to have the claimed report.
const val = (e) => { const v = ok(e) ? (showing === "rel" ? e[2] : e[0]) : null; return v > 0 ? v : null; };
// Two reports of one build, measured seconds apart: same-ish when the error bars overlap, as benchmark-ips says.
const same = (a, b) => a[0] + a[1] > b[0] - b[1] && a[0] - a[1] < b[0] + b[1];
// Two Rubies, measured minutes apart: also same-ish when the gap is within how much the reference varied on that machine, which is larger than benchmark-ips' error bars.
const sameRuby = (a, b, floor) => same(a, b) || Math.abs(a[0] / b[0] - 1) <= (floor || 0);
// The x axis: MRI builds of the selected build as a line, then (interpreter only) the other engines as points.
function columns() {
const line = M.labels.filter((l) => mri(l) && variant(l) === mode);
const other = mode ? [] : M.labels.filter((l) => !mri(l));
return { line, other, all: line.concat(other) };
}
function fromURL() {
const p = new URLSearchParams(location.search);
mode = MODES.some(([m]) => m && m === p.get("build")) && has(p.get("build")) ? p.get("build") : "";
showing = p.get("show") === "rel" && REF ? "rel" : "ips";
}
if (!M.passed) {
const why = [];
why.push(!REF
? "The reference Ruby did not run in this run, so there is no way to tell how steady the machine was. Small differences between Rubies may be the machine, not Ruby."
: M.variation == null
? "The reference Ruby ran only once in this run, so there is no way to tell how steady the machine was."
: `The machine was not steady during this run: the reference Ruby's i/s varied by ${(M.variation * 100).toFixed(1)}% on a typical benchmark (the limit is ${(M.gate * 100).toFixed(0)}%). Small differences between Rubies may be the machine, not Ruby.`);
$("rnote").textContent = why.join(" ");
$("rnote").hidden = false;
}
const runLink = M.run_id ? `<a href="${M.repo}/actions/runs/${esc(M.run_id)}">run ${esc(M.run_id)}</a>` : "a local run";
const at = M.commit ? ` @ <a href="${M.repo}/commit/${esc(M.commit)}">${esc(String(M.commit).slice(0, 7))}</a>` : "";
const day = M.shards.length ? M.shards[0].from.slice(0, 10) : "";
$("rrun").innerHTML = `Results from ${runLink}${at}, ${esc(day)}: ${M.labels.length} builds on ${M.shards.length} machine${M.shards.length === 1 ? "" : "s"}, built ${esc(M.built_at)}.`;
// The claimed report against the fastest other report of one build: { s: "ok" | "eq" | "no", ratio, winner }, or null when they cannot be compared.
function versus(r) {
const c = r && r.entries[0];
if (!ok(c)) return null;
let w = -1;
r.entries.forEach((e, i) => { if (i && ok(e) && (w < 0 || e[0] > r.entries[w][0])) w = i; });
if (w < 0) return null;
const o = r.entries[w];
if (same(c, o)) return { s: "eq", winner: w };
return c[0] > o[0] ? { s: "ok", ratio: c[0] / o[0], winner: w } : { s: "no", ratio: o[0] / c[0], winner: w };
}
// Card icons, as in "Does it hold?"; "~" when the claim wins on some Rubies and loses on others.
const RICON = { ok: "✓", eq: "=", no: "✗", na: "–", mix: "~" };
// Ratios as in "Does it hold?": 1.04×, 2.3×, 35×, 12k×, 27M×.
const x2 = (r) => r >= 1e6 ? (r / 1e6).toFixed(1) + "M×" : r >= 1e4 ? Math.round(r / 1e3) + "k×" : r >= 10 ? Math.round(r) + "×" : r < 1.1 ? r.toFixed(2) + "×" : r.toFixed(1) + "×";
// The Ruby without its build: under YJIT the build is already in the heading.
const rb = (l) => nice(base(l));
// Rubies next to each other on the axis, as "Ruby 2.1 to 2.6" (two: "Ruby 3.1 and 3.2"); a run never spans two engines.
function runsOf(labels, cols) {
const out = [];
let run = [];
const flush = () => { if (run.length) out.push(run.length === 1 ? rb(run[0]) : `${rb(run[0])} ${run.length === 2 ? "and" : "to"} ${base(run[run.length - 1]).replace(/^[a-z]+_/, "")}`); run = []; };
cols.forEach((l) => {
if (!labels.includes(l)) return flush();
if (run.length && base(run[0]).split("_")[0] !== base(l).split("_")[0]) flush();
run.push(l);
});
flush();
return out.join(", ");
}
// What happened, for the selected build: a headline (icon, sentence) and one line of numbers under it.
function verdict(b) {
const cols = columns().all;
const claim = `<code>${esc(b.names[0])}</code>`, nm = (i) => `<code>${esc(b.names[i])}</code>`;
const where = mode ? ` with ${esc(mode.toUpperCase())}` : "";
const res = cols.map((l) => [l, versus(b.runs[l])]).filter(([, v]) => v);
const n = res.length, of = (s) => res.filter(([, v]) => v.s === s);
const wins = of("ok"), ties = of("eq"), losses = of("no");
const all = `${n === 1 ? "the one Ruby" : `all ${n} Rubies`}${where}`;
let icon = "ok", head;
if (!n) { icon = "na"; head = `Did not run on any Ruby${where}`; }
else if (!losses.length && !ties.length) head = b.names.length === 2 ? `${claim} beats ${nm(1)} on <b>${all}</b>` : `${claim} is the fastest on <b>${all}</b>`;
else if (!losses.length && !wins.length) { icon = "eq"; head = b.names.length === 2 ? `${claim} and ${nm(1)} are about the same on <b>${all}</b>` : `${claim} ties with the fastest on <b>${all}</b>`; }
else if (!losses.length) head = `${claim} wins or ties on <b>${all}</b> (${ties.length} tie${ties.length > 1 ? "s" : ""})`;
else {
// Who beat the claim, and on which Rubies: the most frequent winner first, at most two named.
const by = {};
losses.forEach(([l, v]) => { (by[v.winner] = by[v.winner] || []).push(l); });
const top = Object.keys(by).sort((a, c) => by[c].length - by[a].length);
const named = top.slice(0, 2).map((w) => `${nm(+w)} wins on ${esc(runsOf(by[w], cols))}`);
const rest = top.slice(2).reduce((s, w) => s + by[w].length, 0);
if (rest) named.push(`others on ${rest} more`);
if (losses.length === n && top.length === 1) { icon = "no"; head = `${nm(+top[0])} beats ${claim} on <b>${all}</b>`; }
else if (losses.length === n) { icon = "no"; head = `${claim} loses on <b>${all}</b>: ${named.join("; ")}`; }
else if (!wins.length) { icon = "no"; head = `${claim} never wins: it ties on <b>${ties.length} of ${n} Rubies${where}</b>; ${named.join("; ")}`; }
else { icon = "mix"; head = `${claim} wins on <b>${wins.length} of ${n} Rubies${where}</b>${ties.length ? ` (${ties.length} tie${ties.length > 1 ? "s" : ""})` : ""}; ${named.join("; ")}`; }
}
// One tile per fact: a label, one number, and a caption saying who it is about and where.
const tiles = [];
const rival = b.names.length === 2 ? nm(1) : "the fastest other idiom";
const range = (list) => {
const s = list.slice().sort((a, c) => a[1].ratio - c[1].ratio), lo = s[0], hi = s[s.length - 1];
const one = lo === hi || x2(lo[1].ratio) === x2(hi[1].ratio);
return { value: one ? x2(hi[1].ratio) : `${x2(lo[1].ratio)} to ${x2(hi[1].ratio)}`, where: one ? (list.length === 1 ? `on ${esc(rb(hi[0]))}` : "") : `least on ${esc(rb(lo[0]))}, most on ${esc(rb(hi[0]))}` };
};
if (wins.length) {
const g = range(wins);
tiles.push({ label: losses.length ? "Faster, where it wins" : "Faster by", value: g.value, cls: "up", note: `${claim} over ${rival}${g.where ? `<br>${g.where}` : ""}` });
}
if (losses.length) {
const g = range(losses);
tiles.push({ label: "Slower, where it loses", value: g.value, cls: "down", note: `${claim} behind the winner${g.where ? `<br>${g.where}` : ""}` });
}
// Its own speed over the released Rubies of this build; head changes every week, so it is left out.
const change = (t, f) => sameRuby(t, f, b.variation) ? { value: "Same", cls: "" } : { value: pct(t[0], f[0]), cls: t[0] > f[0] ? "up" : "down" };
const line = columns().line.filter((l) => base(l) !== "ruby_head" && b.runs[l] && ok(b.runs[l].entries[0]));
if (line.length > 1) {
const c = change(b.runs[line[line.length - 1]].entries[0], b.runs[line[0]].entries[0]);
tiles.push({ label: `${esc(rb(line[0]))} → ${esc(base(line[line.length - 1]).replace(/^ruby_/, ""))}`, value: c.value, cls: c.cls, note: `Its own i/s on the newer Ruby${c.value === "Same" ? ", within the machine's noise" : ""}` });
}
if (mode && line.length) {
const l = line[line.length - 1], g = b.runs[base(l)] && b.runs[base(l)].entries[0];
if (ok(g)) {
const c = change(b.runs[l].entries[0], g);
tiles.push({ label: `${esc(mode.toUpperCase())} on ${esc(rb(l))}`, value: c.value, cls: c.cls, note: `Its own i/s with ${esc(mode.toUpperCase())} against the interpreter` });
}
}
// Rubies on the chart that are not in the count.
const left = cols.filter((l) => !res.some(([k]) => k === l)), notes = [];
const susp = left.filter((l) => b.runs[l] && b.runs[l].entries.some(sus)), gone = left.filter((l) => !b.runs[l]), odd = left.filter((l) => b.runs[l] && !susp.includes(l));
if (susp.length) notes.push(`Not counted: ${esc(runsOf(susp, cols))}, suspect results (over 1B i/s).`);
if (odd.length) notes.push(`Not compared: ${esc(runsOf(odd, cols))}, an idiom has no result there.`);
if (gone.length) notes.push(`Did not run: ${esc(runsOf(gone, cols))}.`);
return { icon, head, tiles, foot: notes.join(" ") };
}
function tilesHtml(v) {
if (!v.tiles.length) return "";
return `<dl class="stats">${v.tiles.map((t) => `<div><dt>${t.label}</dt><dd class="v ${t.cls}">${t.value}</dd><dd class="n">${t.note}</dd></div>`).join("")}</dl>`;
}
// The y axis is logarithmic, so a 100× gap between two reports and a 10% change between two Rubies are both readable on one chart.
// It spans at least 4×, from the 1-2-5 step under the lowest value to the one over the highest.
// Past 7 ticks, the lines in between are powers of ten only (every other one, or fewer, on a very wide span).
function ticks(lo, hi) {
if (!(lo > 0) || !(hi >= lo) || !isFinite(hi)) { lo = 1; hi = 10; }
if (hi / lo < 4) { const g = Math.sqrt(hi * lo); lo = g / 2; hi = g * 2; }
const steps = [];
for (let k = Math.floor(Math.log10(lo)) - 1; k <= Math.ceil(Math.log10(hi)) + 1; k++) [1, 2, 5].forEach((m) => steps.push(+(m * Math.pow(10, k)).toPrecision(1)));
const bottom = steps.filter((t) => t <= lo).pop(), top = steps.find((t) => t >= hi);
const t = steps.filter((v) => v >= bottom && v <= top);
if (t.length <= 7) return t;
const d0 = Math.log10(bottom), d1 = Math.log10(top), every = Math.ceil((d1 - d0) / 6);
// Inner powers of ten, kept away from both ends (8% of the axis, at least 0.29 of a decade) so labels never overlap.
const room = Math.max(0.29, (d1 - d0) * 0.08), inner = [];
for (let k = Math.ceil(d0); k < d1; k += every) if (k - d0 > room && d1 - k > room) inner.push(+Math.pow(10, k).toPrecision(1));
return [bottom, ...inner, top];
}
// Tick labels without trailing zeros: 2M, 500k, 0.05×.
function tickLabel(t) {
const n = (v) => String(+v.toPrecision(3));
const big = (v) => v >= 1e9 ? n(v / 1e9) + "B" : v >= 1e6 ? n(v / 1e6) + "M" : v >= 1e3 ? n(v / 1e3) + "k" : n(v);
// Far below the claimed idiom, as a fraction: 1/10M× rather than 1e-7×.
if (showing === "rel") return (t < 1e-3 ? "1/" + big(1 / t) : n(t)) + "×";
return big(t);
}
function path(pts, c, extra, w) {
let d = "", pen = false;
pts.forEach((p) => { if (!p) { pen = false; return; } d += (pen ? "L" : "M") + p[0].toFixed(1) + "," + p[1].toFixed(1); pen = true; });
return d ? `<path d="${d}" fill="none" stroke="${c}" stroke-width="${w}" stroke-linejoin="round" stroke-linecap="round"${extra}/>` : "";
}
function chart(b, host) {
const cols = columns();
const W = Math.max(300, host.clientWidth || 300), H = W < 520 ? 210 : 250;
const P = { l: 54, r: 10, t: 14, b: 40 };
const gap = cols.other.length ? 22 : 0, n = Math.max(1, cols.all.length);
const band = (W - P.l - P.r - gap) / n;
const x = (k) => P.l + band * (k + 0.5) + (k >= cols.line.length ? gap : 0);
const ghost = mode ? cols.line.map(base) : [];
// Every drawn value with its error bar; a lower end at or under 0 is left out, since a log axis has no 0.
const lows = [], highs = [];
b.names.forEach((_, i) => cols.all.concat(ghost).forEach((l) => {
const r = b.runs[l], v = r && val(r.entries[i]);
if (v == null) return;
const err = r.entries[i][1] / r.entries[i][0] * v;
lows.push(v - err > 0 ? v - err : v);
highs.push(v + err);
}));
const tk = lows.length ? ticks(Math.min(...lows), Math.max(...highs)) : ticks(1, 10);
const bottom = tk[0], top = tk[tk.length - 1], span = Math.log10(top / bottom);
const y = (v) => P.t + (H - P.t - P.b) * (1 - Math.log10(Math.max(v, bottom) / bottom) / span);
const yb = y(bottom);
let s = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(b.names.join(" vs "))} on every Ruby">`;
tk.forEach((t) => { s += `<g class="tick"><line x1="${P.l}" x2="${W - P.r}" y1="${y(t)}" y2="${y(t)}"/><text x="${P.l - 8}" y="${y(t) + 4}" text-anchor="end">${tickLabel(t)}</text></g>`; });
s += `<line class="base" x1="${P.l}" x2="${W - P.r}" y1="${yb}" y2="${yb}"/>`;
if (showing === "rel" && bottom < 1 && top > 1) s += `<line x1="${P.l}" x2="${W - P.r}" y1="${y(1)}" y2="${y(1)}" stroke="var(--faint)" stroke-dasharray="2 3"/>`;
// x labels: the version on each column ("+yjit" is already in the picker), and under it the engine once per group of columns.
// About 7 px per character at 11 px: on a narrow chart every other version is labelled, and a long engine name shortens.
const cw = 7, every = band < 4 * cw + 2 ? 2 : 1;
const groups = [];
cols.all.forEach((l, k) => { const e = base(l).split("_")[0]; if (groups.length && groups[groups.length - 1].e === e) groups[groups.length - 1].to = k; else groups.push({ e, from: k, to: k }); });
groups.forEach((g) => {
for (let k = g.from; k <= g.to; k += every) s += `<text x="${x(k)}" y="${H - P.b + 16}" text-anchor="middle">${esc(base(cols.all[k]).replace(/^[a-z]+_/, ""))}</text>`;
});
groups.forEach((g) => {
const room = band * (g.to - g.from + 1);
const name = [{ ruby: "Ruby", jruby: "JRuby", truffleruby: "TruffleRuby" }[g.e] || g.e, { truffleruby: "Truffle" }[g.e], { jruby: "JR", truffleruby: "TR" }[g.e]].filter(Boolean).find((n) => n.length * cw <= room) || "";
if (name) s += `<text x="${(x(g.from) + x(g.to)) / 2}" y="${H - P.b + 30}" text-anchor="middle" class="eng">${esc(name)}</text>`;
});
if (cols.other.length) { const gx = x(cols.line.length) - band / 2 - gap / 2; s += `<line x1="${gx}" x2="${gx}" y1="${P.t}" y2="${yb}" stroke="var(--line)"/>`; }
// Under a JIT build: the same Rubies without it, dashed.
if (ghost.length) b.names.forEach((_, i) => { s += path(ghost.map((l, k) => { const r = b.runs[l], v = r && val(r.entries[i]); return v == null ? null : [x(k), y(v)]; }), color(i), ' stroke-dasharray="4 4" opacity=".55"', 1.5); });
b.names.forEach((_, i) => {
const c = color(i);
s += path(cols.line.map((l, k) => { const r = b.runs[l], v = r && val(r.entries[i]); return v == null ? null : [x(k), y(v)]; }), c, "", 2);
cols.all.forEach((l, k) => {
const r = b.runs[l], e = r && r.entries[i];
if (!e) return;
if (sus(e)) { const tx = x(k), ty = P.t + 2 + i * 9; s += `<path d="M${tx - 4},${ty + 6} L${tx},${ty} L${tx + 4},${ty + 6}Z" fill="var(--bg)" stroke="${c}" stroke-width="1.5"/>`; return; }
const v = val(e);
if (v == null) return;
const err = e[1] / e[0] * v;
s += `<line x1="${x(k)}" x2="${x(k)}" y1="${v - err > 0 ? y(v - err) : yb}" y2="${y(v + err)}" stroke="${c}" stroke-width="1" opacity=".5"/>`;
s += `<circle cx="${x(k)}" cy="${y(v)}" r="4" fill="${c}" stroke="var(--bg)" stroke-width="2"/>`;
});
});
s += `<line class="cross" y1="${P.t}" y2="${yb}" x1="0" x2="0"/>`;
cols.all.forEach((l, k) => { s += `<rect class="hit" data-k="${k}" x="${x(k) - band / 2}" y="${P.t}" width="${band}" height="${yb - P.t}"/>`; });
host.innerHTML = s + "</svg><div class=\"tipbox\" hidden></div>";
const tip = host.querySelector(".tipbox"), cross = host.querySelector(".cross");
host.querySelectorAll(".hit").forEach((h) => {
const on = () => {
const k = +h.dataset.k, l = cols.all[k], r = b.runs[l];
cross.setAttribute("x1", x(k)); cross.setAttribute("x2", x(k)); cross.classList.add("on");
let t = `<b>${esc(nice(l))}${l === REF ? " (reference)" : ""}</b>`;
if (!r) t += '<div class="n">Did not run: this benchmark needs a newer Ruby, or the build failed.</div>';
else {
b.names.forEach((nm, i) => {
const e = r.entries[i];
t += `<div class="r"><i style="background:${color(i)}"></i>${esc(nm)}: ${!e ? "did not run" : fmt(e[0]) + " i/s" + (sus(e) ? " (suspect)" : "")}</div>`;
});
const c = r.entries[0], v = versus(r);
if (v) t += `<div class="n">${v.s === "eq" ? `Same-ish as ${esc(b.names[v.winner])}: the gap is within the error.` : v.s === "ok" ? `${esc(b.names[0])} is ${v.ratio.toFixed(2)}× faster than ${esc(b.names[v.winner])}.` : `${esc(b.names[v.winner])} is ${v.ratio.toFixed(2)}× faster than ${esc(b.names[0])}.`}</div>`;
const prev = k > 0 && k < cols.line.length && b.runs[cols.line[k - 1]];
const pc = prev && prev.entries[0];
if (ok(pc) && ok(c)) t += `<div class="n">vs ${esc(nice(cols.line[k - 1]))}: ${sameRuby(c, pc, b.variation) ? "same-ish" : pct(c[0], pc[0])}</div>`;
const g = mode && b.runs[base(l)], gc = g && g.entries[0];
if (ok(gc) && ok(c)) t += `<div class="n">vs no JIT: ${sameRuby(c, gc, b.variation) ? "same-ish" : pct(c[0], gc[0])}</div>`;
}
tip.innerHTML = t; tip.hidden = false;
const scale = host.clientWidth / W;
let left = x(k) * scale + 12;
if (left + tip.offsetWidth > host.clientWidth) left = x(k) * scale - 12 - tip.offsetWidth;
tip.style.left = Math.max(0, left) + "px"; tip.style.top = "6px";
};
h.addEventListener("mouseenter", on);
h.addEventListener("click", on);
h.addEventListener("mouseleave", () => { tip.hidden = true; cross.classList.remove("on"); });
});
}
function table(b) {
const cols = columns();
let h = `<tr><th>Build</th>${b.names.map((nm, i) => `<th>${esc(nm)}${i ? "" : " (claimed)"}</th>`).join("")}<th>Claimed vs fastest other</th></tr>`;
cols.all.forEach((l, k) => {
const r = b.runs[l], prev = k > 0 && k < cols.line.length ? b.runs[cols.line[k - 1]] : null;
h += `<tr><td>${esc(nice(l))}${l === REF ? " (reference)" : ""}</td>`;
b.names.forEach((_, i) => {
const e = r && r.entries[i];
if (!e) { h += '<td class="off">did not run</td>'; return; }
if (sus(e)) { h += `<td class="off">${fmt(e[0])} suspect</td>`; return; }
if (!ok(e)) { h += '<td class="off">0 i/s</td>'; return; }
const pe = prev && prev.entries[i];
const d = ok(pe) ? (sameRuby(e, pe, b.variation) ? ' <small class="sm">same-ish</small>' : ` <small>${pct(e[0], pe[0])}</small>`) : "";
h += `<td>${fmt(e[0])} <small>±${(e[1] / e[0] * 100).toFixed(1)}%</small>${d}</td>`;
});
const v = versus(r);
h += `<td>${!v ? "" : v.s === "eq" ? '<span class="sm">tie</span>' : v.s === "ok" ? `${v.ratio.toFixed(2)}× faster` : `<span class="lose">${v.ratio.toFixed(2)}× slower</span>`}</td></tr>`;
});
return `<div class="tb"><table>${h}</table></div>`;
}
// The machine, inside "Numbers per build": the same on every card but the CPU, so it only shows on the card when the benchmark was noisy.
function machine(b) {
const shard = M.shards.find((x) => x.shard === b.shard) || {};
const parts = [`Ran on ${esc(cpu(shard.cpu))}${M.shards_total > 1 ? `, shard ${b.shard + 1} of ${M.shards_total}` : ""}.`];
if (b.variation != null) parts.push(`${esc(nice(REF))} ran ${b.reference_passes} times; its i/s varied by ${(b.variation * 100).toFixed(1)}%.`);
return `<p class="rfoot">${parts.join(" ")}</p>`;
}
function noisy(b) {
return b.noisy ? `<p class="rfoot noisy">Noisy: ${esc(nice(REF))}'s i/s varied by ${(b.variation * 100).toFixed(1)}% during this benchmark, so gaps that small may be the machine, not Ruby.</p>` : "";
}
// Suspect results are drawn as triangles along the top of the chart.
function hasSuspect(b) {
return columns().all.some((l) => b.runs[l] && b.runs[l].entries.some(sus));
}
const open = new Set();
function render() {
if (mode && !has(mode)) mode = "";
$("rmodes").innerHTML = MODES.map(([m, name]) => `<button type="button" class="chip" data-m="${m}" aria-pressed="${m === mode}" ${has(m) ? "" : 'disabled title="Not built in this run"'}>${name}</button>`).join("");
$("rshow").innerHTML = SHOWS.map(([k, name]) => `<button type="button" class="chip" data-s="${k}" aria-pressed="${k === showing}">${esc(name)}</button>`).join("");
const hints = [];
if (showing === "rel") hints.push(`1× is the claimed idiom on ${nice(REF)}.`);
if (mode) hints.push("Dashed: the same Rubies without the JIT, on the same machine.");
$("rhint").textContent = hints.join(" ");
const count = {};
B.forEach((b) => { count[b.section] = (count[b.section] || 0) + 1; });
const secs = Object.keys(count);
$("rjump").innerHTML = `<option value="">A section</option>` + secs.map((x) => `<option value="${esc(slug(x))}">${esc(x)} (${count[x]})</option>`).join("");
let h = "", sec = null;
B.forEach((b, n) => {
if (b.section !== sec) { sec = b.section; h += `<h2 class="rsec" id="${esc(slug(sec))}">${esc(sec)}<small>${count[sec]} benchmark${count[sec] > 1 ? "s" : ""}</small><span class="on">${esc(MODES.find(([m]) => m === mode)[1])}</span></h2>`; }
const legend = b.names.map((nm, i) => `<span><i style="background:${color(i)}"></i><code>${esc(nm)}</code>${i ? "" : " (claimed)"}</span>`).join("") + (mode ? '<span class="ghost"><i></i>without the JIT</span>' : "") +
(hasSuspect(b) ? '<span class="sus"><svg viewBox="0 0 10 8" aria-hidden="true"><path d="M1,7 L5,1 L9,7Z"/></svg>suspect, over 1B i/s: kept off the chart</span>' : "");
const v = verdict(b);
// Title (what is compared), verdict (what happened), the numbers behind it, then the chart.
h += `<article class="rcard"><h3>${md(b.title)}</h3><p class="verdict"><span class="ico ${v.icon}" aria-hidden="true">${RICON[v.icon]}</span><span>${v.head}</span></p>` +
tilesHtml(v) + `<div class="rlegend">${legend}</div>` +
`<div class="chart" data-n="${n}"></div>${v.foot ? `<p class="rfoot">${v.foot}</p>` : ""}${noisy(b)}` +
`<details data-n="${n}"${open.has(n) ? " open" : ""}><summary>Numbers per build</summary>${machine(b)}${table(b)}</details>` +
`<div class="links"><a href="${M.repo}/blob/main/${encodeURI(b.file)}">View the code</a></div></article>`;
});
$("rout").innerHTML = h;
if (!$("view-across-rubies").hidden) $("rout").querySelectorAll(".chart").forEach((c) => chart(B[+c.dataset.n], c));
fitTitles();
pinned();
}
// A title of many report names stays on one line by getting smaller (20px down to 14px); one too long even at 14px wraps at 14px.
// All writes, then all reads, then all writes: one layout for every card instead of one per card.
function fitTitles() {
const hs = [...$("rout").querySelectorAll(".rcard h3")];
hs.forEach((h) => { h.classList.add("one"); h.style.fontSize = ""; });
const fit = hs.map((h) => [h.clientWidth, h.scrollWidth]);
hs.forEach((h, i) => {
const [room, need] = fit[i];
if (need <= room) return;
You can’t perform that action at this time.
