240240.rcard td .sm { color : var (--yellow-ink ); }
241241.rcard td .off { color : var (--faint ); }
242242.rcard .links { font-size : 13px ; }
243+ /* Section headings: one per folder under code/, with how many benchmarks it shows. */
244+ .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 ); }
245+ .rsec small { font-size : 14px ; font-weight : 400 ; color : var (--muted ); letter-spacing : 0 ; }
246+ .rsec .on { margin-left : auto; font : 500 13px var (--mono ); color : var (--ink ); float : none; }
247+ .find , .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% ; }
248+ .find { width : 260px ; }
249+ .find : focus-visible , .jump : focus-visible { outline : 2px solid var (--green ); outline-offset : 0 ; border-color : var (--green ); }
250+ # rpicker label .lbl + .find + label .lbl { min-width : 0 ; margin-left : 10px ; }
243251.rhow { padding-block : 22px 0 ; display : grid; gap : 8px ; font-size : 14px ; color : var (--muted ); }
244252.rhow p { margin : 0 ; max-width : 75ch ; }
245253
@@ -288,6 +296,7 @@ <h1>How fast is each idiom on each Ruby?</h1>
288296 < div class ="picker " id ="rpicker ">
289297 < div class ="prow "> < span class ="lbl "> Build</ span > < div class ="chips " id ="rmodes "> </ div > < span class ="jkey " id ="rhint "> </ span > </ div >
290298 < div class ="prow "> < span class ="lbl "> Show</ span > < div class ="chips " id ="rshow "> </ div > </ div >
299+ < div class ="prow "> < label class ="lbl " for ="rfind "> Find</ label > < input type ="search " id ="rfind " class ="find " placeholder ="A method or idiom, e.g. gsub " autocomplete ="off "> < label class ="lbl " for ="rjump "> Jump to</ label > < select id ="rjump " class ="jump "> </ select > </ div >
291300 </ div >
292301
293302 < div id ="rout "> </ div >
@@ -626,7 +635,7 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
626635 const MODES = [ [ "" , "Interpreter" ] , [ "yjit" , "YJIT" ] , [ "zjit" , "ZJIT" ] ] ;
627636 // Without the reference (its image failed, say), there is nothing to be relative to.
628637 const SHOWS = REF ? [ [ "ips" , "i/s" ] , [ "rel" , "Relative to " + nice ( REF ) ] ] : [ [ "ips" , "i/s" ] ] ;
629- let mode = "" , showing = "ips" ;
638+ let mode = "" , showing = "ips" , find = "" ;
630639
631640 function esc ( s ) { return String ( s ) . replace ( / [ & < > " ] / g, ( c ) => ( { "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ } [ c ] ) ) ; }
632641 function md ( s ) { return esc ( s ) . replace ( / ` ( [ ^ ` ] + ) ` / g, "<code>$1</code>" ) ; }
@@ -658,6 +667,8 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
658667 const p = new URLSearchParams ( location . search ) ;
659668 mode = MODES . some ( ( [ m ] ) => m && m === p . get ( "build" ) ) && has ( p . get ( "build" ) ) ? p . get ( "build" ) : "" ;
660669 showing = p . get ( "show" ) === "rel" && REF ? "rel" : "ips" ;
670+ find = ( p . get ( "find" ) || "" ) . trim ( ) ;
671+ $ ( "rfind" ) . value = find ;
661672 }
662673
663674 if ( ! M . passed ) {
@@ -771,9 +782,9 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
771782 const t = steps . filter ( ( v ) => v >= bottom && v <= top ) ;
772783 if ( t . length <= 7 ) return t ;
773784 const d0 = Math . log10 ( bottom ) , d1 = Math . log10 ( top ) , every = Math . ceil ( ( d1 - d0 ) / 6 ) ;
774- // Inner powers of ten, none closer than 0.35 of a decade to either end, so labels never overlap.
775- const inner = [ ] ;
776- for ( let k = Math . ceil ( d0 ) ; k < d1 ; k += every ) if ( k - d0 > 0.35 && d1 - k > 0.35 ) inner . push ( + Math . pow ( 10 , k ) . toPrecision ( 1 ) ) ;
785+ // Inner powers of ten, kept away from both ends (8% of the axis, at least 0.29 of a decade) so labels never overlap.
786+ const room = Math . max ( 0.29 , ( d1 - d0 ) * 0.08 ) , inner = [ ] ;
787+ 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 ) ) ;
777788 return [ bottom , ...inner , top ] ;
778789 }
779790 // Tick labels without trailing zeros: 2M, 500k, 0.05×.
@@ -910,9 +921,14 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
910921 if ( showing === "rel" ) hints . push ( `1× is the claimed idiom on ${ nice ( REF ) } .` ) ;
911922 if ( mode ) hints . push ( "Dashed: the same Rubies without the JIT, on the same machine." ) ;
912923 $ ( "rhint" ) . textContent = hints . join ( " " ) ;
924+ const shown = B . map ( ( b , n ) => [ b , n ] ) . filter ( ( [ b ] ) => matches ( b ) ) ;
925+ const count = { } ;
926+ shown . forEach ( ( [ b ] ) => { count [ b . section ] = ( count [ b . section ] || 0 ) + 1 ; } ) ;
927+ const secs = Object . keys ( count ) ;
928+ $ ( "rjump" ) . innerHTML = `<option value="">${ secs . length ? "A section" : "No section" } </option>` + secs . map ( ( x ) => `<option value="${ esc ( slug ( x ) ) } ">${ esc ( x ) } (${ count [ x ] } )</option>` ) . join ( "" ) ;
913929 let h = "" , sec = null ;
914- B . forEach ( ( b , n ) => {
915- if ( b . section !== sec ) { sec = b . section ; h += `<h2>${ esc ( sec ) } <span class="on">${ esc ( MODES . find ( ( [ m ] ) => m === mode ) [ 1 ] ) } </span></h2>` ; }
930+ shown . forEach ( ( [ b , n ] ) => {
931+ 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>` ; }
916932 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>' : "" ) ;
917933 const v = verdict ( b ) ;
918934 h += `<article class="rcard"><p class="what">${ md ( b . title ) } </p><h3><span class="ico ${ v . icon } " aria-hidden="true">${ RICON [ v . icon ] } </span><span>${ v . head } </span></h3>` +
@@ -921,12 +937,37 @@ <h2 class="bh">FastRuby<span>.io</span> keeps fast-ruby running</h2>
921937 `<details data-n="${ n } "${ open . has ( n ) ? " open" : "" } ><summary>Numbers per build</summary>${ table ( b ) } </details>` +
922938 `<div class="links"><a href="${ M . repo } /blob/main/${ encodeURI ( b . file ) } ">View the code</a></div></article>` ;
923939 } ) ;
924- $ ( "rout" ) . innerHTML = h ;
940+ $ ( "rout" ) . innerHTML = h || `<p class="empty">No benchmark matches " ${ esc ( find ) } ". Clear the find box to see all ${ B . length } .</p>` ;
925941 if ( ! $ ( "view-across-rubies" ) . hidden ) $ ( "rout" ) . querySelectorAll ( ".chart" ) . forEach ( ( c ) => chart ( B [ + c . dataset . n ] , c ) ) ;
942+ pinned ( ) ;
943+ }
944+ // A section heading reached from "Jump to" lands under the pinned picker, not behind it.
945+ function pinned ( ) {
946+ const p = $ ( "rpicker" ) ;
947+ document . documentElement . style . setProperty ( "--rpick" , getComputedStyle ( p ) . position === "sticky" ? p . offsetHeight + "px" : "0px" ) ;
948+ }
949+ const slug = ( x ) => "section-" + x . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" ) . replace ( / ^ - | - $ / g, "" ) ;
950+ // Find: every word must appear in the title, a report name, the file or the section.
951+ function matches ( b ) {
952+ const words = find . toLowerCase ( ) . split ( / \s + / ) . filter ( Boolean ) ;
953+ if ( ! words . length ) return true ;
954+ const text = [ b . title , b . file , b . section , ...b . names ] . join ( " " ) . toLowerCase ( ) ;
955+ return words . every ( ( w ) => text . includes ( w ) ) ;
926956 }
927957
928958 $ ( "rmodes" ) . addEventListener ( "click" , ( e ) => { const c = e . target . closest ( ".chip" ) ; if ( c && ! c . disabled ) { mode = c . dataset . m ; window . SiteViews . setParams ( { build : mode || null } , true ) ; render ( ) ; } } ) ;
929959 $ ( "rshow" ) . addEventListener ( "click" , ( e ) => { const c = e . target . closest ( ".chip" ) ; if ( c ) { showing = c . dataset . s ; window . SiteViews . setParams ( { show : showing === "rel" ? "rel" : null } , true ) ; render ( ) ; } } ) ;
960+ // Typing replaces the URL instead of adding to history, so back does not step through every letter.
961+ let typing ;
962+ $ ( "rfind" ) . addEventListener ( "input" , ( ) => {
963+ clearTimeout ( typing ) ;
964+ typing = setTimeout ( ( ) => { find = $ ( "rfind" ) . value . trim ( ) ; window . SiteViews . setParams ( { find : find || null } , false ) ; render ( ) ; } , 150 ) ;
965+ } ) ;
966+ $ ( "rjump" ) . addEventListener ( "change" , ( e ) => {
967+ const target = e . target . value && document . getElementById ( e . target . value ) ;
968+ e . target . value = "" ;
969+ if ( target ) { pinned ( ) ; target . scrollIntoView ( ) ; }
970+ } ) ;
930971 $ ( "rout" ) . addEventListener ( "toggle" , ( e ) => { const d = e . target ; if ( d . matches ( "details" ) ) d . open ? open . add ( + d . dataset . n ) : open . delete ( + d . dataset . n ) ; } , true ) ;
931972 window . addEventListener ( "popstate" , ( ) => { fromURL ( ) ; render ( ) ; } ) ;
932973 // Charts are drawn only when the view is shown, at the width it has then.
0 commit comments