1- import { useEffect , useMemo , useState } from "react" ;
2- import type { CSSProperties , PointerEvent } from "react" ;
1+ import { useEffect , useId , useMemo , useState } from "react" ;
2+ import type { CSSProperties , ChangeEvent , PointerEvent } from "react" ;
33
44import {
55 fetchSimulatorPerformance ,
@@ -97,15 +97,37 @@ export function PerformancePanel({
9797 visible ,
9898 ] ) ;
9999
100+ const processes = performance ?. processes ?? [ ] ;
101+ const selectedPidValue = selectedPid ?? performance ?. selectedPid ?? null ;
102+ const selectedPidInList =
103+ selectedPidValue == null ||
104+ processes . some ( ( process ) => process . pid === selectedPidValue ) ;
100105 const current = performance ?. current ?? null ;
101106 const selectedProcess = useMemo (
102- ( ) =>
103- performance ?. processes . find (
104- ( process ) => process . pid === ( selectedPid ?? performance . selectedPid ) ,
105- ) ?? null ,
106- [ performance , selectedPid ] ,
107+ ( ) => processes . find ( ( process ) => process . pid === selectedPidValue ) ?? null ,
108+ [ processes , selectedPidValue ] ,
107109 ) ;
108110
111+ function selectProcess ( event : ChangeEvent < HTMLSelectElement > ) {
112+ const nextPid = Number ( event . currentTarget . value ) ;
113+ if ( ! Number . isInteger ( nextPid ) ) {
114+ return ;
115+ }
116+ setFollowForeground ( false ) ;
117+ setSelectedPid ( nextPid ) ;
118+ setSample ( null ) ;
119+ }
120+
121+ function followFrontmostProcess ( ) {
122+ setFollowForeground ( true ) ;
123+ setSelectedPid (
124+ performance ?. foregroundProcess ?. processIdentifier ??
125+ performance ?. selectedPid ??
126+ null ,
127+ ) ;
128+ setSample ( null ) ;
129+ }
130+
109131 async function runSample ( ) {
110132 const pid = selectedPid ?? performance ?. selectedPid ?? null ;
111133 if ( ! udid || pid == null ) {
@@ -133,27 +155,36 @@ export function PerformancePanel({
133155
134156 return (
135157 < div className = "performance-panel" >
136- < div className = "performance-process-list" >
137- { performance ?. processes . length ? (
138- performance . processes . map ( ( process ) => (
139- < ProcessButton
140- key = { process . pid }
141- onSelect = { ( ) => {
142- setSelectedPid ( process . pid ) ;
143- setFollowForeground ( process . isForeground ) ;
144- setSample ( null ) ;
145- } }
146- process = { process }
147- selected = {
148- process . pid === ( selectedPid ?? performance . selectedPid )
149- }
150- />
151- ) )
152- ) : (
153- < div className = "performance-empty compact" >
154- { error || "Waiting for an app process." }
155- </ div >
156- ) }
158+ < div className = "performance-target-bar" >
159+ < div className = "performance-process-select-wrap" >
160+ < select
161+ aria-label = "Performance process"
162+ className = "performance-process-select"
163+ disabled = { ! processes . length }
164+ onChange = { selectProcess }
165+ value = { selectedPidValue == null ? "" : String ( selectedPidValue ) }
166+ >
167+ { processes . length ? null : (
168+ < option value = "" > Waiting for an app process</ option >
169+ ) }
170+ { selectedPidValue != null && ! selectedPidInList ? (
171+ < option value = { selectedPidValue } > PID { selectedPidValue } </ option >
172+ ) : null }
173+ { processes . map ( ( process ) => (
174+ < option key = { process . pid } value = { process . pid } >
175+ { processOptionLabel ( process ) }
176+ </ option >
177+ ) ) }
178+ </ select >
179+ </ div >
180+ < button
181+ className = { `performance-follow-button ${ followForeground ? "active" : "" } ` }
182+ disabled = { ! performance ?. foregroundProcess }
183+ onClick = { followFrontmostProcess }
184+ type = "button"
185+ >
186+ Follow Frontmost
187+ </ button >
157188 </ div >
158189
159190 { error && performance ?. processes . length ? (
@@ -295,34 +326,22 @@ export function PerformancePanel({
295326 ) }
296327 </ section >
297328 </ >
298- ) : null }
329+ ) : (
330+ < div className = "performance-empty compact" >
331+ { error || "Collecting performance metrics." }
332+ </ div >
333+ ) }
299334 </ div >
300335 ) ;
301336}
302337
303- function ProcessButton ( {
304- onSelect,
305- process,
306- selected,
307- } : {
308- onSelect : ( ) => void ;
309- process : PerformanceProcess ;
310- selected : boolean ;
311- } ) {
312- return (
313- < button
314- className = { `performance-process ${ selected ? "selected" : "" } ` }
315- onClick = { onSelect }
316- title = { process . command }
317- type = "button"
318- >
319- < span className = "performance-process-name" > { process . process } </ span >
320- < span className = "performance-process-meta" >
321- { process . pid } / { process . role }
322- { process . isForeground ? " / frontmost" : "" }
323- </ span >
324- </ button >
325- ) ;
338+ function processOptionLabel ( process : PerformanceProcess ) : string {
339+ const name = process . appName || process . process ;
340+ const parts = [ `${ name } (${ process . pid } )` , process . role ] ;
341+ if ( process . isForeground ) {
342+ parts . push ( "frontmost" ) ;
343+ }
344+ return parts . join ( " / " ) ;
326345}
327346
328347function Metric ( { label, value } : { label : string ; value : string } ) {
@@ -345,6 +364,7 @@ function Timeline({
345364 value : ( sample : PerformanceSample ) => number ;
346365 valueLabel : ( value : number | null | undefined ) => string ;
347366} ) {
367+ const gradientId = useId ( ) . replace ( / : / g, "" ) ;
348368 const [ hoverIndex , setHoverIndex ] = useState < number | null > ( null ) ;
349369 const values = samples . map ( value ) ;
350370 const latest = values . at ( - 1 ) ?? 0 ;
@@ -353,9 +373,13 @@ function Timeline({
353373 x : values . length <= 1 ? 0 : ( index / ( values . length - 1 ) ) * 100 ,
354374 y : 42 - ( Math . max ( 0 , item ) / max ) * 36 ,
355375 } ) ) ;
356- const points = coordinates
357- . map ( ( point ) => `${ round ( point . x ) } ,${ round ( point . y ) } ` )
376+ const linePath = coordinates
377+ . map (
378+ ( point , index ) =>
379+ `${ index === 0 ? "M" : "L" } ${ round ( point . x ) } ${ round ( point . y ) } ` ,
380+ )
358381 . join ( " " ) ;
382+ const areaPath = linePath ? `${ linePath } L 100 42 L 0 42 Z` : "" ;
359383 const activeIndex =
360384 hoverIndex == null || hoverIndex >= samples . length ? null : hoverIndex ;
361385 const activePoint = activeIndex == null ? null : coordinates [ activeIndex ] ;
@@ -385,8 +409,32 @@ function Timeline({
385409 preserveAspectRatio = "none"
386410 viewBox = "0 0 100 44"
387411 >
388- < line x1 = "0" x2 = "100" y1 = "42" y2 = "42" />
389- { points ? < polyline points = { points } /> : null }
412+ < defs >
413+ < linearGradient id = { gradientId } x1 = "0" x2 = "0" y1 = "0" y2 = "1" >
414+ < stop offset = "0%" stopColor = "var(--accent)" stopOpacity = "0.26" />
415+ < stop offset = "100%" stopColor = "var(--accent)" stopOpacity = "0.02" />
416+ </ linearGradient >
417+ </ defs >
418+ { [ 6 , 18 , 30 , 42 ] . map ( ( y ) => (
419+ < line
420+ className = "performance-chart-grid"
421+ key = { y }
422+ x1 = "0"
423+ x2 = "100"
424+ y1 = { y }
425+ y2 = { y }
426+ />
427+ ) ) }
428+ { areaPath ? (
429+ < path
430+ className = "performance-chart-area"
431+ d = { areaPath }
432+ fill = { `url(#${ gradientId } )` }
433+ />
434+ ) : null }
435+ { linePath ? (
436+ < path className = "performance-chart-line" d = { linePath } />
437+ ) : null }
390438 { activePoint ? (
391439 < >
392440 < line
@@ -456,13 +504,13 @@ function formatRate(value: number | null | undefined): string {
456504
457505function hangLabel ( state : string ) : string {
458506 if ( state === "busy" ) {
459- return "Busy " ;
507+ return "Potential hang " ;
460508 }
461509 if ( state === "quiet" ) {
462- return "Quiet " ;
510+ return "No frame updates " ;
463511 }
464512 if ( state === "responsive" ) {
465- return "Responsive " ;
513+ return "Rendering OK " ;
466514 }
467515 return "Unknown" ;
468516}
0 commit comments