ObjectSpaceManager.java

/*
 * Copyright (c) 2013, 2014 Oracle and/or its affiliates. All rights reserved. This
 * code is released under a tri EPL/GPL/LGPL license. You can use it,
 * redistribute it and/or modify it under the terms of the:
 *
 * Eclipse Public License version 1.0
 * GNU General Public License version 2
 * GNU Lesser General Public License version 2.1
 */
package org.jruby.truffle.runtime.subsystems;

import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.FrameInstance;
import com.oracle.truffle.api.frame.FrameInstanceVisitor;
import com.oracle.truffle.api.frame.FrameSlot;
import org.jruby.runtime.Visibility;
import org.jruby.truffle.nodes.RubyNode;
import org.jruby.truffle.runtime.RubyContext;
import org.jruby.truffle.runtime.core.RubyBasicObject;
import org.jruby.truffle.runtime.core.RubyProc;
import org.jruby.truffle.runtime.core.RubyThread;
import org.jruby.truffle.runtime.util.Consumer;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.CountDownLatch;

/**
 * Supports the Ruby {@code ObjectSpace} module. Object IDs are lazily allocated {@code long}
 * values, mapped to objects with a weak hash map. Finalizers are implemented with weak references
 * and reference queues, and are run in a dedicated Ruby thread (but not a dedicated Java thread).
 */
public class ObjectSpaceManager {

    private static class FinalizerReference extends WeakReference<RubyBasicObject> {

        public List<RubyProc> finalizers = new LinkedList<>();

        public FinalizerReference(RubyBasicObject object, ReferenceQueue<? super RubyBasicObject> queue) {
            super(object, queue);
        }

        public void addFinalizer(RubyProc proc) {
            finalizers.add(proc);
        }

        public List<RubyProc> getFinalizers() {
            return finalizers;
        }

        public void clearFinalizers() {
            finalizers = new LinkedList<>();
        }

    }

    private final RubyContext context;

    private final Map<RubyBasicObject, FinalizerReference> finalizerReferences = new WeakHashMap<>();
    private final ReferenceQueue<RubyBasicObject> finalizerQueue = new ReferenceQueue<>();
    private RubyThread finalizerThread;
    private Thread finalizerJavaThread;
    private boolean stop;
    private CountDownLatch finished = new CountDownLatch(1);

    public ObjectSpaceManager(RubyContext context) {
        this.context = context;
    }

    public void defineFinalizer(RubyBasicObject object, RubyProc proc) {
        RubyNode.notDesignedForCompilation();

        // Record the finalizer against the object

        FinalizerReference finalizerReference = finalizerReferences.get(object);

        if (finalizerReference == null) {
            finalizerReference = new FinalizerReference(object, finalizerQueue);
            finalizerReferences.put(object, finalizerReference);
        }

        finalizerReference.addFinalizer(proc);

        // If there is no finalizer thread, start one

        if (finalizerThread == null) {
            // TODO(CS): should we be running this in a real Ruby thread?

            finalizerThread = new RubyThread(context.getCoreLibrary().getThreadClass(), context.getThreadManager());

            finalizerThread.initialize(context, null, new Runnable() {

                @Override
                public void run() {
                    runFinalizers();
                }

            });
        }
    }

    public void undefineFinalizer(RubyBasicObject object) {
        RubyNode.notDesignedForCompilation();

        final FinalizerReference finalizerReference = finalizerReferences.get(object);

        if (finalizerReference != null) {
            finalizerReference.clearFinalizers();
        }
    }

