Skip to content

Commit 8e894e2

Browse files
committed
feat(react_reconciler): 添加被动效果队列状态跟踪和性能优化
refactor(input_parser): 重构输入解析器以改进转义序列处理 perf(ink): 优化渲染节流逻辑和输入处理性能 test(react_reconciler): 添加调度器和工作循环的测试用例 feat(examples): 为压力测试添加性能指标收集功能 fix(react_reconciler): 修复容器调度和优先级处理逻辑
1 parent f7c48d6 commit 8e894e2

File tree

12 files changed

+1162
-260
lines changed

12 files changed

+1162
-260
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Shared performance metric collection for stress-test examples."""
2+
3+
from __future__ import annotations
4+
5+
import threading
6+
import time
7+
from collections.abc import Callable
8+
from typing import Any
9+
10+
11+
class PerfMetricCollector:
12+
def __init__(self) -> None:
13+
self._lock = threading.Lock()
14+
self.reset()
15+
16+
def reset(self) -> None:
17+
with self._lock:
18+
self._frames = 0
19+
self._window_start = time.time()
20+
self._fps = 0.0
21+
self._render_time_ms = 0.0
22+
23+
def record_render(self, metrics: Any) -> None:
24+
now = time.time()
25+
with self._lock:
26+
self._frames += 1
27+
self._render_time_ms = getattr(metrics, "render_time", 0.0) * 1000
28+
elapsed = now - self._window_start
29+
if elapsed > 0:
30+
self._fps = self._frames / elapsed
31+
if elapsed >= 1.0:
32+
self._frames = 0
33+
self._window_start = now
34+
35+
def snapshot(self) -> dict[str, float]:
36+
with self._lock:
37+
return {
38+
"fps": self._fps,
39+
"render_time_ms": self._render_time_ms,
40+
}
41+
42+
43+
def use_perf_metrics(useEffect, useState, collector: PerfMetricCollector):
44+
metrics, set_metrics = useState({"fps": 0.0, "render_time_ms": 0.0})
45+
46+
def setup_metrics_sync():
47+
running = True
48+
49+
def run():
50+
while running:
51+
set_metrics(collector.snapshot())
52+
time.sleep(0.25)
53+
54+
thread = threading.Thread(target=run, daemon=True)
55+
thread.start()
56+
57+
def cleanup():
58+
nonlocal running
59+
running = False
60+
61+
return cleanup
62+
63+
useEffect(setup_metrics_sync, ())
64+
return metrics
65+

examples/stress-test/stress-test-compare.py

Lines changed: 55 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@
1818
from typing import Any
1919

2020
from 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
2525
SYNC_COLOR = "red"
2626
CONCURRENT_COLOR = "green"
27+
_PERF_COLLECTOR = PerfMetricCollector()
2728

2829

2930
def _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

7365
def 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

240213
def 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

276242
if __name__ == "__main__":

0 commit comments

Comments
 (0)