| // 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<? extends 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(); | 
 |     } | 
 |   } | 
 | } |