Skip to content

Commit 39bcafe

Browse files
committed
Bench run: fix3 dirty propagation, main vs eng-10095
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G8cXh3TUbNtbjm2ERqE62X
1 parent e565a8e commit 39bcafe

3 files changed

Lines changed: 173 additions & 2 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Micro-benchmark for ENG-10095: dirty-propagation runs in full on every mutation.
2+
3+
Measures per-mutation cost with computed vars defined:
4+
1. Proxied list append (each triggers _mark_dirty -> _mark_dirty_computed_vars).
5+
2. Plain int setattr in a loop.
6+
3. Raw list append baseline (no state involved).
7+
"""
8+
9+
import cProfile
10+
import io
11+
import pstats
12+
import time
13+
14+
15+
def build_state():
16+
import reflex as rx
17+
18+
class BenchFix3State(rx.State):
19+
items: list[int] = []
20+
other: int = 0
21+
22+
@rx.var(cache=True)
23+
def total(self) -> int:
24+
return len(self.items)
25+
26+
@rx.var(cache=True)
27+
def other_doubled(self) -> int:
28+
return self.other * 2
29+
30+
return BenchFix3State(_reflex_internal_init=True) # pyright: ignore [reportCallIssue]
31+
32+
33+
def bench_append(state, n: int = 10000) -> float:
34+
proxy = state.items
35+
start = time.perf_counter()
36+
for i in range(n):
37+
proxy.append(i)
38+
return (time.perf_counter() - start) / n * 1_000_000
39+
40+
41+
def bench_setattr(state, n: int = 10000) -> float:
42+
start = time.perf_counter()
43+
for i in range(n):
44+
state.other = i
45+
return (time.perf_counter() - start) / n * 1_000_000
46+
47+
48+
def bench_raw_append(n: int = 100000) -> float:
49+
raw = []
50+
start = time.perf_counter()
51+
for i in range(n):
52+
raw.append(i)
53+
return (time.perf_counter() - start) / n * 1_000_000
54+
55+
56+
def main():
57+
state = build_state()
58+
59+
append_us = bench_append(state)
60+
setattr_us = bench_setattr(state)
61+
raw_us = bench_raw_append()
62+
print(f"proxied list append: {append_us:8.3f} us/op")
63+
print(f"int var setattr: {setattr_us:8.3f} us/op")
64+
print(f"raw list append: {raw_us:8.4f} us/op")
65+
66+
# Correctness spot-check: computed vars still update after the loop.
67+
state._clean()
68+
state.items = [1, 2, 3]
69+
assert state.total == 3
70+
state.other = 21
71+
assert state.other_doubled == 42
72+
print("correctness spot-check passed")
73+
74+
profiler = cProfile.Profile()
75+
profiler.enable()
76+
bench_append(state, n=2000)
77+
profiler.disable()
78+
stream = io.StringIO()
79+
pstats.Stats(profiler, stream=stream).sort_stats("cumulative").print_stats(12)
80+
print("cProfile (2000 proxied appends):")
81+
print(stream.getvalue())
82+
83+
84+
if __name__ == "__main__":
85+
main()
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Micro-benchmark for ENG-10096: __getattribute__ slow path and get_delta caches.
2+
3+
Measures:
4+
1. Framework bookkeeping attribute reads (dirty_vars, parent_state, substates,
5+
_backend_vars) through the state instance.
6+
2. An event-processing-shaped loop: mutate, get_delta, clean.
7+
3. get_skip_vars() throughput.
8+
"""
9+
10+
import cProfile
11+
import io
12+
import pstats
13+
import time
14+
15+
16+
def build_state():
17+
import reflex as rx
18+
19+
class BenchFix4State(rx.State):
20+
items: list[int] = []
21+
count: int = 0
22+
23+
@rx.var(cache=True)
24+
def doubled(self) -> int:
25+
return self.count * 2
26+
27+
@rx.var(cache=True, backend=True)
28+
def _hidden(self) -> int:
29+
return self.count + 1
30+
31+
class BenchFix4Child(BenchFix4State):
32+
child_val: int = 0
33+
34+
return BenchFix4State(_reflex_internal_init=True) # pyright: ignore [reportCallIssue]
35+
36+
37+
def bench_bookkeeping_reads(state, n: int = 50000) -> float:
38+
start = time.perf_counter()
39+
for _ in range(n):
40+
_ = state.dirty_vars
41+
_ = state.parent_state
42+
_ = state.substates
43+
_ = state._backend_vars
44+
return (time.perf_counter() - start) / n * 1_000_000
45+
46+
47+
def bench_event_cycle(state, n: int = 2000) -> float:
48+
start = time.perf_counter()
49+
for i in range(n):
50+
state.count = i
51+
state.get_delta()
52+
state._clean()
53+
return (time.perf_counter() - start) / n * 1_000_000
54+
55+
56+
def bench_skip_vars(state, n: int = 50000) -> float:
57+
cls = type(state)
58+
start = time.perf_counter()
59+
for _ in range(n):
60+
cls.get_skip_vars()
61+
return (time.perf_counter() - start) / n * 1_000_000
62+
63+
64+
def main():
65+
state = build_state()
66+
67+
reads_us = bench_bookkeeping_reads(state)
68+
cycle_us = bench_event_cycle(state)
69+
skip_us = bench_skip_vars(state)
70+
print(f"4 bookkeeping attr reads: {reads_us:8.3f} us/op")
71+
print(f"setattr+get_delta+_clean: {cycle_us:8.3f} us/op")
72+
print(f"get_skip_vars(): {skip_us:8.4f} us/op")
73+
74+
profiler = cProfile.Profile()
75+
profiler.enable()
76+
bench_bookkeeping_reads(state, n=5000)
77+
bench_event_cycle(state, n=500)
78+
profiler.disable()
79+
stream = io.StringIO()
80+
pstats.Stats(profiler, stream=stream).sort_stats("cumulative").print_stats(14)
81+
print("cProfile (5000 bookkeeping reads + 500 event cycles):")
82+
print(stream.getvalue())
83+
84+
85+
if __name__ == "__main__":
86+
main()

benchmarks_scratch/run.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"base_ref": "main",
3-
"head_ref": "claude/reflex-perf-optimizations-01l7a3-eng-10094",
4-
"bench_script": "benchmarks_scratch/bench_fix2_mutable_proxy.py"
3+
"head_ref": "claude/reflex-perf-optimizations-01l7a3-eng-10095",
4+
"bench_script": "benchmarks_scratch/bench_fix3_dirty_propagation.py"
55
}

0 commit comments

Comments
 (0)