Skip to content

Commit b4eee25

Browse files
committed
bridge_example: README covers two-channel design + perf/memory baseline
Replaces the old single-bridge echo description with: * The actual current shape — two PythonBridge channels (JSON control + raw echo) and what each is used for. * Doc-grade descriptions of the three integration tests so a reader knows what each covers before opening the .dart file. * The 2026-06-12 macOS / M2 Pro perf and memory baseline (throughput table, memory deltas, narrative on what the numbers mean). Keeping perf numbers next to the tests that produce them means there's one place to look when the next libdart_bridge or serious-python bump changes the baseline.
1 parent 97ad66a commit b4eee25

1 file changed

Lines changed: 94 additions & 22 deletions

File tree

  • src/serious_python/example/bridge_example
Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,113 @@
11
# bridge_example
22

3-
Minimal Flutter app exercising [`PythonBridge`](../../lib/bridge.dart)
4-
the in-process byte transport between Dart and the embedded CPython
5-
runtime. Sends bytes from Dart to Python via `bridge.send()`, and Python
6-
echoes them back through `dart_bridge.send_bytes()`. No Flet, no msgpack,
7-
no protocol layer — the smallest possible end-to-end demonstration of the
8-
transport.
3+
Direct exercise of [`PythonBridge`](../../lib/bridge.dart) — the in-process byte transport between Dart and the embedded CPython runtime. No Flet, no MsgPack, no protocol layer beyond what this example draws itself. Used as the CI gate for the `serious_python` repo and as the perf / leak baseline for every change to `libdart_bridge`.
94

10-
## Build the Python app bundle
5+
## What it does
6+
7+
Two independent `PythonBridge` channels open at startup; Python registers a handler per channel:
118

12-
Before running, package the Python source into `app/app.zip`:
9+
| Channel | Env var carrying the Dart-side native port to Python | Wire format | Purpose |
10+
|---------|------------------------------------------------------|----------------------------------------------|------------------------------------------|
11+
| Control | `BRIDGE_EXAMPLE_CONTROL_PORT` | UTF-8 JSON, `{"op": …}``{"event": …}` | Interactivity (counter, version), memory snapshots |
12+
| Echo | `BRIDGE_EXAMPLE_ECHO_PORT` | Raw bytes — Python echoes the frame verbatim | Throughput timing, memory hammer loop |
13+
14+
Separating channels means the throughput / memory hot path is just `bridge.send``dart_bridge.send_bytes` echo back, with zero framing tax on either side. The JSON dispatcher only runs for tiny control messages where the encoding cost is irrelevant.
15+
16+
## Build the Python app bundle
1317

1418
```sh
1519
# From this directory:
1620
dart run serious_python:main package app/src --platform Darwin \
1721
--python-version 3.14
1822
```
1923

