KMPCharts/.paul/STATE.md
Trentin Davide af6814e2b8 feat(chart): v0.5.0 architecture + interaction
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>
2026-05-22 00:05:43 +02:00

12 KiB
Raw Blame History

Project State

Project Reference

See: .paul/PROJECT.md (updated 2026-05-21)

Core value: Android devs plot 200Hz+ multi-signal sensor data without frame drops Current focus: v0.4.0 portfolio hardening SHIPPED — next v0.5.0 architecture + interaction

Current Position

Milestone: v0.5.0-architecture (SHIPPED ✓) Phase: All 6 P-groups complete. ABI regenerated. Unify green. Plan: .paul/phases/v0.5.0-architecture-plan.md (v2 deltas applied 2026-05-21) Status: v0.5.0 closed. Next milestone v1.0.0 public release. Last completed: v0.5.0 architecture + interaction (2026-05-21) Last activity: 2026-05-21 — v0.5.0 SHIPPED via kmp-manager 6-phase flow. 14 agent calls. 13 plan tasks done (D1, T1-T10, D3, D4). 107 → 207 tests pass on iosSimulatorArm64 (+100). ABI baseline regen: 161/211 → 428/501 LOC. HARD BREAK on LodMode/LodDecimator/targetFps. Interaction layer (pinch/pan/crosshair) shipped. MinMaxLTTB SOTA 1.80× faster than pure LTTB. All toolchain bumps (Kotlin 2.3.21, Compose-MP 1.11.0, AGP 9.2.0, Gradle 9.5.1). kotlinx-datetime dropped (kotlin.time.Clock). iosX64 dropped (Compose-MP 1.11.0 incompat).

Progress:

  • v0.1.0 milestone: [██████████] 100% SHIPPED
  • v0.2.0 milestone: [██████████] 100% SHIPPED
  • v0.3.0 milestone: [██████████] 100% SHIPPED
  • v0.4.0 milestone: [██████████] 100% SHIPPED
  • v0.5.0 milestone: [██████████] 100% SHIPPED

Loop Position

PLAN ──▶ APPLY ──▶ UNIFY
  ✓        ✓        ✓     [v0.5.0 architecture + interaction SHIPPED — v0.5.0-SNAPSHOT]

Accumulated Context

Decisions

Decision Phase Impact
Canvas custom rendering (no lib) Init Core architecture
Render/data decoupled withFrameNanos Init RenderLoop design
TieredBuffer over flat ring v0.2.0 O(N×constant) memory, 20+ signals
MIN_MAX tier binning (not MEAN) v0.2.0 Composable, no artifacts at tier boundaries
MEAN removed from LodMode v0.2.0 Cleaner API, no synthetic values
LodDecimator as class (pre-alloc scratch) v0.2.0 Zero GC at render time
Sensor cap 200Hz (5_000μs) v0.2.0 Thermal/battery optimization
Package dev.dtrentin v0.2.0 Personal project, not company
iOS targets shipped v0.3.0 iosX64/iosArm64/iosSimulatorArm64 + Platform.ios.kt (iosX64 dropped in v0.5.0 D1)
Drop iosX64 target v0.5.0 D1 Compose-MP 1.11.0 no ios_x64 variant; Apple deprecated Intel Mac sims Xcode 15+; iosArm64 + iosSimulatorArm64 cover modern devs
iOS build pipeline green v0.3.0 iOS 33 tests pass on iOS sim, XCFramework "ChartRealtime" produced
timeIntervalSinceReferenceDate + epoch offset v0.3.0 iOS K/Native binding doesn't expose timeIntervalSince1970; use 978_307_200s constant
applyDefaultHierarchyTemplate() explicit v0.3.0 iOS iosMain/iosTest auto-wired across 3 iOS targets
Manual format helpers in AxisRenderer v0.3.0 iOS String.format JVM-only; private formatFixed/formatScientific/pow10
kotlin.concurrent.Volatile + kotlin.jvm.JvmName imports v0.3.0 iOS Multiplatform replacements for JVM-default annotations
MIT License v0.4.0 T1 Personal project, permissive, no patent grant required
explicitApi() Strict + BCV 0.16.3 + klib enabled v0.4.0 T2 ABI lockdown active; chart-realtime.api 161 LOC + .klib.api 211 LOC baseline
kotlinx-datetime 0.6.2 (NOT 0.7.x) v0.4.0 T4 Kotlin 2.1.0 compat; bump to 0.7.x when Kotlin → 2.1.20+ in v0.5.0
CircularBuffer.writeIndex Long v0.4.0 T5 Overflow 124d @ 200Hz fixed
hasData boolean flag v0.4.0 T8 Replace Float.MAX_VALUE/POSITIVE_INFINITY sentinels in TieredBuffer + RealtimeChart
_signals as @Volatile copy-on-write Map v0.4.0 T9 Replace MutableState; eliminate snapshot writes from data thread for collection
dataVersion as mutableLongStateOf v0.4.0 T10 Compose-observable; drives recomposition data-driven; idle = 0 recompositions
applySnapshot retry helper (Snapshot.withMutableSnapshot + SnapshotApplyConflictException) v0.4.0 T9 Thread-safe push from background thread
RenderLoop.kt deleted v0.4.0 T6/T10 rememberFrameTick removed; targetFps deprecated (KDoc + @Deprecated)
Per-signal scratch arrays in SignalEntry v0.4.0 T11 Single buffer snapshot per signal per frame (was 2); ~1.14 MB/signal
M4 binning (firstTs/V, minTs/V, maxTs/V, lastTs/V) v0.4.0 T12 Replace double-push min/max sentinel; TIER1/2 CAPACITY *4; no vertical spikes at bin boundaries
NaN/Inf/backward-timestamp guards on push v0.4.0 T13 Silent drop; KDoc contract added; per-signal lastPushedTs tracking
NumberFormat internal object v0.4.0 T14 Extracted from AxisRenderer for testability; 38 tests pinned behavior

