pcp: Cache prim index traversals for variant selections
authorsunyab <sunyab@users.noreply.github.com>
Sat, 3 Feb 2024 04:00:32 +0000 (20:00 -0800)
committerpixar-oss <pixar-oss@users.noreply.github.com>
Sat, 3 Feb 2024 04:11:16 +0000 (20:11 -0800)
Pcp now caches information computed used when searching
for variant selections during prim indexing. This includes
the translated path at each node where we look for
authored variant selections, as well as whether that
node actually contained any authored variant selections.

This greatly improves performance when dealing with prim
indexes that require recomputing variant selections many
times. For example, in a synthetic test case involving
a prim with 1000 references, adding 500 variant sets to
that prim previously took ~25.5 seconds; with this change,
it takes ~6.8 seconds. In a production test case with
many inherit and variant arcs, the time spent computing
variants dropped from ~30 seconds to ~4.5 seconds.

The caching mechanism involves two new objects:

- PcpNodeRef_PrivateSubtreeConstIterator, an iterator
  for traversing over a specified subtree within a
  prim index.

- Pcp_TraversalCache, an object that uses the above
  iterator to traverse a subtree and caches information
  at each node in the traversal.

Together, these objects allow us to transform the search
for variant selections from a recursive traversal to a
simple iteration, avoid expensive path translations, and
skip over nodes where we've already determined no
authored variant selections exist.

(Internal change: 2314082)
(Internal change: 2314085)
(Internal change: 2314122)

pxr/usd/pcp/CMakeLists.txt
pxr/usd/pcp/composeSite.cpp
pxr/usd/pcp/composeSite.h
pxr/usd/pcp/node.h
pxr/usd/pcp/node_Iterator.h
pxr/usd/pcp/primIndex.cpp
pxr/usd/pcp/primIndex_Graph.h
pxr/usd/pcp/traversalCache.h [new file with mode: 0644]

index b2ac36dd71ab13c85ac2c00c03cd421f1e809973..d5b07163bb200672636b76af365ab576b2e1e64a 100644 (file)
@@ -63,6 +63,9 @@ pxr_library(pcp
         statistics
         utils
 
+    PRIVATE_HEADERS
+        traversalCache.h
+
     PYTHON_CPPFILES
         moduleDeps.cpp
 
index 72b6a0aa299cf1936dc2034809388281af8574c2..e739bfdef397f21196ab5b98760658dfe5135a0b 100644 (file)
@@ -474,6 +474,19 @@ PcpComposeSiteVariantSelections(
     }
 }
 
