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

181 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<Map>; 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 | S | v0.5.0 (currently safe via routing) |
| 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*