Deferred Issues

Issue Origin Effort Revisit
Dirty flag render (skip frame if no new data) v0.2.0 S ✓ v0.3.0
targetFps=30 default v0.2.0 S ✓ v0.3.0
iOS expect/actual beyond currentTimeMs Init L ✓ v0.3.0
KDoc on public API surface v0.2.0 M ✓ v0.3.0
README with integration example v0.2.0 S ✓ v0.3.0
iOS build/test pipeline (compile + iosTest + XCFramework) v0.3.0 plan M ✓ v0.3.0 iOS
Publish to Maven Central / GitHub Packages v0.2.0 M v1.0.0
iOS demo app (SwiftUI) v0.3.0 iOS L v1.0.0
CI workflow for iOS build (GitHub Actions) v0.3.0 iOS M v1.0.0
Compose compiler unstable warning on SignalConfig v0.3.0 iOS S v0.5.0
LICENSE missing v0.4.0 plan S ✓ v0.4.0 T1 (MIT)
explicitApi + BCV v0.4.0 plan M ✓ v0.4.0 T2
Internal lockdown 6 leaked symbols v0.4.0 plan M ✓ v0.4.0 T3
Drop expect/actual currentTimeMs v0.4.0 plan S ✓ v0.4.0 T4
CircularBuffer.writeIndex Long v0.4.0 plan S ✓ v0.4.0 T5
@Suppress + !! cleanup v0.4.0 plan S ✓ v0.4.0 T6
Dead config removal v0.4.0 plan S ✓ v0.4.0 T7
POSITIVE_INFINITY sentinel replace v0.4.0 plan S ✓ v0.4.0 T8
Compose snapshot thread safety v0.4.0 plan M ✓ v0.4.0 T9
dataVersion mutableLongStateOf v0.4.0 plan M ✓ v0.4.0 T10
Single snapshot per signal per frame v0.4.0 plan M ✓ v0.4.0 T11
M4 binning Tier1/Tier2 v0.4.0 plan M ✓ v0.4.0 T12
NaN/Inf/backward-timestamp guards v0.4.0 plan S ✓ v0.4.0 T13
AxisRenderer format helpers test v0.4.0 plan M ✓ v0.4.0 T14 (38 tests)
TieredBuffer + LTTB test gap v0.4.0 plan M ✓ v0.4.0 T15 (25 tests)
Bump kotlinx-datetime 0.6.2 → 0.7.x v0.4.0 T4 S v0.5.0 (needs Kotlin 2.1.20+)
Long overflow at value ≥ 1e19 in NumberFormat.formatFixed v0.4.0 T14
LTTB upper bound inclusive vs snapshot half-open semantic mismatch v0.4.0 T15 S v0.5.0
Remove ChartConfig.targetFps (now @Deprecated, ignored) v0.4.0 T10 S v0.5.0
Memory budget: ~1.14 MB/signal post-CAPACITY *4 (was ~760 KB pre-bump) v0.4.0 T12 doc document in README post-v0.4.0

Reviewer Audit Findings (2026-05-21)

Source: 3 parallel reviewer agents (android-reviewer + mobile-performance-reviewer + mobile-architect).

S0 (block portfolio) findings:

  • LICENSE missing → legalmente nessuno può usare la lib
  • Compose snapshot violation: push() mutate MutableState da thread arbitrario senza withMutableSnapshot
  • dataVersion @Volatile NON è Compose-observable, recomposition driven solo da frameTick side-effect
  • Render NON zero-alloc: Pair + Stroke + TextLayoutResult + Strings per tick per frame (~50-100 KB/s)
  • Snapshot doppia per signal per frame (8x lavoro su 4 segnali)
  • No interaction layer (touch/zoom/pan) → fatale per "real-time chart" piece

