Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multiconductor PF interactive performance

A retained multiconductor power-flow session (McPfSession in Rust, createMcPfSession in the browser engine) keeps the prepared network, the compensation operator, its sparse LU factorization, and the last converged voltages. This page states what an ordinary load edit is allowed to cost, how that is measured, and the measurements behind it (#132).

Target and budgets

BudgetValueMeasured
UI-visible latency, single-branch load edit, generated ~10,000-bus feeder (9,950 buses, 32,412 terminals), Chromium on Apple M-series≤ 100 ms p9515.6 ms p95
Per-edit responseconstant size, ≤ 2 KiB (McPfSummary)563–571 bytes at every size up to 676,530 terminals
Results-panel detail page (one bus, 20 equipment ports)≤ 32 KiB≈ 4.9 KB
Terminal arrays, fetched on demand16 bytes per terminal per quantity, transferred rather than copied0.5 MiB (32,412 terminals) to 10.3 MiB (676,530)
Network materializations during an ordinary edit00
Live engine heap after an editunchanged (± 64 KiB)unchanged (0 bytes)
WASM linear memory across editsno growthno growth over 20 edits
Transient heap during an editproportional to terminals, no network or result copyabout 85 bytes per terminal

The 180 ms debounce that coalesces rapid edits in the Studies panel is a deliberate delay and is excluded from every latency figure. Saving, exporting, attaching geography, and opening the full result build portable documents on demand; their cost is reported below but is not part of an ordinary edit.

What an edit costs

An ordinary edit replaces the absolute set of load-branch powers. The session updates only the prepared current laws of the loads that changed (physical nominal admittance is recomputed; the fixed-point compensation reference and the LU factorization stay frozen), warm-starts from the last converged voltages, and reduces the converged phasors to a summary: convergence, residuals, the load voltage range and violation count, source power, passive loss, and counts. The summary runs the same output finiteness checks as the full result without building per-terminal records. The base network and instance are never copied or rebuilt by an edit; materialization_count counts the edited networks built for portable output, and stays zero across ordinary edits.

The browser makes one engine round trip per edit. Terminal and equipment detail is fetched only when a view asks for it: the Studies panel requests one bus’s terminals and one 20-row equipment page, terminalVoltages() and terminalCurrents() return transferable Float64Arrays, and result() still builds the complete McPfResult for export. Detail pages carry the solve count they describe, so a page from an older operating point is discarded.

The four latency components are measured separately:

ComponentMeaningHow it is measured
Numerical solveEngine time for the edit request: the warm fixed point and the summaryengine_ms from the worker, split by the session profile into load evaluation, KCL residuals and compensated right-hand sides, retained LU solves, and the summary
Network materializationBuilding an edited network for portable outputZero per edit (materializationCount()); inputModule() and snapshot() are timed separately
Serialization and parsingMoving the response to the pageSummary JSON in the engine, transfer (round trip minus engine time), and JSON.parse on the page
UI-visibleFrom the edit request to the updated state and the next animation frameRound trip, parse, and one requestAnimationFrame; with the results panel open, plus its detail request

Results

Measurements below use deterministic generated feeders (benchmarks::mc_feeder), on an Apple M5 Pro (18 CPUs, 64 GiB). feeder-10k and feeder-106k match the shapes reported in #132; x300k and x650k are exploratory points at the SMART-DS Greensboro and San Francisco P5U conductor-node counts. Each preset converges in 9–11 iterations with a minimum load-branch voltage of about 0.93 pu; single-branch edits take 6–8. “Before” is the session as of 0.3.0 (commit ee23ab8, which cloned the network, rebuilt the instance, and returned the complete result on every edit), run with the same harness, inputs, and edit sequence.

PresetBusesTerminalsLinesTransformersLoads (branches)Module
feeder-10k9,95032,4129,2996507,721 (9,299)11.4 MiB
feeder-106k27,239106,03827,80302,103 (3,391)21.7 MiB
x300k81,406316,90483,09506,285 (10,135)65.1 MiB
x650k173,787676,530177,391013,418 (21,636)139.2 MiB

Browser

Chromium 153 (headless), the shipped WASM build (release profile, opt-level “s”), 20 single-branch edits.

PresetUI-visible median / p95, beforeafterPer-edit response, beforeafter
feeder-10k147.5 / 153.3 ms15.2 / 15.6 ms25.8 MB result + 1.4 MB load branches566 B
feeder-106k427.4 / 446.7 ms50.0 / 65.3 ms75.9 MB + 0.5 MB568 B
x300k1,365 / 1,480 ms181 / 203 ms227.1 MB + 1.6 MB569 B

Opening the results panel adds one detail request of 0.1–0.2 ms and about 4.9 KB. Median components after the change:

PresetEngine solveLoad evaluationKCL and matvecLU solvesSummaryTransfer + parseMaterialization
feeder-10k11.8 ms1.73.24.71.4< 0.1 ms0
feeder-106k49.8 ms0.79.532.34.4< 0.1 ms0
x300k180.6 ms2.730.8121.713.1< 0.2 ms0

Before the change the corresponding engine call took 102.6, 299.2, and 979 ms (solve, network copy, instance rebuild, result construction, and JSON serialization together), and parsing the result on the page took a further 37.3, 111.5, and 356 ms. Building the complete result on demand now costs 51 / 153 / 450 ms in the engine plus 41 / 117 / 361 ms of parsing, only when it is requested.

Creating a session (parse, prepare, factor, first solve, summary, and the load branch list) takes 256 ms, 832 ms, and 2.79 s, compared with 428 ms, 1.22 s, and 3.98 s when the full result was fetched with it.

Native

release-py profile (opt-level 3), 20 single-branch edits; the counting allocator reports heap bytes.

PresetEdit median / p95, beforeafterResult or summary JSON per edit, beforeafterEdit heap peak, beforeafter
feeder-10k42.9 / 46.8 ms + 20.8 ms JSON5.7 / 6.5 ms24.6 MiB565 B39.1 MiB2.6 MiB
feeder-106k105.5 / 119.3 ms + 61.3 ms18.8 / 25.3 ms72.4 MiB569 B80.2 MiB8.7 MiB
x300k356.3 / 381.5 ms + 185.1 ms72.4 / 79.5 ms216.6 MiB570 B274.7 MiB25.9 MiB
x650k733.3 / 768.8 ms + 399.2 ms139.0 / 154.6 ms464.4 MiB571 B570.0 MiB55.3 MiB
PresetLoad evaluationKCL and matvecLU solvesSummaryCold start, before → afterRetained heap, before → after
feeder-10k0.8 ms1.42.40.7182 → 131 ms106 → 67 MiB
feeder-106k0.4 ms3.711.62.1442 → 391 ms319 → 230 MiB
x300k1.5 ms12.646.06.51.43 → 1.19 s968 → 667 MiB
x650k3.4 ms25.587.514.13.12 → 2.61 s1.86 → 1.25 GiB

The retained heap fell because the session no longer keeps a second copy of the stored module text and freezes the stamped admittances into compressed rows. A feeder-wide 1.05× edit takes 9.5 ms, 31 ms, 97 ms, and 216 ms (before: 47, 129, 392, and 890 ms) and agrees with a freshly prepared solve to 8.7×10⁻¹⁰ V / 3.5×10⁻⁹ A, 6.8×10⁻⁹ V / 2.0×10⁻⁸ A, 5.1×10⁻⁹ V / 2.9×10⁻⁸ A, and 5.1×10⁻⁹ V / 4.2×10⁻⁸ A, unchanged from before, with one factorization throughout. Results are bit-identical to the previous solver on every oracle and load-model fixture, for one-shot solves and for warm session edits.

Memory in the browser engine

PresetLive heap after createLinear memory after createTransient per editLinear memory after 20 edits, before → afterPage JS heap after edits, before → after
feeder-10k47.8 MiB115.6 MiB2.6 MiB171 → 115.6 MiB195 → 4.6 MiB
feeder-106k177.8 MiB394.1 MiB8.7 MiB587 → 394.1 MiB543 → 3.4 MiB
x300k508.5 MiB1,008 MiB25.9 MiB1,394 → 1,008 MiB541 → 5.1 MiB

WASM linear memory only grows, so it records the largest transient since the worker started; the live heap is what a session holds. Saving or exporting is the memory high-water mark: at 106,038 terminals the snapshot takes 668 ms, peaks at 563 MiB of engine heap, and grows linear memory to 938 MiB.

Reproduce

# Native: any preset, a stored PowerIO module, or a raw BMOPF file.
just mc-pf-bench feeder-106k
cargo run -p benchmarks --profile release-py --bin mc-pf-session-bench -- --bmopf case.json
# The CI regression (deterministic checks only, never timing).
just mc-pf-regression

# Browser: build the WASM, write the modules, then run the opt-in spec.
npm run wasm
cargo run -p benchmarks --profile release-py --bin mc-pf-session-bench -- \
  --preset feeder-10k --write-module target/mc-pf-bench/modules/feeder-10k.pio.json
TELLEGEN_MC_BENCH=1 \
TELLEGEN_MC_BENCH_MODULES=$PWD/target/mc-pf-bench/modules/feeder-10k.pio.json \
  npx --prefix apps/web playwright test --config apps/web/playwright.config.ts mc-pf-bench

The native harness writes native-<label>.json and .md, and the browser spec writes browser-<label>.json, under target/mc-pf-bench/.

Scope and limits

  • The generated feeders reproduce the dimensions of the cases reported in #132, not their files: the original ~10,000-bus case needed 21 cold iterations and carried a 70.3 MB BMOPF document, while the generated preset needs 9 and 11.4 MiB. --bmopf and --module run the same measurements on a real file.
  • x650k’s 139 MiB module exceeds the browser’s 128 MiB input boundary, so the 676,530-terminal point is native only. A saved Study embeds its input, its PowerIO solution, and the full result; at 106,038 terminals that is about 138 MiB, beyond the same boundary. Reading back a stored solution with more than 65,536 terminal values needs PowerIO 0.11.4 (eigenergy/powerio#550), which reads up to 4,194,304 per vector; the document-size contract is tracked in eigenergy/powerio#544.
  • With copying and result construction removed, the retained LU solves are the largest share of a large edit. Backend speed, conditioning, and the faer/KLU comparison are tracked in #153.
  • At the largest sizes the cold start is dominated by preparation (admittance stamping), 1.48 of 2.61 s at 676,530 terminals; the per-load lookup that made preparation quadratic in the load count has been removed.
  • Raw SMART-DS import and nonlinear Tellegen solves of those regions are not established; the exploratory presets are generated models of the same size.