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..9fd75a55f 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 @@ -27,6 +27,7 @@ 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 +40,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; @@ -82,13 +82,12 @@ public ObjectMarker(int[] roots, boolean[] bits, IIndexReader.IOne2ManyIndex out public class FjObjectMarker extends RecursiveAction { final int position; - final boolean[] visited; + final ConcurrentBitField visited; final boolean topLevel; - private FjObjectMarker(final int position, final boolean[] visited, final boolean topLevel) + private FjObjectMarker(final int position, final ConcurrentBitField visited, final boolean topLevel) { - // mark as soon as we are created and about to be queued - visited[position] = true; + // it is assumed the mark has already happened before task creation this.position = position; this.visited = visited; this.topLevel = topLevel; @@ -120,10 +119,9 @@ void compute(final int outboundPosition, final int levelsLeft) for (int r : process) { - if (!visited[r]) + boolean claimed = visited.compareAndSet(r, false, true); + if (claimed) { - visited[r] = true; - if (levelsLeft <= 0) { new FjObjectMarker(r, visited, false).fork(); } else { @@ -153,9 +151,12 @@ public int markSingleThreaded() @Override public void markMultiThreaded(int threads) throws InterruptedException { + ConcurrentBitField bitField = new ConcurrentBitField(bits); + List rootTasks = IntStream.of(roots) .filter(r -> !bits[r]) - .mapToObj(r -> new FjObjectMarker(r, bits, true)) + .peek(bitField::set) + .mapToObj(r -> new FjObjectMarker(r, bitField, true)) .collect(Collectors.toList()); progressListener.beginTask(Messages.ObjectMarker_MarkingObjects, rootTasks.size()); @@ -170,6 +171,7 @@ public void markMultiThreaded(int threads) throws InterruptedException // wait until completion } + bitField.intoBooleanArrayNonAtomic(bits); progressListener.done(); } 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); } + } +}