20-
Substitute `Darwin` with `Linux`, `Windows`, or `Android` for those
21-
targets (each platform plugin's CMake/Gradle pipeline downloads the
22-
prebuilt `dart_bridge` native binary from
23-
[flet-dev/dart-bridge](https://github.com/flet-dev/dart-bridge) at build
24-
time — no `--bridge` flag, no PyPI wheel).
24+
Substitute `Darwin` with `Linux`, `Windows`, `iOS`, or `Android`. Each platform plugin's CMake / Gradle pipeline downloads the prebuilt `dart_bridge` native binary from [flet-dev/dart-bridge](https://github.com/flet-dev/dart-bridge) at build time — no `--bridge` flag, no PyPI wheel.
2525

2626
## Run
2727

2828
```sh
2929
flutter run -d macos # or -d linux / windows / <device id>
3030
```
3131

32-
How it works:
32+
Tap `+` / `` to send `{"op": "inc"}` / `{"op": "dec"}` on the control channel; the displayed counter updates from the Python-side `{"event": "count", "value": …}` response. The version banner is populated by an analogous `{"op": "version"}` round-trip on `initState`.
33+
34+
## Integration tests
35+
36+
Three tests under [`integration_test/`](integration_test/):
37+
38+
| Test | What it covers |
39+
|----------------------------|------------------------------------------------------------------------------------------------------|
40+
| `interactivity_test.dart` | Counter +/-, version banner. Asserts UI text via Flutter widget keys; matches `EXPECTED_PYTHON_VERSION` if supplied via `--dart-define`. |
41+
| `throughput_test.dart` | Size sweep 1 KB → 16 MB, 100 round-trips each. Logs min/p50/p95/mean + MB/s. Floor assertion at ≥ 1 MB. |
42+
| `memory_test.dart` | 1 000 × 1 MB echo round-trips (~2 GB total). Snapshots Python `tracemalloc` + RSS before/after. Asserts `traced_delta < 5 MB`. |
43+
44+
```sh
45+
# After `dart run serious_python:main package …`:
46+
flutter test integration_test/throughput_test.dart -d macos
47+
flutter test integration_test/memory_test.dart -d macos
48+
flutter test integration_test/interactivity_test.dart -d macos \
49+
--dart-define=EXPECTED_PYTHON_VERSION=3.14
50+
```
51+
52+
---
53+
54+
# Performance & memory baseline
55+
56+
> Measured 2026-06-12 on the bridge_example integration tests. Re-running these is the canonical way to refresh the numbers after any `libdart_bridge` / `serious-python` bump.
57+
58+
## Test environment
59+
60+
| | |
61+
|----------------|----------------------------------------------------------|
62+
| Hardware | MacBook Pro M2 Pro, 32 GB |
63+
| OS | macOS 26.5 |
64+
| Python | CPython 3.14.6 (embedded via `libdart_bridge`) |
65+
| Flutter | 3.41.7, Debug build |
66+
| Test harness | `bridge_example/integration_test/` (`flutter test integration_test`) |
67+
68+
## Throughput
69+
70+
Methodology — round-trip = `Dart.send(N bytes) → Python echo handler → Dart receives N bytes back`. Throughput counted as `(2 × N) / mean_seconds` since both directions cross the bridge. Payload is `Random()`-seeded so each iteration is unique. 100 iterations per size.
71+
72+
| Payload | min | p50 | p95 | mean | Throughput |
73+
|--------:|--------:|--------:|--------:|--------:|---------------:|
74+
| 1 KB | 0.08 ms | 0.08 ms | 0.11 ms | 0.08 ms | 23 MB/s |
75+
| 64 KB | 0.07 ms | 0.09 ms | 0.12 ms | 0.09 ms | 1.3 GB/s |
76+
| 256 KB | 0.11 ms | 0.16 ms | 0.33 ms | 0.21 ms | 2.4 GB/s |
77+
| 1 MB | 0.24 ms | 0.31 ms | 0.84 ms | 0.45 ms | **4.5 GB/s** |
78+
| 4 MB | 0.82 ms | 1.01 ms | 3.83 ms | 1.55 ms | 5.2 GB/s |
79+
| 16 MB | 2.45 ms | 3.32 ms | 9.12 ms | 4.45 ms | **7.2 GB/s** |
80+
81+
### What the curve says
82+
83+
- **Below ~64 KB the transport is call-overhead-bound.** Every round-trip pays a fixed ~80 µs floor (Dart isolate scheduling + Python GIL acquisition + two `bridge.send_bytes` calls). At 1 KB this overhead swamps the byte work — 23 MB/s is the *call rate*, not the *memory rate*.
84+
- **At 64 KB → 16 MB it scales linearly with payload size.** Throughput goes from 1.3 GB/s to 7.2 GB/s as the per-byte cost (memory copy + Dart Native API marshalling) dominates the fixed per-call cost.
85+
- **7.2 GB/s at 16 MB is within an order of magnitude of M2 Pro's main-memory bandwidth ceiling** (~200 GB/s theoretical, ~50 GB/s achievable for non-tuned `memcpy`). Practically: the bridge is memory-copy-bound, which is the best you can do without a true zero-copy shared-buffer scheme.
86+
87+
For comparison: a Unix-domain socket transport on the same hardware tops out near 1 GB/s for similar-sized payloads, because every byte traverses the kernel.
88+
89+
## Memory
90+
91+
Methodology — 1 000 × 1 MB echo round-trips (~2 GB of bytes crossing the bridge total). Python heap measured via `tracemalloc.get_traced_memory()` (load-bearing leak signal — unit-stable, byte-accurate). RSS measured via `resource.getrusage(RUSAGE_SELF).ru_maxrss` on the Python side and `ProcessInfo.currentRss` on the Dart side (informational only — page residency, not retention).
92+
93+
| Metric | Before | After | Delta |
94+
|-------------------------------------|----------:|----------:|--------------------------------------------:|
95+
| Python heap (`tracemalloc`) | 10 179 B | 10 179 B | **0 B** |
96+
| Python RSS (`ru_maxrss`) | 350 MB | 457 MB | +112 MB |
97+
| Dart RSS (`ProcessInfo.currentRss`) | 1 232 MB | 1 239 MB | +7 MB |
98+
| Python `tracemalloc` peak | 11 446 B | 1.0 MB | +~1 MB (per-iteration buffer; reclaimed) |
99+
100+
### What the deltas say
101+
102+
- **`traced_delta = 0 B` after 2 GB of throughput.** This is the only number that matters for "does the bridge leak?" — Python's heap accounting is exact down to the byte. Zero growth means **the bridge does not retain a single byte** of the data it transports.
103+
- **`traced_peak` rose to ~1 MB** — exactly the size of one in-flight payload, then reclaimed. The bridge holds at most one frame's worth of bytes per direction.
104+
- **RSS growth (Python +112 MB, Dart +7 MB) is OS-level page residency**, not retention. macOS keeps recently-faulted-in pages mapped for performance; the kernel will release them under pressure. This is normal behaviour for any process that has briefly touched a lot of memory, and is decoupled from actual ownership of bytes.
105+
106+
## Bottom line
33107

34-
1. Dart creates a `PythonBridge`; its `port` (a `ReceivePort.sendPort.nativePort`)
35-
is exported to Python via the `BRIDGE_EXAMPLE_PORT` env var passed to
36-
`SeriousPython.run()`.
37-
2. Python reads the env var, registers a handler keyed on that port via
38-
`dart_bridge.set_enqueue_handler_func(port, handler)`, and echoes any
39-
incoming frame with a `b"echo: "` prefix using `dart_bridge.send_bytes(port, ...)`.
40-
3. Dart's `bridge.send()` returns `false` until the Python-side handler
41-
is registered; the example retries briefly to cover that startup race.
108+
| Concern | Result |
109+
|---------------------------------------------|-----------------------------------------------------------------|
110+
| Speed at typical Flet message size (~KB) | ~80 µs per round-trip (~12 000 messages/sec round-trip rate) |
111+
| Speed at "moving files / images" sizes (MB) | **4.5 GB/s at 1 MB, 7.2 GB/s at 16 MB** — memory-bandwidth class |
112+
| Leaks? | **None. 2 GB moved, Python heap unchanged.** |
113+
| Bytes retained per round-trip | Zero (peak ≤ 1 MB during one frame, then reclaimed) |

0 commit comments

Comments
 (0)