+bool
+PcpComposeSiteHasVariantSelections(
+    PcpLayerStackRefPtr const &layerStack,
+    SdfPath const &path)
+{
+    for (auto const& layer : layerStack->GetLayers()) {
+        if (layer->HasField(path, SdfFieldKeys->VariantSelection)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 void
 PcpComposeSiteChildNames(SdfLayerRefPtrVector const &layers,
                          SdfPath const &path,
index 5adcd00be4712ce2159d5cdbca9bd448c78191f1..c47409036a8327cca85b844e39b8ad4914f2edcc 100644 (file)
@@ -371,6 +371,12 @@ PcpComposeSiteVariantSelections(PcpNodeRef const &node,
         node.GetLayerStack(), node.GetPath(), result);
 }
 
+PCP_API
+bool
+PcpComposeSiteHasVariantSelections(
+    PcpLayerStackRefPtr const &layerStack,
+    SdfPath const &path);
+
 /// Compose child names.
 /// If the optional \p orderField is provided, its order will be applied.
 PCP_API
index f6b2f7bdd66f30f4eefe96498e417d2c34d4ba9e..96244ace25f4ae3316c452e32b6820989d68a6ea 100644 (file)
@@ -331,6 +331,8 @@ private:
     friend class PcpNodeRef_ChildrenReverseIterator;
     friend class PcpNodeRef_PrivateChildrenConstIterator;
     friend class PcpNodeRef_PrivateChildrenConstReverseIterator;
+    friend class PcpNodeRef_PrivateSubtreeConstIterator;
+    template <class T> friend class Pcp_TraversalCache;
 
     // Private constructor for internal use.
     PcpNodeRef(PcpPrimIndex_Graph* graph, size_t idx)
index 833cebc25eaba55f21b97955e1c4bf140607c0c4..de7d6c453dfb903becd80ac3c012d2a0dad3f95c 100644 (file)
@@ -28,6 +28,7 @@
 
 #include "pxr/pxr.h"
 #include "pxr/usd/pcp/node.h"
+#include "pxr/usd/pcp/primIndex.h"
 #include "pxr/usd/pcp/primIndex_Graph.h"
 
 PXR_NAMESPACE_OPEN_SCOPE
@@ -244,6 +245,137 @@ Pcp_GetChildren(const PcpNodeRef& node)
                             IteratorType(node, /* end = */ true));
 }
 
+/// \class PcpNodeRef_PrivateSubtreeConstIterator
+///
+/// Object used to iterate over all nodes in a subtree rooted at a
+/// given node in the prim index graph in strong-to-weak order.
+class PcpNodeRef_PrivateSubtreeConstIterator
+{
+public:
+    using iterator_category = std::forward_iterator_tag;
+    using value_type = const PcpNodeRef;
+    using reference = const PcpNodeRef&;
+    using pointer = const PcpNodeRef*;
+    using difference_type = std::ptrdiff_t;
+
+    /// If \p end is false, constructs an iterator representing the
+    /// beginning of the subtree of nodes starting at \p node.
+    ///
+    /// If \p end is true, constructs an iterator representing the
+    /// next weakest node after the subtree of nodes starting at \p node.
+    /// This may be an invalid node if \p node is the root node.
+    PcpNodeRef_PrivateSubtreeConstIterator(const PcpNodeRef& node, bool end)
+        : _node(node)
+        , _nodes(&_node._graph->_GetNode(0))
+        , _pruneChildren(false)
+    {
+        if (end) {
+            _MoveToNext();
+        }
+    }
+    
+    /// Causes the next increment of this iterator to ignore
+    /// descendants of the current node.
+    void PruneChildren()
+    {
+        _pruneChildren = true;
+    }
+
+    reference operator*() const { return _node; }
+    pointer operator->() const { return &_node; }
+
+    PcpNodeRef_PrivateSubtreeConstIterator& operator++()
+    {
+        if (_pruneChildren || !_MoveToFirstChild()) {
+            _MoveToNext();
+        }
+        _pruneChildren = false;
+        return *this;
+    }
+
+    PcpNodeRef_PrivateSubtreeConstIterator operator++(int)
+    {
+        PcpNodeRef_PrivateSubtreeConstIterator result(*this);
+        ++(*this);
+        return result;
+    }
+
+    bool operator==(const PcpNodeRef_PrivateSubtreeConstIterator& other) const
+    { return _node == other._node; }
+
+    bool operator!=(const PcpNodeRef_PrivateSubtreeConstIterator& other) const
+    { return !(*this == other); }
+
+private:
+    // If the current node has child nodes, move this iterator to the
+    // first child and return true. Otherwise return false.
+    bool _MoveToFirstChild()
+    {
+        auto& curNodeIdx = _node._nodeIdx;
+        const auto& nodeIndexes = _nodes[curNodeIdx].indexes;
+        const auto& invalid = PcpPrimIndex_Graph::_Node::_invalidNodeIndex;
+
+        if (nodeIndexes.firstChildIndex != invalid) {
+            curNodeIdx = nodeIndexes.firstChildIndex;
+            return true;
+        }
+        return false;
+    }
+
+    // If the current node has a direct sibling, move this iterator to
+    // that node. Otherwise, move this iterator to the next sibling of
+    // the nearest ancestor node with siblings. If no such node exists,
+    // (i.e., the current node is the weakest node in the index), this
+    // iterator will point to an invalid node.
+    void _MoveToNext()
+    {
+        auto& curNodeIdx = _node._nodeIdx;
+        const PcpPrimIndex_Graph::_Node::_Indexes* nodeIndexes = nullptr;
+        const auto& invalid = PcpPrimIndex_Graph::_Node::_invalidNodeIndex;
+
+        while (curNodeIdx != invalid) {
+            // See if we can move to the current node's next sibling.
+            nodeIndexes = &_nodes[curNodeIdx].indexes;
+            if (nodeIndexes->nextSiblingIndex != invalid) {
+                curNodeIdx = nodeIndexes->nextSiblingIndex;
+                break;
+            }
+
+            // If we can't, move to the current node's parent and try again.
+            curNodeIdx = nodeIndexes->arcParentIndex;
+        }
+    }
+
+private:
+    PcpNodeRef _node;
+    const PcpPrimIndex_Graph::_Node* _nodes;
+    bool _pruneChildren;
+};
+
+// Wrapper type for range-based for loops.
+class PcpNodeRef_PrivateSubtreeConstRange
+{
+public:
+    PcpNodeRef_PrivateSubtreeConstRange(const PcpNodeRef& node)
+        : _begin(node, /* end = */ false)
+        , _end(node, /* end = */ true)
+    { }
+
+    PcpNodeRef_PrivateSubtreeConstIterator begin() const { return _begin; }
+    PcpNodeRef_PrivateSubtreeConstIterator end() const { return _end; }
+
+private:
+    PcpNodeRef_PrivateSubtreeConstIterator _begin, _end;
+};
+
+/// Return node range for subtree rooted at the given \p node.
+inline
+PcpNodeRef_PrivateSubtreeConstRange
+Pcp_GetSubtreeRange(const PcpNodeRef& node)
+{
+    return PcpNodeRef_PrivateSubtreeConstRange(node);
+}
+
 PXR_NAMESPACE_CLOSE_SCOPE
 
 #endif // PXR_USD_PCP_NODE_ITERATOR_H
index 540272da2ea0e2d528768cf06cf05cbdaed5eae8..2780af3c69bacd06f99ce4eb4728426a7fa224a0 100644 (file)
@@ -41,6 +41,7 @@
 #include "pxr/usd/pcp/primIndex_StackFrame.h"
 #include "pxr/usd/pcp/statistics.h"
 #include "pxr/usd/pcp/strengthOrdering.h"
+#include "pxr/usd/pcp/traversalCache.h"
 #include "pxr/usd/pcp/types.h"
 #include "pxr/usd/pcp/utils.h"
 #include "pxr/usd/ar/resolver.h"
@@ -1000,6 +1001,26 @@ struct Pcp_PrimIndexer
     using _TaskUniq = pxr_tsl::robin_set<Task, TfHash>;
     _TaskUniq taskUniq;
 
+    // Caches for finding variant selections in the prim index. The map
+    // of caches is constructed lazily because this map isn't always
+    // needed. In particular, prim indexing doesn't look for variant
+    // selections in recursive prim indexing calls.
+    struct _VariantSelectionInfo
+    {
+        // Path in associate node's layer stack at which variant
+        // selections are authored.
+        SdfPath sitePath;
+
+        // Whether authored selections were found or not yet checked.
+        enum Status { AuthoredSelections, NoSelections, Unknown };
+        Status status = Unknown;
+    };
+
+    using _VariantTraversalCache = Pcp_TraversalCache<_VariantSelectionInfo>;
+    using _VariantTraversalCaches = std::unordered_map<
+        std::pair<PcpNodeRef, SdfPath>, _VariantTraversalCache, TfHash>;
+    std::optional<_VariantTraversalCaches> variantTraversalCache;
+
     const bool evaluateImpliedSpecializes;
     const bool evaluateVariants;
 
@@ -1029,6 +1050,16 @@ struct Pcp_PrimIndexer
         return _GetOriginatingIndex(previousFrame, outputs);
     }
 
