Conversation
Live time-series plots stutter because Trace2DLtd rescans the whole ring buffer (minXSearch) on every eviction: with strictly increasing X the evicted oldest point always holds minX, so this runs every sample, and it happens under synchronized(chart) - the monitor Chart2D.paintComponent() needs - starving the Swing EDT. Trace2DLtdMonotonicX overrides minXSearch/maxXSearch to O(1) (oldest/youngest ring element) whenever the buffer is known sorted ascending by X, tracking a non-decreasing-X run length so it falls back to the exact superclass scan on rewind/replay. Y is left to the superclass (already amortized O(1)). Swap the three live-plot construction sites in BasicPlotManagerPC; existing (Trace2DLtd) casts still work as it extends Trace2DLtd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjgNkS6wtPkQUdK3KdHVdN
Each ATrace2D.addPoint() takes synchronized(chart) - the same monitor Chart2D.paintComponent() holds - so plotting a multi-channel sample or an FFT frame grabbed the monitor once per point, repeatedly locking out the EDT. filterDataAndPlot now wraps its per-sample multi-trace loop in a single synchronized(mChart) (monitors are reentrant, so addPoint's inner lock is free); the hold is bounded by the trace count. Add addPointsToTrace(...) that adds many points to one trace under one lock in bounded 256-point chunks, and use it for the FFT bin loop. Event-marker, downsampling and per-sample filtering semantics are unchanged; BasicProcessWithCallBack is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjgNkS6wtPkQUdK3KdHVdN
…harden monotonic trace - filterDataAndPlot accessed mListofTraces (size/get) while holding the new chart monitor (chart -> list order), while clearAllDataBuffer and the trace-resize path hold the mListofTraces monitor while calling chart-locking trace mutators (list -> chart). Snapshot the trace list before entering the chart monitor so the cycle cannot form. - Trace2DLtdMonotonicX: NaN X (jchart2d discontinuation marker) now contributes nothing to the ascending run instead of being absorbed by the first-point guard; in-place point mutation (STATE_CHANGED) resets the run so the class stays a safe drop-in for Trace2DLtd; tracking fields made volatile so the unlocked conservative resets cannot tear and spuriously enable the fast path; corrected the setMaxSize-grow Javadoc reasoning. Correctness test: 11596 checks vs stock Trace2DLtd, 0 mismatches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjgNkS6wtPkQUdK3KdHVdN
|
Adversarial review summary (automated reviewer pass over this changeset, verified against decompiled jchart2d 3.3.2 bytecode) Verified sound:
Critical finding, fixed before this PR was opened: the first version of the batching held the chart monitor while calling Minor hardening also applied: NaN-X discontinuity markers now reset the ascending run (previously absorbed by the first-point guard); in-place point mutation ( Notes, no action needed:
Generated by Claude Code |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
…den snapshot indexing Review follow-ups on the batched chart locking in filterDataAndPlot: - Swing under the chart monitor: updateHrPanelIfVisible() is overridden downstream (Consensys PlotManagerPC -> setPnlHR -> setLblHRValue) to do JLabel setText/revalidate/repaint from the data thread. Calling it inside synchronized(chart) meant the data thread took chart -> Swing tree / RepaintManager locks while the EDT takes tree lock -> chart inside Chart2D.paintComponent(): a lock inversion. The props of each trace needing an HR update are now recorded under the monitor and updateHrPanelIfVisible() is called after it is released - same args, same number of calls (one per matching trace), same order. - Console I/O under the monitor: throwExceptionSignalNotFound() (which dumps the whole ObjectCluster per missing signal) and printSignalProps() are now also recorded and replayed after the monitor is released, so a blocking console write can no longer stall the EDT. Debug output content is unchanged: printSignalProps() gained an overload taking an already-read trace size, so the size is still sampled at the original point in the loop. - The deferral buffers are reused instance fields, not per-sample allocations, and are only touched while the mListofPropertiestoPlot monitor is held (which the method holds for the whole batch). They are cleared at the start of each batch so a throw out of the loop cannot leak entries. - Off-by-one: the trace bounds check was 'indexOfTrace > length' (pre-existing as '> mListofTraces.size()'); index == length is already out of bounds, so it is now '>='. - Snapshot staleness: the trace array is snapshotted before entering the chart monitor (to preserve the mListofTraces -> chart lock ordering), so a trace removed mid-sample can still be in it. Added a cheap null / getRenderer() guard. A full "still attached" check is not viable here: jchart2d 3.3.2's Chart2D.removeTrace() does not clear the trace renderer and Chart2D .getTraces() builds a fresh TreeSet per call. - The per-sample toArray() at the snapshot is left as is (documented): any reuse would need the mListofTraces monitor at exactly the point where not taking it is the whole reason for the snapshot. Lock ordering re-verified: nothing inside synchronized(chartMonitor) synchronizes on mListofTraces or calls into Swing. addPointsToTrace's chunked chart lock does neither either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
There was a problem hiding this comment.
🟡 Changes recommended
The deferred replay path can dereference mChart when it may be null (explicitly supported by the new chartMonitor fallback), risking a NullPointerException.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR targets Live Data plot stutter in the PC plotting layer (jchart2d) by avoiding per-sample O(n) X-bound rescans for monotonic time-series data and by reducing lock contention between the data thread and the Swing EDT.
Changes:
- Introduces
Trace2DLtdMonotonicX, aTrace2DLtdsubclass that provides O(1) min/max X when the ring buffer is provably monotonic in X. - Switches live plot trace construction in
BasicPlotManagerPCto useTrace2DLtdMonotonicX. - Batches chart-monitor locking in
filterDataAndPlot(...)and adds a chunkedaddPointsToTrace(...)helper for FFT bin plotting; defers Swing/console work until after releasing the chart lock.
File summaries
| File | Description |
|---|---|
| ShimmerDriverPC/src/main/java/com/shimmerresearch/guiUtilities/plot/Trace2DLtdMonotonicX.java | Adds a monotonic-X optimized Trace2DLtd variant to eliminate redundant min/max X rescans. |
| ShimmerDriverPC/src/main/java/com/shimmerresearch/guiUtilities/plot/BasicPlotManagerPC.java | Adopts the new trace type and batches chart locking + defers I/O/Swing work to reduce EDT starvation. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…dy fast-path edge cases Round-2 review follow-ups. - Deferred work was dropped on the "Trace does not exist" throw: that throw is inside synchronized(chartMonitor) while the replay ran after the block, so the HR updates and console dumps already recorded for earlier traces in the sample were lost (they used to run inline, and the downstream Consensys HR override counts calls, so a dropped call is observable). The loop's exception is now stashed, the replay runs, and the exception is rethrown afterwards. Replay therefore happens on every exit path. - Bounded the chart-monitor hold. Holding it across every trace of a sample made the worst-case EDT wait N_traces x O(buffer), because Trace2DLtd.addPointInternal still runs a full minYSearch/maxYSearch whenever the evicted point held the Y extreme - which for monotone or drifting Y channels (battery, temperature, GSR baseline, counters) is essentially every sample. The monitor is now released and re-acquired every TRACE_BATCH_MAX (8) traces, alongside the existing POINT_BATCH_MAX. The trace snapshot is still taken once, before the first acquisition, so the mListofTraces -> chart ordering is unchanged. Trace2DLtdMonotonicX's javadoc no longer implies the Y rescan is a non-issue; it now says Y is left to the superclass and that monotone Y channels still pay it. - Trace2DLtdMonotonicX: the O(1) X fast path returned before stock minXSearch/maxXSearch's trailing expandMinXErrorBarBounds/ expandMaxXErrorBarBounds, so an installed error bar policy would have been ignored. Both overrides now delegate wholly to the superclass when getErrorBarPolicies() is non-empty, and the "behaves identically to Trace2DLtd" wording is replaced by a description of that opt-out. - Dropped the getRenderer()==null snapshot-staleness guard. It could not detect staleness (Chart2D.removeTrace does not clear the renderer) and what it did catch was a never-attached trace, silently swallowing jchart2d's IllegalStateException and skipping the mCurrentXValue update. Only a plain null-check on the trace remains, with an accurate comment. - Replay now uses a single ordered list of DeferredPlotAction records instead of one list per kind, so the original per-trace interleaving is preserved - the two console kinds share System.out, so grouping by kind reordered debug output. The stale "the three kinds are independent" comment is gone. - The deferral buffers are method-local again. As instance fields they relied on mListofPropertiestoPlot being a stable monitor, but it is a public non-final field that AbstractPlotManager's constructors reassign. The per-sample allocation is dwarfed by the mListofTraces.toArray() snapshot the batch already needs, and the "reused rather than allocated per sample" comment is corrected. - HR recording stays unconditional: no HR-enabled flag exists in the base class (only the Consensys override knows mIsHRVisible), and that override counts calls, so it must stay one recorded action per matching trace. The comment now says so. Re-verified: nothing inside synchronized(chartMonitor) touches mListofTraces or calls Swing, the snapshot precedes the first acquisition, and the replay runs on all exit paths. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
|
Adversarial review — round 2 (after merging master and addressing round-1 findings) Branch was merged with Verified sound
Findings (addressed in
Merge + compile The merged master range (Verisense sensors, Still needs the real-hardware UHD streaming check in the PR description, ideally including a drifting-Y channel such as battery to exercise item 2. Generated by Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
There is a confirmed thread-safety issue in the new trace snapshotting logic and a confirmed avoidable allocation/GC cost in the hot path that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
…hen not overridden - filterDataAndPlot() supports mChart == null (the chart-monitor fallback), and the deferred replay can run throwExceptionSignalNotFound() or printSignalProps() in that state, both of which printed mChart.getName(). Both now go through getChartNameForPrinting(), which yields "<no chart>" when no chart is set. The third replayed action, updateHrPanelIfVisible(), is a no-op in this class and the downstream override touches only the props, the ObjectCluster and its own labels, so it needs nothing. - The per-trace deferred HR action is now only recorded when the runtime class actually overrides updateHrPanelIfVisible(); in this base class it is a no-op, so the record and its allocation were pure waste. Detected once per instance by walking the class hierarchy with getDeclaredMethod() (the method is protected, so getMethod() would not see it) from the runtime class up to BasicPlotManagerPC, cached in a final field. Any unexpected reflective failure defaults to true, i.e. to the previous unconditional behaviour. Verified: the base class resolves to false and an overriding subclass to true, so the Consensys PlotManagerPC path keeps exactly one call per matching trace. - Documented why the mListofTraces snapshot needs no explicit synchronized(mListofTraces): the field is a Collections.synchronizedList, so toArray() already copies under that list's own mutex, and the index alignment with mListofPropertiestoPlot that actually matters is protected by the mListofPropertiestoPlot monitor held for the whole sample. No code change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces non-trivial concurrency and performance-path changes in the live plotting hot path that require careful human validation (especially under real streaming/EDT load).
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
filterDataAndPlot() allocated an ArrayList for the deferred chart-monitor work on every sample, even though on the typical sample nothing is recorded into it (no debug mode, no missing signal, and no updateHrPanelIfVisible() override). The local now starts null and is created on first record, via a small recordDeferredPlotAction() helper that returns the list to assign back; replayDeferredPlotActions() treats null as "nothing recorded" and returns immediately. Semantics are unchanged: actions are still appended in the order they are recorded and replayed in that same order, and the replay still runs on every exit path from the batched loop, the "Trace does not exist" throw included. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
There was a problem hiding this comment.
🔵 Needs a closer look
The change materially alters concurrency/locking behavior in a performance-critical Swing plotting path and should receive final human review plus real-hardware validation before approval.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q29AEJZbwa7aVYeAj61Py
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core live-plot concurrency/locking behavior and trace-bound computations in a UI-critical path and needs final human review plus real-hardware streaming validation to ensure there are no responsiveness or correctness regressions.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
Fixes the stuttering/janky Live Data plots (worst at UHD) at their root causes in the jchart2d-based plotting layer. Diagnosis was verified against the actual jchart2d bytecode the build resolves (3.3.2 — note the
libs/folder carries a stale 3.2.2 jar):Trace2DLtd.addPointInternal()evicts the oldest ring-buffer point once full, and when the evicted point holds an extreme it runs a full linear rescan of the buffer (minXSearch()etc.). For time-series data X is monotonically increasing, so the evicted point always holds min-X → an O(buffer) scan on essentially every sample in steady state.ATrace2D.addPoint()while holdingsynchronized(chart)— the same monitorChart2D.paintComponent()needs — so the data thread starves the Swing EDT, and the per-sample lock churn multiplies it.Changes (ShimmerDriverPC)
New
Trace2DLtdMonotonicX(extendsTrace2DLtd) — overridesaddPointInternalandminXSearch/maxXSearch. When the ring buffer is provably sorted ascending by X (tracked via the run of consecutive non-decreasing-X insertions ≥ live buffer size), min/max X are answered in O(1) from the buffer's oldest/youngest elements. Anything that breaks the guarantee — backward X (rewind/replay), NaN-X discontinuity markers, in-place point mutation (STATE_CHANGED), buffer resize — falls back to the exact superclass scan until a full buffer of monotonic samples restores it. Y-range maintenance is deliberately left to the superclass (already amortized O(1) for random Y). Swapped in at the three live-plot construction sites inBasicPlotManagerPC; existing(Trace2DLtd)casts and the downsampler's runtimesetMaxSize()calls are unaffected.Batched chart locking (
BasicPlotManagerPC) —filterDataAndPlot's multi-trace loop now acquires the chart monitor once per sample instead of once per trace-point (Java monitors are reentrant, so the library's internal per-point lock becomes free), and a newaddPointsToTrace(trace, x[], y[], from, to)adds FFT bins in bounded 256-point chunks per lock acquisition. The trace list is snapshotted before entering the chart monitor to preserve the app's existingmListofTraces → chartlock ordering (see review notes below).Cross-repo note
Shimmer-Advance-API'sShimmerDriverAdvancedPCincludes:ShimmerDriverPCfrom source in itssettings.gradle, so Consensys picks this up with no changes on that side (verified compiling). ConsensysApps is not involved.Testing
gradle compileJava:ShimmerDriverPCandShimmerDriverAdvancedPC(Shimmer-Advance-API, building against these sources) both pass.Trace2DLtdMonotonicXvs stockTrace2DLtdthrough a headlessChart2D: monotonic streaming past eviction,setMaxSizeshrink+grow mid-stream, rewind/replay, repeated equal X — 11,596 min/max checks, 0 mismatches.🤖 Generated with Claude Code
https://claude.ai/code/session_01JjgNkS6wtPkQUdK3KdHVdN
Generated by Claude Code