    private void runFinalizers() {
        // Run in a loop

        while (true) {
            // Is there a finalizer ready to immediately run?

            FinalizerReference finalizerReference = (FinalizerReference) finalizerQueue.poll();

            if (finalizerReference != null) {
                runFinalizers(finalizerReference);
                continue;
            }

            // Check if we've been asked to stop

            if (stop) {
                break;
            }

            // Leave the global lock and wait on the finalizer queue

            final RubyThread runningThread = context.getThreadManager().leaveGlobalLock();
            finalizerJavaThread = Thread.currentThread();

            try {
                finalizerReference = (FinalizerReference) finalizerQueue.remove();
            } catch (InterruptedException e) {
                continue;
            } finally {
                context.getThreadManager().enterGlobalLock(runningThread);
            }

            runFinalizers(finalizerReference);
        }

        finished.countDown();
    }

    private static void runFinalizers(FinalizerReference finalizerReference) {
        try {
            for (RubyProc proc : finalizerReference.getFinalizers()) {
                proc.rootCall();
            }
        } catch (Exception e) {
            // MRI seems to silently ignore exceptions in finalizers
        }
    }

    public void shutdown() {
        RubyNode.notDesignedForCompilation();

        context.getThreadManager().enterGlobalLock(finalizerThread);

        try {
            // Tell the finalizer thread to stop and wait for it to do so

            if (finalizerThread != null) {
                stop = true;

                if (finalizerJavaThread != null) {
                    finalizerJavaThread.interrupt();
                }

                context.getThreadManager().leaveGlobalLock();

                try {
                    finished.await();
                } catch (InterruptedException e) {
                } finally {
                    context.getThreadManager().enterGlobalLock(finalizerThread);
                }
            }

            // Run any finalizers for objects that are still live

            for (FinalizerReference finalizerReference : finalizerReferences.values()) {
                runFinalizers(finalizerReference);
            }
        } finally {
            context.getThreadManager().leaveGlobalLock();
        }
    }

    public static interface ObjectGraphVisitor {

        boolean visit(RubyBasicObject object);

    }

    private Map<Long, RubyBasicObject> liveObjects;
    private ObjectGraphVisitor visitor;

    public Map<Long, RubyBasicObject> collectLiveObjects() {
        RubyNode.notDesignedForCompilation();

        // TODO(CS): probably a race condition here if multiple threads try to collect at the same time

        liveObjects = new HashMap<>();

        visitor = new ObjectGraphVisitor() {

            @Override
            public boolean visit(RubyBasicObject object) {
                return liveObjects.put(object.getObjectID(), object) == null;
            }

        };

        context.getSafepointManager().pauseAllThreadsAndExecute(new Consumer<Boolean>() {

            @Override
            public void accept(Boolean isPausingThread) {
                synchronized (liveObjects) {
                    context.getCoreLibrary().getGlobalVariablesObject().visitObjectGraph(visitor);
                    context.getCoreLibrary().getMainObject().visitObjectGraph(visitor);
                    context.getCoreLibrary().getObjectClass().visitObjectGraph(visitor);
                    visitCallStack(visitor);
                }
            }

        });

        return Collections.unmodifiableMap(liveObjects);
    }

    public void visitCallStack(final ObjectGraphVisitor visitor) {
        visitFrameInstance(Truffle.getRuntime().getCurrentFrame(), visitor);

        Truffle.getRuntime().iterateFrames(new FrameInstanceVisitor<Object>() {
            @Override
            public Void visitFrame(FrameInstance frameInstance) {
                visitFrameInstance(frameInstance, visitor);
                return null;
            }
        });
    }

    public void visitFrameInstance(FrameInstance frameInstance, ObjectGraphVisitor visitor) {
        visitFrame(frameInstance.getFrame(FrameInstance.FrameAccess.READ_ONLY, true), visitor);
    }

    public void visitFrame(Frame frame, ObjectGraphVisitor visitor) {
        if (frame == null) {
            return;
        }

        for (FrameSlot slot : frame.getFrameDescriptor().getSlots()) {
            Object value = frame.getValue(slot);
            if (!(value instanceof Visibility)) { // TODO(cs): Better condition for hidden local variables
                if (value instanceof RubyBasicObject) {
                    ((RubyBasicObject) value).visitObjectGraph(visitor);
                }
            }
        }
    }

}