diff --git a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/snapshot/ObjectMarker.java b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/snapshot/ObjectMarker.java index 248feaae6..796923121 100644 --- a/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/snapshot/ObjectMarker.java +++ b/plugins/org.eclipse.mat.parser/src/org/eclipse/mat/parser/internal/snapshot/ObjectMarker.java @@ -19,14 +19,18 @@ import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.concurrent.CountedCompleter; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveAction; +import java.util.concurrent.RecursiveTask; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.LongAdder; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.eclipse.mat.SnapshotException; import org.eclipse.mat.collect.BitField; +import org.eclipse.mat.collect.ConcurrentBitField; import org.eclipse.mat.parser.index.IIndexReader; import org.eclipse.mat.parser.internal.Messages; import org.eclipse.mat.snapshot.ExcludedReferencesDescriptor; @@ -39,7 +43,6 @@ public class ObjectMarker implements IObjectMarker { int[] roots; - // TODO we can create a new BitField called ConcurrentBitField that would be 1/8th footprint boolean[] bits; IIndexReader.IOne2ManyIndex outbound; IProgressListener progressListener; @@ -61,7 +64,7 @@ public class ObjectMarker implements IObjectMarker // inline = 10: 2.0 sec // inline = 50: 3.0 sec // (previous impl, for reference, 5.1 sec) - + final int LEVELS_RUN_INLINE = 4; public ObjectMarker(int[] roots, boolean[] bits, IIndexReader.IOne2ManyIndex outbound, @@ -79,107 +82,171 @@ public ObjectMarker(int[] roots, boolean[] bits, IIndexReader.IOne2ManyIndex out this.progressListener = progressListener; } - public class FjObjectMarker extends RecursiveAction - { - final int position; - final boolean[] visited; - final boolean topLevel; - - private FjObjectMarker(final int position, final boolean[] visited, final boolean topLevel) - { - // mark as soon as we are created and about to be queued - visited[position] = true; - this.position = position; - this.visited = visited; - this.topLevel = topLevel; - } - - public void compute() - { - if (progressListener.isCanceled()) - { return; } - - compute(position, LEVELS_RUN_INLINE); - - // only mark progress from the top level tasks; as each root level element - // is completed, a progress marker is updated - if (topLevel) - { - synchronized (progressListener) { - progressListener.worked(1); - } - } - } - - void compute(final int outboundPosition, final int levelsLeft) - { - // TODO can this be an IteratorInt to avoid allocating arrays? - // not yet supported by IndexReader but could be in future; - // esp. for very large outbounds (ie: arrays) - final int[] process = outbound.get(outboundPosition); - - for (int r : process) - { - if (!visited[r]) - { - visited[r] = true; - - if (levelsLeft <= 0) { - new FjObjectMarker(r, visited, false).fork(); - } else { - compute(r, levelsLeft - 1); - } - } - } - } - } - @Override public int markSingleThreaded() { - int before = countMarked(); try { - markMultiThreaded(1); + return (int) markMultiThreadedInner(1); } catch (InterruptedException e) { throw new RuntimeException(e); } - int after = countMarked(); - return after - before; } @Override public void markMultiThreaded(int threads) throws InterruptedException { - List rootTasks = IntStream.of(roots) + markMultiThreadedInner(threads); + } + + public long markMultiThreadedInner(int threads) throws InterruptedException + { + ConcurrentBitField bitField = new ConcurrentBitField(bits); + + int[] claimedRoots = IntStream.of(roots) .filter(r -> !bits[r]) - .mapToObj(r -> new FjObjectMarker(r, bits, true)) - .collect(Collectors.toList()); + .peek(bitField::set) + .toArray(); - progressListener.beginTask(Messages.ObjectMarker_MarkingObjects, rootTasks.size()); + progressListener.beginTask(Messages.ObjectMarker_MarkingObjects, claimedRoots.length); - ForkJoinPool pool = new ForkJoinPool(threads); - rootTasks.forEach(r -> pool.execute(r)); - rootTasks.forEach(FjObjectMarker::join); + LongAdder count = new LongAdder(); + count.add(claimedRoots.length); - pool.shutdown(); - while (!pool.awaitTermination(1000, TimeUnit.MILLISECONDS)) - { - // wait until completion + ForkJoinPool pool = new ForkJoinPool(threads); + try { + ObjectMarkerRootCC root = new ObjectMarkerRootCC(null, claimedRoots, bitField, LEVELS_RUN_INLINE, count); + // blocks until all work is done + pool.invoke(root); + } + finally { + pool.shutdown(); + while (!pool.awaitTermination(1000, TimeUnit.MILLISECONDS)) + { + // wait until completion + } } + bitField.intoBooleanArrayNonAtomic(bits); progressListener.done(); + + return count.sum(); } - int countMarked() - { - int marked = 0; - for (boolean b : bits) - if (b) - marked++; - return marked; + final class ObjectMarkerRootCC extends CountedCompleter { + final int[] roots; + final ConcurrentBitField visited; + final int levelsInline; + final LongAdder count; + + ObjectMarkerRootCC(CountedCompleter parent, int[] roots, ConcurrentBitField visited, int levelsInline, LongAdder count) { + super(parent); + this.roots = roots; + this.visited = visited; + this.levelsInline = levelsInline; + this.count = count; + } + + @Override + public void compute() { + if (progressListener.isCanceled()) { + tryComplete(); + return; + } + + // fork one CC per root + addToPendingCount(roots.length); + for (int r : roots) { + new ObjectMarkerCC(this, r, visited, levelsInline, true, count).fork(); + } + + // completes when children complete + tryComplete(); + } + } + + final class ObjectMarkerCC extends CountedCompleter { + final int position; + final ConcurrentBitField visited; + final int levelsLeft; + final boolean topLevel; + final LongAdder count; + + ObjectMarkerCC( + CountedCompleter parent, + int position, + ConcurrentBitField visited, + int levelsLeft, + boolean topLevel, + LongAdder count + ) { + super(parent); + this.position = position; + this.visited = visited; + this.levelsLeft = levelsLeft; + this.topLevel = topLevel; + this.count = count; + } + + @Override + public void compute() { + if (progressListener.isCanceled()) { + tryComplete(); + return; + } + + final int[] process = outbound.get(position); + + if (levelsLeft > 0) { + // inline traversal + for (int r : process) { + if (!visited.compareAndSet(r, false, true)) continue; + count.increment(); + + new ObjectMarkerCC( + this, r, visited, levelsLeft - 1, false, count + ).compute(); + } + onDone(); + tryComplete(); + return; + } + + // fork boundary + int forks = 0; + for (int r : process) { + if (!visited.compareAndSet(r, false, true)) continue; + count.increment(); + + forks++; + new ObjectMarkerCC( + this, r, visited, 0, false, count + ).fork(); + } + + if (forks == 0) { + onDone(); + tryComplete(); + } else { + addToPendingCount(forks); + // children will call tryComplete() + } + } + + @Override + public void onCompletion(CountedCompleter caller) { + onDone(); + } + + private void onDone() { + if (topLevel) { + synchronized (progressListener) { + progressListener.worked(1); + } + } + } } @Override @@ -280,7 +347,7 @@ public int markSingleThreaded(ExcludedReferencesDescriptor[] excludeSets, ISnaps } private boolean refersOnlyThroughExcluded(int referrerId, int referentId, - ExcludedReferencesDescriptor[] excludeSets, BitField excludeObjectsBF, + ExcludedReferencesDescriptor[] excludeSets, BitField excludeObjectsBF, List refCache, ISnapshot snapshot) throws SnapshotException { diff --git a/plugins/org.eclipse.mat.report/META-INF/MANIFEST.MF b/plugins/org.eclipse.mat.report/META-INF/MANIFEST.MF index 9bfed33c1..68f9fb9ca 100644 --- a/plugins/org.eclipse.mat.report/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.mat.report/META-INF/MANIFEST.MF @@ -4,7 +4,7 @@ Bundle-Name: %Bundle-Name Bundle-Vendor: %Bundle-Vendor Bundle-SymbolicName: org.eclipse.mat.report;singleton:=true Bundle-Version: 1.17.0.qualifier -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-RequiredExecutionEnvironment: JavaSE-17 Export-Package: org.eclipse.mat, org.eclipse.mat.collect, org.eclipse.mat.query, diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/collect/ConcurrentBitField.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/collect/ConcurrentBitField.java new file mode 100644 index 000000000..04e6d6ca3 --- /dev/null +++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/collect/ConcurrentBitField.java @@ -0,0 +1,216 @@ +/******************************************************************************* + * Copyright (c) 2008, 2024 SAP AG, IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jason Koch (Netflix, Inc) - implementation + *******************************************************************************/ +package org.eclipse.mat.collect; + +import java.util.concurrent.atomic.AtomicLongArray; + +/** + * This class manages huge bit fields. It is much faster than + * {@link java.util.BitSet} and was specifically developed to be used with huge + * bit sets in ISnapshot (e.g. needed in virtual GC traces). Out of performance + * reasons no method does any parameter checking, i.e. only valid values are + * expected. This is a fully thread-safe/concurrent implementation. + */ +public final class ConcurrentBitField +{ + + private final AtomicLongArray bits; + private final int size; + + private static final int SHIFT = 0x6; + private static final int MASK = 0x3f; + + /** + * Creates a bit field with the given number of bits. Size is expected to be + * positive. + * @param size the maximum size of the BitField + */ + public ConcurrentBitField(int size) + { + if (size <= 0) + { + throw new IllegalArgumentException("size must be > 0"); + } + this.size = size; + this.bits = new AtomicLongArray((((size) - 1) >>> SHIFT) + 1); + } + + public ConcurrentBitField(boolean[] bits) + { + if (bits.length == 0) + { + throw new IllegalArgumentException("bits must have at least one element"); + } + this.size = bits.length; + this.bits = new AtomicLongArray((((size) - 1) >>> SHIFT) + 1); + + for (int i = 0; i < size; i++) + { + if (bits[i]) + set(i); + } + } + + /** + * Sets the bit on the given index. Index is expected to be in range - out + * of performance reasons no checks are done! + * @param index The 0-based index into the BitField. + */ + public final void set(int index) + { + final int slot = index >>> SHIFT; + final long flag = (1L << (index & MASK)); + + while (true) + { + final long existing = bits.get(slot); + final long next = existing | flag; + if (next == existing) + { return; } + + if (bits.compareAndSet(slot, existing, next)) + { return; } + } + } + + /** + * Clears the bit on the given index. Index is expected to be in range - out + * of performance reasons no checks are done! + * @param index The 0-based index into the BitField. + */ + public final void clear(int index) + { + final int slot = index >>> SHIFT; + final long flag = (1L << (index & MASK)); + + while (true) + { + final long existing = bits.get(slot); + final long next = existing & (~flag); + if (next == existing) + { return; } + + if (bits.compareAndSet(slot, existing, next)) + { return; } + } + } + + /** + * Compare and set the value atomically. NB multiple underlying CAS + * might be competing, but only once ever for the same bit. + * @param index + * @return true if successful. False return indicates that the actual value + * was not equal to the expected value. + */ + public final boolean compareAndSet(int index, boolean expectedValue, boolean newValue) { + int slot = index >>> SHIFT; + long flag = (1L << (index & MASK)); + + // We need to do a two-pass here + // Load the value, then update the mask, and if then attempt a CAS + // there are two possibilities for CAS failure: + // (1) someone changed the flag we are interested in + // (2) someone changed a different flag in the block + // Therefore, we need to use full compare and exchange rather than + // compare and set. + + while (true) + { + final long existing = bits.get(slot); + final boolean currentBit = (existing & flag) != 0; + + // expected bit does not match + if (currentBit != expectedValue) + { + return false; + } + + final long nextValue = newValue + ? (existing | flag) + : (existing & ~flag); + + // we know that expected matches, and now next matches, then done + if (nextValue == existing) + { + return true; + } + + final long witness = bits.compareAndExchange(slot, existing, nextValue); + + // cas succeeded + if (witness == existing) + { + return true; + } + + // cas failed, but why? + // check the returned value, and, if it was changed by someone else + // the CAS fails + boolean witnessBit = (witness & flag) != 0L; + if (witnessBit != expectedValue) + { + return false; + } + + // otherwise, we know that the bit was OK but some other bits in the + // slot changed we can safely retry + } + } + + /** + * Gets the bit on the given index. Index is expected to be in range - out + * of performance reasons no checks are done! + * @param index The 0-based index into the BitField. + * @return true if the BitField was set, false if it was cleared or never set. + */ + public final boolean get(int index) { + final int slot = index >>> SHIFT; + final long flag = (1L << (index & MASK)); + + final long existing = bits.get(slot); + return (existing & flag) != 0; + } + + /** + * The size of the bitfield. + * @return + */ + public final int size() { + return this.size; + } + + /** + * Gets the full array. Note that this is _not_ a thread-safe snapshot. + * @return + */ + public final boolean[] toBooleanArrayNonAtomic() { + final boolean[] result = new boolean[size]; + intoBooleanArrayNonAtomic(result); + return result; + } + + /** + * Gets the full array. Note that this is _not_ a thread-safe snapshot. + * @param output array to fill + */ + public final void intoBooleanArrayNonAtomic(boolean[] output) { + if (output.length != size) + { + throw new IllegalArgumentException("output length must match bitset length"); + } + for (int i = 0; i < size; i++) + { + output[i] = get(i); + } + } +} diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java index b9175e229..79e0f2f3a 100644 --- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/AllTests.java @@ -30,6 +30,7 @@ org.eclipse.mat.tests.collect.CommandTests.class, // org.eclipse.mat.tests.collect.SortTest.class, // org.eclipse.mat.tests.collect.ExtractCollectionEntriesTest.class, // + org.eclipse.mat.tests.collect.ConcurrentBitFieldTest.class, // org.eclipse.mat.tests.parser.GzipTests.class, // org.eclipse.mat.tests.parser.TestIndex.class, // org.eclipse.mat.tests.parser.TestIndex1to1.class, // diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/collect/ConcurrentBitFieldTest.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/collect/ConcurrentBitFieldTest.java new file mode 100644 index 000000000..02b646f4a --- /dev/null +++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/collect/ConcurrentBitFieldTest.java @@ -0,0 +1,447 @@ +package org.eclipse.mat.tests.collect; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.*; + +import java.io.*; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Random; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.eclipse.mat.collect.ConcurrentBitField; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class ConcurrentBitFieldTest +{ + + // --- basic shape / boundaries ------------------------------------------------ + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void booleanArrayCtorInitializesAllBitsAcrossSlots() + { + // length crosses multiple 64-bit slots and ends mid-slot + int n = 64 * 3 + 7; + boolean[] init = new boolean[n]; + + // Pattern: set primes and every 5th bit; leave others false + for (int i = 0; i < n; i++) + { + if (isPrime(i) || (i % 5 == 0)) init[i] = true; + } + + ConcurrentBitField bf = new ConcurrentBitField(init); + + // Verify exact mapping + for (int i = 0; i < n; i++) + { + assertEquals("bit " + i, init[i], bf.get(i)); + } + + // Verify mutability after construction doesn’t affect bf + init[0] = !init[0]; + assertEquals( + "post-mutation of source array must not affect bf", + !init[0], // we flipped source, bf should still have original + bf.get(0) == false ? false : true /* force boolean */ + ); + + // Flip a few and ensure API still works + for (int i = 0; i < n; i += 17) + { + boolean cur = bf.get(i); + assertTrue(bf.compareAndSet(i, cur, !cur)); + assertEquals(!cur, bf.get(i)); + } + } + + @Test + public void booleanArrayCtorRejectsEmpty() { + thrown.expect(IllegalArgumentException.class); + new ConcurrentBitField(new boolean[0]); + } + + // helper + private static boolean isPrime(int x) { + if (x < 2) return false; + if (x % 2 == 0) return x == 2; + for (int d = 3; d * d <= x; d += 2) if (x % d == 0) return false; + return true; + } + + @Test + public void sizeReportsCorrectly() { + ConcurrentBitField bf = new ConcurrentBitField(1_000); + assertThat(bf.size(), equalTo(1_000)); + } + + @Test + public void singleLengthValue() { + ConcurrentBitField bf = new ConcurrentBitField(1); + assertThat(bf.size(), equalTo(1)); + assertThat(bf.get(0), equalTo(false)); + bf.set(0); + assertThat(bf.get(0), equalTo(true)); + bf.clear(0); + assertThat(bf.get(0), equalTo(false)); + } + + @Test + public void lastIndexWorks() { + int n = 257; // crosses a 64-bit boundary (slot 0..4) + ConcurrentBitField bf = new ConcurrentBitField(n); + assertFalse(bf.get(n - 1)); + bf.set(n - 1); + assertTrue(bf.get(n - 1)); + bf.clear(n - 1); + assertFalse(bf.get(n - 1)); + } + + @Test + public void setClearIdempotent() { + ConcurrentBitField bf = new ConcurrentBitField(128); + bf.set(5); + bf.set(5); + assertTrue(bf.get(5)); + bf.clear(5); + bf.clear(5); + assertFalse(bf.get(5)); + } + + @Test + public void toBooleanArrayNonAtomicMatchesSingleThreadState() { + int n = 200; + ConcurrentBitField bf = new ConcurrentBitField(n); + for (int i = 0; i < n; i += 5) bf.clear(i); + for (int i = 0; i < n; i += 3) bf.set(i); + boolean[] snap = bf.toBooleanArrayNonAtomic(); + for (int i = 0; i < n; i++) + { + assertEquals(bf.get(i), snap[i]); + } + } + + // --- compareAndSet semantics ------------------------------------------------- + + @Test + public void casReturnsTrueOnNoOpWhenAlreadyMatches() { + ConcurrentBitField bf = new ConcurrentBitField(64); + // ensure bit is set + assertTrue(bf.compareAndSet(10, false, true)); + assertTrue(bf.get(10)); + // expected=true, newValue=true -> no-op, should return true + assertTrue(bf.compareAndSet(10, true, true)); + assertTrue(bf.get(10)); + // expected=false should fail now + assertFalse(bf.compareAndSet(10, false, true)); + // clear via CAS + assertTrue(bf.compareAndSet(10, true, false)); + assertFalse(bf.get(10)); + // expected=true should now fail + assertFalse(bf.compareAndSet(10, true, false)); + } + + @Test + public void casSucceedsDespiteOtherBitsChangingInSameSlot() throws Exception + { + // Pick index i and a “noise” bit j in same 64-slot. + final int base = 128; // start of slot #2 + final int i = base + 3; // target bit + final int j = base + 17; // noise bit (same 64-lane) + + ConcurrentBitField bf = new ConcurrentBitField(256); + // Ensure target starts cleared + assertFalse(bf.get(i)); + + CountDownLatch start = new CountDownLatch(1); + AtomicLong flips = new AtomicLong(); + + Thread toggler = new Thread(() -> { + try { + start.await(); + } + catch (InterruptedException ignored) + {} + // Hammer a different bit in the same slot to force CAS “witness” mismatch + for (int k = 0; k < 200_000; k++) + { + bf.set(j); + bf.clear(j); + flips.incrementAndGet(); + } + }); + + toggler.start(); + start.countDown(); + + // Attempt CAS on target bit while other bits in same word are changing. + boolean ok = bf.compareAndSet(i, false, true); + assertTrue("CAS on target bit should succeed even if other bits change", ok); + assertTrue(bf.get(i)); + + toggler.join(); + assertTrue("Toggler should have done work", flips.get() > 0); + } + + // --- randomized single-thread correctness vs BitSet baseline ---------------- + + @Test + public void randomizedAgainstBitSet() { + int n = 10_000; + long seed = 42L; + Random rnd = new Random(seed); + ConcurrentBitField bf = new ConcurrentBitField(n); + BitSet bs = new BitSet(n); + + for (int t = 0; t < 200_000; t++) + { + int idx = rnd.nextInt(n); + int op = rnd.nextInt(4); + switch (op) + { + case 0: + bf.set(idx); + bs.set(idx); + break; + + case 1: + bf.clear(idx); + bs.clear(idx); + break; + + case 2: + { + boolean exp = bs.get(idx); + boolean ret = bf.compareAndSet(idx, exp, !exp); + if (ret) bs.flip(idx); + } + break; + + case 3: + { + assertEquals(bs.get(idx), bf.get(idx)); + } + break; + } + } + + // Final full comparison + for (int i = 0; i < n; i++) + { + assertEquals(bs.get(i), bf.get(i)); + } + } + + // --- multi-threaded set/clear correctness ----------------------------------- + + @Test(timeout = 15_000) + public void parallelSetDisjointRanges() throws Exception + { + int n = 1 << 16; + ConcurrentBitField bf = new ConcurrentBitField(n); + int threads = Math.max(4, Runtime.getRuntime().availableProcessors()); + ExecutorService pool = Executors.newFixedThreadPool(threads); + + CountDownLatch go = new CountDownLatch(1); + List> fs = new ArrayList<>(); + for (int t = 0; t < threads; t++) + { + final int tid = t; + fs.add( + pool.submit(() -> { + int start = (tid * n) / threads; + int end = ((tid + 1) * n) / threads; + try { + go.await(); + } + catch (InterruptedException ignored) + {} + for (int i = start; i < end; i++) + bf.set(i); + }) + ); + } + go.countDown(); + for (Future f : fs) + f.get(); + pool.shutdown(); + + for (int i = 0; i < n; i++) + assertTrue(bf.get(i)); + } + + @Test(timeout = 20_000) + public void parallelMixedSetClearSameRange() throws Exception + { + int n = 200_000; // multiple slots + ConcurrentBitField bf = new ConcurrentBitField(n); + int threads = Math.max(4, Runtime.getRuntime().availableProcessors()); + ExecutorService pool = Executors.newFixedThreadPool(threads); + + // Half threads set even indices, half clear even indices, others random CAS flips. + int setThreads = threads / 3; + int clearThreads = threads / 3; + int casThreads = threads - setThreads - clearThreads; + + CyclicBarrier barrier = new CyclicBarrier(threads); + List> tasks = new ArrayList<>(); + + for (int t = 0; t < setThreads; t++) + { + tasks.add(() -> { + barrier.await(); + for (int i = 0; i < n; i += 2) + bf.set(i); + return null; + }); + } + for (int t = 0; t < clearThreads; t++) + { + tasks.add(() -> { + barrier.await(); + for (int i = 0; i < n; i += 2) + bf.clear(i); + return null; + }); + } + for (int t = 0; t < casThreads; t++) + { + tasks.add(() -> { + Random r = ThreadLocalRandom.current(); + barrier.await(); + for (int k = 0; k < 150_000; k++) + { + int i = r.nextInt(n); + boolean cur = bf.get(i); + bf.compareAndSet(i, cur, !cur); // flip attempt + } + return null; + }); + } + + pool.invokeAll(tasks); + pool.shutdown(); + pool.awaitTermination(10, TimeUnit.SECONDS); + + // Simple invariant: every bit is either set or not set; verify API consistency. + for (int i = 0; i < Math.min(n, 20000); i++) + { + // sample to keep runtime bounded + boolean g = bf.get(i); + if (g) + { + // clearing should succeed with expected=true + assertTrue(bf.compareAndSet(i, true, false)); + assertFalse(bf.get(i)); + } + else + { + // setting should succeed with expected=false + assertTrue(bf.compareAndSet(i, false, true)); + assertTrue(bf.get(i)); + } + } + } + + // --- stress: heavy contention on same-slot bits ------------------------------ + + @Test(timeout = 25_000) + public void heavyContentionSameSlotIsLinearizable() throws Exception + { + final int slotBase = 1024; // choose slot-aligned base + final int BITS = 64; // full slot + final int ROUNDS = 200_000; + + ConcurrentBitField bf = new ConcurrentBitField(slotBase + BITS); + ExecutorService pool = Executors.newFixedThreadPool(8); + CountDownLatch start = new CountDownLatch(1); + AtomicInteger ops = new AtomicInteger(); + + List> tasks = new ArrayList<>(); + for (int t = 0; t < 8; t++) + { + tasks.add(() -> { + Random r = ThreadLocalRandom.current(); + start.await(); + for (int k = 0; k < ROUNDS; k++) + { + int bit = slotBase + r.nextInt(BITS); + if ((k & 1) == 0) + { + bf.set(bit); + } + else + { + boolean cur = bf.get(bit); + bf.compareAndSet(bit, cur, !cur); + } + ops.incrementAndGet(); + } + return null; + }); + } + + List> futs = tasks + .stream() + .map(pool::submit) + .collect(Collectors.toList()); + + start.countDown(); + for (Future f : futs) + f.get(); + pool.shutdown(); + + assertTrue("did work", ops.get() >= 8 * ROUNDS); + + // Sanity: bits are readable and stable under single-thread probe + int ones = 0; + for (int i = 0; i < BITS; i++) + { + if (bf.get(slotBase + i)) + ones++; + } + // Not asserting a particular count; just that get() is coherent: + boolean[] snap = bf.toBooleanArrayNonAtomic(); + int ones2 = 0; + for (int i = 0; i < BITS; i++) + { + if (snap[slotBase + i]) + ones2++; + } + assertEquals(ones, ones2); + } + + // --- regression: creating non-multiple-of-64 size and touching edges -------- + + @Test + public void nonMultipleOf64EdgesSafe() + { + int n = (64 * 7) + 13; + ConcurrentBitField bf = new ConcurrentBitField(n); + // touch first/last of each slot + last element overall + for (int s = 0; s < (n + 63) / 64; s++) + { + int first = s * 64; + int last = Math.min(n - 1, s * 64 + 63); + bf.set(first); + bf.set(last); + assertTrue(bf.get(first)); + assertTrue(bf.get(last)); + bf.clear(first); + bf.clear(last); + assertFalse(bf.get(first)); + assertFalse(bf.get(last)); + } + assertFalse(bf.get(n - 1)); + } +} diff --git a/testing/jcstress-tests/.gitignore b/testing/jcstress-tests/.gitignore new file mode 100644 index 000000000..27571f48d --- /dev/null +++ b/testing/jcstress-tests/.gitignore @@ -0,0 +1,3 @@ +jcstress-results-*bin.gz +results/* +target/* diff --git a/testing/jcstress-tests/README.md b/testing/jcstress-tests/README.md new file mode 100644 index 000000000..1802df068 --- /dev/null +++ b/testing/jcstress-tests/README.md @@ -0,0 +1,24 @@ +## jcstress tests + +Use this to perform any concurrency validation via jcstress. This is a little +difficult to integrate with the MAT project proper, since Tycho and Eclipse +Orbit repos have no knowledge of jcstress. As a workaround, this project allows +one to perform validation testing during development. + +### Wire up a new test + +1. Set up a new test in the src/main/... path. + +2. As needed, provide a symlink to the code in the eclipse MAT plugin. + +### Build and run + +```bash +$ cd testing/jcstress-tests +$ mvn clean verify +$ java -jar target/jcstress.jar +### wait some time + +### review results in results/ +$ ls -la results/ +``` diff --git a/testing/jcstress-tests/pom.xml b/testing/jcstress-tests/pom.xml new file mode 100644 index 000000000..61a30d2da --- /dev/null +++ b/testing/jcstress-tests/pom.xml @@ -0,0 +1,122 @@ + + + + 4.0.0 + + org.eclipse.mat + jcstress-tests + 1.0 + jar + + JCStress test sample + + + + + 3.2 + + + + + org.openjdk.jcstress + jcstress-core + ${jcstress.version} + + + + + UTF-8 + + + 0.16 + + + 1.8 + + + jcstress + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${javac.target} + ${javac.target} + ${javac.target} + full + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + main + package + + shade + + + ${uberjar.name} + + + org.openjdk.jcstress.Main + + + META-INF/TestList + + + + + + + + + + diff --git a/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitField.java b/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitField.java new file mode 120000 index 000000000..54c001875 --- /dev/null +++ b/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitField.java @@ -0,0 +1 @@ +../../../../../../../../../plugins/org.eclipse.mat.report/src/org/eclipse/mat/collect/ConcurrentBitField.java \ No newline at end of file diff --git a/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitFieldJCStress.java b/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitFieldJCStress.java new file mode 100644 index 000000000..ad79b2377 --- /dev/null +++ b/testing/jcstress-tests/src/main/java/org/eclipse/mat/collect/ConcurrentBitFieldJCStress.java @@ -0,0 +1,106 @@ +package org.eclipse.mat.collect; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.Z_Result; +import org.openjdk.jcstress.infra.results.ZZ_Result; +import org.openjdk.jcstress.infra.results.ZZZ_Result; + +public class ConcurrentBitFieldJCStress { + + // 1) set vs set on same bit: final must be true. + @JCStressTest + @State + @Outcome(id = "true", expect = Expect.ACCEPTABLE, desc = "Bit set.") + @Outcome(id = "false", expect = Expect.FORBIDDEN, desc = "Idempotence violated.") + public static class SetSetSameBit { + final ConcurrentBitField bf = new ConcurrentBitField(128); + final int i = 5; + @Actor public void a1() { bf.set(i); } + @Actor public void a2() { bf.set(i); } + @Arbiter public void arb(Z_Result r) { r.r1 = bf.get(i); } + } + + // 2) set vs clear on same bit: both end-states are valid. + @JCStressTest + @State + @Outcome(id = "true", expect = Expect.ACCEPTABLE, desc = "Set wins.") + @Outcome(id = "false", expect = Expect.ACCEPTABLE, desc = "Clear wins.") + public static class SetClearRaceSameBit { + final ConcurrentBitField bf = new ConcurrentBitField(128); + final int i = 7; + @Actor public void a1() { bf.set(i); } + @Actor public void a2() { bf.clear(i); } + @Arbiter public void arb(Z_Result r) { r.r1 = bf.get(i); } + } + + // 3) CAS must not be perturbed by other-bit churn in SAME slot. + // Only valid: cas=true, final=true. + @JCStressTest + @State + @Outcome(id = "true, true", expect = Expect.ACCEPTABLE, desc = "CAS succeeded; bit set.") + @Outcome(id = "true, false", expect = Expect.FORBIDDEN, desc = "Lost own update without a clearer on i.") + @Outcome(id = "false, true", expect = Expect.FORBIDDEN, desc = "CAS reported false though expected still held (wrong witness handling).") + @Outcome(id = "false, false",expect = Expect.FORBIDDEN, desc = "CAS should succeed from initial false.") + public static class CasSurvivesOtherBitMutationSameSlot { + final ConcurrentBitField bf = new ConcurrentBitField(128); + final int i = 10; // target bit for CAS (false -> true) + final int j = 11; // unrelated bit in same 64-bit slot + @Actor public void a1(ZZ_Result r) { r.r1 = bf.compareAndSet(i, false, true); } + @Actor public void a2() { bf.set(j); bf.clear(j); bf.set(j); } + @Arbiter public void arb(ZZ_Result r) { r.r2 = bf.get(i); } + } + + // 4) Conflicting CAS on SAME bit from initial false. + // Valid triples: + // A(true,false,true) : B runs first and fails; A sets true. + // A(true,true,false) : A sets true; B flips it to false. + // All others are forbidden. + @JCStressTest + @State + @Outcome(id = "true, false, true", expect = Expect.ACCEPTABLE, desc = "A wins; B fails; final true.") + @Outcome(id = "true, true, false", expect = Expect.ACCEPTABLE, desc = "A then B; final false.") + @Outcome(id = "false, true, false", expect = Expect.FORBIDDEN, desc = "B cannot succeed before A from initial false.") + @Outcome(id = "false, false, true", expect = Expect.FORBIDDEN, desc = "A cannot fail from initial false.") + @Outcome(id = "false, false, false",expect = Expect.FORBIDDEN, desc = "Both failing from initial false is impossible.") + @Outcome(id = "true, false, false", expect = Expect.FORBIDDEN, desc = "A succeeded but final false without B success.") + @Outcome(id = "false, true, true", expect = Expect.FORBIDDEN, desc = "B succeeded but final true.") + @Outcome(id = "true, true, true", expect = Expect.FORBIDDEN, desc = "Both succeeded but final true (should be false).") + public static class ConflictingCASSameBit { + final ConcurrentBitField bf = new ConcurrentBitField(128); + final int i = 20; + @Actor public void a1(ZZZ_Result r) { r.r1 = bf.compareAndSet(i, false, true); } + @Actor public void a2(ZZZ_Result r) { r.r2 = bf.compareAndSet(i, true, false); } + @Arbiter public void arb(ZZZ_Result r) { r.r3 = bf.get(i); } + } + + // 5) CAS independent across different slots. + // Only valid: cas=true, final=true. + @JCStressTest + @State + @Outcome(id = "true, true", expect = Expect.ACCEPTABLE, desc = "Independent slot churn ignored.") + @Outcome(id = "true, false", expect = Expect.FORBIDDEN, desc = "Own update lost without clearer on i.") + @Outcome(id = "false, true", expect = Expect.FORBIDDEN, desc = "CAS should not fail from initial false.") + @Outcome(id = "false, false",expect = Expect.FORBIDDEN, desc = "CAS should not fail from initial false.") + public static class CasIndependentAcrossSlots { + final ConcurrentBitField bf = new ConcurrentBitField(256); + final int i = 3; // slot 0 + final int k = 3 + 64; // slot 1 + @Actor public void a1(ZZ_Result r) { r.r1 = bf.compareAndSet(i, false, true); } + @Actor public void a2() { bf.set(k); bf.clear(k); bf.set(k); bf.clear(k); } + @Arbiter public void arb(ZZ_Result r) { r.r2 = bf.get(i); } + } + + // 6) clear vs clear on same bit: final must be false. + @JCStressTest + @State + @Outcome(id = "false", expect = Expect.ACCEPTABLE, desc = "Bit cleared.") + @Outcome(id = "true", expect = Expect.FORBIDDEN, desc = "Clear is not idempotent if ends true.") + public static class ClearClearSameBit { + final ConcurrentBitField bf = new ConcurrentBitField(128); + final int i = 9; + { bf.set(i); } // start true, two clears race + @Actor public void a1() { bf.clear(i); } + @Actor public void a2() { bf.clear(i); } + @Arbiter public void arb(Z_Result r) { r.r1 = bf.get(i); } + } +}