Repository: hpi-swa/trufflesqueak
Pull request #195: ObjectGraphUtils traversal optimizations
Author: login: Sabine-Schnabeltier
Created: 2025-05-20T02:01:16Z | Merged: 2025-05-25T09:40:07Z

Changed ObjectTracer to mark unmarked objects when adding them to the workStack. This avoids an extra check in all of the traversals and eliminates the potential examination of an object multiple times.

Since the workStack is a stack, reordered the initialization to put the specialObjectsArray (and its contents) at the top of the stack and changed tracePointers to add the class after the inner pointers so that the class would be traversed before the other pointers.

Rearranged pointersBecomeOneWay and allInstancesOf so that up to 4 tasks can work independently on subgraphs of the entire object graph.


Diff:
diff --git a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/AbstractSqueakObjectWithClassAndHash.java b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/AbstractSqueakObjectWithClassAndHash.java
index 282703ef1..15c7e0d1c 100644
--- a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/AbstractSqueakObjectWithClassAndHash.java
+++ b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/AbstractSqueakObjectWithClassAndHash.java
@@ -6,9 +6,13 @@
  */
 package de.hpi.swa.trufflesqueak.model;
 
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+
 import org.graalvm.collections.UnmodifiableEconomicMap;
 
 import com.oracle.truffle.api.CompilerAsserts;
+import com.oracle.truffle.api.CompilerDirectives;
 import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
 import com.oracle.truffle.api.nodes.IndirectCallNode;
 import com.oracle.truffle.api.nodes.Node;
@@ -42,6 +46,19 @@ public abstract class AbstractSqueakObjectWithClassAndHash extends AbstractSquea
     private ClassObject squeakClass;
     private int squeakHashAndBits;
 
