1818from typing import Any
1919
2020from pyinkcli import Box , Text , render , useInput
21- from pyinkcli .hooks import useMemo , useState , useTransition
21+ from pyinkcli .hooks import useEffect , useState , useTransition
22+ from perf_metrics import PerfMetricCollector , use_perf_metrics
2223
2324
24- # Colors for visual distinction
2525SYNC_COLOR = "red"
2626CONCURRENT_COLOR = "green"
27+ _PERF_COLLECTOR = PerfMetricCollector ()
2728
2829
2930def _generate_data (count : int ) -> list [dict [str , Any ]]:
30- """Generate test data."""
3131 return [
3232 {
3333 "id" : i ,
@@ -43,71 +43,46 @@ def _expensive_transform(
4343 multiplier : int ,
4444 delay_per_item_ms : float = 2 ,
4545) -> list [dict [str , Any ]]:
46- """
47- Expensive transformation - simulates real-world heavy computation.
48-
49- Args:
50- items: Input items
51- multiplier: Calculation multiplier
52- delay_per_item_ms: Artificial delay per item (ms) - controls how "heavy" the computation is
53- """
5446 result = []
5547 for item in items :
56- # Busy wait to simulate work
5748 if delay_per_item_ms > 0 :
5849 start = time .time ()
5950 target = delay_per_item_ms / 1000.0
6051 while time .time () - start < target :
61- # Simulate work
6252 _ = item ["value" ] * 1.001
6353
6454 computed = (item ["value" ] * multiplier ) % 10000
65- result .append ({
66- ** item ,
67- "computed" : round (computed , 2 ),
68- "category_label" : f"Cat-{ item ['category' ]} " ,
69- })
55+ result .append (
56+ {
57+ ** item ,
58+ "computed" : round (computed , 2 ),
59+ "category_label" : f"Cat-{ item ['category' ]} " ,
60+ }
61+ )
7062 return result
7163
7264
7365def DualModeApp (num_items : int , delay_per_item_ms : float ):
74- """
75- App that shows both sync and concurrent rendering side by side.
76-
77- The LEFT side uses regular useState (sync).
78- The RIGHT side uses useTransition (concurrent).
79- """
80-
81- # Shared base data
8266 base_items , set_base_items = useState (lambda : _generate_data (num_items ))
8367
84- # Sync side state
8568 sync_multiplier , set_sync_multiplier = useState (1 )
8669 sync_result = _expensive_transform (base_items , sync_multiplier , delay_per_item_ms )
8770
88- # Concurrent side state
8971 concurrent_multiplier , set_concurrent_multiplier = useState (1 )
9072 deferred_multiplier , set_deferred_multiplier = useState (1 )
9173 is_pending , start_transition = useTransition ()
74+ performance_metrics = use_perf_metrics (useEffect , useState , _PERF_COLLECTOR )
9275
93- # Use deferred multiplier for expensive computation
9476 concurrent_result = _expensive_transform (
9577 base_items ,
9678 deferred_multiplier ,
97- delay_per_item_ms * 0.5 , # Slightly less delay for fair comparison
79+ delay_per_item_ms * 0.5 ,
9880 )
9981
100- # FPS counter
101- fps_ref = {"frames" : 0 , "last_time" : time .time (), "fps" : 0 }
102- fps_ref ["frames" ] += 1
103- now = time .time ()
104- if now - fps_ref ["last_time" ] >= 1.0 :
105- fps_ref ["fps" ] = fps_ref ["frames" ] / (now - fps_ref ["last_time" ])
106- fps_ref ["frames" ] = 0
107- fps_ref ["last_time" ] = now
82+ def sync_deferred_multiplier () -> None :
83+ set_deferred_multiplier (concurrent_multiplier )
10884
109- # Auto-update trigger
110- from pyinkcli .hooks import useEffect
85+ useEffect (sync_deferred_multiplier , (concurrent_multiplier ,))
11186
11287 def setup_auto_update ():
11388 running = True
@@ -116,23 +91,23 @@ def update_loop():
11691 nonlocal running
11792 while running :
11893 time .sleep (1.0 )
119-
120- # Trigger updates on both sides
12194 set_sync_multiplier (lambda m : (m % 100 ) + 1 )
12295 start_transition (lambda : set_concurrent_multiplier (lambda m : (m % 100 ) + 1 ))
12396
12497 thread = threading .Thread (target = update_loop , daemon = True )
12598 thread .start ()
126- return lambda : setattr (thread , "running" , False )
12799
128- useEffect (setup_auto_update , [])
100+ def cleanup ():
101+ nonlocal running
102+ running = False
103+
104+ return cleanup
105+
106+ useEffect (setup_auto_update , ())
129107
130- # Manual controls
131108 def handle_input (char , key ):
132109 if key .enter :
133- # Sync: immediate update
134110 set_sync_multiplier (lambda m : m + 1 )
135- # Concurrent: transition update
136111 start_transition (lambda : set_concurrent_multiplier (lambda m : m + 1 ))
137112 elif char == "r" :
138113 set_base_items (_generate_data (num_items ))
@@ -143,40 +118,34 @@ def handle_input(char, key):
143118
144119 useInput (handle_input )
145120
146- # Calculate stats
147121 sync_visible = sync_result [:15 ]
148122 concurrent_visible = concurrent_result [:15 ]
149123
150- # Build item rows
151124 def build_rows (items , color ):
152- rows = []
153- for item in items :
154- rows .append (
155- Text (
156- f" { item ['id' ]:04d} | { item ['value' ]:4d} | { item ['computed' ]:>10.2f} | { item ['category_label' ]} " ,
157- color = color ,
158- )
125+ return [
126+ Text (
127+ f" { item ['id' ]:04d} | { item ['value' ]:4d} | { item ['computed' ]:>10.2f} | { item ['category_label' ]} " ,
128+ color = color ,
159129 )
160- return rows
130+ for item in items
131+ ]
161132
162- # Mode indicators
163- sync_status = f"Sync (immediate)" if sync_multiplier == deferred_multiplier else "Sync (processing)"
164- concurrent_status = (
165- f"Concurrent { ('[PENDING]' if is_pending else '[DONE]' )} "
133+ sync_status = (
134+ "Sync (immediate)" if sync_multiplier == deferred_multiplier else "Sync (processing)"
166135 )
136+ concurrent_status = f"Concurrent { ('[PENDING]' if is_pending else '[DONE]' )} "
167137
168138 return Box (
169- # Header
170139 Box (
171140 Text (" Performance Comparison: SYNC vs CONCURRENT " , bold = True , reverse = True ),
172141 Text (f" | Items: { num_items } | Work/item: { delay_per_item_ms } ms" , dimColor = True ),
173- Text (f" | FPS: { fps_ref ['fps' ]:.1f} " , dimColor = True ),
142+ Text (
143+ f" | FPS: { performance_metrics ['fps' ]:.1f} | Render: { performance_metrics ['render_time_ms' ]:.1f} ms" ,
144+ dimColor = True ,
145+ ),
174146 flexDirection = "column" ,
175147 ),
176-
177148 Box (marginTop = 1 ),
178-
179- # Side by side headers
180149 Box (
181150 Box (
182151 Text (" SYNC MODE " , bold = True , color = SYNC_COLOR , reverse = True ),
@@ -192,8 +161,6 @@ def build_rows(items, color):
192161 ),
193162 flexDirection = "row" ,
194163 ),
195-
196- # Column headers
197164 Box (
198165 Box (
199166 Text (" ID | Value | Computed | Cat" , bold = True , underline = True , dimColor = True ),
@@ -205,46 +172,47 @@ def build_rows(items, color):
205172 ),
206173 flexDirection = "row" ,
207174 ),
208-
209- # Data rows
210175 * [
211176 Box (
212- Box (* build_rows ([sync_visible [i ] if i < len (sync_visible ) else {"id" : 0 , "value" : 0 , "computed" : 0 , "category_label" : "" }], SYNC_COLOR ), flexDirection = "column" , width = 48 ) if i < len (sync_visible ) else Box (Text ("" , width = 48 )),
213- Box (* build_rows ([concurrent_visible [i ] if i < len (concurrent_visible ) else {"id" : 0 , "value" : 0 , "computed" : 0 , "category_label" : "" }], CONCURRENT_COLOR ), flexDirection = "column" , width = 48 ) if i < len (concurrent_visible ) else Box (Text ("" , width = 48 )),
177+ Box (
178+ * build_rows (
179+ [sync_visible [i ]] if i < len (sync_visible ) else [],
180+ SYNC_COLOR ,
181+ ),
182+ flexDirection = "column" ,
183+ width = 48 ,
184+ ) if i < len (sync_visible ) else Box (Text ("" , width = 48 )),
185+ Box (
186+ * build_rows (
187+ [concurrent_visible [i ]] if i < len (concurrent_visible ) else [],
188+ CONCURRENT_COLOR ,
189+ ),
190+ flexDirection = "column" ,
191+ width = 48 ,
192+ ) if i < len (concurrent_visible ) else Box (Text ("" , width = 48 )),
214193 flexDirection = "row" ,
215194 )
216195 for i in range (15 )
217196 ],
218-
219197 Box (marginTop = 1 ),
220-
221- # Controls
222198 Box (
223199 Text (" Controls: " , bold = True ),
224200 Text ("↑: Sync update | ↓: Concurrent update | Enter: Both | R: Refresh" , dimColor = True ),
225201 flexDirection = "column" ,
226202 ),
227-
228- # Explanation
229203 Box (
230204 Text (" Note: In SYNC mode, each update blocks the UI. " , color = SYNC_COLOR ),
231205 Text (" In CONCURRENT mode, updates can be interrupted. " , color = CONCURRENT_COLOR ),
232206 flexDirection = "column" ,
233207 marginTop = 1 ,
234208 ),
235-
236209 flexDirection = "column" ,
237210 )
238211
239212
240213def main ():
241214 parser = argparse .ArgumentParser (description = "Compare sync vs concurrent performance" )
242- parser .add_argument (
243- "--items" ,
244- type = int ,
245- default = 300 ,
246- help = "Number of items (default: 300)" ,
247- )
215+ parser .add_argument ("--items" , type = int , default = 300 , help = "Number of items (default: 300)" )
248216 parser .add_argument (
249217 "--delay" ,
250218 type = float ,
@@ -265,12 +233,10 @@ def main():
265233 print ("=" * 60 + "\n " )
266234
267235 def app ():
268- return DualModeApp (
269- num_items = args .items ,
270- delay_per_item_ms = args .delay ,
271- )
236+ return DualModeApp (num_items = args .items , delay_per_item_ms = args .delay )
272237
273- render (app , concurrent = True ).wait_until_exit ()
238+ _PERF_COLLECTOR .reset ()
239+ render (app , concurrent = True , on_render = _PERF_COLLECTOR .record_render ).wait_until_exit ()
274240
275241
276242if __name__ == "__main__" :
0 commit comments