// Copyright 2016 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.skyframe; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.collect.Maps; import com.google.common.collect.Maps.EntryTransformer; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.util.GroupedList; import java.util.Collection; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * Class that allows clients to be notified on each access of the graph. Clients can simply track * accesses, or they can block to achieve desired synchronization. Clients should call {@link * TrackingAwaiter#INSTANCE#assertNoErrors} at the end of tests in case exceptions were swallowed in * async threads. */ public class NotifyingHelper { public static MemoizingEvaluator.GraphTransformerForTesting makeNotifyingTransformer( final Listener listener) { return new MemoizingEvaluator.GraphTransformerForTesting() { @Override public InMemoryGraph transform(InMemoryGraph graph) { return new NotifyingInMemoryGraph(graph, listener); } @Override public QueryableGraph transform(QueryableGraph graph) { return new NotifyingQueryableGraph(graph, listener); } @Override public ProcessableGraph transform(ProcessableGraph graph) { return new NotifyingProcessableGraph(graph, listener); } }; } protected final Listener graphListener; protected final EntryTransformer<SkyKey, ThinNodeEntry, NodeEntry> wrapEntry = new EntryTransformer<SkyKey, ThinNodeEntry, NodeEntry>() { @Nullable @Override public NotifyingNodeEntry transformEntry(SkyKey key, @Nullable ThinNodeEntry nodeEntry) { return wrapEntry(key, nodeEntry); } }; NotifyingHelper(Listener graphListener) { this.graphListener = new ErrorRecordingDelegatingListener(graphListener); } /** Subclasses should override if they wish to subclass NotifyingNodeEntry. */ @Nullable protected NotifyingNodeEntry wrapEntry(SkyKey key, @Nullable ThinNodeEntry entry) { return entry == null ? null : new NotifyingNodeEntry(key, entry); } static class NotifyingQueryableGraph implements QueryableGraph { private final QueryableGraph delegate; protected final NotifyingHelper notifyingHelper; NotifyingQueryableGraph(QueryableGraph delegate, Listener graphListener) { this.notifyingHelper = new NotifyingHelper(graphListener); this.delegate = delegate; } NotifyingQueryableGraph(QueryableGraph delegate, NotifyingHelper helper) { this.notifyingHelper = helper; this.delegate = delegate; } @Override public Map<SkyKey, ? extends NodeEntry> getBatch( @Nullable SkyKey requestor, Reason reason, Iterable<SkyKey> keys) throws InterruptedException { return Maps.transformEntries( delegate.getBatch(requestor, reason, keys), notifyingHelper.wrapEntry); } @Nullable @Override public NodeEntry get(@Nullable SkyKey requestor, Reason reason, SkyKey key) throws InterruptedException { return notifyingHelper.wrapEntry(key, delegate.get(requestor, reason, key)); } @Override public Iterable<SkyKey> getCurrentlyAvailableNodes(Iterable<SkyKey> keys, Reason reason) { return delegate.getCurrentlyAvailableNodes(keys, reason); } } static class NotifyingProcessableGraph extends NotifyingQueryableGraph implements ProcessableGraph { protected final ProcessableGraph delegate; NotifyingProcessableGraph(ProcessableGraph delegate, Listener graphListener) { this(delegate, new NotifyingHelper(graphListener)); } NotifyingProcessableGraph(ProcessableGraph delegate, NotifyingHelper helper) { super(delegate, helper); this.delegate = delegate; } @Override public void remove(SkyKey key) { delegate.remove(key); } @Override public Map<SkyKey, ? extends NodeEntry> createIfAbsentBatch( @Nullable SkyKey requestor, Reason reason, Iterable<SkyKey> keys) throws InterruptedException { for (SkyKey key : keys) { notifyingHelper.graphListener.accept(key, EventType.CREATE_IF_ABSENT, Order.BEFORE, null); } return Maps.transformEntries( delegate.createIfAbsentBatch(requestor, reason, keys), notifyingHelper.wrapEntry); } @Override public DepsReport analyzeDepsDoneness(SkyKey parent, Collection<SkyKey> deps) throws InterruptedException { return delegate.analyzeDepsDoneness(parent, deps); } } /** * Graph/value entry events that the receiver can be informed of. When writing tests, feel free to * add additional events here if needed. */ public enum EventType { CREATE_IF_ABSENT, ADD_REVERSE_DEP, REMOVE_REVERSE_DEP, GET_TEMPORARY_DIRECT_DEPS, SIGNAL, SET_VALUE, MARK_DIRTY, MARK_CLEAN, IS_CHANGED, GET_VALUE_WITH_METADATA, IS_DIRTY, IS_READY, CHECK_IF_DONE, GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE } /** * Whether the given event is about to happen or has just happened. For some events, both will be * published, for others, only one. When writing tests, if you need an additional one to be * published, feel free to add it. */ public enum Order { BEFORE, AFTER } /** Receiver to be informed when an event for a given key occurs. */ public interface Listener { @ThreadSafe void accept(SkyKey key, EventType type, Order order, Object context); Listener NULL_LISTENER = new Listener() { @Override public void accept(SkyKey key, EventType type, Order order, Object context) {} }; } private static class ErrorRecordingDelegatingListener implements Listener { private final Listener delegate; private ErrorRecordingDelegatingListener(Listener delegate) { this.delegate = delegate; } @Override public void accept(SkyKey key, EventType type, Order order, Object context) { try { delegate.accept(key, type, order, context); } catch (Exception e) { TrackingAwaiter.INSTANCE.injectExceptionAndMessage( e, "In NotifyingGraph: " + Joiner.on(", ").join(key, type, order, context)); throw e; } } } /** {@link NodeEntry} that informs a {@link Listener} of various method calls. */ protected class NotifyingNodeEntry extends DelegatingNodeEntry { private final SkyKey myKey; private final ThinNodeEntry delegate; protected NotifyingNodeEntry(SkyKey key, ThinNodeEntry delegate) { myKey = key; this.delegate = delegate; } @Override protected NodeEntry getDelegate() { return (NodeEntry) delegate; } @Override protected ThinNodeEntry getThinDelegate() { return delegate; } @Override public DependencyState addReverseDepAndCheckIfDone(SkyKey reverseDep) throws InterruptedException { graphListener.accept(myKey, EventType.ADD_REVERSE_DEP, Order.BEFORE, reverseDep); DependencyState result = super.addReverseDepAndCheckIfDone(reverseDep); graphListener.accept(myKey, EventType.ADD_REVERSE_DEP, Order.AFTER, reverseDep); return result; } @Override public void removeReverseDep(SkyKey reverseDep) throws InterruptedException { graphListener.accept(myKey, EventType.REMOVE_REVERSE_DEP, Order.BEFORE, reverseDep); super.removeReverseDep(reverseDep); graphListener.accept(myKey, EventType.REMOVE_REVERSE_DEP, Order.AFTER, reverseDep); } @Override public GroupedList<SkyKey> getTemporaryDirectDeps() { graphListener.accept(myKey, EventType.GET_TEMPORARY_DIRECT_DEPS, Order.BEFORE, null); return super.getTemporaryDirectDeps(); } @Override public boolean signalDep(Version childVersion) { graphListener.accept(myKey, EventType.SIGNAL, Order.BEFORE, childVersion); boolean result = super.signalDep(childVersion); graphListener.accept(myKey, EventType.SIGNAL, Order.AFTER, childVersion); return result; } @Override public Set<SkyKey> setValue(SkyValue value, Version version) throws InterruptedException { graphListener.accept(myKey, EventType.SET_VALUE, Order.BEFORE, value); Set<SkyKey> result = super.setValue(value, version); graphListener.accept(myKey, EventType.SET_VALUE, Order.AFTER, value); return result; } @Override public MarkedDirtyResult markDirty(boolean isChanged) throws InterruptedException { graphListener.accept(myKey, EventType.MARK_DIRTY, Order.BEFORE, isChanged); MarkedDirtyResult result = super.markDirty(isChanged); graphListener.accept(myKey, EventType.MARK_DIRTY, Order.AFTER, isChanged); return result; } @Override public Set<SkyKey> markClean() throws InterruptedException { graphListener.accept(myKey, EventType.MARK_CLEAN, Order.BEFORE, this); Set<SkyKey> result = super.markClean(); graphListener.accept(myKey, EventType.MARK_CLEAN, Order.AFTER, this); return result; } @Override public boolean isChanged() { graphListener.accept(myKey, EventType.IS_CHANGED, Order.BEFORE, this); return super.isChanged(); } @Override public boolean isDirty() { graphListener.accept(myKey, EventType.IS_DIRTY, Order.BEFORE, this); return super.isDirty(); } @Override public boolean isReady() { graphListener.accept(myKey, EventType.IS_READY, Order.BEFORE, this); return super.isReady(); } @Override public SkyValue getValueMaybeWithMetadata() throws InterruptedException { graphListener.accept(myKey, EventType.GET_VALUE_WITH_METADATA, Order.BEFORE, this); return super.getValueMaybeWithMetadata(); } @Override public DependencyState checkIfDoneForDirtyReverseDep(SkyKey reverseDep) throws InterruptedException { graphListener.accept(myKey, EventType.CHECK_IF_DONE, Order.BEFORE, reverseDep); return super.checkIfDoneForDirtyReverseDep(reverseDep); } @Override public Iterable<SkyKey> getAllDirectDepsForIncompleteNode() throws InterruptedException { graphListener.accept( myKey, EventType.GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE, Order.BEFORE, this); return super.getAllDirectDepsForIncompleteNode(); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("delegate", getThinDelegate()).toString(); } } }