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