S1 (visibili) findings:

  • CircularBuffer.writeIndex Int overflow dopo 124 giorni @ 200Hz
  • Tier1/Tier2 binning C7 artifacts (push doppio min poi max stesso ts)
  • 6 leaked internal symbols pubblici (CircularBuffer, TieredBuffer, LodDecimator, dataVersion, rememberFrameTick, capacity constants)
  • 3 dead config field (maxBufferSeconds, xLabelDecimals, SignalConfig.label)
  • @Suppress("UNUSED_EXPRESSION") hack + !! non-null assertion in render hot path
  • Test gap: AxisRenderer format helpers untested + buggy (Q4 formatFixed Long overflow), TieredBuffer untested (143 LOC), LTTB branch untested
  • expect/actual currentTimeMs over-engineered — kotlinx-datetime 0.7.0+ replaces it
  • iOS targets build ma README + samples Android-only

Claims aggiustati:

  • "Zero GC at render time" → falso oggi, vero post v0.5.0 T8/T9
  • "Lock-free SPSC ring buffer" → impreciso, fragile JMM su Native
  • "200Hz sustained" → plausibile JVM, borderline mid Android multi-signal pre-v0.5.0 T11
  • "1h buffer" → vero ma costa 720KB+ per signal (non documentato)

Posizionamento mercato: Nicchia genuinamente vuota: nessun KMP chart lib (Vico, KoalaPlot, AAY-chart, netguru) targeta real-time 200Hz multi-signal con bounded memory. Differentiation reale ma serve interaction layer per essere prendibile sul serio.

Blockers/Concerns

  • LICENSE missing → ✓ MIT shipped in v0.4.0 T1
  • API surface non lockdown → ✓ explicitApi + BCV shipped in v0.4.0 T2-T3
  • Compose snapshot thread safety → ✓ withMutableSnapshot + retry shipped in v0.4.0 T9
  • No interaction layer (touch/zoom/pan) → blocking "real-time chart" claim (v0.5.0 scope)
  • targetFps deprecated but not removed → minor noise in public API (v0.5.0)

Session Continuity

Last session: 2026-05-21 (v0.4.0 portfolio hardening shipped via kmp-manager group-by-group flow) Stopped at: v0.4.0 closed. 15 plan tasks done. 107 tests pass. Full verify suite green. Next action: kickoff v0.5.0 architecture + interaction layer Resume context:

  • Library: chart-realtime, dev.dtrentin:chart-realtime:0.5.0-SNAPSHOT (version unbumped post-v0.4.0; bump deferred to v1.0.0 release)
  • App: AndroidChartsApp consuming library, builds clean
  • Remote: ssh://git@3nt-git.duckdns.org:222/davide.trentin/KMPCharts.git
  • Package: dev.dtrentin.chart
  • iOS: 3 targets compile, link, test (107 tests). XCFramework name ChartRealtime, baseName CamelCase.
  • Local Mac setup: Xcode.app at /Applications/Xcode.app, iOS sim runtime installed (iPhone 17 family), local.properties points to ~/Library/Android/sdk, JDK 21 Temurin via JAVA_HOME.
  • ABI baseline committed: chart-realtime/api/chart-realtime.api (161 LOC) + chart-realtime.klib.api (211 LOC)
  • Plans: v0.4.0 ✓ SHIPPED · v0.5.0 + v1.0.0 PLANNED
  • Effort residuo verso v1.0.0: ~7-10 giorni full-time

v0.4.0 close summary (2026-05-21)

15 plan tasks executed across 8 P-groups via kmp-manager:

  • P0: T1 (LICENSE MIT) + T7 (dead config removal)
  • P1: T2 (explicitApi + BCV 0.16.3 + klib enabled)
  • P2: T3 (internal lockdown) + T4 (kotlinx-datetime 0.6.2)
  • P3: T5 (writeIndex Long) + T8 (hasData boolean flag)
  • P4: T6 (RealtimeChart cleanup) + T9 (snapshot thread safety) + T10 (mutableLongStateOf)
  • P5: T11 (single snapshot per signal) + T12 (M4 binning)
  • P6: T13 (NaN/Inf/backward guards)
  • P7: T14 (NumberFormat extract + 38 tests) + T15 (TieredBuffer/LTTB +25 tests)

Test count: 33 → 107 (+74). ABI: 211/272 → 161/211 LOC (locked, baseline committed). RenderLoop.kt deleted. Platform.kt + .android.kt + .ios.kt deleted. 0 !! assertions in commonMain. 0 @Suppress hacks. explicitApi Strict.

Deviations from plan:

  • T4 kotlinx-datetime 0.6.2 instead of 0.7.x (Kotlin 2.1.0 compat — 0.7.x needs Kotlin 2.1.20+). Bump deferred to v0.5.0.
  • T7 ChartConfig.targetFps NOT removed (would break ABI of data class). Marked @Deprecated, ignored at runtime. Full removal in v0.5.0.
  • T11 memory per signal ~1.14 MB (plan estimate was ~760 KB based on pre-bump TOTAL_CAPACITY). Acceptable.
  • T13 unknown-signal push no longer bumps dataVersion (was bumping in v0.3.0). Existing test push_unknownSignal_stillIncrementsDataVersion replaced with pushIgnoresUnknownSignalName per new contract.

STATE.md — Updated after every significant action