com.google.devtools.build.skyframe.SkyFunctionEnvironment.java Source code

Java tutorial

Introduction

Here is the source code for com.google.devtools.build.skyframe.SkyFunctionEnvironment.java

Source

// 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 static com.google.devtools.build.skyframe.ParallelEvaluator.isDoneForBuild;
import static com.google.devtools.build.skyframe.ParallelEvaluator.maybeGetValueFromError;

import com.google.common.base.Function;
import com.google.common.base.Predicates;
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.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
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.Preconditions;
import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
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.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
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 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 Map<SkyKey, ValueWithMetadata> bubbleErrorInfo;
    /** The values previously declared as dependencies. */
    private final Map<SkyKey, NodeEntry> directDeps;

    /**
     * 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 Collection<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);
            }
        }
    };
    private final ParallelEvaluatorContext evaluatorContext;

    SkyFunctionEnvironment(SkyKey skyKey, GroupedList<SkyKey> directDeps, Set<SkyKey> oldDeps,
            ParallelEvaluatorContext evaluatorContext) throws InterruptedException {
        this(skyKey, directDeps, null, oldDeps, evaluatorContext);
    }

    SkyFunctionEnvironment(SkyKey skyKey, GroupedList<SkyKey> directDeps,
            @Nullable Map<SkyKey, ValueWithMetadata> bubbleErrorInfo, Set<SkyKey> oldDeps,
            ParallelEvaluatorContext evaluatorContext) throws InterruptedException {
        this.skyKey = skyKey;
        this.oldDeps = oldDeps;
        this.evaluatorContext = evaluatorContext;
        this.directDeps = Collections.<SkyKey, NodeEntry>unmodifiableMap(
                batchPrefetch(skyKey, directDeps, oldDeps, /*assertDone=*/ bubbleErrorInfo == null, skyKey));
        this.bubbleErrorInfo = bubbleErrorInfo;
        Preconditions.checkState(!this.directDeps.containsKey(ErrorTransienceValue.KEY),
                "%s cannot have a dep on ErrorTransienceValue during building", skyKey);
    }

    private Map<SkyKey, ? extends NodeEntry> batchPrefetch(SkyKey requestor, GroupedList<SkyKey> depKeys,
            Set<SkyKey> oldDeps, boolean assertDone, SkyKey keyForDebugging) throws InterruptedException {
        Iterable<SkyKey> depKeysAsIterable = Iterables.concat(depKeys);
        Iterable<SkyKey> keysToPrefetch = depKeysAsIterable;
        if (PREFETCH_OLD_DEPS) {
            ImmutableSet.Builder<SkyKey> keysToPrefetchBuilder = ImmutableSet.builder();
            keysToPrefetchBuilder.addAll(depKeysAsIterable).addAll(oldDeps);
            keysToPrefetch = keysToPrefetchBuilder.build();
        }
        Map<SkyKey, ? extends NodeEntry> batchMap = evaluatorContext.getBatchValues(requestor, Reason.PREFETCH,
                keysToPrefetch);
        if (PREFETCH_OLD_DEPS) {
            batchMap = ImmutableMap.<SkyKey, NodeEntry>copyOf(
                    Maps.filterKeys(batchMap, Predicates.in(ImmutableSet.copyOf(depKeysAsIterable))));
        }
        if (batchMap.size() != depKeys.numElements()) {
            throw new IllegalStateException("Missing keys for " + keyForDebugging + ": "
                    + Sets.difference(depKeys.toSet(), batchMap.keySet()));
        }
        if (assertDone) {
            for (Map.Entry<SkyKey, ? extends NodeEntry> entry : batchMap.entrySet()) {
                Preconditions.checkState(entry.getValue().isDone(), "%s had not done %s", keyForDebugging, entry);
            }
        }
        return batchMap;
    }

    private void checkActive() {
        Preconditions.checkState(building, skyKey);
    }

    NestedSet<TaggedEvents> buildEvents(NodeEntry entry, boolean missingChildren) throws InterruptedException {
        // Aggregate the nested set of events from the direct deps, also adding the events from
        // building this value.
        NestedSetBuilder<TaggedEvents> eventBuilder = NestedSetBuilder.stableOrder();
        ImmutableList<Event> events = eventHandler.getEvents();
        if (!events.isEmpty()) {
            eventBuilder.add(new TaggedEvents(getTagFromKey(), events));
        }
        if (evaluatorContext.getStoredEventFilter().storeEvents()) {
            // Only do the work of processing children if we're going to store events.
            GroupedList<SkyKey> depKeys = entry.getTemporaryDirectDeps();
            Collection<SkyValue> deps = getDepValuesForDoneNodeMaybeFromError(depKeys);
            if (!missingChildren && depKeys.numElements() != deps.size()) {
                throw new IllegalStateException("Missing keys for " + skyKey + ". Present values: " + deps
                        + " requested from: " + depKeys + ", " + entry);
            }
            for (SkyValue value : deps) {
                eventBuilder.addTransitive(ValueWithMetadata.getEvents(value));
            }
        }
        return eventBuilder.build();
    }

    /**
     * If this node has an error, that is, if errorInfo is non-null, do nothing. Otherwise, set
     * errorInfo to the union of the child errors that were recorded earlier by getValueOrException,
     * if there are any.
     *
     * <p>Child errors are remembered, if there are any and yet the parent recovered without error, so
     * that subsequent noKeepGoing evaluations can stop as soon as they encounter a node whose
     * (transitive) children had experienced an error, even if that (transitive) parent node had been
     * able to recover from it during a keepGoing build. This behavior can be suppressed by setting
     * {@link ParallelEvaluatorContext#storeErrorsAlongsideValues} to false, which will cause nodes
     * with values to have no stored error info. This may be useful if this graph will only ever be
     * used for keepGoing builds, since in that case storing errors from recovered nodes is pointless.
     */
    private void finalizeErrorInfo() {
        if (errorInfo == null && (evaluatorContext.storeErrorsAlongsideValues() || value == null)
                && !childErrorInfos.isEmpty()) {
            errorInfo = ErrorInfo.fromChildErrors(skyKey, childErrorInfos);
        }
    }

    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, boolean isDirectlyTransient) 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 (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();
        }

        this.errorInfo = Preconditions.checkNotNull(errorInfo, skyKey);
    }

    private Map<SkyKey, SkyValue> getValuesMaybeFromError(Iterable<SkyKey> keys) throws InterruptedException {
        // Use a HashMap, not an ImmutableMap.Builder, because we have not yet deduplicated these keys
        // and ImmutableMap.Builder does not tolerate duplicates.  The map will be thrown away
        // shortly in any case.
        Map<SkyKey, SkyValue> result = new HashMap<>();
        ArrayList<SkyKey> missingKeys = new ArrayList<>();
        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);
            if (value == null) {
                missingKeys.add(key);
            } else {
                result.put(key, value);
            }
        }
        Map<SkyKey, ? extends NodeEntry> missingEntries = evaluatorContext.getBatchValues(skyKey,
                Reason.DEP_REQUESTED, missingKeys);
        for (SkyKey key : missingKeys) {
            result.put(key, getValueOrNullMarker(missingEntries.get(key)));
        }
        return result;
    }

    /**
     * Returns just the values of the deps in {@code depKeys}, looking at {@code bubbleErrorInfo},
     * {@link #directDeps}, and the backing {@link #evaluatorContext#graph} in that order. Any deps
     * that are not yet done will not have their values present in the returned collection.
     */
    private Collection<SkyValue> getDepValuesForDoneNodeMaybeFromError(GroupedList<SkyKey> depKeys)
            throws InterruptedException {
        int keySize = depKeys.numElements();
        List<SkyValue> result = new ArrayList<>(keySize);
        // depKeys consists of all known deps of this entry. That should include all the keys in
        // directDeps, and any keys in bubbleErrorInfo. We expect to have to retrieve the keys that
        // are not in either one.
        int expectedMissingKeySize = Math
                .max(keySize - directDeps.size() - (bubbleErrorInfo == null ? 0 : bubbleErrorInfo.size()), 0);
        ArrayList<SkyKey> missingKeys = new ArrayList<>(expectedMissingKeySize);
        for (SkyKey key : Iterables.concat(depKeys)) {
            SkyValue value = maybeGetValueFromErrorOrDeps(key);
            if (value == null) {
                missingKeys.add(key);
            } else {
                result.add(value);
            }
        }
        for (NodeEntry entry : evaluatorContext.getBatchValues(skyKey, Reason.DEP_REQUESTED, missingKeys)
                .values()) {
            result.add(getValueOrNullMarker(entry));
        }
        return result;
    }

    @Nullable
    private SkyValue maybeGetValueFromErrorOrDeps(SkyKey key) throws InterruptedException {
        return maybeGetValueFromError(key, directDeps.get(key), bubbleErrorInfo);
    }

    private static SkyValue getValueOrNullMarker(@Nullable NodeEntry nodeEntry) throws InterruptedException {
        return isDoneForBuild(nodeEntry) ? nodeEntry.getValueMaybeWithMetadata() : NULL_MARKER;
    }

    @Override
    protected Map<SkyKey, ValueOrUntypedException> getValueOrUntypedExceptions(Iterable<SkyKey> depKeys)
            throws InterruptedException {
        checkActive();
        Map<SkyKey, SkyValue> values = getValuesMaybeFromError(depKeys);
        for (Map.Entry<SkyKey, SkyValue> depEntry : values.entrySet()) {
            SkyKey depKey = depEntry.getKey();
            SkyValue depValue = depEntry.getValue();
            if (depValue == NULL_MARKER) {
                if (directDeps.containsKey(depKey)) {
                    throw new IllegalStateException("Undone key " + depKey + " was already in deps of " + skyKey
                            + "( dep: " + evaluatorContext.getGraph().get(skyKey, Reason.OTHER, depKey)
                            + ", parent: " + evaluatorContext.getGraph().get(null, Reason.OTHER, skyKey));
                }
                valuesMissing = true;
                addDep(depKey);
                continue;
            }
            ErrorInfo errorInfo = ValueWithMetadata.getMaybeErrorInfo(depEntry.getValue());
            if (errorInfo != null) {
                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;
                    }
                }
            }

            if (!directDeps.containsKey(depKey)) {
                if (bubbleErrorInfo == null) {
                    addDep(depKey);
                }
                evaluatorContext.getReplayingNestedSetEventVisitor().visit(ValueWithMetadata.getEvents(depValue));
            }
        }

        return Maps.transformValues(values, new Function<SkyValue, ValueOrUntypedException>() {
            @Override
            public ValueOrUntypedException apply(SkyValue maybeWrappedValue) {
                if (maybeWrappedValue == NULL_MARKER) {
                    return ValueOrExceptionUtils.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 ValueOrExceptionUtils.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 ValueOrExceptionUtils.ofNull();
                }

                if (exception != null) {
                    // Give builder a chance to handle this exception.
                    return ValueOrExceptionUtils.ofExn(exception);
                }
                // In a cycle.
                Preconditions.checkState(!Iterables.isEmpty(errorInfo.getCycleInfo()), "%s %s %s", skyKey,
                        errorInfo, maybeWrappedValue);
                return ValueOrExceptionUtils.ofNull();
            }
        });
    }

    @Override
    public <E1 extends Exception, E2 extends Exception, E3 extends Exception, E4 extends Exception, E5 extends Exception> Map<SkyKey, ValueOrException5<E1, E2, E3, E4, E5>> getValuesOrThrow(
            Iterable<SkyKey> depKeys, Class<E1> exceptionClass1, Class<E2> exceptionClass2,
            Class<E3> exceptionClass3, Class<E4> exceptionClass4, Class<E5> exceptionClass5)
            throws InterruptedException {
        newlyRequestedDeps.startGroup();
        Map<SkyKey, ValueOrException5<E1, E2, E3, E4, E5>> result = super.getValuesOrThrow(depKeys, exceptionClass1,
                exceptionClass2, exceptionClass3, exceptionClass4, exceptionClass5);
        newlyRequestedDeps.endGroup();
        return result;
    }

    private void addDep(SkyKey key) {
        newlyRequestedDeps.add(key);
    }

    /**
     * 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 EventHandler getListener() {
        checkActive();
        return eventHandler;
    }

    void doneBuilding() {
        building = false;
    }

    GroupedListHelper<SkyKey> getNewlyRequestedDeps() {
        return newlyRequestedDeps;
    }

    Collection<NodeEntry> getDirectDepsValues() {
        return directDeps.values();
    }

    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 node entry is informed if the node's value and error are definitive via the flag {@code
     * completeValue}.
     */
    void commit(NodeEntry primaryEntry, EnqueueParentBehavior enqueueParents) throws InterruptedException {
        // Construct the definitive error info, if there is one.
        finalizeErrorInfo();

        // 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.

        NestedSet<TaggedEvents> events = buildEvents(primaryEntry, /*missingChildren=*/ false);
        Version valueVersion;
        SkyValue valueWithMetadata;
        if (value == null) {
            Preconditions.checkNotNull(errorInfo, "%s %s", skyKey, primaryEntry);
            valueWithMetadata = ValueWithMetadata.error(errorInfo, events);
        } 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, events);
        }
        if (!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, primaryEntry.getTemporaryDirectDeps().toSet());
            Collection<? extends NodeEntry> oldDepEntries = evaluatorContext.getGraph()
                    .getBatch(skyKey, Reason.RDEP_REMOVAL, depsToRemove).values();
            for (NodeEntry oldDepEntry : oldDepEntries) {
                oldDepEntry.removeReverseDep(skyKey);
            }
        }
        // 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.
        // We could consider using max(childVersions) here instead of graphVersion. When full
        // versioning is implemented, this would allow evaluation at a version between
        // max(childVersions) and graphVersion to re-use this result.
        Set<SkyKey> reverseDeps = primaryEntry.setValue(valueWithMetadata, evaluatorContext.getGraphVersion());
        // Note that if this update didn't actually change the value entry, this version may not
        // be the graph version.
        valueVersion = primaryEntry.getVersion();
        Preconditions.checkState(valueVersion.atMost(evaluatorContext.getGraphVersion()),
                "%s should be at most %s in the version partial ordering", valueVersion,
                evaluatorContext.getGraphVersion());

        // Tell the receiver that this value was built. If valueVersion.equals(graphVersion), it was
        // evaluated this run, and so was changed. Otherwise, it is less than graphVersion, 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.
        evaluatorContext.getProgressReceiver().evaluated(skyKey, new SkyValueSupplier(primaryEntry),
                valueVersion.equals(evaluatorContext.getGraphVersion()) ? EvaluationState.BUILT
                        : EvaluationState.CLEAN);

        evaluatorContext.signalValuesAndEnqueueIfReady(skyKey, reverseDeps, valueVersion, enqueueParents);

        evaluatorContext.getReplayingNestedSetEventVisitor().visit(events);
    }

    @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;
    }
}