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>
13 KiB
13 KiB
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): Intbisect 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.ktchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLttb.ktchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/lod/MinMaxLod.ktchart-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:
LodModeenum →LodStrategyinterface.ChartConfig.lodMode: LodMode→lodStrategy: LodStrategy. Backward compat:LodMode.MIN_MAX_LTTBshorthand. - Agent: kmp-architect + data-impl
- Verify: nuovi test
MinMaxLttbVisualFidelityTest, perf ≥ MIN_MAX puro su large datasets - Pattern: MinMaxLTTB arXiv 2305.00332, Rust ref impl
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. DefaultLineSignalRenderer. SignalConfig.renderer: SignalRenderer = LineSignalRenderer(per-signal opt-in)- Agent: kmp-architect
- Verify: existing rendering unchanged, plus
customRendererPlugsIntest
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
ChartConfigctor@Deprecated(level = WARNING)conReplaceWith. - 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.ktRealtimeChartState.kt
- Action:
@Immutablesu data class + sealed root.@StablesuRealtimeChartState. - Agent: compose-guide
- Verify: Compose compiler report
chart-realtimemostra 0 unstable classes nel public API../gradlew :chart-realtime:compileReleaseKotlinAndroid -Pkotlinx.compose.reportetc.
T7 — Interaction layer S0 (touch/zoom/pan/crosshair)
- New files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/ChartInteractionState.ktchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/interaction/InteractionConfig.kt
- Modify:
RealtimeChart.ktaggiungeModifier.pointerInputcondetectTransformGestures(pinch) +detectDragGestures(pan) +detectTapGestures(crosshair toggle). - State:
viewportOffsetMs: Long— pan offset historyviewportScale: Float— zoom factor on xWindowSecondsmode: 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—Strokecached per signalchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:54-131—TextStylehoisted inremember, labelTextLayoutResultcached viaremember(label, style)
- Action: introduce
internal class FrameCachesholding 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, Modifier.drawWithCache - Android Devs
T9 — Replace Pair<Float,Float> return + Map iteration alloc
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:144-152—resolveYRangeriempieFloatArray(2)out paramchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:70,79,93— Map iter via cachedsignalsArraychart-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:
LodStrategyinterface +MinMaxLttbdefault, perf ≥ MIN_MAX puro su 100k+ samples - A2:
SignalRendererinterface, defaultLineSignalRenderer, user custom renderer impl-able in 50 LoC - A3:
AxisFormatterinterface 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
./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 (
LodModeremoved) + T5 (oldChartConfigctor +targetFpsremoved). No@Deprecatedbridges. ABI baseline regen. - Versioning: stay
0.5.0-SNAPSHOTat close. No tag. No publish. - Datetime: drop
kotlinx-datetimeentirely. Usekotlin.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.tomlgradle/wrapper/gradle-wrapper.properties(if Gradle bump needed for AGP 9.2.0)chart-realtime/build.gradle.kts(dropkotlinx-datetimedep)chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:16,90(swapkotlinx.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:compileKotlinAndroidgreen./gradlew :chart-realtime:iosSimulatorArm64Test107 tests pass (regression gate)./gradlew :chart-realtime:apiCheckgreen (no ABI delta from toolchain alone)./gradlew :chart-realtime:assembleReleasegreen
D3 — LTTB upper-bound semantic fix (P5)
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt(or post-T2lod/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:
formatFixedLong path overflows @ |v|≥1e19. Add guard → route toformatScientificbefore overflow. - Agent: data-impl
- Verify: new test
formatFixed_handlesLongOverflow+ existing 38 NumberFormat tests pass
Task semantic changes
T2 (was: deprecated bridges) → HARD BREAK
- Remove
LodModeenum entirely (was:ChartConfig.lodMode: LodMode→lodStrategy: LodStrategy). - No
LodMode.MIN_MAX_LTTBshorthand. Pure interface. - ABI baseline regen:
./gradlew :chart-realtime:apiDump+ commit.
T5 (was: deprecated old ctor) → HARD BREAK
- Remove
ChartConfigoriginal ctor entirely. Replace with new ctor takingDataConfig + AxisConfig + RenderConfig. - Remove
targetFps: Int?field. Replaced byRenderConfig.frameRate: FrameRate. - No
@Deprecatedbridges. - ABI baseline regen.
T6 — verify SignalConfig stability covered
- Add
@ImmutabletoSignalConfig(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.apicommitted. - A12:
LodModesymbol absent,targetFpssymbol absent,kotlinx-datetimedep absent. No@Deprecatedbridges in commonMain public API.