+    _VariantTraversalCache& GetVariantTraversalCache(
+        PcpNodeRef const& node, SdfPath const& pathInNode) {
+        if (!variantTraversalCache) {
+            variantTraversalCache.emplace();
+        }
+
+        return variantTraversalCache->try_emplace(
+            {node, pathInNode}, node, pathInNode).first->second;
+    }
+
     // Map the given node's path to the root of the final prim index
     // being computed.
     SdfPath MapNodePathToRoot(PcpNodeRef const& node) const {
@@ -3760,106 +3791,82 @@ _ComposeVariantSelectionForNode(
     const SdfPath& pathInNode,
     const std::string & vset,
     std::string *vsel,
-    PcpNodeRef *nodeWithVsel,
     Pcp_PrimIndexer *indexer)
 {
-    TF_VERIFY(!pathInNode.IsEmpty());
-
-    // We are using path-translation to walk between nodes, so we
-    // are working exclusively in namespace paths, which must have
-    // no variant selection.
-    TF_VERIFY(!pathInNode.ContainsPrimVariantSelection(),
-              "Unexpected variant selection in namespace path <%s>",
-              pathInNode.GetText());
-
-    // If this node has an authored selection, use that.
-    // Note that we use this even if the authored selection is
-    // the empty string, which explicitly selects no variant.
-    if (_NodeCanContributeToVariant(node, pathInNode)) {
-        PcpLayerStackSite site(node.GetLayerStack(), pathInNode);
-        // pathInNode is a namespace path, not a storage path,
-        // so it will contain no variant selection (as verified above).
-        // To find the storage site, we need to insert any variant
-        // selection for this node.
-        if (node.GetArcType() == PcpArcTypeVariant) {
-            // We need to use the variant node's path at introduction
-            // instead of it's current path (i.e. node.GetPath()) because
-            // pathInNode may be an ancestor of the current path when
-            // dealing with ancestral variants.
-            const SdfPath variantPath = node.GetPathAtIntroduction();
-            site.path = pathInNode.ReplacePrefix(
-                variantPath.StripAllVariantSelections(),
-                variantPath);
-        }
-
-        std::unordered_set<std::string> exprVarDependencies;
-        PcpErrorVector errors;
+    std::unordered_set<std::string> exprVarDependencies;
+    PcpErrorVector errors;
 
-        const bool foundSelection = 
-            PcpComposeSiteVariantSelection(
-                site.layerStack, site.path, vset, vsel, 
-                &exprVarDependencies, &errors);
+    const bool foundSelection = 
+        PcpComposeSiteVariantSelection(
+            node.GetLayerStack(), pathInNode, vset, vsel, 
+            &exprVarDependencies, &errors);
 
-        if (!exprVarDependencies.empty()) {
-            indexer->outputs->expressionVariablesDependency.AddDependencies(
-                site.layerStack, std::move(exprVarDependencies));
-        }
-
-        if (!errors.empty()) {
-            for (const PcpErrorBasePtr& err : errors) {
-                indexer->RecordError(err);
-            }
-        }
+    if (!exprVarDependencies.empty()) {
+        indexer->outputs->expressionVariablesDependency.AddDependencies(
+            node.GetLayerStack(), std::move(exprVarDependencies));
+    }
 
-        if (foundSelection) {
-            *nodeWithVsel = node;
-            return true;
+    if (!errors.empty()) {
+        for (const PcpErrorBasePtr& err : errors) {
+            indexer->RecordError(err);
         }
     }
 
-    return false;
+    return foundSelection;
 }
 
 // Check the tree of nodes rooted at the given node for any node
 // representing a prior selection for the given variant set for the path.
 static bool
 _FindPriorVariantSelection(
-    const PcpNodeRef& node,
-    const SdfPath &pathInNode,
+    const PcpNodeRef& startNode,
+    const SdfPath &pathInStartNode,
     const std::string & vset,
     std::string *vsel,
-    PcpNodeRef *nodeWithVsel)
+    PcpNodeRef *nodeWithVsel,
+    Pcp_PrimIndexer *indexer)
 {
-    // If this node represents a variant selection at the same
-    // effective depth of namespace, then check its selection.
-    if (node.GetArcType() == PcpArcTypeVariant) {
-        const SdfPath nodePathAtIntroduction = node.GetPathAtIntroduction();
-        const std::pair<std::string, std::string> nodeVsel =
-            nodePathAtIntroduction.GetVariantSelection();
-        if (nodeVsel.first == vset) {
-            // The node has a variant selection for the variant set we're 
-            // looking for, but we still have to check that the node actually
-            // represents the prim path we're choosing a variant selection for
-            // (as opposed to a different prim path that just happens to have
-            // a variant set with the same name.
-            if (nodePathAtIntroduction.GetPrimPath() == pathInNode) {
-                *vsel = nodeVsel.second;
-                *nodeWithVsel = node;
-                return true;
-            }
-        }
-    }
+    auto& traverser = 
+        indexer->GetVariantTraversalCache(startNode, pathInStartNode);
 
-    TF_FOR_ALL(child, Pcp_GetChildrenRange(node)) {
-        const SdfPath pathInChild = 
-            child->GetMapToParent().MapTargetToSource(pathInNode);
-        if (pathInChild.IsEmpty()) {
-            continue;
-        }
+    // Don't use a range-based for loop here so we can avoid asking for
+    // the path in the current node (which incurs expensive path translations)
+    // until we're absolutely sure we need it.
+    for (auto it = traverser.begin(), e = traverser.end(); it != e; ++it) {
+        const PcpNodeRef node = it.Node();
 
-        if (_FindPriorVariantSelection(
-                *child, pathInChild, vset, vsel, nodeWithVsel)) {
-            return true;
+        // If this node represents a variant selection at the same
+        // effective depth of namespace, then check its selection.
+        if (node.GetArcType() == PcpArcTypeVariant) {
+            const SdfPath nodePathAtIntroduction = node.GetPathAtIntroduction();
+            const std::pair<std::string, std::string> nodeVsel =
+                nodePathAtIntroduction.GetVariantSelection();
+            if (nodeVsel.first == vset) {
+                const SdfPath& pathInNode = it.PathInNode();
+
+                // If the path didn't translate to this node, it won't translate
+                // to any of the node's children, so we might as well prune the
+                // traversal here. 
+                //
+                // We don't do this check earlier because we don't want to call
+                // PathInNode unless absolutely necessary, as it runs relatively
+                // expensive path translations.
+                if (pathInNode.IsEmpty()) {
+                    it.PruneChildren();
+                    continue;
+                }
+
+                // The node has a variant selection for the variant set we're
+                // looking for, but we still have to check that the node
+                // actually represents the prim path we're choosing a variant
+                // selection for (as opposed to a different prim path that just
+                // happens to have a variant set with the same name.
+                if (nodePathAtIntroduction.GetPrimPath() == it.PathInNode()) {
+                    *vsel = nodeVsel.second;
+                    *nodeWithVsel = node;
+                    return true;
+                }
+            }
         }
     }
 
@@ -3868,27 +3875,70 @@ _FindPriorVariantSelection(
 
 static bool
 _ComposeVariantSelectionAcrossNodes(
-    const PcpNodeRef& node,
-    const SdfPath& pathInNode,
+    const PcpNodeRef& startNode,
+    const SdfPath& pathInStartNode,
     const std::string & vset,
     std::string *vsel,
     PcpNodeRef *nodeWithVsel,
     Pcp_PrimIndexer *indexer)
 {
     // Compose variant selection in strong-to-weak order.
-    if (_ComposeVariantSelectionForNode(
-            node, pathInNode, vset, vsel, nodeWithVsel, indexer)) {
-        return true;
-    }
+    auto& traverser = 
+        indexer->GetVariantTraversalCache(startNode, pathInStartNode);
 
-    TF_FOR_ALL(child, Pcp_GetChildrenRange(node)) {
-        const PcpNodeRef& childNode = *child;
-        const SdfPath pathInChildNode =
-            childNode.GetMapToParent().MapTargetToSource(pathInNode);
+    for (auto it = traverser.begin(), e = traverser.end(); it != e; ++it) {
+        auto [node, pathInNode, info] = *it;
+
+        // If path translation to this node failed, it will fail for all
+        // other children so we can skip them entirely
+        if (pathInNode.IsEmpty()) {
+            it.PruneChildren();
+            continue;
+        }
+
+        if (!_NodeCanContributeToVariant(node, pathInNode)) {
+            continue;
+        }
 
-        if (!pathInChildNode.IsEmpty() &&
-            _ComposeVariantSelectionAcrossNodes(
-                *child, pathInChildNode, vset, vsel, nodeWithVsel, indexer)) {
+        // Precompute whether the layer stack has any authored variant
+        // selections and cache that away.
+        using Info = Pcp_PrimIndexer::_VariantSelectionInfo;
+        if (info.status == Info::Unknown) {
+            info.sitePath = [&node=node, &pathInNode=pathInNode]() {
+                // pathInNode is a namespace path, not a storage path,
+                // so it will contain no variant selection (as verified above).
+                // To find the storage site, we need to insert any variant
+                // selection for this node.
+                if (node.GetArcType() == PcpArcTypeVariant) {
+                    // We need to use the variant node's path at introduction
+                    // instead of it's current path (i.e. node.GetPath()) because
+                    // pathInNode may be an ancestor of the current path when
+                    // dealing with ancestral variants.
+                    const SdfPath variantPath = node.GetPathAtIntroduction();
+                    return pathInNode.ReplacePrefix(
+                        variantPath.StripAllVariantSelections(),
+                        variantPath);
+                }
+                return pathInNode;
+            }();
+
+            info.status = 
+                PcpComposeSiteHasVariantSelections(
+                    node.GetLayerStack(), info.sitePath) ?
+                Info::AuthoredSelections : Info::NoSelections;
+        }
+
+        // If no variant selections are authored here, we can skip.
+        if (info.status == Info::NoSelections) {
+            continue;
+        }
+
+        // If this node has an authored selection, use that.
+        // Note that we use this even if the authored selection is
+        // the empty string, which explicitly selects no variant.
+        if (_ComposeVariantSelectionForNode(
+                node, info.sitePath, vset, vsel, indexer)) {
+            *nodeWithVsel = node;
             return true;
         }
     }
@@ -3943,7 +3993,7 @@ _ComposeVariantSelection(
     // First check if we have already resolved this variant set in the current
     // prim index.
     if (_FindPriorVariantSelection(
-            startNode, pathInStartNode, vset, vsel, nodeWithVsel)) {
+            startNode, pathInStartNode, vset, vsel, nodeWithVsel, indexer)) {
 
         PCP_INDEXING_MSG(
             indexer, node, *nodeWithVsel,
index 3ff00b3e35d0892032ff88ba33db9cac12331bec..0cadf001f456c1150e364093b3301db4767ec11d 100644 (file)
@@ -234,6 +234,8 @@ private:
     friend class PcpNodeRef_ChildrenReverseIterator;
     friend class PcpNodeRef_PrivateChildrenConstIterator;
     friend class PcpNodeRef_PrivateChildrenConstReverseIterator;
+    friend class PcpNodeRef_PrivateSubtreeConstIterator;
+    template <class T> friend class Pcp_TraversalCache;
 
     // NOTE: These accessors assume the consumer will be changing the node
     //       and may cause shared node data to be copied locally.
diff --git a/pxr/usd/pcp/traversalCache.h b/pxr/usd/pcp/traversalCache.h
new file mode 100644 (file)
index 0000000..db621d6
--- /dev/null
@@ -0,0 +1,228 @@
+//
+// Copyright 2024 Pixar
+//
+// Licensed under the Apache License, Version 2.0 (the "Apache License")
+// with the following modification; you may not use this file except in
+// compliance with the Apache License and the following modification to it:
+// Section 6. Trademarks. is deleted and replaced with:
+//
+// 6. Trademarks. This License does not grant permission to use the trade
+//    names, trademarks, service marks, or product names of the Licensor
+//    and its affiliates, except as required to comply with Section 4(c) of
+//    the License and to reproduce the content of the NOTICE file.
+//
+// You may obtain a copy of the Apache License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the Apache License with the above modification is
+// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the Apache License for the specific
+// language governing permissions and limitations under the Apache License.
+//
+#ifndef PXR_USD_PCP_TRAVERSAL_CACHE_H
+#define PXR_USD_PCP_TRAVERSAL_CACHE_H
+
+#include "pxr/pxr.h"
+
+#include "pxr/usd/pcp/node.h"
+#include "pxr/usd/pcp/node_Iterator.h"
+#include "pxr/usd/pcp/primIndex_Graph.h"
+#include "pxr/usd/sdf/path.h"
+
+#include <optional>
+#include <tuple>
+#include <vector>
+
+PXR_NAMESPACE_OPEN_SCOPE
+
+/// \class Pcp_TraversalCache
+///
+/// Caches the traversal of a subtree in a prim index starting at a
+/// given node and with a specified path within that node's layer stack.
+/// As clients traverse through the subtree, the starting path will
+/// be translated to each node and cached, so that repeated traversals
+/// will not incur the same path translation costs. Clients may also
+/// store data associated with each node in the subtree.
+template <class Data>
+class Pcp_TraversalCache
+{
+public:
+    /// \class iterator
+    /// Object for iterating over the subtree of nodes cached by
+    /// the owning Pcp_TraversalCache.
+    class iterator
+    {
+    public:
+        /// value type is a tuple of (Node(), PathInNode(), AssociatedData()).
+        /// See performance note on PathInNode().
+        using value_type = std::tuple<PcpNodeRef const, SdfPath const, Data&>;
+
+        /// Return the current node.
+        PcpNodeRef Node()
+        {
+            return *_iter;
+        }
+
+        /// Return the original traversal path given to the owning
+        /// Pcp_TraversalCache translated to the current node.
+        ///
+        /// This function will translate and cache the traversal path for
+        /// this node and all parent nodes if they have not already been
+        /// computed.
+        SdfPath const& PathInNode()
+        {
+            return *_owner->_GetEntry(*_iter, /* computePaths = */ true).path;
+        }
+
+        /// Return a reference to the data associated with the current node.
+        Data& AssociatedData()
+        {
+            return _owner->_GetEntry(*_iter, /* computePaths = */ false).data;
+        }
+
+        /// Return value_type. Note that this will incur the cost of path
+        /// translations described in PathInNode(). If you don't need the
+        /// translated path, use one of the other member functions to avoid
+        /// this cost.
+        value_type operator*()
+        {
+            _Entry& e = _owner->_GetEntry(*_iter, /* computePaths = */ true);
+            return std::tie(*_iter, *e.path, e.data);
+        }
+
+        /// Causes the next increment of this iterator to ignore descendants
+        /// of the current node.
+        void PruneChildren()
+        {
+            _iter.PruneChildren();
+        }
+
+        iterator& operator++()
+        {
+            ++_iter;
+            return *this;
+        }
+
+        iterator operator++(int)
+        {
+            iterator result(*this);
+            ++_iter;
+            return result;
+        }
+
+        bool operator==(iterator const& rhs) const
+        { return std::tie(_owner, _iter) == std::tie(rhs._owner, rhs._iter); }
+
+        bool operator!=(iterator const& rhs) const
+        { return !(*this == rhs); }
+
+    private:
+        friend class Pcp_TraversalCache;
+        iterator(
+            Pcp_TraversalCache* owner,
+            PcpNodeRef_PrivateSubtreeConstIterator iter)
+            : _owner(owner)
+            , _iter(iter)
+        { }
+
+        Pcp_TraversalCache* const _owner = nullptr;
+        PcpNodeRef_PrivateSubtreeConstIterator _iter;
+    };
+
+    /// Construct a traversal cache for the subtree rooted at \p startNode and
+    /// the path \p pathInNode. \p pathInNode must be in \p startNode's
+    /// namespace.
+    Pcp_TraversalCache(PcpNodeRef const& startNode, SdfPath const& pathInNode)
+        : _startNode(startNode)
+    {
+        _ResizeForGraph();
+        _cache[_startNode._GetNodeIndex()].path = pathInNode;
+    }
+
+    Pcp_TraversalCache(Pcp_TraversalCache const&) = delete;
+    Pcp_TraversalCache(Pcp_TraversalCache &&) = delete;
+    Pcp_TraversalCache& operator=(Pcp_TraversalCache const&) = delete;
+    Pcp_TraversalCache& operator=(Pcp_TraversalCache &&) = delete;
+
+    iterator begin()
+    {
+        _ResizeForGraph();
+        return iterator(
+            this, 
+            PcpNodeRef_PrivateSubtreeConstIterator(
+                _startNode, /* end = */ false));
+    }
+
+    iterator end()
+    {
+        _ResizeForGraph();
+        return iterator(
+            this, 
+            PcpNodeRef_PrivateSubtreeConstIterator(
+                _startNode, /* end = */ true));
+    }
+
+private:
+    struct _Entry
+    {
+        // Traversal path translated to the entry's corresponding node
+        std::optional<SdfPath> path;
+
+        // Client data associated with this entry's corresponding node.
+        Data data;
+    };
+
+    void _ResizeForGraph()
+    {
+        PcpPrimIndex_Graph const* graph = _startNode.GetOwningGraph();
+
+        // We assume the graph will never shrink.
+        TF_VERIFY(graph->_GetNumNodes() >= _cache.size());
+
+        if (graph->_GetNumNodes() > _cache.size()) {
+            _cache.resize(graph->_GetNumNodes());
+        }
+    }
+
+    SdfPath _TranslatePathsForNode(PcpNodeRef const& node)
+    {
+        // "Recursively" map the path from the parent node to this node.
+        // This terminates because we'll eventually reach _startNode,
+        // and we populated its path in the c'tor.
+        _Entry& entry = _cache[node._GetNodeIndex()];
+        if (!entry.path) {
+            PcpNodeRef const parentNode = node.GetParentNode();
+            _Entry& parentEntry = _cache[parentNode._GetNodeIndex()];
+            if (!parentEntry.path) {
+                parentEntry.path = _TranslatePathsForNode(parentNode);
+            }
+            
+            SdfPath const& pathInParent = *(parentEntry.path);
+            entry.path = pathInParent.IsEmpty() ? SdfPath() : 
+                node.GetMapToParent().MapTargetToSource(pathInParent);
+        }
+
+        return *entry.path;
+    }
+
+    _Entry& _GetEntry(PcpNodeRef const& node, bool computePaths)
+    {
+        TF_VERIFY(node._GetNodeIndex() < _cache.size());
+
+        // If requested, make sure the translated path is populated before
+        // returning the _Entry to the caller.
+        if (computePaths) {
+            _TranslatePathsForNode(node);
+        }
+        return _cache[node._GetNodeIndex()];
+    }
+
+    PcpNodeRef _startNode;
+    std::vector<_Entry> _cache;
+};
+
+PXR_NAMESPACE_CLOSE_SCOPE
+
+#endif