Skip to content

Commit cce36c8

Browse files
committed
Refine Signals performance insights
Assisted-By: devx/633e51ab-4160-4b2c-a196-017418800531
1 parent 2913926 commit cce36c8

7 files changed

Lines changed: 787 additions & 31 deletions

File tree

packages/devtools-ui/src/components/PerformanceInsights.tsx

Lines changed: 201 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { ComponentChildren } from "preact";
1+
import { Fragment, type ComponentChildren } from "preact";
2+
import { useSignal } from "@preact/signals";
23
import { 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";
49
import { PERFORMANCE_OBSERVATION_LIMIT } from "../models/UpdatesModel";
510

611
const 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+
42128
export 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>

packages/devtools-ui/src/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export type {
2323
export type {
2424
PerformanceInsightsData,
2525
PerformanceInstanceSummary,
26+
NoOutputChangeOccurrence,
27+
HotspotTier,
2628
} from "./models/PerformanceInsightsModel";
2729
export type { ThemeMode } from "./models/ThemeModel";
2830

packages/devtools-ui/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export {
2424
type PerformanceObservation,
2525
type PerformanceInsightsData,
2626
type PerformanceInstanceSummary,
27+
type NoOutputChangeOccurrence,
28+
type HotspotTier,
2729
} from "./context";
2830

2931
// Types

0 commit comments

Comments
 (0)