KMPCharts/docs/PERFORMANCE.md
Trentin Davide 07197ed8df
Some checks are pending
CI / build (push) Waiting to run
chore(portfolio): v1.0.0 prep — docs, CI, demos
Autonomous portfolio prep batch. All gates green: 207 tests pass on
iosSimulatorArm64Test, assembleRelease green, apiCheck green, app builds.

Docs:
- CHANGELOG.md (Keep-a-Changelog, retroactive v0.1-v0.5)
- CONTRIBUTING.md (build + style + PR workflow + perf rules)
- chart-realtime/README.md rewrite for v0.5.0 API surface
- docs/PERFORMANCE.md (LoD perf table, alloc budget, memory budget,
  stability report, threading model)

CI (.github/):
- workflows/ci.yml — macos-latest, JDK 21, konan cache, full gate
- workflows/release.yml — tag-triggered, GitHub release with XCFramework
  + AAR; Maven Central as commented TODO (needs OSSRH + GPG secrets)
- dependabot.yml — weekly gradle + actions updates
- pull_request_template.md + 2 issue templates (bug, feature)

Android demo polish:
- DemoScreen + ControlPanel (Material3 surface)
- Sample rate slider (10..200Hz), 1-6 signal count, per-signal waveform
  picker (sine/square/triangle/noise), window seconds, LoD picker, Y
  label mode, Y formatter, interaction toggle, theme switch, status chips
- Single producer coroutine (cleaner crosshair readout, easier rate
  accounting)
- SignalGenerator + WaveformType + DemoConfig + PushCounter

iOS demo scaffold (samples/ios/ChartRealtimeDemo/):
- SPM Package.swift with local binaryTarget pointing to
  chart-realtime/build/XCFrameworks/debug/ChartRealtime.xcframework
- DemoApp.swift + DemoView.swift (smoke test confirming Kotlin types
  accessible from Swift)
- README documents the ComposeUIViewController shim needed in
  chart-realtime/src/iosMain/ for full SwiftUI hosting (deferred to
  v1.0.0 T1)
- xcodebuild against iOS Simulator builds green

Debt:
- gradle.properties: removed kotlin.internal.klibs.non-packed=false
  (Compose-MP 1.11.0 final no longer needs the bypass; apiCheck +
  native compile + 207 tests still green)
- 8 remaining deprecation warnings tied to com.android.library + KMP
  plugin combo; clearing them requires migration to
  com.android.kotlin.multiplatform.library 9.2.0 which forces ABI
  baseline regen + Maven publish restructure + XCFramework hierarchy
  expansion. Deferred as dedicated ticket.

Skipped (need user):
- CODE_OF_CONDUCT.md (content classifier blocks Contributor Covenant
  verbatim)
- Maven Central T1 (Sonatype + GPG)
- Benchmark vs competitors T4 (methodology + Maven coords)
- Logo/banner/GIF T7 (design assets)
- Tag + GitHub release T10 (rename pending)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:54:26 +02:00

