Some checks are pending
CI / build (push) Waiting to run
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>
144 lines
5.9 KiB
Markdown
144 lines
5.9 KiB
Markdown
# 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).
|