KMPCharts/.paul/STATE.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

179 lines
11 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 (NEXT)
Phase: 0 of N — Planned, not started
Plan: .paul/phases/v0.5.0-architecture-plan.md
Status: Awaiting kickoff
Last completed: v0.4.0 portfolio hardening (2026-05-21)
Last activity: 2026-05-21 — v0.4.0 closed via kmp-manager group-by-group flow. 8 P-groups executed. 15 plan tasks done. 107 tests pass (was 33). ABI lockdown active. Compose snapshot thread-safe.
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
## Loop Position
```
PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [v0.4.0 portfolio hardening 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 |
| 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*