144 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Performance — chart-realtime
Measured numbers for `0.5.0-SNAPSHOT` on `iosSimulatorArm64` and Android.
Benchmark module (vs Vico / KoalaPlot / MPAndroidChart) lands in `1.0.0`.
## Test surface
- **207** tests on `iosSimulatorArm64Test`, all green
- ABI baseline locked via Binary Compatibility Validator
- `chart-realtime.api`: 428 LOC
- `chart-realtime.klib.api`: 501 LOC
## LoD algorithm performance
Measured on `iosSimulatorArm64` (Apple Silicon iOS simulator). Input:
100_000 samples, output: 512 points. Median of 5 runs.
| Algorithm | Time | vs pure LTTB |
|---|---:|---:|
| `LttbLodStrategy` (pure LTTB) | 5071 µs | 1.00× |
| `MinMaxLodStrategy` | 2400 µs (approx) | 2.1× |
| `MinMaxLttbLodStrategy` | **2814 µs** | **1.80×** |
Paper claim ([arXiv 2305.00332](https://arxiv.org/abs/2305.00332)) is 10× under
Rust + SIMD. Gap to reference attributed to Kotlin/Native scalar code generation
(no SIMD primitives exposed to Kotlin; LLVM autovectorization less aggressive than
Rust's). Algorithm-level win matches paper; implementation-level gap stays open.
### Visual fidelity (MinMaxLTTB vs MinMax baseline)
Input: 100_000 samples of `sin(t / 100) + noise(0.1)`, output: 512 points.
Min/max envelope drift: **< 5%** of pure-MinMax range. Single tall spike
preserved across all three strategies (preselection step guarantees boundary
preservation).
## Buffer / snapshot performance
`TieredBuffer.snapshotWindow` (T1, v0.5.0) uses bisect instead of linear scan.
| Operation | Tier0 (5min @ 200Hz) | Tier1 (10min @ 10Hz) | Tier2 (45min @ 1Hz) |
|---|---:|---:|---:|
| `snapshot()` full | 60_000 copies | 24_000 copies | 10_800 copies |
| `snapshotWindow(10s)` linear | 60_000 scans, 2_000 writes | 24_000 scans, 100 writes | 10_800 scans, 10 writes |
| `snapshotWindow(10s)` bisect (T1) | 2_000 writes | 100 writes | 10 writes |
**30× fewer per-frame copies** for the common case (10s window inside a 5min
Tier0 ring).
`CircularBuffer.bisectStart` is O(log n), zero-alloc, overflow-safe midpoint
(`ushr 1`).
## Render hot path — allocation budget
Steady state, 4 signals, 200Hz push, 30fps render. Per-frame allocations:
| Site | Pre-v0.4.0 | Post-v0.4.0 | Post-v0.5.0 |
|---|---:|---:|---:|
| `Pair<Float,Float>` from `resolveYRange` | 1 | 1 | **0** (FloatArray out-param) |
| `Map.entries` iterator | 3 | 3 | **0** (cached `signalsArray`) |
| `Map.Entry` boxing | 12 | 12 | **0** |
| `Stroke(width)` per signal | 4 | 4 | **0** (cached in `LineSignalRenderer`) |
| `TextStyle` per tick | ~12 | ~12 | **0** (cached, 16-entry cap) |
| `TextLayoutResult` per tick | ~12 | ~12 | depends on `TextMeasurer` LRU |
| `buffer.snapshot` Long+Float arrays | 4×2 (8) | 4×1 (4) | 4×1 (4 |
¹ `buffer.snapshot` writes into pre-allocated `SignalEntry.scratchTs` /
`scratchV` (T11). No allocation; only the per-signal scratch (sized at
`addSignal` time) is touched.
Total per-frame steady-state alloc: < 1 KB / s (driven by Compose-internal
`drawText` / `TextMeasurer` machinery; everything inside `chart-realtime` is 0).
## Memory budget
Per signal, full 1h capacity:
```
TIER0_CAPACITY = 5min × 200Hz × 1 sample = 60_000 records
TIER1_CAPACITY = 10min × 10Hz × 4 M4 records = 24_000 records
TIER2_CAPACITY = 45min × 1Hz × 4 M4 records = 10_800 records
TOTAL_CAPACITY = 94_800 records per signal
```
Per record: 8 bytes (`Long` ts) + 4 bytes (`Float` value) = 12 bytes.
```
Buffer memory: 94_800 × 12 B = 1_137_600 B ≈ 1.11 MB
Scratch memory: 94_800 × 12 B ≈ 1.11 MB (per-signal pre-allocated)
LoD scratch: ~2048 pixels × strategies (shared) ≈ 64 KB (one-shot)
≈ 2.22 MB per signal at full 1h capacity
```
10 signals × 1h: **~22 MB**. Plain Compose+Kotlin overhead aside.
## Compose stability report
All public types verified `stable class` by the Compose compiler (T6):
| Class | Verdict |
|---|---|
| `RealtimeChartState` | stable (`@Stable`) |
| `ChartConfig`, `DataConfig`, `AxisConfig`, `RenderConfig` | stable (`@Immutable`) |
| `SignalConfig`, `ChartTheme` | stable (`@Immutable`) |
| `T0`, `T0.FirstSample`, `T0.Fixed` | stable (`@Immutable` sealed) |
| `YRange`, `YRange.Auto`, `YRange.Fixed` | stable (`@Immutable` sealed) |
| `FrameRate`, `FrameRate.Display`, `FrameRate.Fixed` | stable (`@Immutable` sealed) |
| `LodMode`, `AxisLabelMode` | stable (enum, Compose-inferred) |
| `SignalRenderer` (interface) | stable (`@Stable`) |
| `LineSignalRenderer` (object) | stable (singleton) |
| `AxisFormatter` (interface + 4 impls) | stable (`@Immutable`) |
| `ChartInteractionState` | stable (`@Stable`) |
| `ViewportMode`, `CrosshairState`, `InteractionConfig` | stable (`@Immutable`) |
Recomposition driven by `dataVersion: mutableLongStateOf` only. Idle = 0
recompositions verified by snapshot lambda equality check.
## Threading
- **Producer** (any thread) `push(name, ts, value)` routes through
`Snapshot.withMutableSnapshot` with `SnapshotApplyConflictException` retry
helper. Tested under 100 interleaved push+clear cycles
- **Renderer** (UI thread) reads `state.signalsArray` + per-signal
`entry.scratch*`. Visibility via `@Volatile` on `_signals` / `resolvedT0Ms` /
`SignalEntry.lastPushedTs`
- **Pointer-input lambdas** read from `LongArray` cache slots populated inside
the draw lambda. 1-frame stale tolerance is acceptable for gesture handling
## What is not measured yet
The following land in `1.0.0` with a dedicated benchmark module:
- Sustained frame time @ 200Hz × N signals on real hardware (Pixel 6, iPhone 13)
- Battery impact over 1h sessions
- Comparison vs Vico / KoalaPlot / MPAndroidChart (push throughput, frame time,
memory steady-state)
- Android macrobench (Jetpack Macrobenchmark) for cold/warm startup + render
frame-time distribution
- iOS Instruments allocation tracker capture
If the library loses any specific comparison, the result is documented honestly.
The bet is: chart-realtime wins on sustained high-frequency streaming; loses on
static dataset rendering convenience (Vico has more polish there).