- Dirty-flag render: dataVersion counter in RealtimeChartState, skip Canvas draw when no new data since last frame (zero GPU work when sensor idle) - targetFps=30 default in ChartConfig (pass null for max display refresh rate) - iOS targets: iosX64, iosArm64, iosSimulatorArm64 + Platform.ios.kt actual - KDoc: RealtimeChart params, dataVersion, ChartConfig.targetFps, LodMode entries, AxisLabelMode entries - README: fixed Maven coords, added LoD/AxisLabels/Material3 sections, updated feature list and performance notes - Tests: RealtimeChartStateTest (17 tests, dataVersion + signal lifecycle) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
8.2 KiB
Markdown
159 lines
8.2 KiB
Markdown
---
|
||
phase: v0.2.0-polish
|
||
plan: v0.2.0
|
||
subsystem: chart-realtime
|
||
tags: [kmp, compose, canvas, buffer, lod, tiered-buffer, lttb, min-max, axis, material3]
|
||
|
||
requires:
|
||
- phase: v0.1.0-mvp
|
||
provides: CircularBuffer, LodDecimator, RealtimeChart composable, basic AxisRenderer
|
||
|
||
provides:
|
||
- TieredBuffer (3-tier 1h ring buffer, MIN_MAX binning, zero-alloc)
|
||
- LodMode enum (MIN_MAX, LTTB)
|
||
- LTTB render-time decimation
|
||
- AxisLabelMode (INSIDE/BESIDE/HIDDEN) with chartLeft/chartBottom insets
|
||
- T0-relative X-axis timestamps with mm:ss/hh:mm:ss formatting
|
||
- Material3 dynamic theming (light/dark, wallpaper-based)
|
||
- Camera cutout / edge-to-edge support
|
||
- Sensor domain layer (Clean Architecture)
|
||
- Koin DI wiring
|
||
- Package: dev.dtrentin.chart
|
||
- Git repository on ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git
|
||
|
||
affects: [future iOS target, future persistence layer]
|
||
|
||
tech-stack:
|
||
added: [koin-4.0.0, kotlinx-coroutines-android, androidx-lifecycle-runtime-compose]
|
||
patterns: [TieredBuffer hot-path write, MIN_MAX composable decimation, LodDecimator as class with pre-alloc scratch]
|
||
|
||
key-files:
|
||
created:
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/TieredBuffer.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/LodMode.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/AxisLabelMode.kt
|
||
- sensor-domain/src/commonMain/kotlin/dev/dtrentin/sensor/domain/
|
||
modified:
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt
|
||
- chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartTheme.kt
|
||
|
||
key-decisions:
|
||
- "TieredBuffer over flat ring: O(signals × constant) memory, scales to 20+ signals"
|
||
- "MIN_MAX binning for tiers: composable operation, zero visual discontinuity at boundaries"
|
||
- "MEAN removed: introduces values never in raw signal, disinformation for sensor data"
|
||
- "LodDecimator as class: pre-alloc scratch arrays, zero GC at render time"
|
||
- "Sensor rate capped at 200Hz: thermal/battery optimization"
|
||
- "Package dev.dtrentin: personal project, not company"
|
||
|
||
patterns-established:
|
||
- "MIN_MAX composability: MIN(bin_mins)=global_MIN across tier boundaries"
|
||
- "TieredBuffer snapshot always time-sorted: tier2→tier1→tier0 with tier0Start=max(windowStart,tier0Boundary)"
|
||
- "Render arrays pre-alloc in RealtimeChart via remember { }, zero alloc per frame"
|
||
|
||
duration: ~8h (multi-session)
|
||
started: 2026-05-20T00:00:00Z
|
||
completed: 2026-05-21T00:00:00Z
|
||
---
|
||
|
||
# v0.2.0 Plan Summary
|
||
|
||
**chart-realtime KMP library rebuilt from scratch: TieredBuffer (1h/20-signal), LTTB+MIN_MAX LoD, axis labels with INSIDE/BESIDE/HIDDEN modes, Material3 dynamic theming, Clean Architecture sensor domain — shipped as v0.5.0-SNAPSHOT.**
|
||
|
||
## Performance
|
||
|
||
| Metric | Value |
|
||
|--------|-------|
|
||
| Duration | ~8h multi-session |
|
||
| Started | 2026-05-20 |
|
||
| Completed | 2026-05-21 |
|
||
| Versions shipped | 0.2.0→0.3.0→0.4.0→0.4.1→0.5.0-SNAPSHOT |
|
||
| Git commits | 3 (initial + 2 incremental) |
|
||
| Files modified | 20+ |
|
||
|
||
## Acceptance Criteria Results
|
||
|
||
| Criterion | Status | Notes |
|
||
|-----------|--------|-------|
|
||
| Axis text labels visible | Pass | AxisRenderer TextMeasurer, INSIDE/BESIDE/HIDDEN modes |
|
||
| Dynamic X-window from UI | Pass | Slider 2–3600s, StateFlow, MAX_BUFFER_SECONDS=3600 |
|
||
| Material3 theme compliance | Pass | rememberMaterialChartTheme(), dynamic color API 31+ |
|
||
| User-decided signal colors | Pass | SignalConfig.color, ChartTheme separate |
|
||
| Camera cutout / edge-to-edge | Pass | enableEdgeToEdge() + Scaffold innerPadding |
|
||
| 1h data support without OOM | Pass | TieredBuffer ~18MB for 6 signals |
|
||
| No tier boundary visual artifact | Pass | MIN_MAX composable + tier0 lower-bound fix |
|
||
| LodDecimator zero-alloc | Pass | Class with pre-alloc scratch arrays |
|
||
| Negative X-axis values | Pass | formatTimeSec handles negative elapsed time |
|
||
| Sensor rate cap 200Hz | Pass | SensorRepositoryImpl 5_000μs |
|
||
| Package renamed dev.dtrentin | Pass | eu.henesis → dev.dtrentin everywhere |
|
||
| Remote git repository | Pass | ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git |
|
||
|
||
## Accomplishments
|
||
|
||
- **TieredBuffer**: 3-tier ring buffer (tier0=5min@200Hz, tier1=10min@10Hz MIN_MAX, tier2=45min@1Hz MIN_MAX). ~18MB for 6 signals vs 259MB naive flat at 1000Hz assumption
|
||
- **MIN_MAX composability**: tier boundary artifacts eliminated. MIN(tier_mins)=global_MIN mathematically proven. MEAN removed as inherently dishonest for raw sensor data
|
||
- **LTTB algorithm**: O(n) Largest-Triangle-Three-Buckets in LodDecimator. Preserves peaks, smooth trend line, better than mean without signal distortion
|
||
- **Axis overhaul**: INSIDE/BESIDE/HIDDEN modes, chartLeft/chartBottom insets, T0-relative timestamps (mm:ss/hh:mm:ss), scientific notation for |y|<0.01 or >1e5, negative values, grid toggle
|
||
|
||
## Deviations from v0.2.0 Plan
|
||
|
||
| Type | Count | Description |
|
||
|------|-------|-------------|
|
||
| Scope additions | 15+ | All features beyond Material3/axis/xWindow/scratch |
|
||
| Auto-fixed bugs | 8 | Tier boundary, LTTB samplePtr, X label sticky, Y label clipping, LoD path unravel, etc. |
|
||
| Deferred | 1 | iOS expect/actual — still commonMain only |
|
||
|
||
**Key deviations:**
|
||
- Phase 4 (scratch optimization): addressed differently — LodDecimator converted from `object` to `class` with pre-allocated instance fields, not param-passing as planned
|
||
- CircularBuffer replaced by TieredBuffer at storage level: larger scope than originally planned
|
||
- MEAN LodMode added then removed in same milestone after analysis showed it was harmful
|
||
|
||
## Key Decisions
|
||
|
||
| Decision | Rationale | Impact |
|
||
|----------|-----------|--------|
|
||
| TieredBuffer over flat ring | O(N×constant) memory, scales to 20+ signals at any rate | Core buffer architecture |
|
||
| MIN_MAX tier binning | Composable: MIN(mins)=global_MIN, no artifacts | Eliminates tier discontinuity |
|
||
| MEAN removed from LodMode | Introduces synthetic values never in raw signal | Cleaner API, honest visualization |
|
||
| Sensor cap 200Hz not FASTEST | Thermal/battery optimization, 200Hz exceeds visual need | Reduces CPU interrupts 4-10× |
|
||
| dev.dtrentin namespace | Personal project, not company IP | Package identity |
|
||
|
||
## Bugs Fixed
|
||
|
||
| Bug | Root Cause | Fix |
|
||
|-----|-----------|-----|
|
||
| TieredBuffer tier artifact | tier0 snapshot lacked `ts >= tier0BoundaryMs` filter → double-inclusion → col-sort broken | Added `tier0Start = maxOf(windowStart, tier0BoundaryMs)` |
|
||
| LoD path unravel | LodDecimator mapped out-of-window samples (tRelMs<0) to x=0 | Added in-window filter before binning |
|
||
| X label sticky at axis | `coerceAtLeast(chartLeft)` clamped all early labels to axis line | Replaced with `if (labelX >= chartLeft)` skip |
|
||
| Y labels clipped bottom | Used raw `14f` px instead of density-aware sp | `with(this) { 12.sp.toPx() }` |
|
||
| Signal line outside chart | No clipRect on drawPath | Added `clipRect(left=chartLeft…)` |
|
||
| Negative X shows "0s" | `coerceAtLeast(0L)` | `abs()` + `-` prefix for negative |
|
||
| OOM at MAX_X_WINDOW=3600 | MAX_SAMPLE_RATE=1000Hz assumed, 259MB pre-alloc | Reduced to 200Hz, TieredBuffer replaces flat ring |
|
||
|
||
## Next Phase Readiness
|
||
|
||
**Ready:**
|
||
- Library stable at v0.5.0-SNAPSHOT, published to mavenLocal
|
||
- Remote git: `ssh://3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git`
|
||
- AndroidChartsApp consuming library, builds clean
|
||
- Clean Architecture sensor domain in place
|
||
|
||
**Potential next work:**
|
||
- Dirty flag render (skip frame if no new data) — thermal/battery improvement
|
||
- targetFps=30 default — thermal improvement
|
||
- iOS expect/actual for currentTimeMs (currently Android only)
|
||
- Publish to Maven Central or GitHub Packages
|
||
- KDoc on public API surface
|
||
- README with integration example
|
||
|
||
**Deferred:**
|
||
- iOS support (expect/actual beyond currentTimeMs)
|
||
- Adaptive sensor rate based on xWindowSeconds
|
||
|
||
---
|
||
*Phase: v0.2.0-polish, completed 2026-05-21*
|