@@ -52,7 +52,8 @@ const markup = `
5252.node:hover{background:var(--tile-hover)}
5353.node .kind{font-size:11px; color:var(--accent-2); padding:2px 6px; border:1px solid var(--border-2); border-radius:999px; background:var(--tile-bg)}
5454.node .name{font-weight:600}
55- .node .time{margin-left:auto; font-variant-numeric:tabular-nums; color:var(--rose-300)}
55+ .node .meta{margin-left:auto; display:flex; align-items:center; gap:6px; font-variant-numeric:tabular-nums; color:var(--rose-300)}
56+ .node .meta span{display:inline-flex; align-items:center; gap:4px; padding:2px 8px; border-radius:999px; border:1px solid var(--tile-border); background:var(--tile-bg)}
5657
5758.rfw-detail{flex:1;background:var(--chip-bg);display:flex;flex-direction:column}
5859.rfw-detail .rfw-subheader{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid var(--border)}
@@ -61,6 +62,10 @@ const markup = `
6162.kv b{color:var(--rose-200)}
6263
6364.mono{font-variant-numeric:tabular-nums}
65+ .timeline{display:flex; flex-direction:column; gap:6px; font-variant-numeric:tabular-nums}
66+ .timeline-item{display:flex; align-items:center; gap:8px}
67+ .timeline-item .mono{background:var(--tile-bg); border:1px solid var(--tile-border); padding:2px 6px; border-radius:6px}
68+ .timeline-item .duration{opacity:.75}
6469
6570/* Network panel */
6671.net-list{flex:1; overflow:auto; padding:8px}
@@ -402,10 +407,24 @@ function renderTree(list, root = true) {
402407 const el = document . createElement ( "div" ) ;
403408 el . className = "node" ;
404409 el . dataset . id = node . id ;
410+ const metrics = [ ] ;
411+ if ( typeof node . average === "number" && Number . isFinite ( node . average ) ) {
412+ metrics . push ( `${ node . average . toFixed ( 1 ) } ms avg` ) ;
413+ } else if ( typeof node . time === "number" && Number . isFinite ( node . time ) ) {
414+ metrics . push ( `${ node . time . toFixed ( 1 ) } ms` ) ;
415+ }
416+ if ( typeof node . updates === "number" && node . updates > 0 ) {
417+ metrics . push ( `${ node . updates } ×` ) ;
418+ }
419+ const meta = metrics . length
420+ ? `<span class="meta">${ metrics
421+ . map ( ( txt ) => `<span>${ escapeHTML ( txt ) } </span>` )
422+ . join ( "" ) } </span>`
423+ : "" ;
405424 el . innerHTML = `
406- <span class="kind">${ node . kind } </span>
407- <span class="name">${ node . name } </span>
408- <span class="time"> ${ ( node . time || 0 ) . toFixed ( 1 ) } ms</span>
425+ <span class="kind">${ escapeHTML ( node . kind || "" ) } </span>
426+ <span class="name">${ escapeHTML ( node . name || "" ) } </span>
427+ ${ meta }
409428 ` ;
410429 el . addEventListener ( "click" , ( ) => selectNode ( node ) ) ;
411430 frag . appendChild ( el ) ;
@@ -429,10 +448,15 @@ function selectNode(n) {
429448 detailKind . textContent = n . kind ;
430449 const rows = [ ] ;
431450 rows . push ( renderRow ( "Path" , n . path || "" ) ) ;
432- rows . push ( renderRow ( "Render time" , `${ ( n . time || 0 ) . toFixed ( 2 ) } ms` ) ) ;
451+ if ( typeof n . updates === "number" ) rows . push ( renderRow ( "Updates" , String ( n . updates ) ) ) ;
452+ if ( typeof n . average === "number" && Number . isFinite ( n . average ) )
453+ rows . push ( renderRow ( "Average render" , formatMs ( n . average ) ) ) ;
454+ if ( typeof n . time === "number" && Number . isFinite ( n . time ) )
455+ rows . push ( renderRow ( "Last render" , formatMs ( n . time ) ) ) ;
456+ if ( typeof n . total === "number" && Number . isFinite ( n . total ) )
457+ rows . push ( renderRow ( "Total render" , formatMs ( n . total ) ) ) ;
433458 if ( n . owner ) rows . push ( renderRow ( "Owner" , n . owner ) ) ;
434459 if ( n . hostComponent ) rows . push ( renderRow ( "Host component" , n . hostComponent ) ) ;
435- if ( typeof n . updates === "number" ) rows . push ( renderRow ( "Updates" , String ( n . updates ) ) ) ;
436460 if ( n . props && Object . keys ( n . props ) . length )
437461 rows . push ( renderJSONRow ( "Props" , n . props ) ) ;
438462 if ( n . slots && Object . keys ( n . slots ) . length )
@@ -448,6 +472,8 @@ function selectNode(n) {
448472 rows . push ( renderJSONRow ( "Store state" , n . store . state ) ) ;
449473 }
450474 }
475+ if ( Array . isArray ( n . timeline ) && n . timeline . length )
476+ rows . push ( renderTimelineRow ( "Timeline" , n . timeline ) ) ;
451477 detailKV . innerHTML = rows . join ( "" ) || renderRow ( "Info" , "No metadata available" ) ;
452478}
453479
@@ -461,6 +487,28 @@ function escapeHTML(s) {
461487 ) ;
462488}
463489
490+ function formatMs ( value ) {
491+ if ( typeof value !== "number" || ! Number . isFinite ( value ) ) return "0.00 ms" ;
492+ const digits = value >= 100 ? 0 : 2 ;
493+ return `${ value . toFixed ( digits ) } ms` ;
494+ }
495+
496+ function renderTimelineRow ( label , events ) {
497+ const items = events
498+ . map ( ( ev ) => {
499+ const offset = formatMs ( ev . at ?? 0 ) ;
500+ const duration =
501+ typeof ev . duration === "number" && ev . duration > 0
502+ ? `<span class="mono duration">${ formatMs ( ev . duration ) } </span>`
503+ : "" ;
504+ return `<div class="timeline-item"><span class="mono">${ offset } </span><span>${ escapeHTML (
505+ ev . kind || "" ,
506+ ) } </span>${ duration } </div>`;
507+ } )
508+ . join ( "" ) ;
509+ return `<b>${ escapeHTML ( label ) } </b><div class="timeline">${ items } </div>` ;
510+ }
511+
464512function formatJSON ( value ) {
465513 try {
466514 return JSON . stringify ( value , null , 2 ) ;
@@ -501,19 +549,45 @@ function countNodes(list) {
501549 return total ;
502550}
503551
552+ function aggregateRenderAverage ( list ) {
553+ let total = 0 ;
554+ let count = 0 ;
555+ const walk = ( nodes ) => {
556+ nodes . forEach ( ( n ) => {
557+ if (
558+ typeof n . total === "number" &&
559+ typeof n . updates === "number" &&
560+ n . updates > 0
561+ ) {
562+ total += n . total ;
563+ count += n . updates ;
564+ }
565+ if ( Array . isArray ( n . children ) && n . children . length ) walk ( n . children ) ;
566+ } ) ;
567+ } ;
568+ walk ( list ) ;
569+ if ( ! count ) return null ;
570+ return total / count ;
571+ }
572+
504573function refreshTree ( ) {
505574 try {
506575 if ( typeof globalThis . RFW_DEVTOOLS_TREE === "function" ) {
507576 const data = JSON . parse ( globalThis . RFW_DEVTOOLS_TREE ( ) ) ;
508577 renderTree ( data ) ;
509578 const k = $ ( "#kpiNodes" ) ;
510579 if ( k ) k . textContent = String ( countNodes ( data ) ) ;
580+ const avg = aggregateRenderAverage ( data ) ;
581+ const r = $ ( "#kpiRender" ) ;
582+ if ( r ) r . textContent = avg == null ? "n/a" : formatMs ( avg ) ;
511583 return ;
512584 }
513585 } catch { }
514586 if ( treeContainer ) treeContainer . textContent = "" ;
515587 const k = $ ( "#kpiNodes" ) ;
516588 if ( k ) k . textContent = "0" ;
589+ const r = $ ( "#kpiRender" ) ;
590+ if ( r ) r . textContent = "n/a" ;
517591}
518592
519593let fpsSample = 0 ,
0 commit comments