Ships v0.5.0 via kmp-manager 6-phase flow. 13 plan tasks (D1, T1-T10, D3, D4), 14 agent calls, 6 P-groups. 107 → 207 tests on iosSimulatorArm64. Toolchain (D1): - Kotlin 2.1.0 → 2.3.21 - Compose-Multiplatform 1.8.0 → 1.11.0 - Android Gradle Plugin 8.7.3 → 9.2.0 - Gradle 8.11.1 → 9.5.1 - coroutines 1.9.0 → 1.11.0, kotlin-test → 2.3.21 - Drop kotlinx-datetime; use stdlib kotlin.time.Clock - Drop iosX64 target (Compose-MP 1.11.0 has no ios_x64 variant) Architecture (T1-T6, T10): - TieredBuffer.snapshotWindow: bisect-based windowed snapshot - New lod/ package: LodStrategy interface + MinMax/Lttb/MinMaxLttb impls (MinMaxLttb SOTA per arXiv 2305.00332, 1.80× faster than pure LTTB) - New render/SignalRenderer: public interface + LineSignalRenderer object - New render/AxisFormatter: 4 default impls (Time, Decimal, DateTime, Unit) - HARD BREAK: deleted LodMode, LodDecimator, ChartConfig.targetFps - ChartConfig split: DataConfig + AxisConfig + RenderConfig + FrameRate sealed - @Immutable/@Stable on all public types (0 unstable) - RealtimeChartState.clear() API Interaction layer (T7): - New interaction/ package - ChartInteractionState + rememberChartInteractionState() - ViewportMode sealed: Following / Frozen / History(anchorMs) - Pinch zoom + drag pan + tap crosshair gestures - Swipe-to-edge resumes Following - InverseProjection: pixel → ms + bisect nearest-sample Perf finishing (T8, T9, D3): - LineSignalRenderer Stroke cache, AxisRenderer TextStyle cache - resolveYRange Pair<Float,Float> → FloatArray out-param - RealtimeChartState.signalsArray cached (invalidated on add/remove only) - LTTB upper-bound aligned to half-open [start, start+windowMs) semantic Correctness (D4): - NumberFormat.formatFixed Long overflow guard @ |v|≥1e19 ABI baseline regenerated: - chart-realtime.api: 161 → 428 LOC - chart-realtime.klib.api: 211 → 501 LOC Modules touched: chart-realtime (lib), app (consumer), gradle (toolchain), .paul (state). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
236 lines
13 KiB
Markdown
236 lines
13 KiB
Markdown
# v0.5.0 — Architecture + Interaction Plan
|
|
|
|
## Source
|
|
mobile-architect verdict 2026-05-21: "Library is a closed renderer with single skin. Anything beyond first-party use case requires forking." Senza interaction layer "no senior reviewer will take a real-time chart claim seriously."
|
|
|
|
## Goal
|
|
Aprire estensibilità + aggiungere interaction layer (S0 per portfolio). MinMaxLTTB SOTA algorithm. Config split. Bisect tier snapshot per perf.
|
|
|
|
## Prereq
|
|
v0.4.0 shipped (hygiene + correctness).
|
|
|
|
## Scope IN
|
|
- Interaction layer: pinch zoom, drag pan, crosshair, tap-to-inspect
|
|
- Renderer/LodStrategy/AxisFormatter interfaces
|
|
- MinMaxLTTB algorithm (replaces both MIN_MAX and LTTB attuali)
|
|
- Bisect-based windowed snapshot (30x perf win)
|
|
- ChartConfig split DataConfig/AxisConfig/RenderConfig
|
|
- Compose stability annotations (@Stable, @Immutable)
|
|
|
|
## Scope OUT
|
|
- Multi-Y axis / log scale → v0.6.0+
|
|
- Annotations / threshold lines → v0.6.0+
|
|
- Headless data export → backlog
|
|
|
|
---
|
|
|
|
## Tasks
|
|
|
|
### T1 — Bisect tier snapshot (perf S0)
|
|
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt`, `TieredBuffer.kt`
|
|
- Goal: `snapshotWindow(startMs, endMs, outTs, outV): Int` bisect su timestamps monotonici. Cuts 30x copies su 10s window in 5min buffer.
|
|
- Agent: data-impl + mobile-performance-reviewer review
|
|
- Verify: macrobench frame time -50% addizionale post v0.4.0 T11
|
|
|
|
### T2 — LodStrategy interface + MinMaxLTTB impl
|
|
- New files:
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/LodStrategy.kt`
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLttb.kt`
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLod.kt`
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/LttbLod.kt`
|
|
- Move + rename: `buffer/LodDecimator.kt` → `lod/LodDecimator.kt` (delegate to strategy)
|
|
- Goal: interface `LodStrategy { fun decimate(...): Int }`. Default = `MinMaxLttb` (10x faster than LTTB puro per arXiv 2305.00332).
|
|
- API change: `LodMode` enum → `LodStrategy` interface. `ChartConfig.lodMode: LodMode` → `lodStrategy: LodStrategy`. Backward compat: `LodMode.MIN_MAX_LTTB` shorthand.
|
|
- Agent: kmp-architect + data-impl
|
|
- Verify: nuovi test `MinMaxLttbVisualFidelityTest`, perf ≥ MIN_MAX puro su large datasets
|
|
- Pattern: [MinMaxLTTB arXiv 2305.00332](https://arxiv.org/pdf/2305.00332), [Rust ref impl](https://github.com/andrei-ng/minmaxlttb-rs)
|
|
|
|
### T3 — SignalRenderer interface
|
|
- New file: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt` (interface)
|
|
- Move impl → `LineSignalRenderer.kt`
|
|
- Goal: pluggable rendering. Future-proof per `ScatterRenderer`, `AreaRenderer`, `BarRenderer`. Default `LineSignalRenderer`.
|
|
- `SignalConfig.renderer: SignalRenderer = LineSignalRenderer` (per-signal opt-in)
|
|
- Agent: kmp-architect
|
|
- Verify: existing rendering unchanged, plus `customRendererPlugsIn` test
|
|
|
|
### T4 — AxisFormatter interface
|
|
- New file: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisFormatter.kt`
|
|
- Default impls: `TimeAxisFormatter` (current behavior), `DecimalAxisFormatter`, `DateTimeAxisFormatter`, `UnitAxisFormatter("mV", "°C", "Hz")`.
|
|
- `AxisConfig.xFormatter: AxisFormatter = TimeAxisFormatter`, `yFormatter: AxisFormatter = DecimalAxisFormatter`
|
|
- Agent: kmp-architect + ui-impl
|
|
- Verify: user can pass `UnitAxisFormatter("ms")` and labels show "5 ms"
|
|
|
|
### T5 — Split ChartConfig → DataConfig + AxisConfig + RenderConfig
|
|
- File: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt`
|
|
- New: `DataConfig`, `AxisConfig`, `RenderConfig`. `ChartConfig` = wrapper o builder DSL.
|
|
- `targetFps: Int?` → `sealed class FrameRate { object Display; data class Fixed(val fps: Int) }`
|
|
- Mark old `ChartConfig` ctor `@Deprecated(level = WARNING)` con `ReplaceWith`.
|
|
- Agent: kmp-architect
|
|
- Verify: README example updated, compile green con deprecation warning
|
|
|
|
### T6 — Compose stability annotations
|
|
- Files (tutti i data class in model/):
|
|
- `ChartConfig.kt`, `SignalConfig.kt`, `ChartTheme.kt`, `T0.kt`, `YRange.kt`, `LodMode.kt`, `AxisLabelMode.kt`
|
|
- `RealtimeChartState.kt`
|
|
- Action: `@Immutable` su data class + sealed root. `@Stable` su `RealtimeChartState`.
|
|
- Agent: compose-guide
|
|
- Verify: Compose compiler report `chart-realtime` mostra 0 unstable classes nel public API. `./gradlew :chart-realtime:compileReleaseKotlinAndroid -Pkotlinx.compose.report` etc.
|
|
|
|
### T7 — Interaction layer S0 (touch/zoom/pan/crosshair)
|
|
- New files:
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/ChartInteractionState.kt`
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/InteractionConfig.kt`
|
|
- Modify: `RealtimeChart.kt` aggiunge `Modifier.pointerInput` con `detectTransformGestures` (pinch) + `detectDragGestures` (pan) + `detectTapGestures` (crosshair toggle).
|
|
- State:
|
|
- `viewportOffsetMs: Long` — pan offset history
|
|
- `viewportScale: Float` — zoom factor on xWindowSeconds
|
|
- `mode: ViewportMode { Following, Frozen, History(offsetMs) }`
|
|
- `crosshair: CrosshairState?` — pixel position + projected (timestampMs, value) per ogni signal
|
|
- Inverse projection helper (pixel → data) per tooltip
|
|
- Agent: ui-impl + compose-guide
|
|
- Verify:
|
|
- pinch zoom restringe/allarga xWindowSeconds
|
|
- drag pan sposta viewport (mode Frozen automatically)
|
|
- tap mostra crosshair con valori istantanei
|
|
- tap su crosshair attivo → dismiss
|
|
- swipe a destra estrema → mode Following (auto-scroll resumes)
|
|
- New tests: `ChartInteractionStateTest`, integration test composable
|
|
|
|
### T8 — Cache Stroke + TextStyle + TextLayoutResult per zero-alloc reale
|
|
- Files:
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt:50` — `Stroke` cached per signal
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:54-131` — `TextStyle` hoisted in `remember`, label `TextLayoutResult` cached via `remember(label, style)`
|
|
- Action: introduce `internal class FrameCaches` holding stroke per signal + label LRU.
|
|
- Agent: compose-guide + mobile-performance-reviewer
|
|
- Verify: allocation tracker show < 1 KB/s steady-state at 30fps, 4 signals
|
|
- Pattern: [TextMeasurer LRU - composables.com](https://composables.com/jetpack-compose/androidx.compose.ui/ui-text/classes/TextMeasurer/api), [Modifier.drawWithCache - Android Devs](https://developer.android.com/develop/ui/compose/graphics/draw/overview)
|
|
|
|
### T9 — Replace Pair<Float,Float> return + Map iteration alloc
|
|
- Files:
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:144-152` — `resolveYRange` riempie `FloatArray(2)` out param
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:70,79,93` — Map iter via cached `signalsArray`
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt` — invalidate signalsArray su addSignal/removeSignal
|
|
- Agent: mobile-performance-reviewer + data-impl
|
|
- Verify: Map iter allocation count = 0 in macrobench
|
|
|
|
### T10 — clear() / reset() su RealtimeChartState
|
|
- File: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt`
|
|
- Goal: `fun clear()` purge tutti i buffer + reset dataVersion + reset resolvedT0Ms. Public API.
|
|
- Agent: data-impl
|
|
- Verify: test `clearResetsState`
|
|
|
|
---
|
|
|
|
## Acceptance v0.5.0
|
|
|
|
- A1: `LodStrategy` interface + `MinMaxLttb` default, perf ≥ MIN_MAX puro su 100k+ samples
|
|
- A2: `SignalRenderer` interface, default `LineSignalRenderer`, user custom renderer impl-able in 50 LoC
|
|
- A3: `AxisFormatter` interface con 4 default impls (Time, Decimal, DateTime, Unit)
|
|
- A4: ChartConfig split, FrameRate sealed, deprecated old API con ReplaceWith
|
|
- A5: Compose compiler report: 0 unstable public types
|
|
- A6: Interaction layer: pinch zoom + drag pan + crosshair tap funzionanti
|
|
- A7: Bisect snapshot taglia copy 30x su 10s window in 5min buffer (macrobench)
|
|
- A8: Render zero-alloc reale (< 1 KB/s in macrobench steady)
|
|
- A9: `RealtimeChartState.clear()` API + test
|
|
|
|
## Parallelism
|
|
| Group | Tasks |
|
|
|-------|-------|
|
|
| P0 | T1, T6, T10 (parallel) |
|
|
| P1 | T2, T3, T4 (interfaces parallel) |
|
|
| P2 | T5 (config split, depends on T2-T4 interfaces) |
|
|
| P3 | T7 (interaction layer, biggest task) |
|
|
| P4 | T8, T9 (perf finishing) |
|
|
|
|
## Estimated agent calls
|
|
~14 agent calls. ~6-8 giorni full-time (with v2 deltas).
|
|
|
|
## Risks
|
|
- T2 (MinMaxLTTB): break LodMode enum API → migration required by consumers
|
|
- T7 (interaction): biggest scope, multi-modal touch testing manual
|
|
- T5 (config split): breaking API change → HARD BREAK per intake decision
|
|
- D1 (toolchain bump): Kotlin 2.1→2.3 + Compose 1.8→1.11 behavior surprises
|
|
- BCV 0.18.1 (Jul 2022 release age): agent verifies latest at apply, fallback 0.16.3
|
|
- AGP 9.2.0 may need Gradle wrapper bump
|
|
|
|
## Verification commands
|
|
```bash
|
|
./gradlew :chart-realtime:apiDump
|
|
./gradlew :chart-realtime:apiCheck
|
|
./gradlew :chart-realtime:iosSimulatorArm64Test
|
|
./gradlew :app:installDebug # manual interaction test
|
|
./gradlew :chart-realtime:assembleRelease
|
|
./gradlew :chart-realtime:publishToMavenLocal --dry-run
|
|
```
|
|
|
|
---
|
|
|
|
## v2 deltas (intake-confirmed 2026-05-21)
|
|
|
|
Source: kmp-manager intake. Applied directly by manager (mechanical scope additions, no architectural change).
|
|
|
|
### Decisions locked
|
|
- **Breaking API**: HARD BREAK on T2 (`LodMode` removed) + T5 (old `ChartConfig` ctor + `targetFps` removed). No `@Deprecated` bridges. ABI baseline regen.
|
|
- **Versioning**: stay `0.5.0-SNAPSHOT` at close. No tag. No publish.
|
|
- **Datetime**: drop `kotlinx-datetime` entirely. Use `kotlin.time.Clock.System.now().toEpochMilliseconds()` (stable since Kotlin 2.3.0, no `@OptIn`).
|
|
- **Toolchain**: full alignment — Kotlin 2.3.21, Compose-MP 1.11.0, AGP 9.2.0, coroutines + kotlin-test aligned, BCV verify-latest.
|
|
|
|
### New tasks
|
|
|
|
#### D1 — Toolchain bump (P0, blocks all)
|
|
- Files:
|
|
- `gradle/libs.versions.toml`
|
|
- `gradle/wrapper/gradle-wrapper.properties` (if Gradle bump needed for AGP 9.2.0)
|
|
- `chart-realtime/build.gradle.kts` (drop `kotlinx-datetime` dep)
|
|
- `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:16,90` (swap `kotlinx.datetime.Clock` → `kotlin.time.Clock`)
|
|
- Goal: Kotlin 2.1.0→2.3.21, Compose-MP 1.8.0→1.11.0, AGP 8.7.3→9.2.0, coroutines 1.9.0→latest, kotlin-test→2.3.21, BCV→verify-latest (fallback 0.16.3). Drop kotlinx-datetime. Single call site swap.
|
|
- Agent: gradle-expert
|
|
- Verify:
|
|
- `./gradlew :chart-realtime:compileKotlinAndroid` green
|
|
- `./gradlew :chart-realtime:iosSimulatorArm64Test` 107 tests pass (regression gate)
|
|
- `./gradlew :chart-realtime:apiCheck` green (no ABI delta from toolchain alone)
|
|
- `./gradlew :chart-realtime:assembleRelease` green
|
|
|
|
#### D3 — LTTB upper-bound semantic fix (P5)
|
|
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt` (or post-T2 `lod/LttbLod.kt`)
|
|
- Goal: align LTTB upper-bound with snapshot half-open `[lo, hi)` semantic. Currently inclusive → off-by-one at window boundary.
|
|
- Agent: data-impl
|
|
- Verify: existing 33 LodDecimator tests pass + 1 new boundary test
|
|
|
|
#### D4 — NumberFormat Long overflow guard (P1)
|
|
- Files: `chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/NumberFormat.kt`
|
|
- Goal: `formatFixed` Long path overflows @ |v|≥1e19. Add guard → route to `formatScientific` before overflow.
|
|
- Agent: data-impl
|
|
- Verify: new test `formatFixed_handlesLongOverflow` + existing 38 NumberFormat tests pass
|
|
|
|
### Task semantic changes
|
|
|
|
#### T2 (was: deprecated bridges) → HARD BREAK
|
|
- Remove `LodMode` enum entirely (was: `ChartConfig.lodMode: LodMode` → `lodStrategy: LodStrategy`).
|
|
- No `LodMode.MIN_MAX_LTTB` shorthand. Pure interface.
|
|
- ABI baseline regen: `./gradlew :chart-realtime:apiDump` + commit.
|
|
|
|
#### T5 (was: deprecated old ctor) → HARD BREAK
|
|
- Remove `ChartConfig` original ctor entirely. Replace with new ctor taking `DataConfig + AxisConfig + RenderConfig`.
|
|
- Remove `targetFps: Int?` field. Replaced by `RenderConfig.frameRate: FrameRate`.
|
|
- No `@Deprecated` bridges.
|
|
- ABI baseline regen.
|
|
|
|
#### T6 — verify SignalConfig stability covered
|
|
- Add `@Immutable` to `SignalConfig` (closes deferred v0.3.0 issue: Compose compiler unstable warning).
|
|
|
|
### Updated P-groups
|
|
| Group | Tasks |
|
|
|-------|-------|
|
|
| P0 | D1 (toolchain, blocks all) |
|
|
| P1 | T1, T6, T10, D4 |
|
|
| P2 | T2, T3, T4 |
|
|
| P3 | T5 |
|
|
| P4 | T7 |
|
|
| P5 | T8, T9, D3 |
|
|
| P6 | ABI regen + apiCheck + unify |
|
|
|
|
### Acceptance additions
|
|
- A10: Kotlin 2.3.21 + Compose-MP 1.11.0 + AGP 9.2.0 green on all targets; 107 tests pass on iosSimulatorArm64.
|
|
- A11: ABI baseline regenerated post-T2/T5; `chart-realtime.api` + `chart-realtime.klib.api` committed.
|
|
- A12: `LodMode` symbol absent, `targetFps` symbol absent, `kotlinx-datetime` dep absent. No `@Deprecated` bridges in commonMain public API.
|