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>
11 KiB
11 KiB
v0.4.0 — Portfolio Hardening Plan
Source
Verdict consolidato 3 reviewer (android-reviewer + mobile-performance-reviewer + mobile-architect) 2026-05-21. Library oggi = prototipo solido NON portfolio-grade. Stima totale: ~10-12 giorni full-time.
Goal
Portare chart-realtime da "promising prototype" a "senior-engineer-built KMP library". Fix issue critiche, lockdown ABI, correttezza thread-safety, zero-alloc render reale.
Scope IN (Fase 1 + 2 del plan completo)
- Hygiene base: LICENSE, explicitApi, binary-compatibility-validator, dead code removal
- Correttezza: Compose snapshot thread safety, M4 binning, single-snapshot per frame, NaN/Inf guards
- Test coverage: AxisRenderer format helpers + TieredBuffer + LTTB
Scope OUT
- Interaction layer (touch/zoom/pan) → v0.5.0
- MinMaxLTTB algorithm swap → v0.5.0
- Marketing (Maven Central, screenshots, CI badges, demo iOS) → v1.0.0
Tasks
T1 — LICENSE (S0 blocker)
- File:
/Users/henesis/dev/dav/KMPCharts/LICENSE - Action: scegli MIT o Apache 2.0. Aggiungi file root + reference in README +
chart-realtime/build.gradle.ktspom config (per Maven publish futuro). - Agent: manager direct (10 LoC)
- Verify:
ls LICENSE+ git tracked
T2 — explicitApi + binary-compatibility-validator
- Files:
chart-realtime/build.gradle.kts, rootbuild.gradle.kts,gradle/libs.versions.toml - Action:
- Aggiungi
kotlin { explicitApi() }achart-realtime/build.gradle.kts - Plugin
org.jetbrains.kotlinx.binary-compatibility-validatorversione corrente - Config
apiValidation { @OptIn(kotlinx.validation.ExperimentalBCVApi::class) klib { enabled = true } } - Commit
chart-realtime/api/chart-realtime.apidump
- Aggiungi
- Agent: gradle-expert
- Verify:
./gradlew :chart-realtime:apiDump+ check committed file - Pattern: RevenueCat KMP example
T3 — Internal lockdown (6 leaked symbols)
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:33→dataVersioninternalchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt→ class internalchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/TieredBuffer.kt→ class + companion internalchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/LodDecimator.kt→ class internalchart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/RenderLoop.kt:16→rememberFrameTickinternal
- Action: cambia visibility a
internal. Fixare call siteRealtimeChart.kt:51-54che usaTieredBuffer.TOTAL_CAPACITY(resta accessibile da stesso modulo). - Agent: kmp-architect
- Verify:
./gradlew :chart-realtime:apiCheck+ compile green
T4 — Drop expect/actual currentTimeMs → kotlinx-datetime
- Files:
- Delete:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/Platform.kt - Delete:
chart-realtime/src/androidMain/kotlin/dev/dtrentin/chart/Platform.android.kt - Delete:
chart-realtime/src/iosMain/kotlin/dev/dtrentin/chart/Platform.ios.kt - Modify:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:64(callsite) - Modify:
chart-realtime/build.gradle.ktsadd kotlinx-datetime dep - Modify:
gradle/libs.versions.tomladd kotlinx-datetime ≥ 0.7.0
- Delete:
- Action: import
kotlinx.datetime.Clock. Replace callsite conClock.System.now().toEpochMilliseconds(). - Agent: kmp-architect
- Verify:
compileKotlinIosSimulatorArm64+compileDebugKotlinAndroidgreen - Pattern: kotlinx-datetime GitHub
T5 — CircularBuffer.writeIndex: Int → Long (overflow critico dopo 124 giorni @ 200Hz)
- File:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/CircularBuffer.kt:15-46 - Action:
writeIndex: Int → Long- Aggiorna call site
(writeIndex % capacity).toInt()per index access - Update
snapshotboundary mathcurrentWrite - capacity
- Agent: data-impl
- Verify: nuovo test
writeIndexSurvivesIntMaxValuein CircularBufferTest
T6 — Rimuovi @Suppress("UNUSED_EXPRESSION") frameTick + !!
- File:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:50,59,63,90,103 - Action:
- Linea 59: delete (recomposition già triggata da line 41 read)
- Linea 63: aggiungi
val t0 = state.resolvedT0Ms ?: return@Canvas - Linea 90: replace
state.resolvedT0Ms!!cont0capturato - Linea 50/103: replace
mutableLongStateOfconremember { longArrayOf(-1L) }→ versionHolder[0] (no snapshot write in draw)
- Agent: compose-guide
- Verify: compose preview funziona + 33 test pass
T7 — Remove dead config
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt:18→maxBufferSeconds(ignored by TieredBuffer)chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/ChartConfig.kt:24→xLabelDecimals(ignored by formatTimeSec)chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/model/SignalConfig.kt:17→label(never used)
- Action: rimuovi i 3 field. Update README dove menzionano.
- Agent: kmp-architect
- Verify: compile green + test green
T8 — Replace POSITIVE_INFINITY sentinel
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:77-87chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/TieredBuffer.kt:35,54
- Action: introdurre
var hasData = falseflag invece di confronto float-equality sentinel. - Agent: data-impl
- Verify: test esistenti pass + nuovo test
tier1BinAcceptsFloatMaxValueSample
T9 — Compose snapshot thread safety
- File:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:42-66 - Action: wrap
push,addSignal,removeSignalbody inSnapshot.withMutableSnapshot { }. CatchSnapshotApplyConflictExceptioncon retry loop. - Alt: replace
_signals: MutableState<Map>conmutableLongStateOfversione bumped on mutation + plain volatile map → eliminate snapshot writes from background thread. - Agent: kmp-architect + compose-guide (paired review)
- Verify: stress test multi-producer (2+ thread call push 200Hz). Aggiungere
pushFromBackgroundThreadIsSafetest. - Pattern: Zach Klipp Snapshot guide
T10 — dataVersion → mutableLongStateOf (drop frameTick dependency)
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:33chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:41,60chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/RenderLoop.kt→ ridurre frameTick a heartbeat opzionale o eliminarlo
- Action:
dataVersioncomemutableLongStateOf(0L). Canvas reads diventano Compose-observable. Recomposition data-driven, no più dipendenza da rememberFrameTick polling. - Agent: compose-guide
- Verify: idle behavior — no new data = no recomposition. Use Layout Inspector recomposition count.
T11 — Single snapshot per signal per frame
- Files:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChart.kt:79-86chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/render/SignalRenderer.kt:30
- Action: snapshot una sola volta per signal in RealtimeChart, calcola dataMin/dataMax nello stesso loop, passa
countadrawSignal(rimuovi snapshot interno). - Agent: kmp-architect
- Verify: macrobench frame time -30% con 4 signals
T12 — M4 binning per Tier1/Tier2 (fix artefatti visivi C7)
- File:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/buffer/TieredBuffer.kt:30-66 - Action: per bin store
(firstTs, firstV, lastTs, lastV, minV, maxV)invece di doppio push(midTs, min), (midTs, max). Rispetta ordine cronologico downstream. - Agent: data-impl
- Verify: nuovi test
tier1BinPreservesM4,tier2BinPreservesM4,noVerticalSpikesAtBinBoundaries - Pattern: M4 paper / MinMaxLTTB arXiv 2305.00332
T13 — NaN/Inf/backward-timestamp guards
- File:
chart-realtime/src/commonMain/kotlin/dev/dtrentin/chart/RealtimeChartState.kt:52 - Action:
pushrifiuta!value.isFinite()(early return)pushrifiutatimestampMs < lastPushedTsper signal (drop or exposePushResultsealed)- KDoc contratto: "Samples must have finite value and monotonic non-decreasing timestamps"
- Agent: data-impl
- Verify: test
pushRejectsNaN,pushRejectsBackwardTimestamp
T14 — Test gap: AxisRenderer format helpers
- File new:
chart-realtime/src/commonTest/kotlin/dev/dtrentin/chart/render/NumberFormatTest.kt - Refactor: extract
formatFixed,formatScientific,formatTimeSec,niceInterval,pow10daAxisRendererprivato →internal object NumberFormat. - Action: 20+ edge case test (0.999 rounding, 1.005 banker's, 99.999 carry, 1e19 Long overflow, negative, NaN, very large exp, time -65s, time 3700s)
- Agent: unit-test-writer
- Verify:
iosSimulatorArm64Testcount passa da 33 a 50+
T15 — Test gap: TieredBuffer + LodDecimator LTTB branch
- Files new:
chart-realtime/src/commonTest/kotlin/dev/dtrentin/chart/buffer/TieredBufferTest.kt- Extend
LodDecimatorTest.ktcon LTTB branch coverage
- Action: 10+ test su tier roll, bin flush, snapshot window crossing 3 tier boundaries, clear()
- Agent: unit-test-writer
- Verify: coverage TieredBuffer ≥80%
Acceptance v0.4.0
- A1:
./gradlew :chart-realtime:apiCheckgreen con .api dump committato - A2:
kotlin { explicitApi() }attivo, nessun warning visibility - A3: LICENSE presente, riferito in README + Gradle publish config
- A4: Tutti i symbol "leaked" sono
internal(CircularBuffer, TieredBuffer, LodDecimator, dataVersion, rememberFrameTick, capacity constants) - A5: kotlinx-datetime sostituisce expect/actual currentTimeMs (3 file cancellati)
- A6: 0
!!non null assertion in commonMain - A7: 0
@Suppressmagic-fix annotations - A8: Multi-producer push test green (50 thread × 100 push each, no lost samples, no race exceptions)
- A9: Macrobench frame time -30% post T11 (single snapshot)
- A10: iosSimulatorArm64Test ≥ 60 test (da 33)
- A11: Compose snapshot conformance: writes da background thread sempre via
withMutableSnapshot - A12: NaN/Inf samples rifiutati, backward timestamps droppati
- A13: TieredBuffer M4 binning, no vertical spike artifacts
- A14: README aggiornato — rimuovi dead config refs, claim "zero GC at render" qualificato
Parallelism
| Group | Tasks |
|---|---|
| P0 | T1, T7 (parallel, file-isolated) |
| P1 | T2 (Gradle config) |
| P2 | T3, T4 (parallel: visibility + kotlinx-datetime in disjoint files) |
| P3 | T5, T8 (buffer fixes parallel) |
| P4 | T6, T9, T10 (compose layer) |
| P5 | T11, T12 (render perf + M4) |
| P6 | T13 (input validation) |
| P7 | T14, T15 (test gap fill, parallel) |
Estimated agent calls
~15 agent calls. ~3-4 giorni full-time.
Risks
- T9 (snapshot thread safety) may surface API changes — minor breaking
- T12 (M4 binning) requires LodDecimator MIN_MAX consumer update — coupled
- T10 may remove
rememberFrameTickpublic API — breaking, mitigate via @Deprecated
Verification commands (consolidated)
./gradlew :chart-realtime:apiCheck
./gradlew :chart-realtime:compileKotlinIosX64 :chart-realtime:compileKotlinIosArm64 :chart-realtime:compileKotlinIosSimulatorArm64
./gradlew :chart-realtime:iosSimulatorArm64Test
./gradlew :chart-realtime:assembleRelease
./gradlew :chart-realtime:assembleChartRealtimeXCFramework