Roll back https://github.com/bazelbuild/bazel/commit/ed9d6390eb78303631c25b3d4776cb07eeded544 due to memory regression.

PiperOrigin-RevId: 315934735
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleConfiguredTargetUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleConfiguredTargetUtil.java
index d71b44d..7fff89b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleConfiguredTargetUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/StarlarkRuleConfiguredTargetUtil.java
@@ -33,6 +33,7 @@
 import com.google.devtools.build.lib.analysis.test.InstrumentedFilesInfo;
 import com.google.devtools.build.lib.collect.nestedset.Depset;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet.NestedSetDepthException;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.packages.AdvertisedProviderSet;
@@ -651,11 +652,23 @@
 
   private static void assertExecutableSymlinkPresent(
       Runfiles runfiles, Artifact executable, Location loc) throws EvalException {
-    // Extracting the map from Runfiles flattens a depset.
-    // TODO(cparsons): Investigate: Avoiding this flattening may be an efficiency win.
-    Map<PathFragment, Artifact> symlinks = runfiles.asMapWithoutRootSymlinks();
-    if (!symlinks.containsValue(executable)) {
-      throw new EvalException(loc, "main program " + executable + " not included in runfiles");
+    try {
+      // Extracting the map from Runfiles flattens a depset.
+      // TODO(cparsons): Investigate: Avoiding this flattening may be an efficiency win.
+      Map<PathFragment, Artifact> symlinks = runfiles.asMapWithoutRootSymlinks();
+      if (!symlinks.containsValue(executable)) {
+        throw new EvalException(loc, "main program " + executable + " not included in runfiles");
+      }
+    } catch (NestedSetDepthException exception) {
+      throw new EvalException(
+          loc,
+          "depset exceeded maximum depth "
+              + exception.getDepthLimit()
+              + ". This was only discovered when attempting to flatten the runfiles depset "
+              + "returned by the rule implementation function. the size of depsets is unknown "
+              + "until flattening. "
+              + "See https://github.com/bazelbuild/bazel/issues/9180 for details and possible "
+              + "solutions.");
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/Depset.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Depset.java
index a26131c..ba3a521 100644
--- a/src/main/java/com/google/devtools/build/lib/collect/nestedset/Depset.java
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/Depset.java
@@ -15,6 +15,7 @@
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet.NestedSetDepthException;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.syntax.Dict;
@@ -28,7 +29,6 @@
 import com.google.devtools.build.lib.syntax.StarlarkThread;
 import com.google.devtools.build.lib.syntax.StarlarkValue;
 import java.util.List;
-import java.util.concurrent.atomic.AtomicInteger;
 import javax.annotation.Nullable;
 import net.starlark.java.annot.StarlarkBuiltin;
 import net.starlark.java.annot.StarlarkDocumentationCategory;
@@ -341,7 +341,18 @@
               + "on the depset and vice versa.",
       useStarlarkThread = true)
   public StarlarkList<Object> toListForStarlark(StarlarkThread thread) throws EvalException {
-    return StarlarkList.copyOf(thread.mutability(), this.toList());
+    try {
+      return StarlarkList.copyOf(thread.mutability(), this.toList());
+    } catch (NestedSetDepthException exception) {
+      throw new EvalException(
+          null,
+          "depset exceeded maximum depth "
+              + exception.getDepthLimit()
+              + ". This was only discovered when attempting to flatten the depset for to_list(), "
+              + "as the size of depsets is unknown until flattening. "
+              + "See https://github.com/bazelbuild/bazel/issues/9180 for details and possible "
+              + "solutions.");
+    }
   }
 
   /** Create a Depset from the given direct and transitive components. */
@@ -548,13 +559,17 @@
       result = legacyDepsetConstructor(items, order, direct, transitive, semantics);
     }
 
-    // check depth limit
-    int depth = result.getSet().getDepth();
-    int limit = depthLimit.get();
-    if (depth > limit) {
-      throw Starlark.errorf("depset depth %d exceeds limit (%d)", depth, limit);
+    if (semantics.debugDepsetDepth()) {
+      // Flatten the underlying nested set. If the set exceeds the depth limit, then this will
+      // throw a NestedSetDepthException.
+      // This is an extremely inefficient check and should be only done in the
+      // "--debug_depset_depth" mode.
+      try {
+        result.toList(); // may throw exception
+      } catch (NestedSetDepthException ex) {
+        throw Starlark.errorf("depset exceeded maximum depth %d", ex.getDepthLimit());
+      }
     }
-
     return result;
   }
 
@@ -587,17 +602,4 @@
   private static boolean isEmptyStarlarkList(Object o) {
     return o instanceof Sequence && ((Sequence) o).isEmpty();
   }
-
-  /**
-   * Sets the maximum depth for nested sets constructed by the Starlark {@code depset} function (as
-   * set by {@code --nested_set_depth_limit}).
-   *
-   * @return whether the new limit differs from the old
-   */
-  public static boolean setDepthLimit(int newLimit) {
-    int oldValue = depthLimit.getAndSet(newLimit);
-    return oldValue != newLimit;
-  }
-
-  private static final AtomicInteger depthLimit = new AtomicInteger(3500);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java
index 550dc76..fc2ff8c 100644
--- a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSet.java
@@ -36,6 +36,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 import javax.annotation.Nullable;
@@ -85,36 +86,31 @@
   // The relative order of direct and transitive is determined by the Order.
   // All empty sets have children==EMPTY_CHILDREN, not null.
   //
-  // The first slot in an array is not a true element, but the Integer depth of the graph,
-  // which is one greater than that of the deepest successor. Thus arrays other than
-  // EMPTY_CHILDREN have length >= 3: the depth plus 2 or more successors.
-  //
   // Please be careful to use the terms of the conceptual model in the API documentation,
   // and the terms of the physical representation in internal comments. They are not the same.
-  // In graphical terms, the "direct" elements are the graph successors that are leaves,
-  // and the "transitive" elements are the graph successors that are non-leaves, and
-  // non-leaf nodes have an out-degree of at least 2.
   //
-  // TODO(adonovan): rename this field and all accessors that use the same format to
-  // something less suggestive such as 'repr' or 'impl', and rename all uses of children
-  // meaning "logical graph successors" to 'successors'.
+  // TODO(adonovan): rename this field and related accessors to something less suggestive,
+  // such as 'state' or 'repr' or 'node'.  The public API no longer mentions "children".
   final Object children;
 
-  // memo is a bitfield, lazily populated by lockedExpand, that indicates whether the
-  // ith successor (a non-leaf) should be visited, or skipped because that subgraph would
-  // contribute nothing to the flattening as it contains only elements previously seen in
-  // the traversal. All NestedSets of depth < 3, that is, those whose successors are
-  // all leaves, share the empty NO_MEMO array.
   @Nullable private byte[] memo;
 
-  private static final byte[] NO_MEMO = {};
-  @AutoCodec static final Object[] EMPTY_CHILDREN = {0};
+  /**
+   * The application depth limit of nested sets. Nested sets over this depth will throw {@link
+   * NestedSetDepthException} on flattening of the depset.
+   *
+   * <p>This limit should be set by command line option processing in the Bazel server.
+   */
+  private static final AtomicInteger expansionDepthLimit = new AtomicInteger(3500);
+
+  private static final byte[] LEAF_MEMO = {};
+  @AutoCodec static final Object[] EMPTY_CHILDREN = {};
 
   /** Construct an empty NestedSet. Should only be called by Order's class initializer. */
   NestedSet(Order order) {
     this.orderAndSize = order.ordinal();
     this.children = EMPTY_CHILDREN;
-    this.memo = NO_MEMO;
+    this.memo = LEAF_MEMO;
   }
 
   NestedSet(
@@ -149,10 +145,9 @@
     // the same child, which is a problem for the fast path in toList().
     Set<E> alreadyInserted = ImmutableSet.of();
     // The candidate array of children.
-    Object[] children = new Object[1 + direct.size() + transitive.size()];
-    int n = 1; // current position in children array (skip depth slot)
+    Object[] children = new Object[direct.size() + transitive.size()];
+    int n = 0; // current position in children
     boolean leaf = true;  // until we find otherwise
-    int depth = 2;
 
     for (int pass = 0; pass <= 1; ++pass) {
       if ((pass == 0) == preorder && !direct.isEmpty()) {
@@ -175,10 +170,9 @@
           Object c = subset.getChildrenInternal(interruptStrategy);
           if (c instanceof Object[]) {
             Object[] a = (Object[]) c;
-            if (a.length < 3) {
+            if (a.length < 2) {
               throw new AssertionError(a.length);
             }
-            depth = Math.max(depth, 1 + depth(a));
             children[n++] = a;
             leaf = false;
           } else {
@@ -196,28 +190,27 @@
       }
     }
 
-    // n == |successors| + 1
     // If we ended up wrapping exactly one item or one other set, dereference it.
-    if (n == 2) {
-      this.children = children[1];
-    } else if (n == 1) {
+    if (n == 1) {
+      this.children = children[0];
+    } else if (n == 0) {
       this.children = EMPTY_CHILDREN;
+    } else if (n < children.length) {
+      this.children = Arrays.copyOf(children, n);
     } else {
-      children[0] = depth;
-      if (n < children.length) {
-        children = Arrays.copyOf(children, n); // shrink to save space
-      }
       this.children = children;
     }
     if (leaf) {
-      this.memo = NO_MEMO;
+      this.memo = LEAF_MEMO;
     }
   }
 
-  // Precondition: EMPTY_CHILDREN is used as the canonical empty array.
   private NestedSet(Order order, Object children, @Nullable byte[] memo) {
     this.orderAndSize = order.ordinal();
-    this.children = children;
+    this.children =
+        children instanceof Object[] && ((Object[]) children).length == 0
+            ? EMPTY_CHILDREN
+            : children;
     this.memo = memo;
   }
 
@@ -237,7 +230,7 @@
     boolean hasChildren =
         children instanceof Object[]
             && (Arrays.stream((Object[]) children).anyMatch(child -> child instanceof Object[]));
-    byte[] memo = hasChildren ? null : NO_MEMO;
+    byte[] memo = hasChildren ? null : LEAF_MEMO;
     return new NestedSet<>(order, children, memo);
   }
 
@@ -323,9 +316,7 @@
       Predicate<Object> descend, Consumer<E> f, Object node) {
     if (descend.test(node)) {
       if (node instanceof Object[]) {
-        Object[] children = (Object[]) node;
-        for (int i = 1; i < children.length; i++) { // skip depth
-          Object child = children[i];
+        for (Object child : (Object[]) node) {
           forEachElementImpl(descend, f, child);
         }
       } else {
@@ -348,22 +339,6 @@
     return isSingleton(children);
   }
 
-  /**
-   * Returns the depth of the nested set graph. The empty set has depth zero. A leaf node with a
-   * single element has depth 1. A non-leaf node has a depth one greater than its deepest successor.
-   */
-  public int getDepth() {
-    return depth(getChildren());
-  }
-
-  private static int depth(Object children) {
-    return children == EMPTY_CHILDREN
-        ? 0 //
-        : children instanceof Object[]
-            ? (Integer) ((Object[]) children)[0] //
-            : 1;
-  }
-
   private static boolean isSingleton(Object children) {
     // Singleton sets are special cased in serialization, and make no calls to storage.  Therefore,
     // we know that any NestedSet with a ListenableFuture member is not a singleton.
@@ -541,13 +516,8 @@
    */
   private ImmutableList<E> expand(Object[] children) {
     // This value is only set in the constructor, so safe to test here with no lock.
-    if (memo == NO_MEMO) {
-      // The children array contains only leaf nodes. (It doesn't necessarily mean cardinality <=
-      // 1.)
-      // Use the array-sharing hack to return an (immutable) alias for the underlying data.
-      // ImutableList.subList (and reverse, if later called) use decorators, not copying.
-      ImmutableList<E> r = ImmutableList.copyOf(new ArraySharingCollection<>(children));
-      return r.subList(1, r.size()); // skip depth
+    if (memo == LEAF_MEMO) {
+      return ImmutableList.copyOf(new ArraySharingCollection<>(children));
     }
     CompactHashSet<E> members = lockedExpand(children);
     if (members != null) {
@@ -584,26 +554,27 @@
 
   /**
    * If this is the first call for this object, fills {@code this.memo} and returns a set from
-   * {@link #walk}. Otherwise returns null, in which case some other thread must have completely
-   * populated memo; the caller should use {@link #replay} instead.
+   * {@link #walk}. Otherwise returns null; the caller should use {@link #replay} instead.
    */
   private synchronized CompactHashSet<E> lockedExpand(Object[] children) {
-    // Postcondition: memo is completely populated.
     if (memo != null) {
       return null;
     }
     CompactHashSet<E> members = CompactHashSet.createWithExpectedSize(128);
     CompactHashSet<Object> sets = CompactHashSet.createWithExpectedSize(128);
     sets.add(children);
-    int nsuccs = children.length - 1; // skip depth
-    // Allocate less memo than we might need, on the optimistic
-    // assumption that later bits are all zero (redundant successors)
-    // which need not be represented explictly.
-    memo = new byte[Math.min(ceildiv(nsuccs, 8), 8)];
-    int pos = walk(sets, members, children, /*pos=*/ 0);
-    int bytes = ceildiv(pos, 8);
+    memo = new byte[Math.min((children.length + 7) / 8, 8)];
+    int pos =
+        walk(
+            sets,
+            members,
+            children,
+            /* pos= */ 0,
+            /* currentDepth= */ 1,
+            expansionDepthLimit.get());
+    int bytes = (pos + 7) / 8;
     if (bytes <= memo.length - 16) {
-      memo = Arrays.copyOf(memo, bytes); // shrink to save space
+      memo = Arrays.copyOf(memo, bytes);
     }
     Preconditions.checkState(members.size() < (Integer.MAX_VALUE >> 2));
     orderAndSize |= (members.size()) << 2;
@@ -618,9 +589,16 @@
    * <p>Returns the final value of {@code pos}.
    */
   private int walk(
-      CompactHashSet<Object> sets, CompactHashSet<E> members, Object[] children, int pos) {
-    for (int i = 1; i < children.length; i++) { // skip depth
-      Object child = children[i];
+      CompactHashSet<Object> sets,
+      CompactHashSet<E> members,
+      Object[] children,
+      int pos,
+      int currentDepth,
+      int maxDepth) {
+    if (currentDepth > maxDepth) {
+      throw new NestedSetDepthException(maxDepth);
+    }
+    for (Object child : children) {
       if ((pos >> 3) >= memo.length) {
         memo = Arrays.copyOf(memo, memo.length * 2);
       }
@@ -628,7 +606,7 @@
         if (sets.add(child)) {
           int prepos = pos;
           int presize = members.size();
-          pos = walk(sets, members, (Object[]) child, pos + 1);
+          pos = walk(sets, members, (Object[]) child, pos + 1, currentDepth + 1, maxDepth);
           if (presize < members.size()) {
             memo[prepos >> 3] |= (byte) (1 << (prepos & 7));
           } else {
@@ -656,8 +634,7 @@
    */
   private static <E> int replay(
       ImmutableList.Builder<E> output, Object[] children, byte[] memo, int pos) {
-    for (int i = 1; i < children.length; i++) { // skip depth
-      Object child = children[i];
+    for (Object child : children) {
       if ((memo[pos >> 3] & (1 << (pos & 7))) != 0) {
         if (child instanceof Object[]) {
           pos = replay(output, (Object[]) child, memo, pos + 1);
@@ -672,67 +649,60 @@
     return pos;
   }
 
-  // ceildiv(x/y) returns ⌈x/y⌉.
-  private static int ceildiv(int x, int y) {
-    return (x + y - 1) / y;
+  /**
+   * Sets the application depth limit of nested sets. When flattening a {@link NestedSet} deeper
+   * than this limit, a {@link NestedSetDepthException} will be thrown.
+   *
+   * <p>This limit should be set by command line option processing.
+   *
+   * @return true if the previous limit was different than this new limit
+   */
+  public static boolean setApplicationDepthLimit(int newLimit) {
+    int oldValue = expansionDepthLimit.getAndSet(newLimit);
+    return oldValue != newLimit;
+  }
+
+  /** An exception thrown when a nested set exceeds the application's depth limits. */
+  public static final class NestedSetDepthException extends RuntimeException {
+    private final int depthLimit;
+
+    public NestedSetDepthException(int depthLimit) {
+      this.depthLimit = depthLimit;
+    }
+
+    /** Returns the depth limit that was exceeded which resulted in this exception being thrown. */
+    public int getDepthLimit() {
+      return depthLimit;
+    }
   }
 
   /**
-   * Returns a new NestedSet containing the same elements, but represented using a graph node whose
-   * out-degree does not exceed {@code maxDegree}, which must be at least 2. The operation is
-   * shallow, not deeply recursive. The resulting set's iteration order is undefined.
+   * Returns a new NestedSet containing the same elements, but represented using a logical graph
+   * with the specified maximum out-degree, which must be at least 2. The resulting set's iteration
+   * order is undefined.
    */
-  // TODO(adonovan): move this hack into BuildEventStreamer. And rename 'size' to 'degree'.
-  public NestedSet<E> splitIfExceedsMaximumSize(int maxDegree) {
-    Preconditions.checkArgument(maxDegree >= 2, "maxDegree must be at least 2");
+  public NestedSet<E> splitIfExceedsMaximumSize(int maximumSize) {
+    Preconditions.checkArgument(maximumSize >= 2, "maximumSize must be at least 2");
     Object children = getChildren(); // may wait for a future
     if (!(children instanceof Object[])) {
       return this;
     }
-    Object[] succs = (Object[]) children;
-
-    int nsuccs = succs.length - 1; // skip depth
-    if (nsuccs <= maxDegree) {
+    Object[] arr = (Object[]) children;
+    int size = arr.length;
+    if (size <= maximumSize) {
       return this;
     }
-
-    // Cut succs into n pieces each of at most maxDegree.
-    // The arrays succs, pieces, and pieces[i>1] all have an initial depth Integer.
-    int npieces = ceildiv(nsuccs, maxDegree);
-    Object[] pieces = new Object[1 + npieces];
-    pieces[0] = 1 + (int) succs[0]; // depth
-    for (int i = 0; i < npieces; i++) {
-      int piecelen = maxDegree;
-      if (nsuccs < (i + 1) * maxDegree) {
-        // short final piece
-        piecelen = nsuccs - i * maxDegree;
-
-        // very short (1-node) final piece? Inline it.
-        if (piecelen == 1) {
-          pieces[1 + i] = succs[1 + i * maxDegree];
-          break;
-        }
-      }
-
-      // Copy succs[...] into piece[1:], updating piece[0] with correct depth.
-      Object[] piece = new Object[1 + piecelen];
-      int depth = 1;
-      for (int j = 0; j < piecelen; j++) {
-        Object x = succs[1 + i * maxDegree + j];
-        piece[1 + j] = x;
-        if (x instanceof Object[]) {
-          depth = Math.max(depth, 1 + depth(x));
-        }
-      }
-      piece[0] = depth;
-      pieces[1 + i] = piece;
+    Object[][] pieces = new Object[((size + maximumSize - 1) / maximumSize)][];
+    for (int i = 0; i < pieces.length; i++) {
+      int max = Math.min((i + 1) * maximumSize, arr.length);
+      pieces[i] = Arrays.copyOfRange(arr, i * maximumSize, max);
     }
 
-    // Each piece is now smaller than maxDegree, but there may be many pieces.
-    // Recursively split pieces. (The recursion affects only the root; it
-    // does not traverse into successors.) In practice, maxDegree is large
-    // enough that the recursion rarely does any work.
-    return new NestedSet<E>(getOrder(), pieces, null).splitIfExceedsMaximumSize(maxDegree);
+    // TODO(adonovan): why is this recursive call here? It looks like it is intended to propagate
+    // the split down to each subgraph, but it clearly never did that, because it would have to be
+    // applied to each element of pieces[i]. More surprising is that it does anything at all: pieces
+    // has already been split. Yet a test breaks if it is removed. Investigate and simplify.
+    return new NestedSet<E>(getOrder(), pieces, null).splitIfExceedsMaximumSize(maximumSize);
   }
 
   /** Returns the list of this node's successors that are themselves non-leaf nodes. */
@@ -761,9 +731,7 @@
       return ImmutableList.of((E) children);
     }
     ImmutableList.Builder<E> res = ImmutableList.builder();
-    Object[] succs = (Object[]) children;
-    for (int i = 1; i < succs.length; i++) { // skip depth
-      Object c = succs[i];
+    for (Object c : (Object[]) children) {
       if (!(c instanceof Object[])) {
         res.add((E) c);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFingerprintCache.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFingerprintCache.java
index 4e73229..46eddd8 100644
--- a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFingerprintCache.java
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetFingerprintCache.java
@@ -79,9 +79,8 @@
     if (children instanceof Object[]) {
       if (!digestMap.readDigest(children, fingerprint)) {
         Fingerprint childrenFingerprint = new Fingerprint();
-        Object[] succs = (Object[]) children;
-        for (int i = 1; i < succs.length; i++) { // skip depth
-          addToFingerprint(mapFn, childrenFingerprint, digestMap, succs[i]);
+        for (Object child : (Object[]) children) {
+          addToFingerprint(mapFn, childrenFingerprint, digestMap, child);
         }
         digestMap.insertAndReadDigest(children, childrenFingerprint, fingerprint);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetOptionsModule.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetOptionsModule.java
index 851d0f4..5e99a5e 100644
--- a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetOptionsModule.java
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetOptionsModule.java
@@ -33,15 +33,17 @@
         documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
         effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
         help =
-            "The maximum depth of the graph internal to a depset (also known as NestedSet), above"
-                + " which the depset() constructor will fail.")
+            "Limit on the depth of NestedSet, which is the internal data structure used to "
+                + "implement `depset` in Starlark. If a depset is flattened during evaluation of "
+                + "Starlark code or a NestedSet is flattened internally, and that data structure "
+                + "has a depth exceeding this limit, then the Bazel invocation will fail.")
     public int nestedSetDepthLimit;
   }
 
   @Override
   public void beforeCommand(CommandEnvironment env) {
     Options options = env.getOptions().getOptions(Options.class);
-    boolean changed = Depset.setDepthLimit(options.nestedSetDepthLimit);
+    boolean changed = NestedSet.setApplicationDepthLimit(options.nestedSetDepthLimit);
     if (changed) {
       env.getSkyframeExecutor().resetEvaluator();
     }
diff --git a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java
index 6b9f152..1a36ff9 100644
--- a/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java
+++ b/src/main/java/com/google/devtools/build/lib/collect/nestedset/NestedSetVisitor.java
@@ -30,6 +30,7 @@
  *
  * @param <E> the data type
  */
+// @ThreadSafety.ThreadSafe
 public final class NestedSetVisitor<E> {
 
   /**
@@ -76,9 +77,8 @@
   private void visitRaw(Object node) {
     if (visited.add(node)) {
       if (node instanceof Object[]) {
-        Object[] children = (Object[]) node;
-        for (int i = 1; i < children.length; i++) { // skip depth
-          visitRaw(children[i]);
+        for (Object child : (Object[]) node) {
+          visitRaw(child);
         }
       } else {
         callback.accept((E) node);
diff --git a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
index 2a9ce32..1324ddd 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/StarlarkSemanticsOptions.java
@@ -64,6 +64,19 @@
   // <== Add new options here in alphabetic order ==>
 
   @Option(
+      name = "debug_depset_depth",
+      defaultValue = "false",
+      documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
+      effectTags = {OptionEffectTag.BUILD_FILE_SEMANTICS},
+      metadataTags = {},
+      help =
+          "Enables an expensive additional check that causes depset construction to fail fast if "
+              + "the depset's depth would exceed the limit specified by "
+              + "`--nested_set_depth_limit`. Ordinarily this failure occurs only when the depset "
+              + "is flattened, which may be far from its point of creation.")
+  public boolean debugDepsetDepth;
+
+  @Option(
       name = "experimental_action_args",
       defaultValue = "true",
       documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
@@ -641,6 +654,7 @@
     StarlarkSemantics semantics =
         StarlarkSemantics.builder()
             // <== Add new options here in alphabetic order ==>
+            .debugDepsetDepth(debugDepsetDepth)
             .experimentalActionArgs(experimentalActionArgs)
             .experimentalAllowIncrementalRepositoryUpdates(
                 experimentalAllowIncrementalRepositoryUpdates)
diff --git a/src/main/java/com/google/devtools/build/lib/packages/Type.java b/src/main/java/com/google/devtools/build/lib/packages/Type.java
index 778019c..da812e8 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/Type.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/Type.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.collect.nestedset.Depset;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet.NestedSetDepthException;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.EvalUtils;
@@ -611,7 +612,17 @@
       if (x instanceof Iterable) {
         iterable = (Iterable<?>) x;
       } else if (x instanceof Depset) {
-        iterable = ((Depset) x).toList();
+        try {
+          iterable = ((Depset) x).toList();
+        } catch (NestedSetDepthException exception) {
+          throw new ConversionException(
+              "depset exceeded maximum depth "
+                  + exception.getDepthLimit()
+                  + ". This was only discovered when attempting to flatten the depset for"
+                  + " iteration, as the size of depsets is unknown until flattening. See"
+                  + " https://github.com/bazelbuild/bazel/issues/9180 for details and possible "
+                  + "solutions.");
+        }
       } else {
         throw new ConversionException(this, x, what);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java b/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
index 74c17e3..5f90065 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/NamedArtifactGroup.java
@@ -125,8 +125,8 @@
 
   /**
    * Given a set whose leaf successors are {@link Artifact} and {@link ExpandedArtifact}, returns a
-   * new NestedSet whose leaf successors are all ExpandedArtifact. Non-leaf successors are
-   * unaltered.
+   * weird Frankenstein object with each leaf successor replaced by ExpandedArtifact. Non-leaf
+   * successors are unaltered.
    */
   static NestedSet<?> expandSet(CompletionContext ctx, NestedSet<?> artifacts) {
     NestedSetBuilder<Object> res = new NestedSetBuilder<>(Order.STABLE_ORDER);
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
index 0b81df0..79ff3d5 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
@@ -291,7 +291,20 @@
               + "str(8) == \"8\"</pre>",
       parameters = {@Param(name = "x", doc = "The object to convert.", noneable = true)})
   public String str(Object x) throws EvalException {
-    return Starlark.str(x);
+    try {
+      return Starlark.str(x);
+    } catch (RuntimeException ex) {
+      // TODO(adonovan): get rid of this somehow.
+      if (ex.getClass().getSimpleName().equals("NestedSetDepthException")) {
+        throw Starlark.errorf(
+            "depset exceeded maximum depth"
+                + ". This was only discovered when attempting to flatten the depset for str(), as "
+                + "the size of depsets is unknown until flattening. "
+                + "See https://github.com/bazelbuild/bazel/issues/9180 for details and possible "
+                + "solutions.");
+      }
+      throw ex;
+    }
   }
 
   @StarlarkMethod(
@@ -721,7 +734,19 @@
     String separator = "";
     for (Object x : args) {
       p.append(separator);
-      p.debugPrint(x);
+      try {
+        p.debugPrint(x);
+      } catch (RuntimeException ex) {
+        // TODO(adonovan): get rid of this somehow.
+        if (ex.getClass().getSimpleName().equals("NestedSetDepthException")) {
+          throw Starlark.errorf(
+              "depset exceeded maximum depth. This was only discovered when attempting to"
+                  + " flatten the depset for print(), as the size of depsets is unknown until"
+                  + " flattening. See https://github.com/bazelbuild/bazel/issues/9180 for details"
+                  + " and possible solutions.");
+        }
+        throw ex;
+      }
       separator = sep;
     }
     // As part of the integration test "starlark_flag_test.sh", if the
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
index f859597..70c6f43 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/StarlarkSemantics.java
@@ -194,6 +194,8 @@
       AutoValue_StarlarkSemantics.class;
 
   // <== Add new options here in alphabetic order ==>
+  public abstract boolean debugDepsetDepth();
+
   public abstract boolean experimentalActionArgs();
 
   public abstract boolean experimentalAllowIncrementalRepositoryUpdates();
@@ -309,6 +311,7 @@
   public static final StarlarkSemantics DEFAULT =
       builder()
           // <== Add new options here in alphabetic order ==>
+          .debugDepsetDepth(false)
           .experimentalActionArgs(true)
           .experimentalAllowTagsPropagation(false)
           .experimentalBuiltinsBzlPath("")
@@ -357,6 +360,8 @@
   public abstract static class Builder {
 
     // <== Add new options here in alphabetic order ==>
+    public abstract Builder debugDepsetDepth(boolean value);
+
     public abstract Builder experimentalActionArgs(boolean value);
 
     public abstract Builder experimentalAllowIncrementalRepositoryUpdates(boolean value);
diff --git a/src/test/java/com/google/devtools/build/lib/collect/nestedset/DepsetTest.java b/src/test/java/com/google/devtools/build/lib/collect/nestedset/DepsetTest.java
index 3f66420..7990c1d 100644
--- a/src/test/java/com/google/devtools/build/lib/collect/nestedset/DepsetTest.java
+++ b/src/test/java/com/google/devtools/build/lib/collect/nestedset/DepsetTest.java
@@ -431,25 +431,19 @@
   }
 
   @Test
-  public void testConstructorDepthLimit() throws Exception {
+  public void testDepthExceedsLimitDuringIteration() throws Exception {
+    NestedSet.setApplicationDepthLimit(2000);
     ev.new Scenario()
         .setUp(
             "def create_depset(depth):",
             "  x = depset([0])",
             "  for i in range(1, depth):",
-            "    x = depset([i], transitive = [x])")
-        .testEval("create_depset(3000)", "None") // succeeds
-        .testIfErrorContains("depset depth 3501 exceeds limit (3500)", "create_depset(4000)");
-
-    Depset.setDepthLimit(100);
-    ev.new Scenario()
-        .setUp(
-            "def create_depset(depth):",
-            "  x = depset([0])",
-            "  for i in range(1, depth):",
-            "    x = depset([i], transitive = [x])")
-        .testEval("create_depset(99)", "None") // succeeds
-        .testIfErrorContains("depset depth 101 exceeds limit (100)", "create_depset(1000)");
+            "    x = depset([i], transitive = [x])",
+            "  for element in x.to_list():",
+            "    str(x)",
+            "  return None")
+        .testEval("create_depset(1000)", "None")
+        .testIfErrorContains("depset exceeded maximum depth", "create_depset(3000)");
   }
 
   @Test
@@ -514,4 +508,37 @@
                 + "--incompatible_disable_depset_inputs=false",
             "depset(items=[0,1])");
   }
+
+  @Test
+  public void testDepsetDepthLimit() throws Exception {
+    NestedSet.setApplicationDepthLimit(2000);
+    ev.new Scenario()
+        .setUp(
+            "def create_depset(depth):",
+            "  x = depset([0])",
+            "  for i in range(1, depth):",
+            "    x = depset([i], transitive = [x])",
+            "  return x",
+            "too_deep_depset = create_depset(3000)",
+            "fine_depset = create_depset(900)")
+        .testEval("fine_depset.to_list()[0]", "0")
+        .testEval("str(fine_depset)[0:6]", "'depset'")
+        .testIfErrorContains("depset exceeded maximum depth", "print(too_deep_depset)")
+        .testIfErrorContains("depset exceeded maximum depth", "str(too_deep_depset)")
+        .testIfErrorContains("depset exceeded maximum depth", "too_deep_depset.to_list()");
+  }
+
+  @Test
+  public void testDepsetDebugDepth() throws Exception {
+    NestedSet.setApplicationDepthLimit(2000);
+    ev.new Scenario("--debug_depset_depth=true")
+        .setUp(
+            "def create_depset(depth):",
+            "  x = depset([0])",
+            "  for i in range(1, depth):",
+            "    x = depset([i], transitive = [x])",
+            "  return x")
+        .testEval("str(create_depset(900))[0:6]", "'depset'")
+        .testIfErrorContains("depset exceeded maximum depth", "create_depset(3000)");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetImplTest.java b/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetImplTest.java
index 41f20ab..83c2397 100644
--- a/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetImplTest.java
+++ b/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetImplTest.java
@@ -286,51 +286,50 @@
 
   @Test
   public void buildInterruptibly_propagatesInterrupt() {
-    NestedSet<String> deserializingNestedSet =
+    NestedSet<String> deserialzingNestedSet =
         NestedSet.withFuture(Order.STABLE_ORDER, SettableFuture.create());
     NestedSetBuilder<String> builder =
-        NestedSetBuilder.<String>stableOrder().addTransitive(deserializingNestedSet).add("a");
+        NestedSetBuilder.<String>stableOrder().addTransitive(deserialzingNestedSet).add("a");
     Thread.currentThread().interrupt();
     assertThrows(InterruptedException.class, builder::buildInterruptibly);
   }
 
   @Test
   public void getChildrenInterruptibly_propagatesInterrupt() {
-    NestedSet<String> deserializingNestedSet =
+    NestedSet<String> deserialzingNestedSet =
         NestedSet.withFuture(Order.STABLE_ORDER, SettableFuture.create());
     Thread.currentThread().interrupt();
-    assertThrows(InterruptedException.class, deserializingNestedSet::getChildrenInterruptibly);
+    assertThrows(InterruptedException.class, deserialzingNestedSet::getChildrenInterruptibly);
   }
 
   @Test
   public void toListWithTimeout_propagatesInterrupt() {
-    NestedSet<String> deserializingNestedSet =
+    NestedSet<String> deserialzingNestedSet =
         NestedSet.withFuture(Order.STABLE_ORDER, SettableFuture.create());
     Thread.currentThread().interrupt();
     assertThrows(
         InterruptedException.class,
-        () -> deserializingNestedSet.toListWithTimeout(Duration.ofDays(1)));
+        () -> deserialzingNestedSet.toListWithTimeout(Duration.ofDays(1)));
   }
 
   @Test
   public void toListWithTimeout_timesOut() {
-    NestedSet<String> deserializingNestedSet =
+    NestedSet<String> deserialzingNestedSet =
         NestedSet.withFuture(Order.STABLE_ORDER, SettableFuture.create());
     assertThrows(
-        TimeoutException.class,
-        () -> deserializingNestedSet.toListWithTimeout(Duration.ofNanos(1)));
+        TimeoutException.class, () -> deserialzingNestedSet.toListWithTimeout(Duration.ofNanos(1)));
   }
 
   @Test
   public void toListWithTimeout_waits() throws Exception {
     SettableFuture<Object[]> future = SettableFuture.create();
-    NestedSet<String> deserializingNestedSet = NestedSet.withFuture(Order.STABLE_ORDER, future);
+    NestedSet<String> deserialzingNestedSet = NestedSet.withFuture(Order.STABLE_ORDER, future);
     Future<ImmutableList<String>> result =
         Executors.newSingleThreadExecutor()
-            .submit(() -> deserializingNestedSet.toListWithTimeout(Duration.ofMinutes(1)));
+            .submit(() -> deserialzingNestedSet.toListWithTimeout(Duration.ofMinutes(1)));
     Thread.sleep(100);
     assertThat(result.isDone()).isFalse();
-    future.set(new Object[] {/*depth=*/ 2, "a", "b"});
+    future.set(new Object[] {"a", "b"});
     assertThat(result.get()).containsExactly("a", "b");
   }
 
@@ -361,36 +360,4 @@
     future.set(new Object[] {"a", "b"});
     assertThat(deserializingNestedSet.isReady()).isTrue();
   }
-
-  @Test
-  public void getDepth() {
-    NestedSet<String> empty = nestedSetBuilder().build();
-    NestedSet<String> justA = nestedSetBuilder("a").build();
-    NestedSet<String> justB = nestedSetBuilder("b").build();
-    NestedSet<String> ab = nestedSetBuilder().addTransitive(justA).addTransitive(justB).build();
-
-    assertThat(empty.getDepth()).isEqualTo(0);
-    assertThat(nestedSetBuilder().addTransitive(empty).addTransitive(empty).build().getDepth())
-        .isEqualTo(0);
-    assertThat(justA.getDepth()).isEqualTo(1);
-    assertThat(justB.getDepth()).isEqualTo(1);
-    assertThat(nestedSetBuilder().addTransitive(empty).addTransitive(empty).build().getDepth())
-        .isEqualTo(0);
-    assertThat(nestedSetBuilder().addTransitive(empty).addTransitive(justA).build().getDepth())
-        .isEqualTo(1);
-    assertThat(nestedSetBuilder().addTransitive(justA).addTransitive(empty).build().getDepth())
-        .isEqualTo(1);
-    assertThat(nestedSetBuilder().addTransitive(justA).addTransitive(justA).build().getDepth())
-        .isEqualTo(1);
-    assertThat(nestedSetBuilder().addTransitive(justA).addTransitive(justB).build().getDepth())
-        .isEqualTo(2);
-    assertThat(
-            nestedSetBuilder("a", "b", "c")
-                .addTransitive(justA)
-                .addTransitive(justB)
-                .addTransitive(ab)
-                .build()
-                .getDepth())
-        .isEqualTo(3);
-  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetTopologyTest.java b/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetTopologyTest.java
index 29c5261..5846eb8 100644
--- a/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetTopologyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/collect/nestedset/NestedSetTopologyTest.java
@@ -162,10 +162,6 @@
     NestedSet<String> s = v.splitIfExceedsMaximumSize(2);
     assertThat(s).isNotSameInstanceAs(v);
     assertThat(collectCheckSize(s, 2)).containsExactly("a", "b", "c", "d", "e");
-
-    // Splitting may increment the graph depth, possibly more than once.
-    assertThat(v.getDepth()).isEqualTo(2);
-    assertThat(s.getDepth()).isEqualTo(4);
   }
 
   private static <T> List<T> collectCheckSize(NestedSet<T> set, int maxSize) {
diff --git a/src/test/java/com/google/devtools/build/lib/packages/StarlarkSemanticsConsistencyTest.java b/src/test/java/com/google/devtools/build/lib/packages/StarlarkSemanticsConsistencyTest.java
index 6aa52d5..212a2f6 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/StarlarkSemanticsConsistencyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/StarlarkSemanticsConsistencyTest.java
@@ -119,6 +119,7 @@
   private static StarlarkSemanticsOptions buildRandomOptions(Random rand) throws Exception {
     return parseOptions(
         // <== Add new options here in alphabetic order ==>
+        "--debug_depset_depth=" + rand.nextBoolean(),
         "--experimental_action_args=" + rand.nextBoolean(),
         "--experimental_disable_external_package=" + rand.nextBoolean(),
         "--experimental_sibling_repository_layout=" + rand.nextBoolean(),
@@ -172,6 +173,7 @@
   private static StarlarkSemantics buildRandomSemantics(Random rand) {
     return StarlarkSemantics.builder()
         // <== Add new options here in alphabetic order ==>
+        .debugDepsetDepth(rand.nextBoolean())
         .experimentalActionArgs(rand.nextBoolean())
         .experimentalDisableExternalPackage(rand.nextBoolean())
         .experimentalSiblingRepositoryLayout(rand.nextBoolean())
diff --git a/src/test/shell/integration/starlark_flag_test.sh b/src/test/shell/integration/starlark_flag_test.sh
index aad02c8..2ca12f1 100755
--- a/src/test/shell/integration/starlark_flag_test.sh
+++ b/src/test/shell/integration/starlark_flag_test.sh
@@ -244,11 +244,11 @@
 
   bazel build //test:test --nested_set_depth_limit=500 &> $TEST_log \
       && fail "Build should have failed at depth limit 500"
-  expect_log "depset depth 501 exceeds limit (500)"
+  expect_log "depset exceeded maximum depth 500"
 
   bazel build //test:test --nested_set_depth_limit=100 &> $TEST_log \
       && fail "Build should have failed at depth limit 100"
-  expect_log "depset depth 101 exceeds limit (100)"
+  expect_log "depset exceeded maximum depth 100"
 
   bazel build //test:test --nested_set_depth_limit=3000 &> $TEST_log \
       || fail "Build should have succeeded at depth limit 3000"