1- import type { ComponentChildren } from "preact" ;
1+ import { Fragment , type ComponentChildren } from "preact" ;
2+ import { useSignal } from "@preact/signals" ;
23import { getContext } from "../context" ;
3- import type { PerformanceInstanceSummary } from "../models/PerformanceInsightsModel" ;
4+ import type {
5+ NoOutputChangeOccurrence ,
6+ PerformanceInstanceSummary ,
7+ } from "../models/PerformanceInsightsModel" ;
8+ import { MIN_BASELINE_POPULATION } from "../models/PerformanceInsightsModel" ;
49import { PERFORMANCE_OBSERVATION_LIMIT } from "../models/UpdatesModel" ;
510
611const MAX_INSIGHT_ROWS = 100 ;
@@ -39,9 +44,107 @@ function VisibleRows({
3944 ) ;
4045}
4146
47+ function HotspotTierBadge ( {
48+ tier,
49+ } : {
50+ tier : PerformanceInstanceSummary [ "hotspotTier" ] ;
51+ } ) {
52+ if ( tier === "severe" ) {
53+ return < span className = "performance-tier severe" > ≥4× baseline</ span > ;
54+ }
55+ return < span className = "performance-tier elevated" > ≥2× baseline</ span > ;
56+ }
57+
58+ function formatTimestamp ( value : number | undefined ) : string {
59+ if ( value === undefined ) return "—" ;
60+ return new Date ( value ) . toLocaleTimeString ( ) ;
61+ }
62+
63+ function DependencyList ( {
64+ dependencies,
65+ } : {
66+ dependencies : NoOutputChangeOccurrence [ "allDependencies" ] ;
67+ } ) {
68+ if ( ! dependencies || dependencies . length === 0 ) {
69+ return < span className = "performance-muted" > No dependency metadata</ span > ;
70+ }
71+ return (
72+ < ul className = "performance-dependency-list" >
73+ { dependencies . map ( dep => (
74+ < li key = { dep . id } className = "performance-dependency-item" >
75+ < span className = { `performance-dependency-type ${ dep . type } ` } >
76+ { dep . type }
77+ </ span >
78+ < span className = "performance-dependency-name" >
79+ { dep . name || "(anonymous)" }
80+ </ span >
81+ < span className = "performance-dependency-id" title = { dep . id } >
82+ { dep . id }
83+ </ span >
84+ </ li >
85+ ) ) }
86+ </ ul >
87+ ) ;
88+ }
89+
90+ function OccurrenceRow ( {
91+ occurrence,
92+ } : {
93+ occurrence : NoOutputChangeOccurrence ;
94+ } ) {
95+ return (
96+ < li className = "performance-occurrence" >
97+ < div className = "performance-occurrence-row" >
98+ < span className = "performance-occurrence-label" > Triggered by</ span >
99+ < span
100+ className = "performance-occurrence-value"
101+ title = { occurrence . subscribedTo }
102+ >
103+ { occurrence . subscribedTo || "Unknown source" }
104+ </ span >
105+ </ div >
106+ < div className = "performance-occurrence-row" >
107+ < span className = "performance-occurrence-label" > Runtime timestamp</ span >
108+ < span className = "performance-occurrence-value" >
109+ { formatTimestamp ( occurrence . timestamp ) }
110+ </ span >
111+ </ div >
112+ < div className = "performance-occurrence-row" >
113+ < span className = "performance-occurrence-label" > Received at</ span >
114+ < span className = "performance-occurrence-value" >
115+ { formatTimestamp ( occurrence . receivedAt ) }
116+ </ span >
117+ </ div >
118+ < div className = "performance-occurrence-deps" >
119+ < span className = "performance-occurrence-label" >
120+ Current dependencies
121+ </ span >
122+ < DependencyList dependencies = { occurrence . allDependencies } />
123+ </ div >
124+ </ li >
125+ ) ;
126+ }
127+
42128export function PerformanceInsights ( ) {
43129 const { performanceStore } = getContext ( ) ;
44130 const insights = performanceStore . insights . value ;
131+ const expanded = useSignal < Set < string > > ( new Set ( ) ) ;
132+
133+ const toggleRow = ( signalId : string ) => {
134+ const next = new Set ( expanded . value ) ;
135+ if ( next . has ( signalId ) ) {
136+ next . delete ( signalId ) ;
137+ } else {
138+ next . add ( signalId ) ;
139+ }
140+ expanded . value = next ;
141+ } ;
142+
143+ const baselineIsDefensible =
144+ insights . hotspotPopulation >= MIN_BASELINE_POPULATION ;
145+ const noHotspotsMessage = baselineIsDefensible
146+ ? `No runtime instance is disproportionately active relative to the median baseline (≥2×). The median per-instance update count is ${ insights . hotspotBaseline } across ${ insights . hotspotPopulation } identified instance${ insights . hotspotPopulation === 1 ? "" : "s" } .`
147+ : "Not enough identified instances to compute a defensible median baseline. Update more tracked Signals to populate hotspots." ;
45148
46149 return (
47150 < div className = "performance-insights" >
@@ -63,8 +166,12 @@ export function PerformanceInsights() {
63166 < div >
64167 < h3 id = "hotspots-heading" > Hotspots</ h3 >
65168 < p >
66- Ranked by observed update events for each runtime instance ID,
67- not by display name. This measures activity, not elapsed time.
169+ A hotspot is a runtime instance whose observed update count is
170+ disproportionate to the median per-instance activity. Only
171+ instances at ≥2× the median baseline are shown; ≥4× is flagged
172+ as severe. Instances are ranked by observed update events for
173+ each runtime instance ID, not by display name. This measures
174+ activity, not elapsed time.
68175 </ p >
69176 </ div >
70177 < span className = "performance-section-count" >
@@ -76,34 +183,52 @@ export function PerformanceInsights() {
76183 < p className = "performance-empty" >
77184 Update a tracked Signal to collect performance observations.
78185 </ p >
186+ ) : insights . hotspots . length === 0 ? (
187+ < p className = "performance-empty" > { noHotspotsMessage } </ p >
79188 ) : (
80189 < div className = "performance-table-scroll" >
81190 < table className = "performance-table" >
82191 < thead >
83192 < tr >
84193 < th > Instance</ th >
85194 < th > Type</ th >
195+ < th > Tier</ th >
86196 < th > Observed events</ th >
87- < th > No-output-change recomputations </ th >
197+ < th > vs. baseline </ th >
88198 </ tr >
89199 </ thead >
90200 < tbody >
91- < VisibleRows items = { insights . hotspots } colSpan = { 4 } >
201+ < VisibleRows items = { insights . hotspots } colSpan = { 5 } >
92202 { summary => (
93203 < tr key = { summary . signalId } >
94204 < td >
95205 < InstanceCell summary = { summary } />
96206 </ td >
97207 < td > { summary . signalType } </ td >
208+ < td >
209+ < HotspotTierBadge tier = { summary . hotspotTier } />
210+ </ td >
98211 < td > { summary . updateCount } </ td >
99- < td > { summary . noOutputChangeCount } </ td >
212+ < td >
213+ { insights . hotspotBaseline > 0
214+ ? `${ ( summary . updateCount / insights . hotspotBaseline ) . toFixed ( 1 ) } × median`
215+ : "—" }
216+ </ td >
100217 </ tr >
101218 ) }
102219 </ VisibleRows >
103220 </ tbody >
104221 </ table >
105222 </ div >
106223 ) }
224+ { insights . hotspotPopulation >= MIN_BASELINE_POPULATION && (
225+ < p className = "performance-note" >
226+ Baseline: median of { insights . hotspotBaseline } observed update
227+ event{ insights . hotspotBaseline === 1 ? "" : "s" } per instance
228+ across { insights . hotspotPopulation } identified instance
229+ { insights . hotspotPopulation === 1 ? "" : "s" } .
230+ </ p >
231+ ) }
107232 { insights . unidentifiedObservationCount > 0 && (
108233 < p className = "performance-note" >
109234 { insights . unidentifiedObservationCount } observed event
@@ -126,7 +251,9 @@ export function PerformanceInsights() {
126251 A counted entry is an instrumented computed recomputation with{ " " }
127252 < code > outputChanged: false</ code > . It does not compare
128253 serialized values and does not claim the recomputation was
129- avoidable.
254+ avoidable. Expand a row to inspect recent occurrences, their
255+ trigger source, and current dependencies — all taken verbatim
256+ from the runtime observation.
130257 </ p >
131258 </ div >
132259 </ div >
@@ -149,23 +276,73 @@ export function PerformanceInsights() {
149276 </ thead >
150277 < tbody >
151278 < VisibleRows items = { insights . redundantWork } colSpan = { 4 } >
152- { summary => (
153- < tr key = { summary . signalId } >
154- < td >
155- < InstanceCell summary = { summary } />
156- </ td >
157- < td > { summary . noOutputChangeCount } </ td >
158- < td > { summary . recomputationCount } </ td >
159- < td >
160- { Math . round (
161- ( summary . noOutputChangeCount /
162- summary . recomputationCount ) *
163- 100
279+ { summary => {
280+ const isOpen = expanded . value . has ( summary . signalId ) ;
281+ return (
282+ < Fragment key = { summary . signalId } >
283+ < tr
284+ className = { `performance-expandable-row${ isOpen ? " expanded" : "" } ` }
285+ >
286+ < td >
287+ < div className = "performance-instance-with-toggle" >
288+ < InstanceCell summary = { summary } />
289+ < button
290+ className = "performance-expand-toggle"
291+ onClick = { ( ) => toggleRow ( summary . signalId ) }
292+ aria-expanded = { isOpen }
293+ aria-label = { `${ isOpen ? "Hide" : "Inspect" } recent causes for ${ summary . signalName || "anonymous computed" } ` }
294+ >
295+ < span aria-hidden = "true" >
296+ { isOpen ? "▾" : "▸" }
297+ </ span >
298+ { isOpen ? "Hide causes" : "Inspect causes" }
299+ </ button >
300+ </ div >
301+ </ td >
302+ < td > { summary . noOutputChangeCount } </ td >
303+ < td > { summary . recomputationCount } </ td >
304+ < td >
305+ { summary . recomputationCount > 0
306+ ? `${ Math . round (
307+ ( summary . noOutputChangeCount /
308+ summary . recomputationCount ) *
309+ 100
310+ ) } %`
311+ : "—" }
312+ </ td >
313+ </ tr >
314+ { isOpen && (
315+ < tr className = "performance-occurrence-detail" >
316+ < td colSpan = { 4 } >
317+ < div className = "performance-occurrence-detail-inner" >
318+ < h4 >
319+ Recent no-output-change recomputations
320+ </ h4 >
321+ { summary . recentOccurrences &&
322+ summary . recentOccurrences . length > 0 ? (
323+ < ol className = "performance-occurrence-list" >
324+ { summary . recentOccurrences . map (
325+ ( occurrence , index ) => (
326+ < OccurrenceRow
327+ key = { `${ occurrence . signalId } -${ occurrence . receivedAt } -${ index } ` }
328+ occurrence = { occurrence }
329+ />
330+ )
331+ ) }
332+ </ ol >
333+ ) : (
334+ < p className = "performance-muted" >
335+ No occurrence metadata retained for this
336+ instance.
337+ </ p >
338+ ) }
339+ </ div >
340+ </ td >
341+ </ tr >
164342 ) }
165- %
166- </ td >
167- </ tr >
168- ) }
343+ </ Fragment >
344+ ) ;
345+ } }
169346 </ VisibleRows >
170347 </ tbody >
171348 </ table >
0 commit comments