view truffle/com.oracle.truffle.api/src/com/oracle/truffle/api/instrument/Instrumenter.java @ 22247:c1c9c6d79f40

Truffle/Instrumentation: remove method Instrumenter.isInstrumentable()
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Wed, 23 Sep 2015 18:26:14 -0700
parents f78c72e2e0b6
children 6d328e688339
line wrap: on
line source

/*
 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.oracle.truffle.api.instrument;

import java.io.PrintStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.impl.Accessor;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.NodeVisitor;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;

/**
 * Access to instrumentation services in an instance of {@link TruffleVM}.
 */
public final class Instrumenter {

    private static final boolean TRACE = false;
    private static final String TRACE_PREFIX = "Instrumenter: ";
    private static final PrintStream OUT = System.out;

    private static void trace(String format, Object... args) {
        if (TRACE) {
            OUT.println(TRACE_PREFIX + String.format(format, args));
        }
    }

    /**
     * Walks an AST, looking for the first node with an assigned {@link SourceSection} and returning
     * the {@link Source}.
     */
    private static Source findSource(Node node) {
        final FindSourceVisitor visitor = new FindSourceVisitor();
        node.accept(visitor);
        return visitor.source;
    }

    private enum ToolState {

        /** Not yet installed, inert. */
        UNINSTALLED,

        /** Installed, collecting data. */
        ENABLED,

        /** Installed, not collecting data. */
        DISABLED,

        /** Was installed, but now removed, inactive, and no longer usable. */
        DISPOSED;
    }

    /**
     * {@linkplain Instrument Instrumentation}-based collectors of data during Guest Language
     * program execution.
     * <p>
     * Tools share a common <em>life cycle</em>:
     * <ul>
     * <li>A newly created tool is inert until {@linkplain Instrumenter#install(Tool) installed}.</li>
     * <li>An installed tool becomes <em>enabled</em> and immediately begins installing
     * {@linkplain Instrument instrumentation} on ASTs and collecting execution data from them.</li>
     * <li>A tool may only be installed once.</li>
     * <li>It should be possible to install multiple instances of a tool, possibly (but not
     * necessarily) configured differently with respect to what data is being collected.</li>
     * <li>Once installed, a tool can be {@linkplain #setEnabled(boolean) enabled and disabled}
     * arbitrarily.</li>
     * <li>A disabled tool:
     * <ul>
     * <li>Collects no data;</li>
     * <li>Retains existing AST instrumentation;</li>
     * <li>Continues to instrument newly created ASTs; and</li>
     * <li>Retains previously collected data.</li>
     * </ul>
     * </li>
     * <li>An installed tool may be {@linkplain #reset() reset} at any time, which leaves the tool
     * installed but with all previously collected data removed.</li>
     * <li>A {@linkplain #dispose() disposed} tool removes all instrumentation (but not
     * {@linkplain Probe probes}) and becomes permanently disabled; previously collected data
     * persists.</li>
     * </ul>
     * <p>
     * Tool-specific methods that access data collected by the tool should:
     * <ul>
     * <li>Return modification-safe representations of the data; and</li>
     * <li>Not change the state of the data.</li>
     * </ul>
     */
    public abstract static class Tool<T extends Tool<?>> {
        // TODO (mlvdv) still thinking about the most appropriate name for this class of tools

        private ToolState toolState = ToolState.UNINSTALLED;

        private Instrumenter instrumenter;

        protected Tool() {
        }

        final void install(Instrumenter inst) {
            checkUninstalled();
            this.instrumenter = inst;

            if (internalInstall()) {
                toolState = ToolState.ENABLED;
            }
            instrumenter.tools.add(this);
        }

        /**
         * @return whether the tool is currently collecting data.
         */
        public final boolean isEnabled() {
            return toolState == ToolState.ENABLED;
        }

