KMPCharts/.paul/phases/v0.5.0-architecture-plan.md
Trentin Davide 0e3b79e2da feat(chart): v0.4.0 portfolio hardening
Ship 15-task plan via kmp-manager group-by-group flow. Library moves
from "prototype" to "senior-engineer-built KMP library" per 3-reviewer
audit (android-reviewer + mobile-performance-reviewer + mobile-architect).

Critical fixes:
- LICENSE MIT (T1) — legal blocker resolved
- explicitApi() Strict + BCV 0.16.3 + klib enabled (T2) — ABI locked
  baseline: chart-realtime.api 161 LOC + chart-realtime.klib.api 211 LOC
- Internal lockdown: CircularBuffer, TieredBuffer, LodDecimator,
  dataVersion, rememberFrameTick (T3)
- kotlinx-datetime 0.6.2 replaces expect/actual currentTimeMs (T4)
  Platform.kt + .android.kt + .ios.kt deleted
- CircularBuffer.writeIndex Int -> Long (T5) — fix 124d overflow @ 200Hz
- Remove !! and @Suppress("UNUSED_EXPRESSION") from commonMain (T6)
- Remove dead config: maxBufferSeconds, xLabelDecimals,
  SignalConfig.label (T7)

Correctness:
- POSITIVE_INFINITY/Float.MAX_VALUE sentinels -> hasData flag (T8)
- Snapshot.withMutableSnapshot + SnapshotApplyConflictException retry
  in push/addSignal/removeSignal (T9)
- dataVersion mutableLongStateOf — Compose-observable, idle = 0
  recompositions (T10) — RenderLoop.kt deleted, targetFps @Deprecated
- Single buffer.snapshot() per signal per frame via SignalEntry
  per-signal scratch arrays (T11) — render-time snapshot cost halved
- M4 binning Tier1/Tier2: firstTs/V + minTs/V + maxTs/V + lastTs/V
  with chronological dedup (T12) — no C7 vertical spike artifacts.
  TIER1/TIER2 CAPACITY x2 -> x4 (TOTAL 77.4k -> 94.8k)
- NaN/Inf/backward-timestamp guards on push (T13) — silent drop,
  per-signal lastPushedTs tracking, KDoc contract

Test gap fill:
- NumberFormat extracted from AxisRenderer to internal object (T14)
  +38 tests pinning rounding/scientific/time-format behavior
- TieredBufferTest +15 tests: tier roll, window-crossing, clear,
  edge cases (T15)
- LodDecimatorTest +10 LTTB-specific branch coverage tests (T15)

Test count: 33 -> 107 (+74).
ABI: 211/272 -> 161/211 LOC (locked, baseline committed).

Deferred to v0.5.0:
- Bump kotlinx-datetime 0.6.2 -> 0.7.x (needs Kotlin 2.1.20+)
- Remove ChartConfig.targetFps (currently @Deprecated, ignored)
- NumberFormat Long overflow @ 1e19 (safe via current routing)
- LTTB upper-bound inclusive vs snapshot half-open semantic mismatch
- Macrobench setup for -30% frame-time verification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:27:15 +02:00

8.7 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): 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.ktlod/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: LodModelodStrategy: 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, 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. 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:50Stroke cached per signal
    • chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/AxisRenderer.kt:54-131TextStyle 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, 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-152resolveYRange 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

~12 agent calls. ~5-7 giorni full-time.

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, mitigare con typealias + deprecated factory

Verification commands

./gradlew :chart-realtime:apiCheck
./gradlew :chart-realtime:iosSimulatorArm64Test
./gradlew :app:installDebug   # manual interaction test
./gradlew :chart-realtime:assembleRelease