|  | // 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.MoreObjects; | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.common.collect.Iterables; | 
|  | import com.google.common.collect.Maps; | 
|  | import com.google.common.collect.Sets; | 
|  | import com.google.common.flogger.GoogleLogger; | 
|  | import com.google.devtools.build.lib.collect.nestedset.NestedSet; | 
|  | import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; | 
|  | import com.google.devtools.build.lib.collect.nestedset.Order; | 
|  | import com.google.devtools.build.lib.events.Event; | 
|  | import com.google.devtools.build.lib.events.ExtendedEventHandler; | 
|  | import com.google.devtools.build.lib.events.ExtendedEventHandler.Postable; | 
|  | import com.google.devtools.build.lib.events.StoredEventHandler; | 
|  | import com.google.devtools.build.lib.util.GroupedList; | 
|  | import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper; | 
|  | import com.google.devtools.build.lib.util.Pair; | 
|  | import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState; | 
|  | import com.google.devtools.build.skyframe.GraphInconsistencyReceiver.Inconsistency; | 
|  | import com.google.devtools.build.skyframe.NodeEntry.DependencyState; | 
|  | import com.google.devtools.build.skyframe.ParallelEvaluatorContext.EnqueueParentBehavior; | 
|  | import com.google.devtools.build.skyframe.QueryableGraph.Reason; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Collection; | 
|  | import java.util.HashMap; | 
|  | import java.util.HashSet; | 
|  | import java.util.LinkedHashSet; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Map.Entry; | 
|  | import java.util.Set; | 
|  | import java.util.concurrent.CountDownLatch; | 
|  | import javax.annotation.Nullable; | 
|  |  | 
|  | /** A {@link SkyFunction.Environment} implementation for {@link ParallelEvaluator}. */ | 
|  | class SkyFunctionEnvironment extends AbstractSkyFunctionEnvironment { | 
|  | private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | 
|  |  | 
|  | private static final SkyValue NULL_MARKER = new SkyValue() {}; | 
|  | private static final boolean PREFETCH_OLD_DEPS = | 
|  | Boolean.parseBoolean( | 
|  | System.getProperty("skyframe.ParallelEvaluator.PrefetchOldDeps", "true")); | 
|  |  | 
|  | private boolean building = true; | 
|  | private SkyKey depErrorKey = null; | 
|  | private final SkyKey skyKey; | 
|  | /** | 
|  | * The deps requested during the previous build of this node. Used for two reasons: (1) They are | 
|  | * fetched eagerly before the node is built, to potentially prime the graph and speed up requests | 
|  | * for them during evaluation. (2) When the node finishes building, any deps from the previous | 
|  | * build that are not deps from this build must have this node removed from them as a reverse dep. | 
|  | * Thus, it is important that all nodes in this set have the property that they have this node as | 
|  | * a reverse dep from the last build, but that this node has not added them as a reverse dep on | 
|  | * this build. That set is normally {@link NodeEntry#getAllRemainingDirtyDirectDeps()}, but in | 
|  | * certain corner cases, like cycles, further filtering may be needed. | 
|  | */ | 
|  | private final Set<SkyKey> oldDeps; | 
|  |  | 
|  | private SkyValue value = null; | 
|  | private ErrorInfo errorInfo = null; | 
|  |  | 
|  | private final FunctionHermeticity hermeticity; | 
|  | @Nullable private Version maxChildVersion = null; | 
|  |  | 
|  | /** If present, takes precedence over {@link #maxChildVersion}. */ | 
|  | @Nullable private Version injectedVersion = null; | 
|  |  | 
|  | /** | 
|  | * This is not {@code null} only during cycle detection and error bubbling. The nullness of this | 
|  | * field is used to detect whether evaluation is in one of those special states. | 
|  | * | 
|  | * <p>When this is not {@code null}, values in this map should be used (while getting | 
|  | * dependencies' values, events, or posts) over values from the graph for keys present in this | 
|  | * map. | 
|  | */ | 
|  | @Nullable private final Map<SkyKey, ValueWithMetadata> bubbleErrorInfo; | 
|  |  | 
|  | /** | 
|  | * The values previously declared as dependencies. | 
|  | * | 
|  | * <p>Values in this map are either {@link #NULL_MARKER} or were retrieved via {@link | 
|  | * NodeEntry#getValueMaybeWithMetadata}. In the latter case, they should be processed using the | 
|  | * static methods of {@link ValueWithMetadata}. | 
|  | */ | 
|  | private final ImmutableMap<SkyKey, SkyValue> previouslyRequestedDepsValues; | 
|  |  | 
|  | /** | 
|  | * The values newly requested from the graph. | 
|  | * | 
|  | * <p>Values in this map are either {@link #NULL_MARKER} or were retrieved via {@link | 
|  | * NodeEntry#getValueMaybeWithMetadata}. In the latter case, they should be processed using the | 
|  | * static methods of {@link ValueWithMetadata}. | 
|  | */ | 
|  | private final Map<SkyKey, SkyValue> newlyRequestedDepsValues = new HashMap<>(); | 
|  |  | 
|  | /** | 
|  | * Keys of dependencies registered via {@link #registerDependencies} if not using {@link | 
|  | * EvaluationVersionBehavior#MAX_CHILD_VERSIONS}. | 
|  | * | 
|  | * <p>The {@link #registerDependencies} method is hacky. Deps registered through it may not have | 
|  | * entries in {@link #newlyRequestedDepsValues}, but they are expected to be done. This set tracks | 
|  | * those keys so that they aren't removed when {@link #removeUndoneNewlyRequestedDeps} is called. | 
|  | */ | 
|  | private final Set<SkyKey> newlyRegisteredDeps = new HashSet<>(); | 
|  |  | 
|  | /** | 
|  | * The grouped list of values requested during this build as dependencies. On a subsequent build, | 
|  | * if this value is dirty, all deps in the same dependency group can be checked in parallel for | 
|  | * changes. In other words, if dep1 and dep2 are in the same group, then dep1 will be checked in | 
|  | * parallel with dep2. See {@link #getValues} for more. | 
|  | */ | 
|  | private final GroupedListHelper<SkyKey> newlyRequestedDeps = new GroupedListHelper<>(); | 
|  |  | 
|  | /** The set of errors encountered while fetching children. */ | 
|  | private final Set<ErrorInfo> childErrorInfos = new LinkedHashSet<>(); | 
|  |  | 
|  | private final StoredEventHandler eventHandler = | 
|  | new StoredEventHandler() { | 
|  | @Override | 
|  | @SuppressWarnings("UnsynchronizedOverridesSynchronized") // only delegates to thread-safe. | 
|  | public void handle(Event e) { | 
|  | checkActive(); | 
|  | if (evaluatorContext.getStoredEventFilter().apply(e)) { | 
|  | super.handle(e); | 
|  | } else { | 
|  | evaluatorContext.getReporter().handle(e); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | @SuppressWarnings("UnsynchronizedOverridesSynchronized") // only delegates to thread-safe. | 
|  | public void post(ExtendedEventHandler.Postable e) { | 
|  | checkActive(); | 
|  | if (e instanceof ExtendedEventHandler.ProgressLike) { | 
|  | evaluatorContext.getReporter().post(e); | 
|  | } else { | 
|  | super.post(e); | 
|  | } | 
|  | } | 
|  | }; | 
|  | private final ParallelEvaluatorContext evaluatorContext; | 
|  |  | 
|  | SkyFunctionEnvironment( | 
|  | SkyKey skyKey, | 
|  | GroupedList<SkyKey> directDeps, | 
|  | Set<SkyKey> oldDeps, | 
|  | ParallelEvaluatorContext evaluatorContext) | 
|  | throws InterruptedException, UndonePreviouslyRequestedDeps { | 
|  | super(directDeps); | 
|  | this.skyKey = skyKey; | 
|  | this.oldDeps = oldDeps; | 
|  | this.evaluatorContext = evaluatorContext; | 
|  | this.bubbleErrorInfo = null; | 
|  | this.hermeticity = skyKey.functionName().getHermeticity(); | 
|  | this.previouslyRequestedDepsValues = | 
|  | batchPrefetch(skyKey, directDeps, oldDeps, /*assertDone=*/ true); | 
|  | Preconditions.checkState( | 
|  | !this.previouslyRequestedDepsValues.containsKey(ErrorTransienceValue.KEY), | 
|  | "%s cannot have a dep on ErrorTransienceValue during building", | 
|  | skyKey); | 
|  | } | 
|  |  | 
|  | SkyFunctionEnvironment( | 
|  | SkyKey skyKey, | 
|  | GroupedList<SkyKey> directDeps, | 
|  | Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, | 
|  | Set<SkyKey> oldDeps, | 
|  | ParallelEvaluatorContext evaluatorContext) | 
|  | throws InterruptedException { | 
|  | super(directDeps); | 
|  | this.skyKey = skyKey; | 
|  | this.oldDeps = oldDeps; | 
|  | this.evaluatorContext = evaluatorContext; | 
|  | this.bubbleErrorInfo = Preconditions.checkNotNull(bubbleErrorInfo); | 
|  | this.hermeticity = skyKey.functionName().getHermeticity(); | 
|  | try { | 
|  | this.previouslyRequestedDepsValues = | 
|  | batchPrefetch(skyKey, directDeps, oldDeps, /*assertDone=*/ false); | 
|  | } catch (UndonePreviouslyRequestedDeps undonePreviouslyRequestedDeps) { | 
|  | throw new IllegalStateException( | 
|  | "batchPrefetch can't throw UndonePreviouslyRequestedDeps unless assertDone is true", | 
|  | undonePreviouslyRequestedDeps); | 
|  | } | 
|  | Preconditions.checkState( | 
|  | !this.previouslyRequestedDepsValues.containsKey(ErrorTransienceValue.KEY), | 
|  | "%s cannot have a dep on ErrorTransienceValue during building", | 
|  | skyKey); | 
|  | } | 
|  |  | 
|  | private ImmutableMap<SkyKey, SkyValue> batchPrefetch( | 
|  | SkyKey requestor, GroupedList<SkyKey> depKeys, Set<SkyKey> oldDeps, boolean assertDone) | 
|  | throws InterruptedException, UndonePreviouslyRequestedDeps { | 
|  | QueryableGraph.PrefetchDepsRequest request = null; | 
|  | if (PREFETCH_OLD_DEPS) { | 
|  | request = new QueryableGraph.PrefetchDepsRequest(requestor, oldDeps, depKeys); | 
|  | evaluatorContext.getGraph().prefetchDeps(request); | 
|  | } | 
|  | Map<SkyKey, ? extends NodeEntry> batchMap = | 
|  | evaluatorContext.getBatchValues( | 
|  | requestor, | 
|  | Reason.PREFETCH, | 
|  | (request != null && request.excludedKeys != null) | 
|  | ? request.excludedKeys | 
|  | : depKeys.getAllElementsAsIterable()); | 
|  | if (batchMap.size() != depKeys.numElements()) { | 
|  | NodeEntry inFlightEntry = null; | 
|  | try { | 
|  | inFlightEntry = evaluatorContext.getGraph().get(null, Reason.OTHER, requestor); | 
|  | } catch (InterruptedException e) { | 
|  | logger.atWarning().withCause(e).log( | 
|  | "Interrupted while getting parent entry for %s for crash", requestor); | 
|  | // We're crashing, don't mask it. | 
|  | Thread.currentThread().interrupt(); | 
|  | } | 
|  | Set<SkyKey> difference = Sets.difference(depKeys.toSet(), batchMap.keySet()); | 
|  | logger.atSevere().log("Missing keys for %s: %s\n\n%s", requestor, difference, inFlightEntry); | 
|  | evaluatorContext | 
|  | .getGraphInconsistencyReceiver() | 
|  | .noteInconsistencyAndMaybeThrow( | 
|  | requestor, difference, Inconsistency.ALREADY_DECLARED_CHILD_MISSING); | 
|  | throw new UndonePreviouslyRequestedDeps(ImmutableList.copyOf(difference)); | 
|  | } | 
|  | ImmutableMap.Builder<SkyKey, SkyValue> depValuesBuilder = | 
|  | ImmutableMap.builderWithExpectedSize(batchMap.size()); | 
|  | for (Entry<SkyKey, ? extends NodeEntry> entry : batchMap.entrySet()) { | 
|  | SkyValue valueMaybeWithMetadata = entry.getValue().getValueMaybeWithMetadata(); | 
|  | boolean depDone = valueMaybeWithMetadata != null; | 
|  | if (assertDone && !depDone) { | 
|  | // A previously requested dep may have transitioned from done to dirty between when the node | 
|  | // was read during a previous attempt to build this node and now. Notify the graph | 
|  | // inconsistency receiver so that we can crash if that's unexpected. | 
|  | evaluatorContext | 
|  | .getGraphInconsistencyReceiver() | 
|  | .noteInconsistencyAndMaybeThrow( | 
|  | skyKey, | 
|  | ImmutableList.of(entry.getKey()), | 
|  | Inconsistency.BUILDING_PARENT_FOUND_UNDONE_CHILD); | 
|  | throw new UndonePreviouslyRequestedDeps(ImmutableList.of(entry.getKey())); | 
|  | } | 
|  | depValuesBuilder.put(entry.getKey(), !depDone ? NULL_MARKER : valueMaybeWithMetadata); | 
|  | if (depDone) { | 
|  | maybeUpdateMaxChildVersion(entry.getValue()); | 
|  | } | 
|  | } | 
|  | return depValuesBuilder.build(); | 
|  | } | 
|  |  | 
|  | private void checkActive() { | 
|  | Preconditions.checkState(building, skyKey); | 
|  | } | 
|  |  | 
|  | Pair<NestedSet<TaggedEvents>, NestedSet<Postable>> buildAndReportEventsAndPostables( | 
|  | NodeEntry entry, boolean expectDoneDeps) throws InterruptedException { | 
|  | EventFilter eventFilter = evaluatorContext.getStoredEventFilter(); | 
|  | if (!eventFilter.storeEventsAndPosts()) { | 
|  | return Pair.of( | 
|  | NestedSetBuilder.emptySet(Order.STABLE_ORDER), | 
|  | NestedSetBuilder.emptySet(Order.STABLE_ORDER)); | 
|  | } | 
|  |  | 
|  | NestedSetBuilder<TaggedEvents> eventBuilder = NestedSetBuilder.stableOrder(); | 
|  | ImmutableList<Event> events = eventHandler.getEvents(); | 
|  | if (!events.isEmpty()) { | 
|  | eventBuilder.add(new TaggedEvents(getTagFromKey(), events)); | 
|  | } | 
|  | NestedSetBuilder<Postable> postBuilder = NestedSetBuilder.stableOrder(); | 
|  | postBuilder.addAll(eventHandler.getPosts()); | 
|  |  | 
|  | GroupedList<SkyKey> depKeys = entry.getTemporaryDirectDeps(); | 
|  | Collection<SkyValue> deps = | 
|  | getDepValuesForDoneNodeFromErrorOrDepsOrGraph( | 
|  | Iterables.filter( | 
|  | depKeys.getAllElementsAsIterable(), | 
|  | eventFilter.depEdgeFilterForEventsAndPosts(skyKey)), | 
|  | expectDoneDeps, | 
|  | depKeys.numElements()); | 
|  | for (SkyValue value : deps) { | 
|  | eventBuilder.addTransitive(ValueWithMetadata.getEvents(value)); | 
|  | postBuilder.addTransitive(ValueWithMetadata.getPosts(value)); | 
|  | } | 
|  | NestedSet<TaggedEvents> taggedEvents = eventBuilder.build(); | 
|  | NestedSet<Postable> postables = postBuilder.build(); | 
|  | evaluatorContext.getReplayingNestedSetEventVisitor().visit(taggedEvents); | 
|  | evaluatorContext.getReplayingNestedSetPostableVisitor().visit(postables); | 
|  | return Pair.of(taggedEvents, postables); | 
|  | } | 
|  |  | 
|  | void setValue(SkyValue newValue) { | 
|  | Preconditions.checkState( | 
|  | errorInfo == null && bubbleErrorInfo == null, | 
|  | "%s %s %s %s", | 
|  | skyKey, | 
|  | newValue, | 
|  | errorInfo, | 
|  | bubbleErrorInfo); | 
|  | Preconditions.checkState(value == null, "%s %s %s", skyKey, value, newValue); | 
|  | value = newValue; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Set this node to be in error. The node's value must not have already been set. However, all | 
|  | * dependencies of this node <i>must</i> already have been registered, since this method may | 
|  | * register a dependence on the error transience node, which should always be the last dep. | 
|  | */ | 
|  | void setError(NodeEntry state, ErrorInfo errorInfo)  throws InterruptedException { | 
|  | Preconditions.checkState(value == null, "%s %s %s", skyKey, value, errorInfo); | 
|  | Preconditions.checkState(this.errorInfo == null, "%s %s %s", skyKey, this.errorInfo, errorInfo); | 
|  |  | 
|  | if (errorInfo.isDirectlyTransient()) { | 
|  | NodeEntry errorTransienceNode = | 
|  | Preconditions.checkNotNull( | 
|  | evaluatorContext | 
|  | .getGraph() | 
|  | .get(skyKey, Reason.RDEP_ADDITION, ErrorTransienceValue.KEY), | 
|  | "Null error value? %s", | 
|  | skyKey); | 
|  | DependencyState triState; | 
|  | if (oldDeps.contains(ErrorTransienceValue.KEY)) { | 
|  | triState = errorTransienceNode.checkIfDoneForDirtyReverseDep(skyKey); | 
|  | } else { | 
|  | triState = errorTransienceNode.addReverseDepAndCheckIfDone(skyKey); | 
|  | } | 
|  | Preconditions.checkState( | 
|  | triState == DependencyState.DONE, "%s %s %s", skyKey, triState, errorInfo); | 
|  | state.addTemporaryDirectDeps(GroupedListHelper.create(ErrorTransienceValue.KEY)); | 
|  | state.signalDep(evaluatorContext.getGraphVersion(), ErrorTransienceValue.KEY); | 
|  | maxChildVersion = evaluatorContext.getGraphVersion(); | 
|  | } | 
|  |  | 
|  | this.errorInfo = Preconditions.checkNotNull(errorInfo, skyKey); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a map of {@code keys} to values or {@link #NULL_MARKER}s, populating the map's contents | 
|  | * by looking in order at: | 
|  | * | 
|  | * <ol> | 
|  | *   <li>{@link #bubbleErrorInfo} | 
|  | *   <li>{@link #previouslyRequestedDepsValues} | 
|  | *   <li>{@link #newlyRequestedDepsValues} | 
|  | *   <li>{@link #evaluatorContext}'s graph accessing methods | 
|  | * </ol> | 
|  | * | 
|  | * <p>All {@code keys} not previously requested will be added to a new group in {@link | 
|  | * #newlyRequestedDeps}. The new group will mirror the order of {@code keys}, minus duplicates. | 
|  | * | 
|  | * <p>Any key whose {@link NodeEntry}--or absence thereof--had to be read from the graph will also | 
|  | * be entered into {@link #newlyRequestedDepsValues} with its value or a {@link #NULL_MARKER}. | 
|  | */ | 
|  | private Map<SkyKey, SkyValue> getValuesFromErrorOrDepsOrGraph(Iterable<? extends SkyKey> keys) | 
|  | throws InterruptedException { | 
|  | // Uses a HashMap, not an ImmutableMap.Builder, because we have not yet deduplicated these keys | 
|  | // and ImmutableMap.Builder does not tolerate duplicates. | 
|  | Map<SkyKey, SkyValue> result = new HashMap<>(); | 
|  | Set<SkyKey> missingKeys = new HashSet<>(); | 
|  | newlyRequestedDeps.startGroup(); | 
|  | for (SkyKey key : keys) { | 
|  | Preconditions.checkState( | 
|  | !key.equals(ErrorTransienceValue.KEY), | 
|  | "Error transience key cannot be in requested deps of %s", | 
|  | skyKey); | 
|  | SkyValue value = maybeGetValueFromErrorOrDeps(key); | 
|  | boolean duplicate; | 
|  | if (value == null) { | 
|  | duplicate = !missingKeys.add(key); | 
|  | } else { | 
|  | duplicate = result.put(key, value) != null; | 
|  | } | 
|  | if (!duplicate && !previouslyRequestedDepsValues.containsKey(key)) { | 
|  | newlyRequestedDeps.add(key); | 
|  | } | 
|  | } | 
|  | newlyRequestedDeps.endGroup(); | 
|  |  | 
|  | if (missingKeys.isEmpty()) { | 
|  | return result; | 
|  | } | 
|  | Map<SkyKey, ? extends NodeEntry> missingEntries = | 
|  | evaluatorContext.getBatchValues(skyKey, Reason.DEP_REQUESTED, missingKeys); | 
|  | for (SkyKey key : missingKeys) { | 
|  | NodeEntry depEntry = missingEntries.get(key); | 
|  | SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry); | 
|  | result.put(key, valueOrNullMarker); | 
|  | newlyRequestedDepsValues.put(key, valueOrNullMarker); | 
|  | if (valueOrNullMarker != NULL_MARKER) { | 
|  | maybeUpdateMaxChildVersion(depEntry); | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the values of done deps in {@code depKeys}, by looking in order at: | 
|  | * | 
|  | * <ol> | 
|  | *   <li>{@link #bubbleErrorInfo} | 
|  | *   <li>{@link #previouslyRequestedDepsValues} | 
|  | *   <li>{@link #newlyRequestedDepsValues} | 
|  | *   <li>{@link #evaluatorContext}'s graph accessing methods | 
|  | * </ol> | 
|  | * | 
|  | * <p>Any key whose {@link NodeEntry}--or absence thereof--had to be read from the graph will also | 
|  | * be entered into {@link #newlyRequestedDepsValues} with its value or a {@link #NULL_MARKER}. | 
|  | * | 
|  | * <p>This asserts that only keys in {@link #newlyRegisteredDeps} require reading from the graph, | 
|  | * because this node is done, and so all other deps must have been previously or newly requested. | 
|  | * | 
|  | * <p>If {@code assertDone}, this asserts that all deps in {@code depKeys} are done. | 
|  | */ | 
|  | private Collection<SkyValue> getDepValuesForDoneNodeFromErrorOrDepsOrGraph( | 
|  | Iterable<SkyKey> depKeys, boolean assertDone, int keySize) throws InterruptedException { | 
|  | List<SkyValue> result = new ArrayList<>(keySize); | 
|  | // depKeys may contain keys in newlyRegisteredDeps whose values have not yet been retrieved from | 
|  | // the graph during this environment's lifetime. | 
|  | int expectedMissingKeys = newlyRegisteredDeps.size(); | 
|  | ArrayList<SkyKey> missingKeys = | 
|  | expectedMissingKeys > 0 ? new ArrayList<>(expectedMissingKeys) : null; | 
|  | ArrayList<SkyKey> unexpectedlyMissingKeys = null; | 
|  |  | 
|  | for (SkyKey key : depKeys) { | 
|  | SkyValue value = maybeGetValueFromErrorOrDeps(key); | 
|  | if (value == null) { | 
|  | if (key == ErrorTransienceValue.KEY) { | 
|  | continue; | 
|  | } | 
|  | if (!newlyRegisteredDeps.contains(key)) { | 
|  | if (unexpectedlyMissingKeys == null) { | 
|  | unexpectedlyMissingKeys = new ArrayList<>(); | 
|  | } | 
|  | unexpectedlyMissingKeys.add(key); | 
|  | if (missingKeys == null) { | 
|  | missingKeys = new ArrayList<>(); | 
|  | } | 
|  | } | 
|  | missingKeys.add(key); | 
|  | } else if (value == NULL_MARKER) { | 
|  | Preconditions.checkState(!assertDone, "%s had not done %s", skyKey, key); | 
|  | } else { | 
|  | result.add(value); | 
|  | } | 
|  | } | 
|  | if (unexpectedlyMissingKeys != null && !unexpectedlyMissingKeys.isEmpty()) { | 
|  | // This may still crash below, if the dep is not done in the graph, but at least it gives the | 
|  | // dep until now to complete its computation, as opposed to the start of this node's | 
|  | // evaluation, which is when most of the structures used by #maybeGetValueFromErrorOrDeps were | 
|  | // created. | 
|  | evaluatorContext | 
|  | .getGraphInconsistencyReceiver() | 
|  | .noteInconsistencyAndMaybeThrow( | 
|  | skyKey, unexpectedlyMissingKeys, Inconsistency.ALREADY_DECLARED_CHILD_MISSING); | 
|  | } | 
|  | if (missingKeys == null || missingKeys.isEmpty()) { | 
|  | return result; | 
|  | } | 
|  | Map<SkyKey, ? extends NodeEntry> missingEntries = | 
|  | evaluatorContext.getBatchValues(skyKey, Reason.DEP_REQUESTED, missingKeys); | 
|  | for (SkyKey key : missingKeys) { | 
|  | NodeEntry depEntry = missingEntries.get(key); | 
|  | SkyValue valueOrNullMarker = getValueOrNullMarker(depEntry); | 
|  | newlyRequestedDepsValues.put(key, valueOrNullMarker); | 
|  | if (valueOrNullMarker == NULL_MARKER) { | 
|  | // TODO(mschaller): handle registered deps that transitioned from done to dirty during eval | 
|  | // But how? Restarting the current node may not help, because this dep was *registered*, not | 
|  | // requested. For now, no node that gets registered as a dep is eligible for | 
|  | // intra-evaluation dirtying, so let it crash. | 
|  | Preconditions.checkState(!assertDone, "%s had not done: %s", skyKey, key); | 
|  | continue; | 
|  | } | 
|  | maybeUpdateMaxChildVersion(depEntry); | 
|  | result.add(valueOrNullMarker); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a value or a {@link #NULL_MARKER} associated with {@code key} by looking in order at: | 
|  | * | 
|  | * <ol> | 
|  | *   <li>{@code bubbleErrorInfo} | 
|  | *   <li>{@link #previouslyRequestedDepsValues} | 
|  | *   <li>{@link #newlyRequestedDepsValues} | 
|  | * </ol> | 
|  | * | 
|  | * <p>Returns {@code null} if no entries for {@code key} were found in any of those three maps. | 
|  | * (Note that none of the maps can have {@code null} as a value.) | 
|  | */ | 
|  | @Nullable | 
|  | SkyValue maybeGetValueFromErrorOrDeps(SkyKey key) { | 
|  | if (bubbleErrorInfo != null) { | 
|  | ValueWithMetadata bubbleErrorInfoValue = bubbleErrorInfo.get(key); | 
|  | if (bubbleErrorInfoValue != null) { | 
|  | return bubbleErrorInfoValue; | 
|  | } | 
|  | } | 
|  | SkyValue directDepsValue = previouslyRequestedDepsValues.get(key); | 
|  | if (directDepsValue != null) { | 
|  | return directDepsValue; | 
|  | } | 
|  | SkyValue newlyRequestedDepsValue = newlyRequestedDepsValues.get(key); | 
|  | if (newlyRequestedDepsValue != null) { | 
|  | return newlyRequestedDepsValue; | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | private static SkyValue getValueOrNullMarker(@Nullable NodeEntry nodeEntry) | 
|  | throws InterruptedException { | 
|  | if (nodeEntry == null) { | 
|  | return NULL_MARKER; | 
|  | } | 
|  | SkyValue valueMaybeWithMetadata = nodeEntry.getValueMaybeWithMetadata(); | 
|  | if (valueMaybeWithMetadata == null) { | 
|  | return NULL_MARKER; | 
|  | } | 
|  | return valueMaybeWithMetadata; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected Map<SkyKey, ValueOrUntypedException> getValueOrUntypedExceptions( | 
|  | Iterable<? extends SkyKey> depKeys) throws InterruptedException { | 
|  | checkActive(); | 
|  | Map<SkyKey, SkyValue> values = getValuesFromErrorOrDepsOrGraph(depKeys); | 
|  | for (Map.Entry<SkyKey, SkyValue> depEntry : values.entrySet()) { | 
|  | SkyKey depKey = depEntry.getKey(); | 
|  | SkyValue depValue = depEntry.getValue(); | 
|  |  | 
|  | if (depValue == NULL_MARKER) { | 
|  | valuesMissing = true; | 
|  | if (previouslyRequestedDepsValues.containsKey(depKey)) { | 
|  | Preconditions.checkState( | 
|  | bubbleErrorInfo != null, | 
|  | "Undone key %s was already in deps of %s( dep: %s, parent: %s )", | 
|  | depKey, | 
|  | skyKey, | 
|  | evaluatorContext.getGraph().get(skyKey, Reason.OTHER, depKey), | 
|  | evaluatorContext.getGraph().get(null, Reason.OTHER, skyKey)); | 
|  | } | 
|  | continue; | 
|  | } | 
|  |  | 
|  | ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(depValue); | 
|  | if (errorInfo != null) { | 
|  | errorMightHaveBeenFound = true; | 
|  | childErrorInfos.add(errorInfo); | 
|  | if (bubbleErrorInfo != null) { | 
|  | // Set interrupted status, to try to prevent the calling SkyFunction from doing anything | 
|  | // fancy after this. SkyFunctions executed during error bubbling are supposed to | 
|  | // (quickly) rethrow errors or return a value/null (but there's currently no way to | 
|  | // enforce this). | 
|  | Thread.currentThread().interrupt(); | 
|  | } | 
|  | if ((!evaluatorContext.keepGoing() && bubbleErrorInfo == null) | 
|  | || errorInfo.getException() == null) { | 
|  | valuesMissing = true; | 
|  | // We arbitrarily record the first child error if we are about to abort. | 
|  | if (!evaluatorContext.keepGoing() && depErrorKey == null) { | 
|  | depErrorKey = depKey; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return Maps.transformValues( | 
|  | values, | 
|  | maybeWrappedValue -> { | 
|  | if (maybeWrappedValue == NULL_MARKER) { | 
|  | return ValueOrUntypedException.ofNull(); | 
|  | } | 
|  | SkyValue justValue = ValueWithMetadata.justValue(maybeWrappedValue); | 
|  | ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(maybeWrappedValue); | 
|  |  | 
|  | if (justValue != null && (evaluatorContext.keepGoing() || errorInfo == null)) { | 
|  | // If the dep did compute a value, it is given to the caller if we are in | 
|  | // keepGoing mode or if we are in noKeepGoingMode and there were no errors computing | 
|  | // it. | 
|  | return ValueOrUntypedException.ofValueUntyped(justValue); | 
|  | } | 
|  |  | 
|  | // There was an error building the value, which we will either report by throwing an | 
|  | // exception or insulate the caller from by returning null. | 
|  | Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, maybeWrappedValue); | 
|  | Exception exception = errorInfo.getException(); | 
|  |  | 
|  | if (!evaluatorContext.keepGoing() && exception != null && bubbleErrorInfo == null) { | 
|  | // Child errors should not be propagated in noKeepGoing mode (except during error | 
|  | // bubbling). Instead we should fail fast. | 
|  | return ValueOrUntypedException.ofNull(); | 
|  | } | 
|  |  | 
|  | if (exception != null) { | 
|  | // Give builder a chance to handle this exception. | 
|  | return ValueOrUntypedException.ofExn(exception); | 
|  | } | 
|  | // In a cycle. | 
|  | Preconditions.checkState( | 
|  | !Iterables.isEmpty(errorInfo.getCycleInfo()), | 
|  | "%s %s %s", | 
|  | skyKey, | 
|  | errorInfo, | 
|  | maybeWrappedValue); | 
|  | return ValueOrUntypedException.ofNull(); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If {@code !keepGoing} and there is at least one dep in error, returns a dep in error. Otherwise | 
|  | * returns {@code null}. | 
|  | */ | 
|  | @Nullable | 
|  | SkyKey getDepErrorKey() { | 
|  | return depErrorKey; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ExtendedEventHandler getListener() { | 
|  | checkActive(); | 
|  | return eventHandler; | 
|  | } | 
|  |  | 
|  | void doneBuilding() { | 
|  | building = false; | 
|  | } | 
|  |  | 
|  | GroupedListHelper<SkyKey> getNewlyRequestedDeps() { | 
|  | return newlyRequestedDeps; | 
|  | } | 
|  |  | 
|  | void removeUndoneNewlyRequestedDeps() { | 
|  | HashSet<SkyKey> undoneDeps = new HashSet<>(); | 
|  | for (SkyKey newlyRequestedDep : newlyRequestedDeps) { | 
|  | if (newlyRegisteredDeps.contains(newlyRequestedDep)) { | 
|  | continue; | 
|  | } | 
|  | SkyValue newlyRequestedDepValue = | 
|  | Preconditions.checkNotNull( | 
|  | newlyRequestedDepsValues.get(newlyRequestedDep), newlyRequestedDep); | 
|  | if (newlyRequestedDepValue == NULL_MARKER) { | 
|  | // The dep was normally requested, and was not done. | 
|  | undoneDeps.add(newlyRequestedDep); | 
|  | } | 
|  | } | 
|  | newlyRequestedDeps.remove(undoneDeps); | 
|  | } | 
|  |  | 
|  | boolean isAnyDirectDepErrorTransitivelyTransient() { | 
|  | Preconditions.checkState( | 
|  | bubbleErrorInfo == null, | 
|  | "Checking dep error transitive transience during error bubbling for: %s", | 
|  | skyKey); | 
|  | for (SkyValue skyValue : previouslyRequestedDepsValues.values()) { | 
|  | ErrorInfo maybeErrorInfo = ValueWithMetadata.getMaybeErrorInfo(skyValue); | 
|  | if (maybeErrorInfo != null && maybeErrorInfo.isTransitivelyTransient()) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | boolean isAnyNewlyRequestedDepErrorTransitivelyTransient() { | 
|  | Preconditions.checkState( | 
|  | bubbleErrorInfo == null, | 
|  | "Checking dep error transitive transience during error bubbling for: %s", | 
|  | skyKey); | 
|  | for (SkyValue skyValue : newlyRequestedDepsValues.values()) { | 
|  | ErrorInfo maybeErrorInfo = ValueWithMetadata.getMaybeErrorInfo(skyValue); | 
|  | if (maybeErrorInfo != null && maybeErrorInfo.isTransitivelyTransient()) { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | return false; | 
|  | } | 
|  |  | 
|  | Collection<ErrorInfo> getChildErrorInfos() { | 
|  | return childErrorInfos; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Apply the change to the graph (mostly) atomically and signal all nodes that are waiting for | 
|  | * this node to complete. Adding nodes and signaling is not atomic, but may need to be changed for | 
|  | * interruptibility. | 
|  | * | 
|  | * <p>Parents are only enqueued if {@code enqueueParents} holds. Parents should be enqueued unless | 
|  | * (1) this node is being built after the main evaluation has aborted, or (2) this node is being | 
|  | * built with --nokeep_going, and so we are about to shut down the main evaluation anyway. | 
|  | * | 
|  | * <p>The reverse deps that would have been enqueued are returned if {@code enqueueParents} is | 
|  | * {@link EnqueueParentBehavior#SIGNAL} or {@link EnqueueParentBehavior#NO_ACTION}, so that the | 
|  | * caller may simulate actions on the parents if desired. Otherwise this method returns null. | 
|  | */ | 
|  | Set<SkyKey> commit(NodeEntry primaryEntry, EnqueueParentBehavior enqueueParents) | 
|  | throws InterruptedException { | 
|  | // Construct the definitive error info, if there is one. | 
|  | if (errorInfo == null) { | 
|  | errorInfo = evaluatorContext.getErrorInfoManager().getErrorInfoToUse( | 
|  | skyKey, value != null, childErrorInfos); | 
|  | } | 
|  |  | 
|  | // We have the following implications: | 
|  | // errorInfo == null => value != null => enqueueParents. | 
|  | // All these implications are strict: | 
|  | // (1) errorInfo != null && value != null happens for values with recoverable errors. | 
|  | // (2) value == null && enqueueParents happens for values that are found to have errors | 
|  | // during a --keep_going build. | 
|  |  | 
|  | Pair<NestedSet<TaggedEvents>, NestedSet<Postable>> eventsAndPostables = | 
|  | buildAndReportEventsAndPostables(primaryEntry, /*expectDoneDeps=*/ true); | 
|  |  | 
|  | SkyValue valueWithMetadata; | 
|  | if (value == null) { | 
|  | Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry); | 
|  | valueWithMetadata = | 
|  | ValueWithMetadata.error(errorInfo, eventsAndPostables.first, eventsAndPostables.second); | 
|  | } else { | 
|  | // We must be enqueueing parents if we have a value. | 
|  | Preconditions.checkState( | 
|  | enqueueParents == EnqueueParentBehavior.ENQUEUE, "%s %s", skyKey, primaryEntry); | 
|  | valueWithMetadata = | 
|  | ValueWithMetadata.normal( | 
|  | value, errorInfo, eventsAndPostables.first, eventsAndPostables.second); | 
|  | } | 
|  | GroupedList<SkyKey> temporaryDirectDeps = primaryEntry.getTemporaryDirectDeps(); | 
|  | if (evaluatorContext.getGraph().storesReverseDeps() && !oldDeps.isEmpty()) { | 
|  | // Remove the rdep on this entry for each of its old deps that is no longer a direct dep. | 
|  | Set<SkyKey> depsToRemove = Sets.difference(oldDeps, temporaryDirectDeps.toSet()); | 
|  | Collection<? extends NodeEntry> oldDepEntries = | 
|  | evaluatorContext.getGraph().getBatch(skyKey, Reason.RDEP_REMOVAL, depsToRemove).values(); | 
|  | for (NodeEntry oldDepEntry : oldDepEntries) { | 
|  | oldDepEntry.removeReverseDep(skyKey); | 
|  | } | 
|  | } | 
|  | DepFingerprintList depFingerprintList = null; | 
|  | if (primaryEntry.canPruneDepsByFingerprint()) { | 
|  | DepFingerprintList.Builder depFingerprintListBuilder = | 
|  | new DepFingerprintList.Builder(temporaryDirectDeps.listSize()); | 
|  | // TODO(janakr): in the common case, all these nodes may be locally cached. Do multi-level | 
|  | // checking a la #getDepValuesForDoneNodeFromErrorOrDepsOrGraph to save graph lookups? | 
|  | Map<SkyKey, ? extends NodeEntry> allDeps = | 
|  | evaluatorContext.getBatchValues( | 
|  | skyKey, Reason.DEP_REQUESTED, temporaryDirectDeps.getAllElementsAsIterable()); | 
|  | for (Collection<SkyKey> depGroup : temporaryDirectDeps) { | 
|  | depFingerprintListBuilder.add( | 
|  | AbstractParallelEvaluator.composeDepFingerprints(depGroup, allDeps)); | 
|  | } | 
|  | depFingerprintList = depFingerprintListBuilder.build(); | 
|  | } | 
|  |  | 
|  | Version evaluationVersion = maxChildVersion; | 
|  | if (bubbleErrorInfo != null) { | 
|  | // Cycles can lead to a state where the versions of done children don't accurately reflect the | 
|  | // state that led to this node's value. Be conservative then. | 
|  | evaluationVersion = evaluatorContext.getGraphVersion(); | 
|  | } else if (injectedVersion != null) { | 
|  | evaluationVersion = injectedVersion; | 
|  | } else if (evaluatorContext.getEvaluationVersionBehavior() | 
|  | == EvaluationVersionBehavior.GRAPH_VERSION | 
|  | || hermeticity == FunctionHermeticity.NONHERMETIC) { | 
|  | evaluationVersion = evaluatorContext.getGraphVersion(); | 
|  | } else if (evaluationVersion == null) { | 
|  | Preconditions.checkState( | 
|  | temporaryDirectDeps.isEmpty(), | 
|  | "No max child version found, but have direct deps: %s %s", | 
|  | skyKey, | 
|  | primaryEntry); | 
|  | evaluationVersion = evaluatorContext.getGraphVersion(); | 
|  | } | 
|  | Version previousVersion = primaryEntry.getVersion(); | 
|  | // If this entry is dirty, setValue may not actually change it, if it determines that | 
|  | // the data being written now is the same as the data already present in the entry. | 
|  | Set<SkyKey> reverseDeps = | 
|  | primaryEntry.setValue(valueWithMetadata, evaluationVersion, depFingerprintList); | 
|  |  | 
|  | // Note that if this update didn't actually change the entry, this version may not be | 
|  | // evaluationVersion. | 
|  | Version currentVersion = primaryEntry.getVersion(); | 
|  | // Tell the receiver that this value was built. If currentVersion.equals(evaluationVersion), it | 
|  | // was evaluated this run, and so was changed. Otherwise, it is less than evaluationVersion, by | 
|  | // the Preconditions check above, and was not actually changed this run -- when it was written | 
|  | // above, its version stayed below this update's version, so its value remains the same. | 
|  | // We use a SkyValueSupplier here because it keeps a reference to the entry, allowing for | 
|  | // the receiver to be confident that the entry is readily accessible in memory. | 
|  | EvaluationState evaluationState = | 
|  | currentVersion.equals(previousVersion) ? EvaluationState.CLEAN : EvaluationState.BUILT; | 
|  | evaluatorContext | 
|  | .getProgressReceiver() | 
|  | .evaluated( | 
|  | skyKey, | 
|  | evaluationState == EvaluationState.BUILT ? value : null, | 
|  | EvaluationSuccessStateSupplier.fromSkyValue(valueWithMetadata), | 
|  | evaluationState); | 
|  |  | 
|  | evaluatorContext.signalValuesAndEnqueueIfReady( | 
|  | skyKey, reverseDeps, currentVersion, enqueueParents); | 
|  |  | 
|  | return enqueueParents == EnqueueParentBehavior.ENQUEUE ? null : reverseDeps; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | private String getTagFromKey() { | 
|  | return evaluatorContext.getSkyFunctions().get(skyKey.functionName()).extractTag(skyKey); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Gets the latch that is counted down when an exception is thrown in {@code | 
|  | * AbstractQueueVisitor}. For use in tests to check if an exception actually was thrown. Calling | 
|  | * {@code AbstractQueueVisitor#awaitExceptionForTestingOnly} can throw a spurious {@link | 
|  | * InterruptedException} because {@link CountDownLatch#await} checks the interrupted bit before | 
|  | * returning, even if the latch is already at 0. See bug "testTwoErrors is flaky". | 
|  | */ | 
|  | CountDownLatch getExceptionLatchForTesting() { | 
|  | return evaluatorContext.getVisitor().getExceptionLatchForTestingOnly(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean inErrorBubblingForTesting() { | 
|  | return bubbleErrorInfo != null; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void registerDependencies(Iterable<SkyKey> keys) throws InterruptedException { | 
|  | if (EvaluationVersionBehavior.MAX_CHILD_VERSIONS.equals( | 
|  | evaluatorContext.getEvaluationVersionBehavior())) { | 
|  | // Need versions when doing MAX_CHILD_VERSIONS, so can't use optimization. To use the | 
|  | // optimization, the caller would have to know the versions of the passed-in keys. Extensions | 
|  | // of the SkyFunction.Environment interface to make that possible could happen. | 
|  | Map<SkyKey, SkyValue> checkSizeMap = getValues(keys); | 
|  | ImmutableSet<SkyKey> keysSet = ImmutableSet.copyOf(keys); | 
|  | if (checkSizeMap.size() != keysSet.size()) { | 
|  | throw new IllegalStateException( | 
|  | "Missing keys when checking dependencies for " | 
|  | + skyKey | 
|  | + ": " | 
|  | + Sets.difference(keysSet, checkSizeMap.keySet())); | 
|  | } | 
|  | return; | 
|  | } | 
|  | newlyRequestedDeps.startGroup(); | 
|  | for (SkyKey key : keys) { | 
|  | if (!previouslyRequestedDepsValues.containsKey(key)) { | 
|  | newlyRequestedDeps.add(key); | 
|  | newlyRegisteredDeps.add(key); | 
|  | } | 
|  | } | 
|  | newlyRequestedDeps.endGroup(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void injectVersionForNonHermeticFunction(Version version) { | 
|  | Preconditions.checkState(hermeticity == FunctionHermeticity.NONHERMETIC, skyKey); | 
|  | injectedVersion = version; | 
|  | } | 
|  |  | 
|  | private void maybeUpdateMaxChildVersion(NodeEntry depEntry) { | 
|  | if (hermeticity != FunctionHermeticity.NONHERMETIC | 
|  | && evaluatorContext.getEvaluationVersionBehavior() | 
|  | == EvaluationVersionBehavior.MAX_CHILD_VERSIONS) { | 
|  | Version depVersion = depEntry.getVersion(); | 
|  | if (maxChildVersion == null || maxChildVersion.atMost(depVersion)) { | 
|  | maxChildVersion = depVersion; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return MoreObjects.toStringHelper(this) | 
|  | .add("skyKey", skyKey) | 
|  | .add("oldDeps", oldDeps) | 
|  | .add("value", value) | 
|  | .add("errorInfo", errorInfo) | 
|  | .add("previouslyRequestedDepsValues", previouslyRequestedDepsValues) | 
|  | .add("newlyRequestedDepsValues", newlyRequestedDepsValues) | 
|  | .add("newlyRegisteredDeps", newlyRegisteredDeps) | 
|  | .add("newlyRequestedDeps", newlyRequestedDeps) | 
|  | .add("childErrorInfos", childErrorInfos) | 
|  | .add("depErrorKey", depErrorKey) | 
|  | .add("hermeticity", hermeticity) | 
|  | .add("maxChildVersion", maxChildVersion) | 
|  | .add("injectedVersion", injectedVersion) | 
|  | .add("bubbleErrorInfo", bubbleErrorInfo) | 
|  | .add("evaluatorContext", evaluatorContext) | 
|  | .toString(); | 
|  | } | 
|  |  | 
|  | /** Thrown during environment construction if previously requested deps are no longer done. */ | 
|  | static class UndonePreviouslyRequestedDeps extends Exception { | 
|  | private final ImmutableList<SkyKey> depKeys; | 
|  |  | 
|  | UndonePreviouslyRequestedDeps(ImmutableList<SkyKey> depKeys) { | 
|  | this.depKeys = depKeys; | 
|  | } | 
|  |  | 
|  | ImmutableList<SkyKey> getDepKeys() { | 
|  | return depKeys; | 
|  | } | 
|  | } | 
|  | } |