        /**
         * Switches tool state between <em>enabled</em> (collecting data) and <em>disabled</em> (not
         * collecting data, but keeping data already collected).
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void setEnabled(boolean isEnabled) {
            checkInstalled();
            internalSetEnabled(isEnabled);
            toolState = isEnabled ? ToolState.ENABLED : ToolState.DISABLED;
        }

        /**
         * Clears any data already collected, but otherwise does not change the state of the tool.
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void reset() {
            checkInstalled();
            internalReset();
        }

        /**
         * Makes the tool permanently <em>disabled</em>, removes instrumentation, but keeps data
         * already collected.
         *
         * @throws IllegalStateException if not yet installed or disposed.
         */
        public final void dispose() {
            checkInstalled();
            internalDispose();
            toolState = ToolState.DISPOSED;
            instrumenter.tools.remove(this);
        }

        /**
         * @return whether the installation succeeded.
         */
        protected abstract boolean internalInstall();

        /**
         * No subclass action required.
         *
         * @param isEnabled
         */
        protected void internalSetEnabled(boolean isEnabled) {
        }

        protected abstract void internalReset();

        protected abstract void internalDispose();

        protected final Instrumenter getInstrumenter() {
            return instrumenter;
        }

        /**
         * Ensure that the tool is currently installed.
         *
         * @throws IllegalStateException
         */
        private void checkInstalled() throws IllegalStateException {
            if (toolState == ToolState.UNINSTALLED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " not yet installed");
            }
            if (toolState == ToolState.DISPOSED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " has been disposed");
            }
        }

        /**
         * Ensure that the tool has not yet been installed.
         *
         * @throws IllegalStateException
         */
        private void checkUninstalled() {
            if (toolState != ToolState.UNINSTALLED) {
                throw new IllegalStateException("Tool " + getClass().getSimpleName() + " has already been installed");
            }
        }
    }

    private final Object vm;

    /** Tools that have been created, but not yet disposed. */
    Set<Tool<? extends Tool<?>>> tools = new HashSet<>();

    private final Set<ASTProber> astProbers = Collections.synchronizedSet(new LinkedHashSet<ASTProber>());

    private final List<ProbeListener> probeListeners = new ArrayList<>();

    /**
     * All Probes that have been created.
     */
    private final List<WeakReference<Probe>> probes = new ArrayList<>();

    /**
     * A global trap that triggers notification just before executing any Node that is Probed with a
     * matching tag.
     */
    @CompilationFinal private SyntaxTagTrap beforeTagTrap = null;

    /**
     * A global trap that triggers notification just after executing any Node that is Probed with a
     * matching tag.
     */
    @CompilationFinal private SyntaxTagTrap afterTagTrap = null;

    private static final class FindSourceVisitor implements NodeVisitor {

        Source source = null;

        public boolean visit(Node node) {
            final SourceSection sourceSection = node.getSourceSection();
            if (sourceSection != null) {
                source = sourceSection.getSource();
                return false;
            }
            return true;
        }
    }

    Instrumenter(Object vm) {
        this.vm = vm;
    }

    /**
     * Prepares an AST node for {@linkplain Instrument instrumentation}, where the node is presumed
     * to be part of a well-formed Truffle AST that has not yet been executed.
     * <p>
     * <em>Probing</em> a node is idempotent:
     * <ul>
     * <li>If the node has not been Probed, modifies the AST by first inserting a
     * {@linkplain #createWrapperNode(Node) wrapper node} between the node and its parent and then
     * returning the newly created Probe associated with the wrapper.</li>
     * <li>If the node has been Probed, returns the Probe associated with its existing wrapper.</li>
     * <li>No more than one {@link Probe} may be associated with a node, so a wrapper may not wrap
     * another wrapper.</li>
     * </ul>
     * It is a runtime error to attempt Probing an AST node with no parent.
     *
     * @return a (possibly newly created) {@link Probe} associated with this node.
     * @throws ProbeException (unchecked) when a probe cannot be created, leaving the AST unchanged
     */
    @SuppressWarnings("rawtypes")
    public Probe probe(Node node) {

        final Node parent = node.getParent();

        if (node instanceof WrapperNode) {
            throw new ProbeException(ProbeFailure.Reason.WRAPPER_NODE, null, node, null);
        }

        if (parent == null) {
            throw new ProbeException(ProbeFailure.Reason.NO_PARENT, null, node, null);
        }

        if (parent instanceof WrapperNode) {
            final WrapperNode wrapper = (WrapperNode) parent;
            if (TRACE) {
                final Probe probe = wrapper.getProbe();
                final SourceSection sourceSection = wrapper.getChild().getSourceSection();
                final String location = sourceSection == null ? "<unknown>" : sourceSection.getShortDescription();
                trace("PROBE FOUND %s %s %s", "Probe@", location, probe.getTagsDescription());
            }
            return wrapper.getProbe();
        }

        if (!ACCESSOR.isInstrumentable(vm, node)) {
            throw new ProbeException(ProbeFailure.Reason.NOT_INSTRUMENTABLE, parent, node, null);
        }

        // Create a new wrapper/probe with this node as its child.
        final WrapperNode wrapper = createWrapperNode(node);

        if (wrapper == null || !(wrapper instanceof Node)) {
            throw new ProbeException(ProbeFailure.Reason.NO_WRAPPER, parent, node, wrapper);
        }

        final Node wrapperNode = (Node) wrapper;

        if (!node.isSafelyReplaceableBy(wrapperNode)) {
            throw new ProbeException(ProbeFailure.Reason.WRAPPER_TYPE, parent, node, wrapper);
        }

        final SourceSection sourceSection = wrapper.getChild().getSourceSection();
        final ProbeNode probeNode = new ProbeNode();
        Class<? extends TruffleLanguage> l = ACCESSOR.findLanguage(wrapper.getChild().getRootNode());
        final Probe probe = new Probe(this, l, probeNode, sourceSection);
        probes.add(new WeakReference<>(probe));
        probeNode.probe = probe;  // package private access
        wrapper.insertEventHandlerNode(probeNode);
        node.replace(wrapperNode);
        if (TRACE) {
            final String location = sourceSection == null ? "<unknown>" : sourceSection.getShortDescription();
            trace("PROBED %s %s %s", "Probe@", location, probe.getTagsDescription());
        }
        for (ProbeListener listener : probeListeners) {
            listener.newProbeInserted(probe);
        }
        return probe;
    }

    /**
     * Adds a {@link ProbeListener} to receive events.
     */
    public void addProbeListener(ProbeListener listener) {
        assert listener != null;
        probeListeners.add(listener);
    }

    /**
     * Removes a {@link ProbeListener}. Ignored if listener not found.
     */
    public void removeProbeListener(ProbeListener listener) {
        probeListeners.remove(listener);
    }

    /**
     * Returns all {@link Probe}s holding a particular {@link SyntaxTag}, or the whole collection of
     * probes if the specified tag is {@code null}.
     *
     * @return A collection of probes containing the given tag.
     */
    public Collection<Probe> findProbesTaggedAs(SyntaxTag tag) {
        final List<Probe> taggedProbes = new ArrayList<>();
        for (WeakReference<Probe> ref : probes) {
            Probe probe = ref.get();
            if (probe != null) {
                if (tag == null || probe.isTaggedAs(tag)) {
                    taggedProbes.add(ref.get());
                }
            }
        }
        return taggedProbes;
    }

    // TODO (mlvdv) generalize to permit multiple "before traps" without a performance hit?
    /**
     * Sets the current "<em>before</em> tag trap"; there can be no more than one in effect.
     * <ul>
     * <li>The before-trap triggers a callback just <strong><em>before</em></strong> execution
     * reaches <strong><em>any</em></strong> {@link Probe} (either existing or subsequently created)
     * with the specified {@link SyntaxTag}.</li>
     * <li>Setting the before-trap to {@code null} clears an existing before-trap.</li>
     * <li>Setting a non{@code -null} before-trap when one is already set clears the previously set
     * before-trap.</li>
     * </ul>
     *
     * @param newBeforeTagTrap The new "before" {@link SyntaxTagTrap} to set.
     */
    public void setBeforeTagTrap(SyntaxTagTrap newBeforeTagTrap) {
        beforeTagTrap = newBeforeTagTrap;
        for (WeakReference<Probe> ref : probes) {
            final Probe probe = ref.get();
            if (probe != null) {
                probe.notifyTrapsChanged();
            }
        }
    }

    // TODO (mlvdv) generalize to permit multiple "after traps" without a performance hit?
    /**
     * Sets the current "<em>after</em> tag trap"; there can be no more than one in effect.
     * <ul>
     * <li>The after-trap triggers a callback just <strong><em>after</em></strong> execution leaves
     * <strong><em>any</em></strong> {@link Probe} (either existing or subsequently created) with
     * the specified {@link SyntaxTag}.</li>
     * <li>Setting the after-trap to {@code null} clears an existing after-trap.</li>
     * <li>Setting a non{@code -null} after-trap when one is already set clears the previously set
     * after-trap.</li>
     * </ul>
     *
     * @param newAfterTagTrap The new "after" {@link SyntaxTagTrap} to set.
     */
    public void setAfterTagTrap(SyntaxTagTrap newAfterTagTrap) {
        afterTagTrap = newAfterTagTrap;
        for (WeakReference<Probe> ref : probes) {
            final Probe probe = ref.get();
            if (probe != null) {
                probe.notifyTrapsChanged();
            }
        }
    }

    /**
     * Enables instrumentation at selected nodes in all subsequently constructed ASTs. Ignored if
     * the argument is already registered, runtime error if argument is {@code null}.
     */
    public void registerASTProber(ASTProber prober) {
        if (prober == null) {
            throw new IllegalArgumentException("Register non-null ASTProbers");
        }
        astProbers.add(prober);
    }

    public void unregisterASTProber(ASTProber prober) {
        astProbers.remove(prober);
    }

    /**
     * <em>Attaches</em> a {@link SimpleInstrumentListener listener} to a {@link Probe}, creating a
     * <em>binding</em> called an {@link Instrument}. Until the Instrument is
     * {@linkplain Instrument#dispose() disposed}, it routes synchronous notification of
     * {@linkplain EventHandlerNode execution events} taking place at the Probe's AST location to
     * the listener.
     *
     * @param probe source of execution events
     * @param listener receiver of execution events
     * @param instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     */
    @SuppressWarnings("static-method")
    public Instrument attach(Probe probe, SimpleInstrumentListener listener, String instrumentInfo) {
        final Instrument instrument = new Instrument.SimpleInstrument(listener, instrumentInfo);
        probe.attach(instrument);
        return instrument;
    }

    /**
     * <em>Attaches</em> a {@link StandardInstrumentListener listener} to a {@link Probe}, creating
     * a <em>binding</em> called an {@link Instrument}. Until the Instrument is
     * {@linkplain Instrument#dispose() disposed}, it routes synchronous notification of
     * {@linkplain EventHandlerNode execution events} taking place at the Probe's AST location to
     * the listener.
     *
     * @param probe source of execution events
     * @param listener receiver of execution events
     * @param instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     */
    @SuppressWarnings("static-method")
    public Instrument attach(Probe probe, StandardInstrumentListener listener, String instrumentInfo) {
        final Instrument instrument = new Instrument.StandardInstrument(listener, instrumentInfo);
        probe.attach(instrument);
        return instrument;
    }

    /**
     * <em>Attaches</em> a {@link AdvancedInstrumentResultListener listener} to a {@link Probe},
     * creating a <em>binding</em> called an {@link Instrument}. Until the Instrument is
     * {@linkplain Instrument#dispose() disposed}, it routes synchronous notification of
     * {@linkplain EventHandlerNode execution events} taking place at the Probe's AST location to
     * the listener.
     * <p>
     * This Instrument executes efficiently, subject to full Truffle optimization, a client-provided
     * AST fragment every time the Probed node is entered.
     * <p>
     * Any {@link RuntimeException} thrown by execution of the fragment is caught by the framework
     * and reported to the listener; there is no other notification.
     *
     * @param probe probe source of execution events
     * @param listener optional client callback for results/failure notification
     * @param rootFactory provider of AST fragments on behalf of the client
     * @param requiredResultType optional requirement, any non-assignable result is reported to the
     *            the listener, if any, as a failure
     * @param instrumentInfo instrumentInfo optional documentation about the Instrument
     * @return a handle for access to the binding
     */
    @SuppressWarnings("static-method")
    public Instrument attach(Probe probe, AdvancedInstrumentResultListener listener, AdvancedInstrumentRootFactory rootFactory, Class<?> requiredResultType, String instrumentInfo) {
        final Instrument instrument = new Instrument.AdvancedInstrument(listener, rootFactory, requiredResultType, instrumentInfo);
        probe.attach(instrument);
        return instrument;
    }

    /**
     * Connects the tool to some part of the Truffle runtime, and enable data collection to start.
     *
     * @return the tool
     * @throws IllegalStateException if the tool has previously been installed or has been disposed.
     */
    public <T extends Tool<?>> T install(T tool) {
        tool.install(this);
        return tool;
    }

    @SuppressWarnings("unused")
    void executionStarted(Source s) {
    }

    void executionEnded() {
    }

    WrapperNode createWrapperNode(Node node) {
        return ACCESSOR.createWrapperNode(vm, node);
    }

    void tagAdded(Probe probe, SyntaxTag tag, Object tagValue) {
        for (ProbeListener listener : probeListeners) {
            listener.probeTaggedAs(probe, tag, tagValue);
        }
    }

    SyntaxTagTrap getBeforeTagTrap() {
        return beforeTagTrap;
    }

    SyntaxTagTrap getAfterTagTrap() {
        return afterTagTrap;
    }

    // TODO (mlvdv) build this in as a VM event?
    /**
     * Enables instrumentation in a newly created AST by applying all registered instances of
     * {@link ASTProber}.
     */
    private void probeAST(RootNode rootNode) {
        if (!astProbers.isEmpty()) {

            String name = "<?>";
            final Source source = findSource(rootNode);
            if (source != null) {
                name = source.getShortName();
            } else {
                final SourceSection sourceSection = rootNode.getEncapsulatingSourceSection();
                if (sourceSection != null) {
                    name = sourceSection.getShortDescription();
                }
            }
            trace("START %s", name);
            for (ProbeListener listener : probeListeners) {
                listener.startASTProbing(source);
            }
            for (ASTProber prober : astProbers) {
                prober.probeAST(this, rootNode);  // TODO (mlvdv)
            }
            for (ProbeListener listener : probeListeners) {
                listener.endASTProbing(source);
            }
            trace("FINISHED %s", name);
        }
    }

    static final class AccessorInstrument extends Accessor {

        @Override
        protected Instrumenter createInstrumenter(Object vm) {
            return new Instrumenter(vm);
        }

        @Override
        protected boolean isInstrumentable(Object vm, Node node) {
            return super.isInstrumentable(vm, node);
        }

        @Override
        public WrapperNode createWrapperNode(Object vm, Node node) {
            return super.createWrapperNode(vm, node);
        }

        @SuppressWarnings("rawtypes")
        @Override
        protected Class<? extends TruffleLanguage> findLanguage(RootNode n) {
            return super.findLanguage(n);
        }

        @SuppressWarnings("rawtypes")
        @Override
        protected Class<? extends TruffleLanguage> findLanguage(Probe probe) {
            return probe.getLanguage();
        }

        @Override
        protected void probeAST(RootNode rootNode) {
            // Normally null vm argument; can be reflectively set for testing
            super.getInstrumenter(testVM).probeAST(rootNode);
        }
    }

    static final AccessorInstrument ACCESSOR = new AccessorInstrument();

    // Normally null; set for testing where the Accessor hasn't been fully initialized
    private static Object testVM = null;

}