Skip to content

Commit ede3b34

Browse files
committed
DEV-896 review fixes: trace-list snapshot to prevent lock inversion, 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
1 parent 5af0117 commit ede3b34

2 files changed

Lines changed: 43 additions & 9 deletions

File tree

ShimmerDriverPC/src/main/java/com/shimmerresearch/guiUtilities/plot/BasicPlotManagerPC.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2008,6 +2008,12 @@ public void filterDataAndPlot(ObjectCluster ojc) throws Exception {
20082008
//per-point synchronized(chart) inside addPoint() is free while we hold this outer lock.
20092009
//The batch is bounded by the number of plotted traces, so the lock hold stays short.
20102010
//Falls back to the already-held mListofPropertiestoPlot monitor if no chart is set yet.
2011+
//IMPORTANT (lock ordering): snapshot mListofTraces BEFORE taking the chart monitor.
2012+
//Other threads (e.g. clearAllDataBuffer, trace resizing) hold the mListofTraces monitor
2013+
//while calling chart-locking trace mutators (removeAllPoints/setMaxSize), i.e.
2014+
//mListofTraces -> chart. Touching mListofTraces while holding the chart monitor here
2015+
//would be the reverse order and a real deadlock cycle.
2016+
ITrace2D[] tracesSnapshot = mListofTraces.toArray(new ITrace2D[0]);
20112017
Object chartMonitor = (mChart != null) ? (Object)mChart : (Object)mListofPropertiestoPlot;
20122018
synchronized(chartMonitor){
20132019
while (entries.hasNext()) {
@@ -2068,10 +2074,10 @@ public void filterDataAndPlot(ObjectCluster ojc) throws Exception {
20682074
continue;
20692075
}
20702076

2071-
if (indexOfTrace>mListofTraces.size()){
2077+
if (indexOfTrace>tracesSnapshot.length){
20722078
throw new Exception("Trace does not exist: (" + traceName + ")");
20732079
}
2074-
ITrace2D currentTrace = mListofTraces.get(indexOfTrace);
2080+
ITrace2D currentTrace = tracesSnapshot[indexOfTrace];
20752081
//utilShimmer.consolePrintErrLn(currentTrace.getMaxY());
20762082

20772083
mCurrentXValue = xData;

ShimmerDriverPC/src/main/java/com/shimmerresearch/guiUtilities/plot/Trace2DLtdMonotonicX.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@
4040
* optimising it is unnecessary and would risk the displayed Y auto-scale.</li>
4141
* <li>{@code setMaxSize(int)} is {@code final} in {@code Trace2DLtd} so it cannot be
4242
* overridden, but no reset hook is needed: {@link #isBufferSortedAscendingByX()} reads
43-
* the live {@code m_buffer.size()} each call, so growing the buffer transparently
44-
* disables the fast path until it refills and shrinking (which only discards the
45-
* oldest, smallest-X elements) keeps the buffer sorted.</li>
43+
* the live {@code m_buffer.size()} each call. Growing leaves the element count and
44+
* ordering unchanged (a sorted buffer stays sorted, so the fast path validly stays
45+
* available); shrinking only discards the oldest, smallest-X elements, which also
46+
* keeps the buffer sorted.</li>
4647
* </ul>
4748
*
4849
* <p>Externally this class behaves identically to {@code Trace2DLtd} (same bounds, same
@@ -52,15 +53,21 @@
5253
*/
5354
public class Trace2DLtdMonotonicX extends Trace2DLtd {
5455

55-
/** X value of the most recently inserted point, used to detect non-decreasing X. */
56-
private double mLastX = Double.NaN;
56+
/**
57+
* X value of the most recently inserted point, used to detect non-decreasing X.
58+
* Volatile: normal updates happen under the chart+trace locks (inside addPoint), but the
59+
* conservative resets in {@link #firePointChanged} may run outside them; volatile prevents
60+
* a torn 64-bit write from ever spuriously enabling the fast path. All unlocked writes are
61+
* resets, which can only (safely) disable it.
62+
*/
63+
private volatile double mLastX = Double.NaN;
5764

5865
/**
5966
* Number of consecutive insertions (ending at the most recent one) whose X was
6067
* non-decreasing. When this is {@code >= m_buffer.size()} the entire current buffer
6168
* content was produced by a non-decreasing run and is therefore sorted ascending by X.
6269
*/
63-
private long mAscendingRunLength = 0L;
70+
private volatile long mAscendingRunLength = 0L;
6471

6572
public Trace2DLtdMonotonicX() {
6673
super();
@@ -94,7 +101,12 @@ private boolean isBufferSortedAscendingByX() {
94101
@Override
95102
protected boolean addPointInternal(ITracePoint2D p) {
96103
double x = p.getX();
97-
if (Double.isNaN(mLastX) || x >= mLastX) {
104+
if (Double.isNaN(x)) {
105+
// NaN X (jchart2d's discontinuation marker) breaks any ordering guarantee for as
106+
// long as it stays in the buffer: contribute nothing to the ascending run, so the
107+
// fast path can only resume once a full buffer of post-NaN points has evicted it.
108+
mAscendingRunLength = 0L;
109+
} else if (Double.isNaN(mLastX) || x >= mLastX) {
98110
// Non-decreasing X: extend the ascending run (cap to avoid overflow; any value
99111
// above the buffer size already means "fully sorted").
100112
if (mAscendingRunLength < Long.MAX_VALUE) {
@@ -112,6 +124,22 @@ protected boolean addPointInternal(ITracePoint2D p) {
112124
return super.addPointInternal(p);
113125
}
114126

127+
/**
128+
* In-place mutation of an existing point ({@code ITracePoint2D.setLocation}) fires a
129+
* {@code STATE_CHANGED} notification and can reorder the buffer arbitrarily, which the
130+
* insertion-time run tracking cannot see. Reset the run so the fast path stays off until
131+
* a full buffer of fresh monotonic insertions restores the guarantee. (Not used by the
132+
* Shimmer streaming paths, but keeps this class a safe drop-in for Trace2DLtd.)
133+
*/
134+
@Override
135+
public void firePointChanged(final ITracePoint2D changed, final int state) {
136+
if (state == ITracePoint2D.STATE_CHANGED) {
137+
mAscendingRunLength = 0L;
138+
mLastX = Double.NaN;
139+
}
140+
super.firePointChanged(changed, state);
141+
}
142+
115143
@Override
116144
protected void minXSearch() {
117145
if (isBufferSortedAscendingByX()) {

0 commit comments

Comments
 (0)