+    /**
+     * Support for atomically accessing the flags contained within squeakHashAndBits.
+     */
+    private static final VarHandle HASH_AND_BITS_HANDLE;
+
+    static {
+        try {
+            HASH_AND_BITS_HANDLE = MethodHandles.lookup().findVarHandle(AbstractSqueakObjectWithClassAndHash.class, "squeakHashAndBits", int.class);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw CompilerDirectives.shouldNotReachHere("Unable to find a VarHandle for squeakHashAndBits", e);
+        }
+    }
+
     // For special/well-known objects only.
     protected AbstractSqueakObjectWithClassAndHash() {
         this(HASH_UNINITIALIZED, null);
@@ -61,7 +78,7 @@ private AbstractSqueakObjectWithClassAndHash(final boolean markingFlag, final Cl
         squeakHashAndBits = AbstractSqueakObjectWithClassAndHash.HASH_UNINITIALIZED;
         squeakClass = klass;
         if (markingFlag) {
-            toggleMarkingFlag();
+            squeakHashAndBits |= MARK_BIT; // set MARK_BIT
         }
     }
 
@@ -132,16 +149,40 @@ public final void setSqueakHash(final int newHash) {
         squeakHashAndBits = (squeakHashAndBits & ~SqueakImageConstants.IDENTITY_HASH_HALF_WORD_MASK) + newHash;
     }
 
-    public final boolean getMarkingFlag() {
-        return (squeakHashAndBits & MARK_BIT) != 0;
+    /**
+     * @return <tt>true</tt> if marked with <tt>currentMarkingFlag</tt>, <tt>false</tt> otherwise;
+     *         thread safe
+     */
+    public final boolean isMarkedWith(final boolean currentMarkingFlag) {
+        return (((int) HASH_AND_BITS_HANDLE.getAcquire(this) & MARK_BIT) != 0) == currentMarkingFlag;
     }
 
-    private void toggleMarkingFlag() {
-        squeakHashAndBits ^= MARK_BIT;
+    /**
+     * Mark this object; thread safe.
+     *
+     * @return <tt>false</tt> if already marked, <tt>true</tt> otherwise
+     */
+    public final boolean tryToMarkWith(final boolean currentMarkingFlag) {
+        int oldValue, newValue;
+        /*
+         * Atomically set the new value with release semantics. If another thread modified 'flags'
+         * since our getAcquire, CAS will fail and we retry.
+         */
+        do {
+            oldValue = (int) HASH_AND_BITS_HANDLE.getAcquire(this);
+            if (((oldValue & MARK_BIT) != 0) == currentMarkingFlag) {
+                return false; // Already marked
+            }
+            newValue = oldValue ^ MARK_BIT; // Flip MARK_BIT
+        } while (!HASH_AND_BITS_HANDLE.compareAndSet(this, oldValue, newValue));
+        return true; // Successfully marked
     }
 
-    public final boolean isMarked(final boolean currentMarkingFlag) {
-        return getMarkingFlag() == currentMarkingFlag;
+    /**
+     * Unmark this object; thread safe.
+     */
+    public final void unmarkWith(final boolean currentMarkingFlag) {
+        tryToMarkWith(!currentMarkingFlag);
     }
 
     @Override
@@ -173,23 +214,6 @@ public final Object send(final SqueakImageContext image, final String selector,
         }
     }
 
-    /**
-     * @return <tt>false</tt> if already marked, <tt>true</tt> otherwise
-     */
-    public final boolean tryToMark(final boolean currentMarkingFlag) {
-        if (getMarkingFlag() == currentMarkingFlag) {
-            return false;
-        } else {
-            toggleMarkingFlag();
-            return true;
-        }
-    }
-
-    public final void unmark(final boolean currentMarkingFlag) {
-        assert getMarkingFlag() == currentMarkingFlag : "Object not marked with currentMarkingFlag: " + currentMarkingFlag;
-        toggleMarkingFlag();
-    }
-
     public void pointersBecomeOneWay(@SuppressWarnings("unused") final Object fromPointer, @SuppressWarnings("unused") final Object toPointer) {
         // Do nothing by default.
     }
diff --git a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/EphemeronObject.java b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/EphemeronObject.java
index b9c121c31..b6f62d294 100644
--- a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/EphemeronObject.java
+++ b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/EphemeronObject.java
@@ -118,9 +118,9 @@ public void setHasBeenSignaled() {
         hasBeenSignaled = true;
     }
 
-    public boolean keyHasBeenMarked(final boolean currentMarkingFlag) {
+    public boolean keyHasBeenMarked(final ObjectTracer tracer) {
         if (instVarAt0Slow(ObjectLayouts.EPHEMERON.KEY) instanceof final AbstractSqueakObjectWithClassAndHash key) {
-            return key.isMarked(currentMarkingFlag);
+            return tracer.isMarked(key);
         } else {
             return true;
         }
diff --git a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/nodes/primitives/impl/StoragePrimitives.java b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/nodes/primitives/impl/StoragePrimitives.java
index 7763292ad..4cd247e2c 100644
--- a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/nodes/primitives/impl/StoragePrimitives.java
+++ b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/nodes/primitives/impl/StoragePrimitives.java
@@ -54,7 +54,6 @@
 import de.hpi.swa.trufflesqueak.nodes.primitives.Primitive.Primitive2WithFallback;
 import de.hpi.swa.trufflesqueak.nodes.primitives.Primitive.Primitive3WithFallback;
 import de.hpi.swa.trufflesqueak.nodes.primitives.SqueakPrimitive;
-import de.hpi.swa.trufflesqueak.util.ArrayUtils;
 import de.hpi.swa.trufflesqueak.util.MiscUtils;
 
 public final class StoragePrimitives extends AbstractPrimitiveFactoryHolder {
@@ -514,7 +513,7 @@ protected abstract static class PrimAllObjectsNode extends AbstractPrimitiveNode
         @Specialization
         protected final ArrayObject doAll(@SuppressWarnings("unused") final Object receiver) {
             final SqueakImageContext image = getContext();
-            return image.asArrayOfObjects(ArrayUtils.toArray(image.objectGraphUtils.allInstances()));
+            return image.asArrayOfObjects(image.objectGraphUtils.allInstances());
         }
     }
 
diff --git a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/FrameAccess.java b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/FrameAccess.java
index 29c71daa8..422c39db7 100644
--- a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/FrameAccess.java
+++ b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/FrameAccess.java
@@ -12,6 +12,7 @@
 import com.oracle.truffle.api.CompilerAsserts;
 import com.oracle.truffle.api.CompilerDirectives;
 import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
+import com.oracle.truffle.api.RootCallTarget;
 import com.oracle.truffle.api.Truffle;
 import com.oracle.truffle.api.frame.Frame;
 import com.oracle.truffle.api.frame.FrameDescriptor;
@@ -31,6 +32,7 @@
 import de.hpi.swa.trufflesqueak.model.NativeObject;
 import de.hpi.swa.trufflesqueak.model.NilObject;
 import de.hpi.swa.trufflesqueak.model.PointersObject;
+import de.hpi.swa.trufflesqueak.nodes.ResumeContextRootNode;
 import de.hpi.swa.trufflesqueak.nodes.context.frame.FrameStackPushNode;
 import de.hpi.swa.trufflesqueak.nodes.context.frame.GetContextOrMarkerNode;
 
@@ -472,25 +474,51 @@ public static void assertReceiverNotNull(final Frame frame) {
         assert getReceiver(frame) != null : "Receiver should not be null";
     }
 
+    public static ContextObject getResumingContextObjectOrSkip(final FrameInstance frameInstance) {
+        if (frameInstance.getCallTarget() instanceof final RootCallTarget rct && rct.getRootNode() instanceof final ResumeContextRootNode rcrn) {
+            /*
+             * Reached end of Smalltalk activations on Truffle frames. From here, tracing should
+             * continue to walk senders via ContextObjects.
+             */
+            return rcrn.getActiveContext(); // break
+        } else {
+            return null; // skip
+        }
+    }
+
     @TruffleBoundary
     public static MaterializedFrame findFrameForMarker(final FrameMarker frameMarker) {
         CompilerDirectives.bailout("Finding materializable frames should never be part of compiled code as it triggers deopts");
         LogUtils.ITERATE_FRAMES.fine("Iterating frames to find a marker...");
-        final Frame frame = Truffle.getRuntime().iterateFrames(frameInstance -> {
+        final Object frameOrResumingContextObject = Truffle.getRuntime().iterateFrames(frameInstance -> {
             final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_ONLY);
             if (!isTruffleSqueakFrame(current)) {
-                return null;
+                return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
             }
             LogUtils.ITERATE_FRAMES.fine(() -> "..." + FrameAccess.getCodeObject(current).toString());
             if (frameMarker == getMarker(current)) {
-                return frameInstance.getFrame(FrameInstance.FrameAccess.MATERIALIZE);
+                return frameInstance.getFrame(FrameInstance.FrameAccess.MATERIALIZE).materialize();
+            } else {
+                return null; // continue with next frame
             }
-            return null;
         });
-        if (frame == null) {
-            throw SqueakException.create("Could not find frame for:", frameMarker);
-        } else {
-            return frame.materialize();
+        if (frameOrResumingContextObject instanceof final MaterializedFrame materializedFrame) {
+            return materializedFrame;
+        } else if (frameOrResumingContextObject instanceof final ContextObject resumingContextObject) {
+            ContextObject currentContext = resumingContextObject;
+            while (true) {
+                if (currentContext.getFrameMarker() == frameMarker) {
+                    return currentContext.getTruffleFrame(); // success
+                }
+                final AbstractSqueakObject sender = currentContext.getMaterializedSender();
+                if (sender instanceof final ContextObject senderContext) {
+                    currentContext = senderContext;
+                } else { // end of chain
+                    assert sender == NilObject.SINGLETON;
+                    break; // fail
+                }
+            }
         }
+        throw SqueakException.create("Could not find frame for:", frameMarker);
     }
 }
diff --git a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/ObjectGraphUtils.java b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/ObjectGraphUtils.java
index 9fa372c7b..8550ffc94 100644
--- a/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/ObjectGraphUtils.java
+++ b/src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/ObjectGraphUtils.java
@@ -6,9 +6,14 @@
  */
 package de.hpi.swa.trufflesqueak.util;
 
-import java.util.AbstractCollection;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 import java.util.logging.Level;
 
 import org.graalvm.collections.EconomicMap;
@@ -16,8 +21,8 @@
 import org.graalvm.collections.UnmodifiableEconomicMap;
 
 import com.oracle.truffle.api.CompilerAsserts;
+import com.oracle.truffle.api.CompilerDirectives;
 import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
-import com.oracle.truffle.api.RootCallTarget;
 import com.oracle.truffle.api.Truffle;
 import com.oracle.truffle.api.frame.Frame;
 import com.oracle.truffle.api.frame.FrameInstance;
@@ -29,17 +34,15 @@
 import de.hpi.swa.trufflesqueak.model.ContextObject;
 import de.hpi.swa.trufflesqueak.model.EphemeronObject;
 import de.hpi.swa.trufflesqueak.model.NilObject;
-import de.hpi.swa.trufflesqueak.nodes.ResumeContextRootNode;
 
 public final class ObjectGraphUtils {
     private static final int ADDITIONAL_SPACE = 10_000;
-
     private static int lastSeenObjects = 500_000;
+    private static final int USABLE_THREAD_COUNT = Math.min(Runtime.getRuntime().availableProcessors(), 4);
+    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(USABLE_THREAD_COUNT, r -> new Thread(r, "TruffleSqueakObjectGraphUtils"));
 
     private final SqueakImageContext image;
     private final boolean trackOperations;
-    /* Using a stack for DFS traversal when walking Smalltalk objects. */
-    private final ArrayDeque<AbstractSqueakObjectWithClassAndHash> workStack = new ArrayDeque<>();
 
     public ObjectGraphUtils(final SqueakImageContext image) {
         this.image = image;
@@ -50,71 +53,146 @@ public static int getLastSeenObjects() {
         return lastSeenObjects;
     }
 
+    public SqueakImageContext getImage() {
+        return image;
+    }
+
+    static final class AllInstancesTask implements Runnable {
+        private final ObjectTracer roots;
+        private final ArrayDeque<AbstractSqueakObjectWithClassAndHash> objects;
+
+        AllInstancesTask(final ObjectTracer theRoots, final ArrayDeque<AbstractSqueakObjectWithClassAndHash> theObjects) {
+            roots = theRoots;
+            objects = theObjects;
+        }
+
+        public void run() {
+            final ObjectTracer tracer = roots.copyEmpty();
+            AbstractSqueakObjectWithClassAndHash root;
+            while ((root = roots.getNextWithLock()) != null) {
+                AbstractSqueakObjectWithClassAndHash currentObject = root;
+                do {
+                    objects.add(currentObject);
+                    tracer.tracePointers(currentObject);
+                } while ((currentObject = tracer.getNext()) != null);
+            }
+        }
+    }
+
     @TruffleBoundary
-    public AbstractCollection<AbstractSqueakObjectWithClassAndHash> allInstances() {
+    public Object[] allInstances() {
         final long startTime = System.nanoTime();
-        final ArrayDeque<AbstractSqueakObjectWithClassAndHash> seen = new ArrayDeque<>(lastSeenObjects + ADDITIONAL_SPACE);
-        final ObjectTracer pending = new ObjectTracer(true);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                seen.add(currentObject);
-                pending.tracePointers(currentObject);
+
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, true);
+
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        final List<ArrayDeque<AbstractSqueakObjectWithClassAndHash>> objectsList = new ArrayList<>(USABLE_THREAD_COUNT);
+        final int initialSize = (lastSeenObjects + ADDITIONAL_SPACE) / USABLE_THREAD_COUNT;
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            final ArrayDeque<AbstractSqueakObjectWithClassAndHash> objects = new ArrayDeque<>(initialSize);
+            objectsList.add(objects);
+            tasks[i] = new AllInstancesTask(roots, objects);
+        }
+        runTasks(tasks);
+
+        int totalSize = 0;
+        for (ArrayDeque<AbstractSqueakObjectWithClassAndHash> deque : objectsList) {
+            totalSize += deque.size();
+        }
+        lastSeenObjects = totalSize;
+
+        final AbstractSqueakObjectWithClassAndHash[] result = new AbstractSqueakObjectWithClassAndHash[totalSize];
+        int i = 0;
+        for (final ArrayDeque<AbstractSqueakObjectWithClassAndHash> deque : objectsList) {
+            for (final AbstractSqueakObjectWithClassAndHash value : deque) {
+                result[i++] = value;
             }
         }
-        lastSeenObjects = seen.size();
+
         if (trackOperations) {
             ObjectGraphOperations.ALL_INSTANCES.addNanos(System.nanoTime() - startTime);
         }
-        return seen;
+        return result;
+    }
+
+    static final class AllInstancesOfTask implements Runnable {
+        private final ClassObject targetClass;
+        private final ObjectTracer roots;
+        private final ArrayDeque<AbstractSqueakObjectWithClassAndHash> objects;
+
+        AllInstancesOfTask(final ClassObject theTargetClass, final ObjectTracer theRoots, final ArrayDeque<AbstractSqueakObjectWithClassAndHash> theObjects) {
+            targetClass = theTargetClass;
+            roots = theRoots;
+            objects = theObjects;
+        }
+
+        public void run() {
+            final ObjectTracer tracer = roots.copyEmpty();
+            AbstractSqueakObjectWithClassAndHash root;
+            while ((root = roots.getNextWithLock()) != null) {
+                AbstractSqueakObjectWithClassAndHash currentObject = root;
+                do {
+                    if (targetClass == currentObject.getSqueakClass()) {
+                        objects.add(currentObject);
+                    }
+                    tracer.tracePointers(currentObject);
+                } while ((currentObject = tracer.getNext()) != null);
+            }
+        }
     }
 
     @TruffleBoundary
     public Object[] allInstancesOf(final ClassObject targetClass) {
         final long startTime = System.nanoTime();
-        final ArrayDeque<AbstractSqueakObjectWithClassAndHash> result = new ArrayDeque<>();
-        final ObjectTracer pending = new ObjectTracer(true);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                if (targetClass == currentObject.getSqueakClass()) {
-                    result.add(currentObject);
-                }
-                pending.tracePointers(currentObject);
+
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, true);
+
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        final List<ArrayDeque<AbstractSqueakObjectWithClassAndHash>> objectsList = new ArrayList<>(USABLE_THREAD_COUNT);
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            final ArrayDeque<AbstractSqueakObjectWithClassAndHash> objects = new ArrayDeque<>();
+            objectsList.add(objects);
+            tasks[i] = new AllInstancesOfTask(targetClass, roots, objects);
+        }
+        runTasks(tasks);
+
+        int totalSize = 0;
+        for (ArrayDeque<AbstractSqueakObjectWithClassAndHash> deque : objectsList) {
+            totalSize += deque.size();
+        }
+
+        final AbstractSqueakObjectWithClassAndHash[] result = new AbstractSqueakObjectWithClassAndHash[totalSize];
+        int i = 0;
+        for (final ArrayDeque<AbstractSqueakObjectWithClassAndHash> deque : objectsList) {
+            for (final AbstractSqueakObjectWithClassAndHash value : deque) {
+                result[i++] = value;
             }
         }
+
         if (trackOperations) {
             ObjectGraphOperations.ALL_INSTANCES_OF.addNanos(System.nanoTime() - startTime);
         }
-        return result.toArray();
+        return result;
     }
 
     @TruffleBoundary
     public AbstractSqueakObject someInstanceOf(final ClassObject targetClass) {
         final long startTime = System.nanoTime();
         final ArrayDeque<AbstractSqueakObjectWithClassAndHash> marked = new ArrayDeque<>(lastSeenObjects / 2);
-        final ObjectTracer pending = new ObjectTracer(true);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
+        final ObjectTracer tracer = ObjectTracer.fromRoots(image, true);
         AbstractSqueakObject result = NilObject.SINGLETON;
         AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                marked.add(currentObject);
-                if (targetClass == currentObject.getSqueakClass()) {
-                    clearWorkStack();
-                    // Unmark marked objects
-                    for (final AbstractSqueakObjectWithClassAndHash object : marked) {
-                        object.unmark(currentMarkingFlag);
-                    }
-                    // Restore marking flag
-                    image.toggleCurrentMarkingFlag();
-                    result = currentObject;
-                    break;
-                } else {
-                    pending.tracePointers(currentObject);
-                }
+        while ((currentObject = tracer.getNext()) != null) {
+            marked.add(currentObject);
+            if (targetClass == currentObject.getSqueakClass()) {
+                result = currentObject;
+                // Unmark marked objects
+                tracer.unmarkAll(marked);
+                // Restore marking flag
+                image.toggleCurrentMarkingFlag();
+                break;
+            } else {
+                tracer.tracePointers(currentObject);
             }
         }
         if (trackOperations) {
@@ -125,33 +203,30 @@ public AbstractSqueakObject someInstanceOf(final ClassObject targetClass) {
 
     @TruffleBoundary
     public AbstractSqueakObject nextObject(final AbstractSqueakObjectWithClassAndHash targetObject) {
+        // Should not be parallelized since Smalltalk is assuming a fixed order enumeration
+        // of all live objects and a parallel search is likely to produce a different result
+        // each time it is called.
         final long startTime = System.nanoTime();
         final ArrayDeque<AbstractSqueakObjectWithClassAndHash> marked = new ArrayDeque<>(lastSeenObjects / 2);
-        final ObjectTracer pending = new ObjectTracer(true);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject = getNextFromWorkStack();
+        final ObjectTracer tracer = ObjectTracer.fromRoots(image, true);
+        AbstractSqueakObjectWithClassAndHash currentObject = tracer.getNext();
         AbstractSqueakObject result = currentObject; // first object
         boolean foundObject = false;
         while (currentObject != null) {
             if (foundObject) {
-                clearWorkStack();
+                result = currentObject;
                 // Unmark marked objects
-                for (final AbstractSqueakObjectWithClassAndHash object : marked) {
-                    object.unmark(currentMarkingFlag);
-                }
+                tracer.unmarkAll(marked);
                 // Restore marking flag
                 image.toggleCurrentMarkingFlag();
-                result = currentObject;
                 break;
             }
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                marked.add(currentObject);
-                if (currentObject == targetObject) {
-                    foundObject = true;
-                }
-                pending.tracePointers(currentObject);
+            marked.add(currentObject);
+            if (currentObject == targetObject) {
+                foundObject = true;
             }
-            currentObject = getNextFromWorkStack();
+            tracer.tracePointers(currentObject);
+            currentObject = tracer.getNext();
         }
         if (trackOperations) {
             ObjectGraphOperations.NEXT_OBJECT.addNanos(System.nanoTime() - startTime);
@@ -172,15 +247,59 @@ public void pointersBecomeOneWay(final Object[] fromPointers, final Object[] toP
         }
     }
 
+    static final class BecomeOneWaySinglePairTask implements Runnable {
+        private final ObjectTracer roots;
+        private final Object from;
+        private final Object to;
+
+        BecomeOneWaySinglePairTask(final ObjectTracer theRoots, final Object fromPointer, final Object toPointer) {
+            roots = theRoots;
+            from = fromPointer;
+            to = toPointer;
+        }
+
+        public void run() {
+            final ObjectTracer tracer = roots.copyEmpty();
+            AbstractSqueakObjectWithClassAndHash root;
+            while ((root = roots.getNextWithLock()) != null) {
+                AbstractSqueakObjectWithClassAndHash currentObject = root;
+                do {
+                    currentObject.pointersBecomeOneWay(from, to);
+                    tracer.tracePointers(currentObject);
+                } while ((currentObject = tracer.getNext()) != null);
+            }
+        }
+    }
+
     private void pointersBecomeOneWaySinglePair(final Object fromPointer, final Object toPointer) {
-        final ObjectTracer pending = new ObjectTracer(false);
-        pointersBecomeOneWayFrames(pending, fromPointer, toPointer);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                currentObject.pointersBecomeOneWay(fromPointer, toPointer);
-                pending.tracePointers(currentObject);
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, false);
+        pointersBecomeOneWayFrames(roots, fromPointer, toPointer);
+
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            tasks[i] = new BecomeOneWaySinglePairTask(roots, fromPointer, toPointer);
+        }
+        runTasks(tasks);
+    }
+
+    static final class BecomeOneWayManyPairsTask implements Runnable {
+        private final ObjectTracer roots;
+        private final EconomicMap<Object, Object> fromToMap;
+
+        BecomeOneWayManyPairsTask(final ObjectTracer theRoots, final EconomicMap<Object, Object> theFromToMap) {
+            roots = theRoots;
+            fromToMap = theFromToMap;
+        }
+
+        public void run() {
+            final ObjectTracer tracer = roots.copyEmpty();
+            AbstractSqueakObjectWithClassAndHash root;
+            while ((root = roots.getNextWithLock()) != null) {
+                AbstractSqueakObjectWithClassAndHash currentObject = root;
+                do {
+                    currentObject.pointersBecomeOneWay(fromToMap);
+                    tracer.tracePointers(currentObject);
+                } while ((currentObject = tracer.getNext()) != null);
             }
         }
     }
@@ -190,24 +309,23 @@ private void pointersBecomeOneWayManyPairs(final Object[] fromPointers, final Ob
         for (int i = 0; i < fromPointers.length; i++) {
             fromToMap.put(fromPointers[i], toPointers[i]);
         }
-        final ObjectTracer pending = new ObjectTracer(false);
-        pointersBecomeOneWayFrames(pending, fromToMap);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                currentObject.pointersBecomeOneWay(fromToMap);
-                pending.tracePointers(currentObject);
-            }
+
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, false);
+        pointersBecomeOneWayFrames(roots, fromToMap);
+
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            tasks[i] = new BecomeOneWayManyPairsTask(roots, fromToMap);
         }
+        runTasks(tasks);
     }
 
     @TruffleBoundary
     private static void pointersBecomeOneWayFrames(final ObjectTracer tracer, final Object fromPointer, final Object toPointer) {
-        Truffle.getRuntime().iterateFrames((frameInstance) -> {
+        final ContextObject resumeContextObject = Truffle.getRuntime().iterateFrames((frameInstance) -> {
             final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_WRITE);
             if (!FrameAccess.isTruffleSqueakFrame(current)) {
-                return null;
+                return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
             }
             final Object[] arguments = current.getArguments();
             for (int i = 0; i < arguments.length; i++) {
@@ -245,14 +363,16 @@ private static void pointersBecomeOneWayFrames(final ObjectTracer tracer, final
             });
             return null;
         });
+        assert resumeContextObject != null : "Failed to find ResumeContextRootNode";
+        tracer.addIfUnmarked(resumeContextObject);
     }
 
     @TruffleBoundary
     private static void pointersBecomeOneWayFrames(final ObjectTracer tracer, final UnmodifiableEconomicMap<Object, Object> fromToMap) {
-        Truffle.getRuntime().iterateFrames((frameInstance) -> {
+        final ContextObject resumeContextObject = Truffle.getRuntime().iterateFrames((frameInstance) -> {
             final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_WRITE);
             if (!FrameAccess.isTruffleSqueakFrame(current)) {
-                return null;
+                return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
             }
             final Object[] arguments = current.getArguments();
             for (int i = 0; i < arguments.length; i++) {
@@ -295,42 +415,67 @@ private static void pointersBecomeOneWayFrames(final ObjectTracer tracer, final
             });
             return null;
         });
+        assert resumeContextObject != null : "Failed to find ResumeContextRootNode";
+        tracer.addIfUnmarked(resumeContextObject);
     }
 
-    @TruffleBoundary
-    public boolean checkEphemerons() {
-        final long startTime = System.nanoTime();
-        final ObjectTracer pending = new ObjectTracer(true);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        final ArrayDeque<EphemeronObject> ephemeronsToBeMarked = new ArrayDeque<>();
-        AbstractSqueakObjectWithClassAndHash currentObject;
+    static final class EphemeronsTask implements Runnable {
+        private final ObjectTracer roots;
+        private final ArrayDeque<EphemeronObject> ephemeronsToBeTraced;
 
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                // Ephemerons are traced in a special way.
-                if (currentObject instanceof final EphemeronObject ephemeronObject) {
-                    // An Ephemeron is traced normally if it has been signaled or its key has been
-                    // marked already.
-                    // Otherwise, they are traced after all other objects.
-                    if (ephemeronObject.hasBeenSignaled() || ephemeronObject.keyHasBeenMarked(currentMarkingFlag)) {
-                        pending.tracePointers(currentObject);
+        EphemeronsTask(final ObjectTracer theRoots, final ArrayDeque<EphemeronObject> ephemerons) {
+            roots = theRoots;
+            ephemeronsToBeTraced = ephemerons;
+        }
+
+        public void run() {
+            final ObjectTracer tracer = roots.copyEmpty();
+            AbstractSqueakObjectWithClassAndHash root;
+            while ((root = roots.getNextWithLock()) != null) {
+                AbstractSqueakObjectWithClassAndHash currentObject = root;
+                do {
+                    // Ephemerons are traced in a special way.
+                    if (currentObject instanceof final EphemeronObject ephemeronObject) {
+                        // An Ephemeron is traced normally if it has been signaled or its key has
+                        // been marked already. Otherwise, they are traced after all other objects.
+                        if (ephemeronObject.hasBeenSignaled() || ephemeronObject.keyHasBeenMarked(tracer)) {
+                            tracer.tracePointers(currentObject);
+                        } else {
+                            synchronized (ephemeronsToBeTraced) {
+                                ephemeronsToBeTraced.add(ephemeronObject);
+                            }
+                        }
                     } else {
-                        ephemeronsToBeMarked.add(ephemeronObject);
+                        // Normal object
+                        tracer.tracePointers(currentObject);
                     }
-                } else {
-                    // Normal object
-                    pending.tracePointers(currentObject);
-                }
+                } while ((currentObject = tracer.getNext()) != null);
             }
         }
+    }
+
+    @TruffleBoundary
+    public boolean checkEphemerons() {
+        final long startTime = System.nanoTime();
+
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, true);
+
+        // Mark and trace all non-ephemeron objects. Mark and trace ephemerons that have
+        // been signaled or whose keys have been marked. Save all other ephemerons for later.
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        final ArrayDeque<EphemeronObject> ephemeronsToBeTraced = new ArrayDeque<>();
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            tasks[i] = new EphemeronsTask(roots, ephemeronsToBeTraced);
+        }
+        runTasks(tasks);
 
         // Now, trace the ephemerons until there are only ephemerons whose keys are reachable
         // through ephemerons.
-        traceRemainingEphemerons(ephemeronsToBeMarked, pending, currentMarkingFlag);
+        traceRemainingEphemerons(ephemeronsToBeTraced, roots);
 
         // Make sure that they do not signal more than once.
-        image.ephemeronsQueue.addAll(ephemeronsToBeMarked);
-        for (EphemeronObject ephemeronObject : ephemeronsToBeMarked) {
+        image.ephemeronsQueue.addAll(ephemeronsToBeTraced);
+        for (EphemeronObject ephemeronObject : ephemeronsToBeTraced) {
             ephemeronObject.setHasBeenSignaled();
         }
         if (trackOperations) {
@@ -339,7 +484,7 @@ public boolean checkEphemerons() {
         return true;
     }
 
-    private void traceRemainingEphemerons(final ArrayDeque<EphemeronObject> ephemeronsToBeMarked, final ObjectTracer pending, final boolean currentMarkingFlag) {
+    private static void traceRemainingEphemerons(final ArrayDeque<EphemeronObject> ephemeronsToBeMarked, final ObjectTracer tracer) {
         // Trace the ephemerons that have marked keys until there are only ephemerons with unmarked
         // keys left.
         while (true) {
@@ -347,8 +492,8 @@ private void traceRemainingEphemerons(final ArrayDeque<EphemeronObject> ephemero
             final Iterator<EphemeronObject> iterator = ephemeronsToBeMarked.iterator();
             while (iterator.hasNext()) {
                 final EphemeronObject ephemeronObject = iterator.next();
-                if (ephemeronObject.keyHasBeenMarked(currentMarkingFlag)) {
-                    pending.tracePointers(ephemeronObject);
+                if (ephemeronObject.keyHasBeenMarked(tracer)) {
+                    tracer.tracePointers(ephemeronObject);
                     iterator.remove();
                     finished = false;
                 }
@@ -356,7 +501,7 @@ private void traceRemainingEphemerons(final ArrayDeque<EphemeronObject> ephemero
             if (finished) {
                 break;
             }
-            finishPendingMarking(pending, currentMarkingFlag);
+            finishPendingMarking(tracer);
         }
 
         if (ephemeronsToBeMarked.isEmpty()) {
@@ -366,61 +511,78 @@ private void traceRemainingEphemerons(final ArrayDeque<EphemeronObject> ephemero
         // Now, we have ephemerons whose keys are reachable only through ephemerons.
         // Mark them to keep consistent marking flags.
         for (EphemeronObject ephemeronObject : ephemeronsToBeMarked) {
-            pending.tracePointers(ephemeronObject);
+            tracer.tracePointers(ephemeronObject);
         }
-        finishPendingMarking(pending, currentMarkingFlag);
+        finishPendingMarking(tracer);
     }
 
-    private void finishPendingMarking(final ObjectTracer pending, final boolean currentMarkingFlag) {
+    private static void finishPendingMarking(final ObjectTracer tracer) {
         AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                pending.tracePointers(currentObject);
-            }
+        while ((currentObject = tracer.getNext()) != null) {
+            tracer.tracePointers(currentObject);
         }
     }
 
-    private AbstractSqueakObjectWithClassAndHash getNextFromWorkStack() {
-        return workStack.pollFirst();
-    }
-
-    private void clearWorkStack() {
-        workStack.clear();
+    private static void runTasks(final Runnable[] tasks) {
+        try {
+            final Future<?>[] futures = new Future[USABLE_THREAD_COUNT];
+            for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+                futures[i] = EXECUTOR.submit(tasks[i]);
+            }
+            for (Future<?> future : futures) {
+                future.get();
+            }
+        } catch (final ExecutionException | InterruptedException e) {
+            throw CompilerDirectives.shouldNotReachHere("runTasks was interrupted");
+        }
     }
 
-    public final class ObjectTracer {
+    public static final class ObjectTracer {
+        /* Using a stack for DFS traversal when walking Smalltalk objects. */
+        private final ArrayDeque<AbstractSqueakObjectWithClassAndHash> workStack = new ArrayDeque<>();
         private final boolean currentMarkingFlag;
 
-        private ObjectTracer(final boolean addObjectsFromFrames) {
-            assert workStack.isEmpty() : "workQueue should be empty";
-            // Flip the marking flag
-            currentMarkingFlag = image.toggleCurrentMarkingFlag();
-            // Add roots
-            addIfUnmarked(image.specialObjectsArray);
+        private ObjectTracer(final boolean markingFlag) {
+            this.currentMarkingFlag = markingFlag;
+        }
+
+        /** Return an empty traversal based on this root traversal. */
+        public ObjectTracer copyEmpty() {
+            return new ObjectTracer(currentMarkingFlag);
+        }
+
+        /**
+         * Return an ObjectTracer initialized with the roots and optionally including objects in the
+         * Truffle frames. Flips the global marking flag.
+         */
+        private static ObjectTracer fromRoots(final SqueakImageContext image, final boolean addObjectsFromFrames) {
+            final ObjectTracer tracer = new ObjectTracer(image.toggleCurrentMarkingFlag());
+            tracer.addRoots(image, addObjectsFromFrames);
+            return tracer;
+        }
+
+        private void addRoots(final SqueakImageContext image, final boolean addObjectsFromFrames) {
+            // Add roots, in reversed order because workStack is LIFO
             if (addObjectsFromFrames) {
                 addObjectsFromFrames();
             }
             // Unreachable ephemerons in the queue must be kept visible to the rest of the image.
             // These are technically "dead" and do not need to be saved when the image is stored on
-            // disk,
-            // but by tracing them we avoid an expensive reachability test in the fetch-next-mourner
-            // primitive.
+            // disk, but by tracing them we avoid an expensive reachability test in the
+            // fetch-next-mourner primitive.
             workStack.addAll(image.ephemeronsQueue);
+
+            // Trace the special objects to give separate roots to the threads.
+            addIfUnmarked(image.specialObjectsArray);
+            tracePointers(image.specialObjectsArray);
         }
 
         private void addObjectsFromFrames() {
             CompilerAsserts.neverPartOfCompilation();
             final ContextObject resumeContextObject = Truffle.getRuntime().iterateFrames(frameInstance -> {
-                if (frameInstance.getCallTarget() instanceof final RootCallTarget rct && rct.getRootNode() instanceof final ResumeContextRootNode rcrn) {
-                    /*
-                     * Reached end of Smalltalk activations on Truffle frames. From here, tracing
-                     * should continue to walk senders via ContextObjects.
-                     */
-                    return rcrn.getActiveContext(); // break
-                }
                 final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_ONLY);
                 if (!FrameAccess.isTruffleSqueakFrame(current)) {
-                    return null; // skip
+                    return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
                 }
                 addAllIfUnmarked(current.getArguments());
                 addIfUnmarked(FrameAccess.getContext(current));
@@ -435,8 +597,20 @@ private void addObjectsFromFrames() {
             addIfUnmarked(resumeContextObject);
         }
 
+        private AbstractSqueakObjectWithClassAndHash getNext() {
+            return workStack.pollFirst();
+        }
+
+        private synchronized AbstractSqueakObjectWithClassAndHash getNextWithLock() {
+            return workStack.pollFirst();
+        }
+
+        public boolean isMarked(final AbstractSqueakObjectWithClassAndHash object) {
+            return object.isMarkedWith(currentMarkingFlag);
+        }
+
         public void addIfUnmarked(final Object object) {
-            if (object instanceof final AbstractSqueakObjectWithClassAndHash o && !o.isMarked(currentMarkingFlag)) {
+            if ((object instanceof final AbstractSqueakObjectWithClassAndHash o) && o.tryToMarkWith(currentMarkingFlag)) {
                 workStack.addFirst(o);
             }
         }
@@ -448,8 +622,20 @@ public void addAllIfUnmarked(final Object[] objects) {
         }
 
         private void tracePointers(final AbstractSqueakObjectWithClassAndHash object) {
-            addIfUnmarked(object.getSqueakClass());
             object.tracePointers(this);
+            addIfUnmarked(object.getSqueakClass());
+        }
+
+        /**
+         * Unmark all objects remaining in the object graph traversal AND in the argument.
+         */
+        private void unmarkAll(final ArrayDeque<AbstractSqueakObjectWithClassAndHash> objects) {
+            for (final AbstractSqueakObjectWithClassAndHash object : workStack) {
+                object.unmarkWith(currentMarkingFlag);
+            }
+            for (final AbstractSqueakObjectWithClassAndHash object : objects) {
+                object.unmarkWith(currentMarkingFlag);
+            }
         }
     }
 


Reviews:
user: login: fniephaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-24T12:53:04Z
user: login: fniephaus
body: LGTM, thanks a lot for another great contribution!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-25T09:05:51Z

Review comments:
diff_hunk: @@ -61,7 +79,7 @@ private AbstractSqueakObjectWithClassAndHash(final boolean markingFlag, final Cl
         squeakHashAndBits = AbstractSqueakObjectWithClassAndHash.HASH_UNINITIALIZED;
         squeakClass = klass;
         if (markingFlag) {
-            toggleMarkingFlag();
+            tryToMarkTrue();
path: src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/model/AbstractSqueakObjectWithClassAndHash.java
user: login: fniephaus
body: This is on the hot path and apparently, the VarHandle cannot be de-virtualized, probably because this is an abstract class.
Since we don't need a thread-safe version here, maybe revert this to the old version here which doesn't run into this problem.
created_at: 2025-05-24T12:53:04Z
author_association: MEMBER
diff_hunk: @@ -472,25 +474,51 @@ public static void assertReceiverNotNull(final Frame frame) {
         assert getReceiver(frame) != null : "Receiver should not be null";
     }
 
+    public static ContextObject getResumingContextObjectOrSkip(final FrameInstance frameInstance) {
+        if (frameInstance.getCallTarget() instanceof final RootCallTarget rct && rct.getRootNode() instanceof final ResumeContextRootNode rcrn) {
+            /*
+             * Reached end of Smalltalk activations on Truffle frames. From here, tracing should
+             * continue to walk senders via ContextObjects.
+             */
+            return rcrn.getActiveContext(); // break
+        } else {
+            return null; // skip
+        }
+    }
+
     @TruffleBoundary
     public static MaterializedFrame findFrameForMarker(final FrameMarker frameMarker) {
         CompilerDirectives.bailout("Finding materializable frames should never be part of compiled code as it triggers deopts");
         LogUtils.ITERATE_FRAMES.fine("Iterating frames to find a marker...");
-        final Frame frame = Truffle.getRuntime().iterateFrames(frameInstance -> {
+        final Object frameOrResumingContextObject = Truffle.getRuntime().iterateFrames(frameInstance -> {
             final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_ONLY);
             if (!isTruffleSqueakFrame(current)) {
-                return null;
+                return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
             }
             LogUtils.ITERATE_FRAMES.fine(() -> "..." + FrameAccess.getCodeObject(current).toString());
             if (frameMarker == getMarker(current)) {
-                return frameInstance.getFrame(FrameInstance.FrameAccess.MATERIALIZE);
+                return frameInstance.getFrame(FrameInstance.FrameAccess.MATERIALIZE).materialize();
+            } else {
+                return null; // continue with next frame
             }
-            return null;
         });
-        if (frame == null) {
-            throw SqueakException.create("Could not find frame for:", frameMarker);
-        } else {
-            return frame.materialize();
+        if (frameOrResumingContextObject instanceof final MaterializedFrame materializedFrame) {
+            return materializedFrame;
+        } else if (frameOrResumingContextObject instanceof final ContextObject resumingContextObject) {
+            ContextObject currentContext = resumingContextObject;
+            while (true) {
path: src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/FrameAccess.java
user: login: fniephaus
body: Instead of checking only the resuming context, I believe this needs to walk the sender chain until the end, which is what it is doing now.
created_at: 2025-05-25T09:04:03Z
author_association: MEMBER
diff_hunk: @@ -190,24 +309,23 @@ private void pointersBecomeOneWayManyPairs(final Object[] fromPointers, final Ob
         for (int i = 0; i < fromPointers.length; i++) {
             fromToMap.put(fromPointers[i], toPointers[i]);
         }
-        final ObjectTracer pending = new ObjectTracer(false);
-        pointersBecomeOneWayFrames(pending, fromToMap);
-        final boolean currentMarkingFlag = pending.currentMarkingFlag;
-        AbstractSqueakObjectWithClassAndHash currentObject;
-        while ((currentObject = getNextFromWorkStack()) != null) {
-            if (currentObject.tryToMark(currentMarkingFlag)) {
-                currentObject.pointersBecomeOneWay(fromToMap);
-                pending.tracePointers(currentObject);
-            }
+
+        final ObjectTracer roots = ObjectTracer.fromRoots(image, false);
+        pointersBecomeOneWayFrames(roots, fromToMap);
+
+        final Runnable[] tasks = new Runnable[USABLE_THREAD_COUNT];
+        for (int i = 0; i < USABLE_THREAD_COUNT; i++) {
+            tasks[i] = new BecomeOneWayManyPairsTask(roots, fromToMap);
         }
+        runTasks(tasks);
     }
 
     @TruffleBoundary
     private static void pointersBecomeOneWayFrames(final ObjectTracer tracer, final Object fromPointer, final Object toPointer) {
-        Truffle.getRuntime().iterateFrames((frameInstance) -> {
+        final ContextObject resumeContextObject = Truffle.getRuntime().iterateFrames((frameInstance) -> {
             final Frame current = frameInstance.getFrame(FrameInstance.FrameAccess.READ_WRITE);
             if (!FrameAccess.isTruffleSqueakFrame(current)) {
-                return null;
+                return FrameAccess.getResumingContextObjectOrSkip(frameInstance);
path: src/de.hpi.swa.trufflesqueak/src/de/hpi/swa/trufflesqueak/util/ObjectGraphUtils.java
user: login: fniephaus
body: This is another issue I fixed along the way, here and in `pointersBecomeOneWayFrames(final ObjectTracer tracer, final UnmodifiableEconomicMap<Object, Object> fromToMap)`.
created_at: 2025-05-25T09:04:48Z
author_association: MEMBER

PR comments:
user: login: fniephaus
created_at: 2025-05-20T06:11:55Z
body: Nice! Using the following as a benchmark:

```
FileStream stdout nextPutAll: (ClassBuilderTest buildSuite run asString); cr; flush.
FileStream stdout nextPutAll: (ClassBuilderTest buildSuite run asString); cr; flush.
Smalltalk snapshot: false andQuit: true.
```

I get, before:

```
$ mx build && $GRAALVM_HOME/bin/trufflesqueak --smalltalk.resource-summary --experimental-options images/test-64bit.1.image /Users/fniephaus/dev/graalvm/trufflesqueak/test.st
...
[trufflesqueak] Running test-64bit.1.image on Interpreted...
[trufflesqueak] Image loaded in 325ms.
19 run in 0:00:00:30.278, 19 passes, 0 expected failures, 0 failures, 0 errors, 0 unexpected passes
19 run in 0:00:00:29.712, 19 passes, 0 expected failures, 0 failures, 0 errors, 0 unexpected passes
[trufflesqueak] # Resource Summary
[trufflesqueak] - Total process time: 62.006s | CPU load: 1.11
[trufflesqueak] 
[trufflesqueak] ## GC Statistics
[trufflesqueak] -     0.0810s ( 0.13% of total time) in  120 GCs of G1 Young Generation
[trufflesqueak] -     0.0010s ( 0.00% of total time) in    2 GCs of G1 Concurrent GC
[trufflesqueak] -     0.0570s ( 0.09% of total time) in    2 GCs of G1 Old Generation
[trufflesqueak] -     0.1390s ( 0.22% of total time) in  124 GCs in total
[trufflesqueak] 
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -     0.8380s ( 1.35% of total time) for   31 'allInstancesOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -    31.3470s (50.55% of total time) for 1257 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
```

After:
```
$ mx build && $GRAALVM_HOME/bin/trufflesqueak --smalltalk.resource-summary --experimental-options images/test-64bit.1.image /Users/fniephaus/dev/graalvm/trufflesqueak/test.st
...
[trufflesqueak] Running test-64bit.1.image on Interpreted...
[trufflesqueak] Image loaded in 318ms.
19 run in 0:00:00:18.281, 19 passes, 0 expected failures, 0 failures, 0 errors, 0 unexpected passes
19 run in 0:00:00:18.252, 19 passes, 0 expected failures, 0 failures, 0 errors, 0 unexpected passes
[trufflesqueak] # Resource Summary
[trufflesqueak] - Total process time: 38.383s | CPU load: 1.72
[trufflesqueak] 
[trufflesqueak] ## GC Statistics
[trufflesqueak] -     0.0870s ( 0.23% of total time) in  130 GCs of G1 Young Generation
[trufflesqueak] -     0.0010s ( 0.00% of total time) in    2 GCs of G1 Concurrent GC
[trufflesqueak] -     0.0570s ( 0.15% of total time) in    2 GCs of G1 Old Generation
[trufflesqueak] -     0.1450s ( 0.38% of total time) in  134 GCs in total
[trufflesqueak] 
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -     0.3710s ( 0.97% of total time) for   31 'allInstancesOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -    12.1300s (31.60% of total time) for 1175 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
```

38.383s are ~61.9% of 62.006s total process time. Well done!
author_association: MEMBER
user: login: Sabine-Schnabeltier
created_at: 2025-05-21T01:49:26Z
body: See Issue #196
author_association: COLLABORATOR
user: login: Sabine-Schnabeltier
created_at: 2025-05-21T23:53:17Z
body: The following test code is showing that the parallel version is incorrectly counting **allInstances**:

```
Object >> test
	| theClass array failing |
	
	theClass := Dictionary.

	array := theClass allInstances.

	Transcript cr; show: theClass allInstances size printString.
	
	failing := OrderedCollection new.

	1 to: 1000 do: [ :i | | all |
		all := theClass allInstances.
		all size = array size ifFalse: [
			failing addLast: all.
			Transcript cr; show: 'FAIL[', i printString, ']: ', all size printString]].
	
	^ { array . failing }

"Object test."
```

The problem goes away when only a single thread is traversing the graph. It appears to be double traversing some subgraphs due to a race condition in `AbstractSqueakObjectWithClassAndHash.tryToMark()`.


author_association: COLLABORATOR
user: login: Sabine-Schnabeltier
created_at: 2025-05-22T00:19:06Z
body: Adding `synchronized` to `AbstractSqueakObjectWithClassAndHash.tryToMark()` fixes the double traversal problem but the overhead results in some reduction in the earlier speed improvement.

I am working on a way to try to claw back some of the lost speed improvement.
author_association: COLLABORATOR
user: login: fniephaus
created_at: 2025-05-22T16:08:42Z
body: > the double traversal problem

Visiting the same object multiple times is not a problem for `becomeForward:`. And for `allInstances`, you could also deduplicate the object list at the end, using a set for example.
author_association: MEMBER
user: login: Sabine-Schnabeltier
created_at: 2025-05-23T00:04:03Z
body: The double traversal problem might also leave the object graph in an inconsistent marking state.

Reimplemented the mark bit routines using lock-free atomic operations rather than using `synchronized` and got back the original speed improvement (perhaps even more?).
author_association: COLLABORATOR
user: login: fniephaus
created_at: 2025-05-25T09:38:35Z
body: The latest results are in:

**[Before (on ubuntu-arm)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15158541125/job/42619020913#step:8:11487):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -    15.6430s ( 2.30% of total time) for  201 'allInstancesOf'
[trufflesqueak] -     0.0590s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   301.7440s (44.30% of total time) for 3640 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 658456.2 ms, [disk (free/total): 37.1GB/72.5GB]
Time: 679.94
```

**[After (on ubuntu-arm)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15236224100/job/42850566622#step:8:11342):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -     9.9380s ( 1.88% of total time) for  197 'allInstancesOf'
[trufflesqueak] -     0.0690s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   158.0030s (29.91% of total time) for 3719 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 506776.3 ms, [disk (free/total): 37.1GB/72.5GB]
Time: 526.985
```

So on the new ubuntu-arm runners, the tests ran in ~77.50% of the time, with `pointersBecomeOneWay` running in only ~52,4% of the time it used to. We still need to take these numbers with a grain of salt because the runners don't provide consistent performance and are thus bad for benchmarking, but the improvements are quite significant. Great!

However, the coverage gate actually regressed a bit. I suspect this is due to a slower runner.

**[Before (on ubuntu with coverage)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15158541125/job/42619020902#step:9:9507):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -    14.9520s ( 1.49% of total time) for  193 'allInstancesOf'
[trufflesqueak] -     0.0550s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   359.5540s (35.88% of total time) for 4442 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 985686.9 ms, [disk (free/total): 13.0GB/72.5GB]
Time: 1,001.142
```

**[After (on ubuntu with coverage)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15236224100/job/42850566625#step:9:9715):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -    19.1830s ( 1.84% of total time) for  209 'allInstancesOf'
[trufflesqueak] -     0.0730s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   407.5480s (39.17% of total time) for 4434 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 1024217.2 ms, [disk (free/total): 13.0GB/72.5GB]
Time: 1,039.423
```

On macos-14, on the other hand, the object graph operation statistics have improved, even though the overall test execution didn't change much. I suspect this is also due to a slower runner.

**[Before (on macos-14)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15158541125/job/42619020896#step:8:12202):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -    12.9660s ( 2.00% of total time) for  233 'allInstancesOf'
[trufflesqueak] -     0.0610s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   234.9600s (36.33% of total time) for 3784 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 626166.7 ms, [disk (free/total): 38.5GB/319.5GB]
Time: 646.127
```

**[After (on macos-14)](https://github.com/hpi-swa/trufflesqueak/actions/runs/15236224100/job/42850566635#step:8:12430):**
```
[trufflesqueak] ## Object Graph Operations Statistics
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'allInstances'
[trufflesqueak] -     9.5210s ( 1.48% of total time) for  241 'allInstancesOf'
[trufflesqueak] -     0.0800s ( 0.01% of total time) for    2 'someInstanceOf'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'nextObject'
[trufflesqueak] -   175.7070s (27.36% of total time) for 4070 'pointersBecomeOneWay'
[trufflesqueak] -     0.0000s ( 0.00% of total time) for    0 'checkEphemerons'
de.hpi.swa.trufflesqueak.test.SqueakSUnitTest finished 618669.4 ms, [disk (free/total): 38.5GB/319.5GB]
Time: 640.562
```

author_association: MEMBER