|  | // 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.auto.value.AutoValue; | 
|  | import com.google.common.base.Joiner; | 
|  | import com.google.common.base.MoreObjects; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.common.collect.Maps; | 
|  | import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; | 
|  | import com.google.devtools.build.skyframe.NodeEntry.DirtyType; | 
|  | import com.google.errorprone.annotations.ForOverride; | 
|  | import java.util.List; | 
|  | 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( | 
|  | Listener listener) { | 
|  | return new MemoizingEvaluator.GraphTransformerForTesting() { | 
|  | @Override | 
|  | public InMemoryGraph transform(InMemoryGraph graph) { | 
|  | return new NotifyingInMemoryGraph(graph, listener); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ProcessableGraph transform(ProcessableGraph graph) { | 
|  | return new NotifyingProcessableGraph(graph, listener); | 
|  | } | 
|  | }; | 
|  | } | 
|  |  | 
|  | final Listener graphListener; | 
|  |  | 
|  | NotifyingHelper(Listener graphListener) { | 
|  | this.graphListener = new ErrorRecordingDelegatingListener(graphListener); | 
|  | } | 
|  |  | 
|  | /** Subclasses should override if they wish to subclass {@link NotifyingNodeEntry}. */ | 
|  | @Nullable | 
|  | @ForOverride | 
|  | NotifyingNodeEntry wrapEntry(SkyKey key, @Nullable NodeEntry entry) { | 
|  | return entry == null ? null : new NotifyingNodeEntry(key, entry); | 
|  | } | 
|  |  | 
|  | static class NotifyingProcessableGraph implements ProcessableGraph { | 
|  | final ProcessableGraph delegate; | 
|  | final NotifyingHelper notifyingHelper; | 
|  |  | 
|  | NotifyingProcessableGraph(ProcessableGraph delegate, Listener graphListener) { | 
|  | this(delegate, new NotifyingHelper(graphListener)); | 
|  | } | 
|  |  | 
|  | NotifyingProcessableGraph(ProcessableGraph delegate, NotifyingHelper notifyingHelper) { | 
|  | this.delegate = delegate; | 
|  | this.notifyingHelper = notifyingHelper; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | @Override | 
|  | public NodeEntry get(@Nullable SkyKey requestor, Reason reason, SkyKey key) | 
|  | throws InterruptedException { | 
|  | var node = delegate.get(requestor, reason, key); | 
|  | // Maintains behavior for tests written when all DEP_REQUESTED calls were made as batch | 
|  | // requests. Now there are optimizations in SkyFunctionEnvironment for looking up deps | 
|  | // individually, but older tests may be written to listen for a GET_BATCH event. | 
|  | if (reason == Reason.DEP_REQUESTED) { | 
|  | notifyingHelper.graphListener.accept(key, EventType.GET_BATCH, Order.BEFORE, reason); | 
|  | } else if (reason == Reason.EVALUATION) { | 
|  | notifyingHelper.graphListener.accept(key, EventType.EVALUATE, Order.BEFORE, node); | 
|  | } | 
|  | return notifyingHelper.wrapEntry(key, node); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public LookupHint getLookupHint(SkyKey key) { | 
|  | return delegate.getLookupHint(key); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void remove(SkyKey key) { | 
|  | delegate.remove(key); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public NodeBatch createIfAbsentBatch( | 
|  | @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) | 
|  | throws InterruptedException { | 
|  | for (SkyKey key : keys) { | 
|  | notifyingHelper.graphListener.accept(key, EventType.CREATE_IF_ABSENT, Order.BEFORE, null); | 
|  | } | 
|  | NodeBatch batch = delegate.createIfAbsentBatch(requestor, reason, keys); | 
|  | return key -> notifyingHelper.wrapEntry(key, batch.get(key)); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Map<SkyKey, ? extends NodeEntry> getBatchMap( | 
|  | @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) | 
|  | throws InterruptedException { | 
|  | for (SkyKey key : keys) { | 
|  | notifyingHelper.graphListener.accept(key, EventType.GET_BATCH, Order.BEFORE, reason); | 
|  | } | 
|  | return Maps.transformEntries( | 
|  | delegate.getBatchMap(requestor, reason, keys), notifyingHelper::wrapEntry); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | @Override | 
|  | public ImmutableSet<SkyKey> prefetchDeps( | 
|  | SkyKey requestor, Set<SkyKey> oldDeps, GroupedDeps previouslyRequestedDeps) | 
|  | throws InterruptedException { | 
|  | return delegate.prefetchDeps(requestor, oldDeps, previouslyRequestedDeps); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public DepsReport analyzeDepsDoneness(SkyKey parent, List<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, | 
|  | EVALUATE, | 
|  | ADD_REVERSE_DEP, | 
|  | ADD_EXTERNAL_DEP, | 
|  | REMOVE_REVERSE_DEP, | 
|  | GET_BATCH, | 
|  | GET_VALUES, | 
|  | GET_TEMPORARY_DIRECT_DEPS, | 
|  | SIGNAL, | 
|  | SET_VALUE, | 
|  | MARK_DIRTY, | 
|  | MARK_CLEAN, | 
|  | IS_CHANGED, | 
|  | GET_LIFECYCLE_STATE, | 
|  | GET_VALUE_WITH_METADATA, | 
|  | IS_DIRTY, | 
|  | IS_READY, | 
|  | CHECK_IF_DONE, | 
|  | ADD_TEMPORARY_DIRECT_DEPS, | 
|  | GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE, | 
|  | RESET_FOR_RESTART_FROM_SCRATCH, | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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, @Nullable Object context); | 
|  |  | 
|  | Listener NULL_LISTENER = (key, type, order, 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, @Nullable 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 == null ? "null" : context)); | 
|  | throw new IllegalStateException(e); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** {@link NodeEntry} that informs a {@link Listener} of various method calls. */ | 
|  | public class NotifyingNodeEntry extends DelegatingNodeEntry { | 
|  | private final SkyKey myKey; | 
|  | private final NodeEntry delegate; | 
|  |  | 
|  | NotifyingNodeEntry(SkyKey key, NodeEntry delegate) { | 
|  | myKey = key; | 
|  | this.delegate = delegate; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public NodeEntry getDelegate() { | 
|  | 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 addExternalDep() { | 
|  | super.addExternalDep(); | 
|  | graphListener.accept(myKey, EventType.ADD_EXTERNAL_DEP, Order.AFTER, null); | 
|  | } | 
|  |  | 
|  | @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 GroupedDeps getTemporaryDirectDeps() { | 
|  | graphListener.accept(myKey, EventType.GET_TEMPORARY_DIRECT_DEPS, Order.BEFORE, null); | 
|  | return super.getTemporaryDirectDeps(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean signalDep(Version childVersion, @Nullable SkyKey childForDebugging) { | 
|  | graphListener.accept(myKey, EventType.SIGNAL, Order.BEFORE, childForDebugging); | 
|  | boolean result = super.signalDep(childVersion, childForDebugging); | 
|  | graphListener.accept(myKey, EventType.SIGNAL, Order.AFTER, childForDebugging); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Set<SkyKey> setValue( | 
|  | SkyValue value, Version graphVersion, @Nullable Version maxTransitiveSourceVersion) | 
|  | throws InterruptedException { | 
|  | graphListener.accept(myKey, EventType.SET_VALUE, Order.BEFORE, value); | 
|  | Set<SkyKey> result = super.setValue(value, graphVersion, maxTransitiveSourceVersion); | 
|  | graphListener.accept(myKey, EventType.SET_VALUE, Order.AFTER, value); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public MarkedDirtyResult markDirty(DirtyType dirtyType) throws InterruptedException { | 
|  | graphListener.accept(myKey, EventType.MARK_DIRTY, Order.BEFORE, dirtyType); | 
|  | MarkedDirtyResult result = super.markDirty(dirtyType); | 
|  | graphListener.accept( | 
|  | myKey, | 
|  | EventType.MARK_DIRTY, | 
|  | Order.AFTER, | 
|  | MarkDirtyAfterContext.create(dirtyType, result != null)); | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public NodeValueAndRdepsToSignal markClean() throws InterruptedException { | 
|  | graphListener.accept(myKey, EventType.MARK_CLEAN, Order.BEFORE, this); | 
|  | NodeValueAndRdepsToSignal 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 isReadyToEvaluate() { | 
|  | graphListener.accept(myKey, EventType.IS_READY, Order.BEFORE, this); | 
|  | return super.isReadyToEvaluate(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public LifecycleState getLifecycleState() { | 
|  | graphListener.accept(myKey, EventType.GET_LIFECYCLE_STATE, Order.BEFORE, this); | 
|  | LifecycleState lifecycleState = super.getLifecycleState(); | 
|  | graphListener.accept(myKey, EventType.GET_LIFECYCLE_STATE, Order.AFTER, lifecycleState); | 
|  | return lifecycleState; | 
|  | } | 
|  |  | 
|  | @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); | 
|  | DependencyState dependencyState = super.checkIfDoneForDirtyReverseDep(reverseDep); | 
|  | graphListener.accept(myKey, EventType.CHECK_IF_DONE, Order.AFTER, reverseDep); | 
|  | return dependencyState; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addSingletonTemporaryDirectDep(SkyKey dep) { | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.BEFORE, dep); | 
|  | super.addSingletonTemporaryDirectDep(dep); | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.AFTER, dep); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addTemporaryDirectDepGroup(List<SkyKey> group) { | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.BEFORE, group); | 
|  | super.addTemporaryDirectDepGroup(group); | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.AFTER, group); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void addTemporaryDirectDepsInGroups(Set<SkyKey> deps, List<Integer> groupSizes) { | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.BEFORE, deps); | 
|  | super.addTemporaryDirectDepsInGroups(deps, groupSizes); | 
|  | graphListener.accept(myKey, EventType.ADD_TEMPORARY_DIRECT_DEPS, Order.AFTER, deps); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableSet<SkyKey> getAllDirectDepsForIncompleteNode() throws InterruptedException { | 
|  | graphListener.accept( | 
|  | myKey, EventType.GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE, Order.BEFORE, this); | 
|  | return super.getAllDirectDepsForIncompleteNode(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void resetEvaluationFromScratch() { | 
|  | delegate.resetEvaluationFromScratch(); | 
|  | graphListener.accept(myKey, EventType.RESET_FOR_RESTART_FROM_SCRATCH, Order.AFTER, this); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return MoreObjects.toStringHelper(this).add("delegate", delegate).toString(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * A pair of {@link DirtyType} and a bit saying whether the dirtying was successful, emitted to | 
|  | * the graph listener as the context {@link Order#AFTER} a call to {@link EventType#MARK_DIRTY} a | 
|  | * node. | 
|  | */ | 
|  | @AutoValue | 
|  | public abstract static class MarkDirtyAfterContext { | 
|  | public abstract DirtyType dirtyType(); | 
|  |  | 
|  | public abstract boolean actuallyDirtied(); | 
|  |  | 
|  | static MarkDirtyAfterContext create(DirtyType dirtyType, boolean actuallyDirtied) { | 
|  | return new AutoValue_NotifyingHelper_MarkDirtyAfterContext(dirtyType, actuallyDirtied); | 
|  | } | 
|  | } | 
|  | } |