@@ -186,6 +186,7 @@ export function render(ctx) {
186186 rawCache ( ctx , data , filter ) ,
187187 negativeCache ( ctx , data , filter ) ,
188188 entityServe ( ctx , data , filter ) ,
189+ entityRegistry ( ctx , data ) ,
189190 breadth ( ctx , filter ) ,
190191 el ( 'div' , { cls : 'scan-foot' } , [ scanFooter ( data ) ] ) ,
191192 knobs ,
@@ -2310,14 +2311,16 @@ function negativeCache(ctx, data, filter) {
23102311// Why an entity serve fell through (plugin `prerender_ops` / `entity_serve`), with what each one means.
23112312const ENTITY_FALL_THROUGHS = [
23122313 [ 'unconfirmed' , 'canonical not rendered or checked since the anchor' ] ,
2313- [ 'has-target' , 'the spelling has a target of its own' ] ,
2314+ [ 'has-target' , 'the spelling has a target the render path keeps' ] ,
2315+ [ 'moved' , 'the registry heard a re-slug since' ] ,
23142316 [ 'no-page' , 'no page for this device' ] ,
23152317 [ 'stale' , 'past its expiry' ] ,
23162318 [ 'not-indexable' , 'not a 200, or not indexable' ] ,
23172319 [ 'invalidated' , 'predates an invalidation' ] ,
2318- [ 'ambiguous' , 'more than one candidate' ] ,
2320+ [ 'ambiguous' , 'more than one candidate, none named by the registry ' ] ,
23192321 [ 'no-sibling' , 'no other target in rotation' ] ,
23202322 [ 'not-self-canonical' , 'its canonical does not name it' ] ,
2323+ [ 'has-query' , 'a query string' ] ,
23212324 [ 'unreadable' , 'body unreadable' ] ,
23222325 [ 'no-prefix' , 'no entity prefix' ] ,
23232326 [ 'error' , 'read failed' ] ,
@@ -2338,10 +2341,13 @@ function entityServe(ctx, data, filter) {
23382341 ( entry ) => entry && typeof entry === 'object' && entry . entityServe === true
23392342 ) ;
23402343 const served = sumCount ( pick ( data , 'bot_serve' , ( s ) => s . method === 'entity' && keepBot ( filter , s . type ) ) ) ;
2344+ // Each evaluation's duration (plugin v0.102.0): what an opted-in route adds to a miss it does not answer.
2345+ const timings = pick ( data , 'prerender_ops' , ( s ) => s . path === 'entity_serve_ms' ) ;
23412346 const help = [
2342- 'A miss for a spelling with no page and no target of its own (an old or invented slug), answered from the ' ,
2343- 'cached render of its entity’s canonical: a fresh, indexable page that names itself, whose canonical was ' ,
2344- 'rendered or checked against the origin since the anchor. Switches: ' ,
2347+ 'A miss for a spelling with no page and no target the render path keeps (an old or invented slug, or one ' ,
2348+ 'suppressed because its page names its canonical elsewhere), answered from the cached render of its ' ,
2349+ 'entity’s canonical: a fresh, indexable page that names itself, whose canonical was rendered or checked ' ,
2350+ 'against the origin since the anchor, and not heard re-slugged since by the entity registry. Switches: ' ,
23452351 el ( 'code' , { text : 'ingress.entityServe' } ) ,
23462352 ' and ' ,
23472353 el ( 'code' , { text : 'entityServe' } ) ,
@@ -2403,12 +2409,120 @@ function entityServe(ctx, data, filter) {
24032409 . join ( ' · ' ) || 'none' ,
24042410 { warn : ev ( 'error' ) + ev ( 'unreadable' ) > 0 }
24052411 ) ,
2412+ timings . length > 0 &&
2413+ stat (
2414+ 'Cost per evaluation' ,
2415+ fmtMs ( weighted ( timings , 'median' ) ) ,
2416+ `median · p95 ≈ ${ fmtMs ( weighted ( timings , 'p95' ) ) } · added to a miss it does not answer`
2417+ ) ,
24062418 ] ) ,
24072419 ! events . length && el ( 'div' , { cls : 'empty' , text : 'No entity-serve evaluations in this range.' } ) ,
24082420 ] ,
24092421 } ) ;
24102422}
24112423
2424+ // Who told the entity registry (plugin `entity_canonical` / `canonical_adopt` context), in the order a re-slug is
2425+ // usually seen: the origin documents crawlers pull, the renders, the serve-time checks, the nightly probe.
2426+ const ENTITY_OBSERVERS = [
2427+ [ 'origin' , 'proxied origin documents' ] ,
2428+ [ 'render' , 'renders' ] ,
2429+ [ 'check' , 'serve-time checks' ] ,
2430+ [ 'probe' , 'the nightly probe' ] ,
2431+ ] ;
2432+
2433+ /**
2434+ * The entity registry (plugin v0.101.0, `entities`): each product's current canonical, learned from every fetch
2435+ * from the origin. `entity_canonical` is every observation by what it did (`moved` is a re-slug seen, by the
2436+ * observer that saw it first); `canonical_adopt` is what was done about a canonical no target held. In an
2437+ * adoption dry run, `would-adopt` plus `capped` is what arming files.
2438+ */
2439+ function entityRegistry ( ctx , data ) {
2440+ const observed = pick ( data , 'prerender_ops' , ( s ) => s . path === 'entity_canonical' ) ;
2441+ const adoptions = pick ( data , 'prerender_ops' , ( s ) => s . path === 'canonical_adopt' ) ;
2442+ const options = optionIndex ( configState ( ctx ) . payload ) ;
2443+ const enabled = options . get ( 'entities.enabled' ) ?. effective === true ;
2444+ const adoptOn = options . get ( 'entities.adopt.enabled' ) ?. effective !== false ;
2445+ const adoptDry = options . get ( 'entities.adopt.dryRun' ) ?. effective !== false ;
2446+ const help = [
2447+ 'One row per product holding its current canonical, written by every fetch from the origin that names it: ' ,
2448+ 'proxied misses, renders, serve-time checks and the nightly probe. A canonical no target holds is filed due ' ,
2449+ 'now (adoption). Switches: ' ,
2450+ el ( 'code' , { text : 'entities' } ) ,
2451+ ' (' ,
2452+ link ( 'Config →' , ( ) => ctx . go ( 'config' ) ) ,
2453+ ').' ,
2454+ ] ;
2455+ if ( ! enabled && ! observed . length && ! adoptions . length ) {
2456+ return card ( 'Entity registry' , {
2457+ head : [ spacer ( ) , pill ( 'off' , '' ) ] ,
2458+ help,
2459+ body : [ el ( 'div' , { cls : 'empty' , text : 'Off.' } ) ] ,
2460+ } ) ;
2461+ }
2462+ const tally = ( combos ) => {
2463+ const by = new Map ( ) ;
2464+ for ( const s of combos ) {
2465+ const key = `${ s . method ?? 'unknown' } |${ s . type ?? 'unknown' } ` ;
2466+ by . set ( key , ( by . get ( key ) ?? 0 ) + s . count ) ;
2467+ }
2468+ return ( outcome , from = null ) =>
2469+ from === null
2470+ ? [ ...by ] . filter ( ( [ key ] ) => key . startsWith ( `${ outcome } |` ) ) . reduce ( ( acc , [ , n ] ) => acc + n , 0 )
2471+ : ( by . get ( `${ outcome } |${ from } ` ) ?? 0 ) ;
2472+ } ;
2473+ const seen = tally ( observed ) ;
2474+ const did = tally ( adoptions ) ;
2475+ const filed = did ( 'adopted' ) + did ( 'reactivated' ) ;
2476+ const wouldFile = did ( 'would-adopt' ) ;
2477+ const byObserver = ( count ) =>
2478+ ENTITY_OBSERVERS . map ( ( [ from , label ] ) => [ label , count ( from ) ] )
2479+ . filter ( ( [ , n ] ) => n > 0 )
2480+ . map ( ( [ label , n ] ) => `${ num ( n ) } ${ label } ` )
2481+ . join ( ' · ' ) || 'none' ;
2482+
2483+ return card ( `Entity registry — ${ scopeLabel ( data ) } ` , {
2484+ head : [
2485+ enabled ? pill ( 'on' , 'ok' ) : pill ( 'off' , 'warn' ) ,
2486+ adoptOn ? ( adoptDry ? pill ( 'adoption dry run' , 'info' ) : pill ( 'adoption armed' , 'ok' ) ) : pill ( 'adoption off' , '' ) ,
2487+ spacer ( ) ,
2488+ ] ,
2489+ help,
2490+ body : [
2491+ stats ( [
2492+ stat (
2493+ 'Re-slugs seen' ,
2494+ fmtCount ( seen ( 'moved' ) ) ,
2495+ byObserver ( ( from ) => seen ( 'moved' , from ) )
2496+ ) ,
2497+ stat ( 'New entities' , fmtCount ( seen ( 'new' ) ) , 'first observation of a product' ) ,
2498+ adoptDry
2499+ ? stat (
2500+ 'Would adopt' ,
2501+ fmtCount ( wouldFile + did ( 'capped' ) ) ,
2502+ `${ num ( wouldFile ) } under the hourly budget · ${ num ( did ( 'capped' ) ) } past it` ,
2503+ { warn : did ( 'capped' ) > 0 }
2504+ )
2505+ : stat (
2506+ 'Adopted' ,
2507+ fmtCount ( filed ) ,
2508+ byObserver ( ( from ) => did ( 'adopted' , from ) + did ( 'reactivated' , from ) ) ,
2509+ {
2510+ warn : did ( 'capped' ) > 0 ,
2511+ }
2512+ ) ,
2513+ stat (
2514+ 'Not adopted' ,
2515+ fmtCount ( did ( 'exists' ) + did ( 'suppressed' ) + did ( 'recent' ) + did ( 'refused' ) + did ( 'error' ) ) ,
2516+ `${ num ( did ( 'exists' ) ) } already held · ${ num ( did ( 'recent' ) ) } recent · ${ num ( did ( 'suppressed' ) ) } suppressed` +
2517+ ( did ( 'error' ) ? ` · ${ num ( did ( 'error' ) ) } errors` : '' ) ,
2518+ { warn : did ( 'error' ) > 0 }
2519+ ) ,
2520+ ] ) ,
2521+ ! observed . length && ! adoptions . length && el ( 'div' , { cls : 'empty' , text : 'No registry activity in this range.' } ) ,
2522+ ] ,
2523+ } ) ;
2524+ }
2525+
24122526/** Days of crawl sketch one breadth read covers — the Crawl breadth panel's trend and the miss recurrence. */
24132527const BREADTH_DAYS = 7 ;
24142528
0 commit comments