summaryrefslogtreecommitdiff
path: root/absl/strings/internal
diff options
context:
space:
mode:
Diffstat (limited to 'absl/strings/internal')
-rw-r--r--absl/strings/internal/cord_internal.cc1
-rw-r--r--absl/strings/internal/cord_internal.h6
-rw-r--r--absl/strings/internal/cord_rep_btree.cc21
-rw-r--r--absl/strings/internal/cord_rep_btree.h32
-rw-r--r--absl/strings/internal/cord_rep_btree_test.cc43
-rw-r--r--absl/strings/internal/cordz_info.cc51
-rw-r--r--absl/strings/internal/cordz_info_statistics_test.cc133
-rw-r--r--absl/strings/internal/cordz_statistics.h3
8 files changed, 248 insertions, 42 deletions
diff --git a/absl/strings/internal/cord_internal.cc b/absl/strings/internal/cord_internal.cc
index f79cb628..1767e6fc 100644
--- a/absl/strings/internal/cord_internal.cc
+++ b/absl/strings/internal/cord_internal.cc
@@ -31,6 +31,7 @@ ABSL_CONST_INIT std::atomic<bool> cord_ring_buffer_enabled(
kCordEnableRingBufferDefault);
ABSL_CONST_INIT std::atomic<bool> shallow_subcords_enabled(
kCordShallowSubcordsDefault);
+ABSL_CONST_INIT std::atomic<bool> cord_btree_exhaustive_validation(false);
void CordRep::Destroy(CordRep* rep) {
assert(rep != nullptr);
diff --git a/absl/strings/internal/cord_internal.h b/absl/strings/internal/cord_internal.h
index 371a7f9c..8ae644ba 100644
--- a/absl/strings/internal/cord_internal.h
+++ b/absl/strings/internal/cord_internal.h
@@ -46,6 +46,12 @@ extern std::atomic<bool> cord_btree_enabled;
extern std::atomic<bool> cord_ring_buffer_enabled;
extern std::atomic<bool> shallow_subcords_enabled;
+// `cord_btree_exhaustive_validation` can be set to force exhaustive validation
+// in debug assertions, and code that calls `IsValid()` explicitly. By default,
+// assertions should be relatively cheap and AssertValid() can easily lead to
+// O(n^2) complexity as recursive / full tree validation is O(n).
+extern std::atomic<bool> cord_btree_exhaustive_validation;
+
inline void enable_cord_btree(bool enable) {
cord_btree_enabled.store(enable, std::memory_order_relaxed);
}
diff --git a/absl/strings/internal/cord_rep_btree.cc b/absl/strings/internal/cord_rep_btree.cc
index 6978cfd2..fd3a0045 100644
--- a/absl/strings/internal/cord_rep_btree.cc
+++ b/absl/strings/internal/cord_rep_btree.cc
@@ -32,6 +32,8 @@ namespace absl {
ABSL_NAMESPACE_BEGIN
namespace cord_internal {
+constexpr size_t CordRepBtree::kMaxCapacity; // NOLINT: needed for c++ < c++17
+
namespace {
using NodeStack = CordRepBtree * [CordRepBtree::kMaxDepth];
@@ -42,6 +44,10 @@ using CopyResult = CordRepBtree::CopyResult;
constexpr auto kFront = CordRepBtree::kFront;
constexpr auto kBack = CordRepBtree::kBack;
+inline bool exhaustive_validation() {
+ return cord_btree_exhaustive_validation.load(std::memory_order_relaxed);
+}
+
// Implementation of the various 'Dump' functions.
// Prints the entire tree structure or 'rep'. External callers should
// not specify 'depth' and leave it to its default (0) value.
@@ -357,7 +363,7 @@ void CordRepBtree::DestroyNonLeaf(CordRepBtree* tree, size_t begin,
Delete(tree);
}
-bool CordRepBtree::IsValid(const CordRepBtree* tree) {
+bool CordRepBtree::IsValid(const CordRepBtree* tree, bool shallow) {
#define NODE_CHECK_VALID(x) \
if (!(x)) { \
ABSL_RAW_LOG(ERROR, "CordRepBtree::CheckValid() FAILED: %s", #x); \
@@ -389,9 +395,9 @@ bool CordRepBtree::IsValid(const CordRepBtree* tree) {
child_length += edge->length;
}
NODE_CHECK_EQ(child_length, tree->length);
- if (tree->height() > 0) {
+ if ((!shallow || exhaustive_validation()) && tree->height() > 0) {
for (CordRep* edge : tree->Edges()) {
- if (!IsValid(edge->btree())) return false;
+ if (!IsValid(edge->btree(), shallow)) return false;
}
}
return true;
@@ -402,16 +408,17 @@ bool CordRepBtree::IsValid(const CordRepBtree* tree) {
#ifndef NDEBUG
-CordRepBtree* CordRepBtree::AssertValid(CordRepBtree* tree) {
- if (!IsValid(tree)) {
+CordRepBtree* CordRepBtree::AssertValid(CordRepBtree* tree, bool shallow) {
+ if (!IsValid(tree, shallow)) {
Dump(tree, "CordRepBtree validation failed:", false, std::cout);
ABSL_RAW_LOG(FATAL, "CordRepBtree::CheckValid() FAILED");
}
return tree;
}
-const CordRepBtree* CordRepBtree::AssertValid(const CordRepBtree* tree) {
- if (!IsValid(tree)) {
+const CordRepBtree* CordRepBtree::AssertValid(const CordRepBtree* tree,
+ bool shallow) {
+ if (!IsValid(tree, shallow)) {
Dump(tree, "CordRepBtree validation failed:", false, std::cout);
ABSL_RAW_LOG(FATAL, "CordRepBtree::CheckValid() FAILED");
}
diff --git a/absl/strings/internal/cord_rep_btree.h b/absl/strings/internal/cord_rep_btree.h
index 7d854731..56e1e4af 100644
--- a/absl/strings/internal/cord_rep_btree.h
+++ b/absl/strings/internal/cord_rep_btree.h
@@ -266,10 +266,28 @@ class CordRepBtree : public CordRep {
// holding a FLAT or EXTERNAL child rep.
static bool IsDataEdge(const CordRep* rep);
- // Diagnostics
- static bool IsValid(const CordRepBtree* tree);
- static CordRepBtree* AssertValid(CordRepBtree* tree);
- static const CordRepBtree* AssertValid(const CordRepBtree* tree);
+ // Diagnostics: returns true if `tree` is valid and internally consistent.
+ // If `shallow` is false, then the provided top level node and all child nodes
+ // below it are recursively checked. If `shallow` is true, only the provided
+ // node in `tree` and the cumulative length, type and height of the direct
+ // child nodes of `tree` are checked. The value of `shallow` is ignored if the
+ // internal `cord_btree_exhaustive_validation` diagnostics variable is true,
+ // in which case the performed validations works as if `shallow` were false.
+ // This function is intended for debugging and testing purposes only.
+ static bool IsValid(const CordRepBtree* tree, bool shallow = false);
+
+ // Diagnostics: asserts that the provided tree is valid.
+ // `AssertValid()` performs a shallow validation by default. `shallow` can be
+ // set to false in which case an exhaustive validation is performed. This
+ // function is implemented in terms of calling `IsValid()` and asserting the
+ // return value to be true. See `IsValid()` for more information.
+ // This function is intended for debugging and testing purposes only.
+ static CordRepBtree* AssertValid(CordRepBtree* tree, bool shallow = true);
+ static const CordRepBtree* AssertValid(const CordRepBtree* tree,
+ bool shallow = true);
+
+ // Diagnostics: dump the contents of this tree to `stream`.
+ // This function is intended for debugging and testing purposes only.
static void Dump(const CordRep* rep, std::ostream& stream);
static void Dump(const CordRep* rep, absl::string_view label,
std::ostream& stream);
@@ -834,11 +852,13 @@ inline CordRepBtree* CordRepBtree::Prepend(CordRepBtree* tree, CordRep* rep) {
#ifdef NDEBUG
-inline CordRepBtree* CordRepBtree::AssertValid(CordRepBtree* tree) {
+inline CordRepBtree* CordRepBtree::AssertValid(CordRepBtree* tree,
+ bool /* shallow */) {
return tree;
}
-inline const CordRepBtree* CordRepBtree::AssertValid(const CordRepBtree* tree) {
+inline const CordRepBtree* CordRepBtree::AssertValid(const CordRepBtree* tree,
+ bool /* shallow */) {
return tree;
}
diff --git a/absl/strings/internal/cord_rep_btree_test.cc b/absl/strings/internal/cord_rep_btree_test.cc
index 7a0b7811..073a7d45 100644
--- a/absl/strings/internal/cord_rep_btree_test.cc
+++ b/absl/strings/internal/cord_rep_btree_test.cc
@@ -24,6 +24,7 @@
#include "gtest/gtest.h"
#include "absl/base/config.h"
#include "absl/base/internal/raw_logging.h"
+#include "absl/cleanup/cleanup.h"
#include "absl/strings/internal/cord_internal.h"
#include "absl/strings/internal/cord_rep_test_util.h"
#include "absl/strings/str_cat.h"
@@ -1346,6 +1347,48 @@ TEST(CordRepBtreeTest, AssertValid) {
CordRep::Unref(tree);
}
+TEST(CordRepBtreeTest, CheckAssertValidShallowVsDeep) {
+ // Restore exhaustive validation on any exit.
+ const bool exhaustive_validation = cord_btree_exhaustive_validation.load();
+ auto cleanup = absl::MakeCleanup([exhaustive_validation] {
+ cord_btree_exhaustive_validation.store(exhaustive_validation);
+ });
+
+ // Create a tree of at least 2 levels, and mess with the original flat, which
+ // should go undetected in shallow mode as the flat is too far away, but
+ // should be detected in forced non-shallow mode.
+ CordRep* flat = MakeFlat("abc");
+ CordRepBtree* tree = CordRepBtree::Create(flat);
+ constexpr size_t max_cap = CordRepBtree::kMaxCapacity;
+ const size_t n = max_cap * max_cap * 2;
+ for (size_t i = 0; i < n; ++i) {
+ tree = CordRepBtree::Append(tree, MakeFlat("Hello world"));
+ }
+ flat->length = 100;
+
+ cord_btree_exhaustive_validation.store(false);
+ EXPECT_FALSE(CordRepBtree::IsValid(tree));
+ EXPECT_TRUE(CordRepBtree::IsValid(tree, true));
+ EXPECT_FALSE(CordRepBtree::IsValid(tree, false));
+ CordRepBtree::AssertValid(tree);
+ CordRepBtree::AssertValid(tree, true);
+#if defined(GTEST_HAS_DEATH_TEST)
+ EXPECT_DEBUG_DEATH(CordRepBtree::AssertValid(tree, false), ".*");
+#endif
+
+ cord_btree_exhaustive_validation.store(true);
+ EXPECT_FALSE(CordRepBtree::IsValid(tree));
+ EXPECT_FALSE(CordRepBtree::IsValid(tree, true));
+ EXPECT_FALSE(CordRepBtree::IsValid(tree, false));
+#if defined(GTEST_HAS_DEATH_TEST)
+ EXPECT_DEBUG_DEATH(CordRepBtree::AssertValid(tree), ".*");
+ EXPECT_DEBUG_DEATH(CordRepBtree::AssertValid(tree, true), ".*");
+#endif
+
+ flat->length = 3;
+ CordRep::Unref(tree);
+}
+
} // namespace
} // namespace cord_internal
ABSL_NAMESPACE_END
diff --git a/absl/strings/internal/cordz_info.cc b/absl/strings/internal/cordz_info.cc
index a3a0b9c0..5c18bbc5 100644
--- a/absl/strings/internal/cordz_info.cc
+++ b/absl/strings/internal/cordz_info.cc
@@ -19,6 +19,7 @@
#include "absl/container/inlined_vector.h"
#include "absl/debugging/stacktrace.h"
#include "absl/strings/internal/cord_internal.h"
+#include "absl/strings/internal/cord_rep_btree.h"
#include "absl/strings/internal/cord_rep_ring.h"
#include "absl/strings/internal/cordz_handle.h"
#include "absl/strings/internal/cordz_statistics.h"
@@ -83,19 +84,23 @@ class CordRepAnalyzer {
// Process all top level linear nodes (substrings and flats).
repref = CountLinearReps(repref, memory_usage_);
- // We should have have either a concat or ring node node if not null.
if (repref.rep != nullptr) {
- assert(repref.rep->tag == RING || repref.rep->tag == CONCAT);
if (repref.rep->tag == RING) {
AnalyzeRing(repref);
+ } else if (repref.rep->tag == BTREE) {
+ AnalyzeBtree(repref);
} else if (repref.rep->tag == CONCAT) {
AnalyzeConcat(repref);
+ } else {
+ // We should have either a concat, btree, or ring node if not null.
+ assert(false);
}
}
// Adds values to output
statistics_.estimated_memory_usage += memory_usage_.total;
- statistics_.estimated_fair_share_memory_usage += memory_usage_.fair_share;
+ statistics_.estimated_fair_share_memory_usage +=
+ static_cast<size_t>(memory_usage_.fair_share);
}
private:
@@ -117,13 +122,13 @@ class CordRepAnalyzer {
// Memory usage values
struct MemoryUsage {
size_t total = 0;
- size_t fair_share = 0;
+ double fair_share = 0.0;
// Adds 'size` memory usage to this class, with a cumulative (recursive)
// reference count of `refcount`
void Add(size_t size, size_t refcount) {
total += size;
- fair_share += size / refcount;
+ fair_share += static_cast<double>(size) / refcount;
}
};
@@ -215,28 +220,32 @@ class CordRepAnalyzer {
}
}
- // Counts the provided ring buffer child into `child_usage`.
- void CountRingChild(const CordRep* child, MemoryUsage& child_usage) {
- RepRef rep{child, static_cast<size_t>(child->refcount.Get())};
- rep = CountLinearReps(rep, child_usage);
- assert(rep.rep == nullptr);
- }
-
- // Analyzes the provided ring. As ring buffers can have many child nodes, the
- // effect of rounding errors can become non trivial, so we compute the totals
- // first at the ring level, and then divide the fair share of the total
- // including children fair share totals.
+ // Analyzes the provided ring.
void AnalyzeRing(RepRef rep) {
statistics_.node_count++;
statistics_.node_counts.ring++;
- MemoryUsage ring_usage;
const CordRepRing* ring = rep.rep->ring();
- ring_usage.Add(CordRepRing::AllocSize(ring->capacity()), 1);
+ memory_usage_.Add(CordRepRing::AllocSize(ring->capacity()), rep.refcount);
ring->ForEach([&](CordRepRing::index_type pos) {
- CountRingChild(ring->entry_child(pos), ring_usage);
+ CountLinearReps(rep.Child(ring->entry_child(pos)), memory_usage_);
});
- memory_usage_.total += ring_usage.total;
- memory_usage_.fair_share += ring_usage.fair_share / rep.refcount;
+ }
+
+ // Analyzes the provided btree.
+ void AnalyzeBtree(RepRef rep) {
+ statistics_.node_count++;
+ statistics_.node_counts.btree++;
+ memory_usage_.Add(sizeof(CordRepBtree), rep.refcount);
+ const CordRepBtree* tree = rep.rep->btree();
+ if (tree->height() > 0) {
+ for (CordRep* edge : tree->Edges()) {
+ AnalyzeBtree(rep.Child(edge));
+ }
+ } else {
+ for (CordRep* edge : tree->Edges()) {
+ CountLinearReps(rep.Child(edge), memory_usage_);
+ }
+ }
}
CordzStatistics& statistics_;
diff --git a/absl/strings/internal/cordz_info_statistics_test.cc b/absl/strings/internal/cordz_info_statistics_test.cc
index 9f2842d9..7430d281 100644
--- a/absl/strings/internal/cordz_info_statistics_test.cc
+++ b/absl/strings/internal/cordz_info_statistics_test.cc
@@ -21,6 +21,7 @@
#include "absl/base/config.h"
#include "absl/strings/cord.h"
#include "absl/strings/internal/cord_internal.h"
+#include "absl/strings/internal/cord_rep_btree.h"
#include "absl/strings/internal/cord_rep_flat.h"
#include "absl/strings/internal/cord_rep_ring.h"
#include "absl/strings/internal/cordz_info.h"
@@ -42,6 +43,8 @@ inline void PrintTo(const CordzStatistics& stats, std::ostream* s) {
namespace {
+using ::testing::Ge;
+
// Creates a flat of the specified allocated size
CordRepFlat* Flat(size_t size) {
// Round up to a tag size, as we are going to poke an exact tag size back into
@@ -134,8 +137,8 @@ size_t SizeOf(const CordRepRing* rep) {
}
// Computes fair share memory used in a naive 'we dare to recurse' way.
-size_t FairShare(CordRep* rep, size_t ref = 1) {
- size_t self = 0, children = 0;
+double FairShareImpl(CordRep* rep, size_t ref) {
+ double self = 0.0, children = 0.0;
ref *= rep->refcount.Get();
if (rep->tag >= FLAT) {
self = SizeOf(rep->flat());
@@ -143,22 +146,32 @@ size_t FairShare(CordRep* rep, size_t ref = 1) {
self = SizeOf(rep->external());
} else if (rep->tag == SUBSTRING) {
self = SizeOf(rep->substring());
- children = FairShare(rep->substring()->child, ref);
+ children = FairShareImpl(rep->substring()->child, ref);
+ } else if (rep->tag == BTREE) {
+ self = SizeOf(rep->btree());
+ for (CordRep*edge : rep->btree()->Edges()) {
+ children += FairShareImpl(edge, ref);
+ }
} else if (rep->tag == RING) {
self = SizeOf(rep->ring());
rep->ring()->ForEach([&](CordRepRing::index_type i) {
- self += FairShare(rep->ring()->entry_child(i));
+ self += FairShareImpl(rep->ring()->entry_child(i), 1);
});
} else if (rep->tag == CONCAT) {
self = SizeOf(rep->concat());
- children = FairShare(rep->concat()->left, ref) +
- FairShare(rep->concat()->right, ref);
+ children = FairShareImpl(rep->concat()->left, ref) +
+ FairShareImpl(rep->concat()->right, ref);
} else {
assert(false);
}
return self / ref + children;
}
+// Returns the fair share memory size from `ShareFhareImpl()` as a size_t.
+size_t FairShare(CordRep* rep, size_t ref = 1) {
+ return static_cast<size_t>(FairShareImpl(rep, ref));
+}
+
// Samples the cord and returns CordzInfo::GetStatistics()
CordzStatistics SampleCord(CordRep* rep) {
InlineData cord(rep);
@@ -191,6 +204,7 @@ MATCHER_P(EqStatistics, stats, "Statistics equal expected values") {
STATS_MATCHER_EXPECT_EQ(node_counts.concat);
STATS_MATCHER_EXPECT_EQ(node_counts.substring);
STATS_MATCHER_EXPECT_EQ(node_counts.ring);
+ STATS_MATCHER_EXPECT_EQ(node_counts.btree);
STATS_MATCHER_EXPECT_EQ(estimated_memory_usage);
STATS_MATCHER_EXPECT_EQ(estimated_fair_share_memory_usage);
@@ -424,6 +438,103 @@ TEST(CordzInfoStatisticsTest, SharedSubstringRing) {
EXPECT_THAT(SampleCord(substring), EqStatistics(expected));
}
+TEST(CordzInfoStatisticsTest, BtreeLeaf) {
+ ASSERT_THAT(CordRepBtree::kMaxCapacity, Ge(3));
+ RefHelper ref;
+ auto* flat1 = Flat(2000);
+ auto* flat2 = Flat(200);
+ auto* substr = Substring(flat2);
+ auto* external = External(3000);
+
+ CordRepBtree* tree = CordRepBtree::Create(flat1);
+ tree = CordRepBtree::Append(tree, substr);
+ tree = CordRepBtree::Append(tree, external);
+ size_t flat3_count = CordRepBtree::kMaxCapacity - 3;
+ size_t flat3_size = 0;
+ for (size_t i = 0; i < flat3_count; ++i) {
+ auto* flat3 = Flat(70);
+ flat3_size += SizeOf(flat3);
+ tree = CordRepBtree::Append(tree, flat3);
+ }
+ ref.NeedsUnref(tree);
+
+ CordzStatistics expected;
+ expected.size = tree->length;
+ expected.estimated_memory_usage = SizeOf(tree) + SizeOf(flat1) +
+ SizeOf(flat2) + SizeOf(substr) +
+ flat3_size + SizeOf(external);
+ expected.estimated_fair_share_memory_usage = expected.estimated_memory_usage;
+ expected.node_count = 1 + 3 + 1 + flat3_count;
+ expected.node_counts.flat = 2 + flat3_count;
+ expected.node_counts.flat_128 = flat3_count;
+ expected.node_counts.flat_256 = 1;
+ expected.node_counts.external = 1;
+ expected.node_counts.substring = 1;
+ expected.node_counts.btree = 1;
+
+ EXPECT_THAT(SampleCord(tree), EqStatistics(expected));
+}
+
+TEST(CordzInfoStatisticsTest, BtreeNodeShared) {
+ RefHelper ref;
+ static constexpr int leaf_count = 3;
+ const size_t flat3_count = CordRepBtree::kMaxCapacity - 3;
+ ASSERT_THAT(flat3_count, Ge(0));
+
+ CordRepBtree* tree = nullptr;
+ size_t mem_size = 0;
+ for (int i = 0; i < leaf_count; ++i) {
+ auto* flat1 = ref.Ref(Flat(2000), 9);
+ mem_size += SizeOf(flat1);
+ if (i == 0) {
+ tree = CordRepBtree::Create(flat1);
+ } else {
+ tree = CordRepBtree::Append(tree, flat1);
+ }
+
+ auto* flat2 = Flat(200);
+ auto* substr = Substring(flat2);
+ mem_size += SizeOf(flat2) + SizeOf(substr);
+ tree = CordRepBtree::Append(tree, substr);
+
+ auto* external = External(30);
+ mem_size += SizeOf(external);
+ tree = CordRepBtree::Append(tree, external);
+
+ for (size_t i = 0; i < flat3_count; ++i) {
+ auto* flat3 = Flat(70);
+ mem_size += SizeOf(flat3);
+ tree = CordRepBtree::Append(tree, flat3);
+ }
+
+ if (i == 0) {
+ mem_size += SizeOf(tree);
+ } else {
+ mem_size += SizeOf(tree->Edges().back()->btree());
+ }
+ }
+ ref.NeedsUnref(tree);
+
+ // Ref count: 2 for top (add 1), 5 for leaf 0 (add 4).
+ ref.Ref(tree, 1);
+ ref.Ref(tree->Edges().front(), 4);
+
+ CordzStatistics expected;
+ expected.size = tree->length;
+ expected.estimated_memory_usage = SizeOf(tree) + mem_size;
+ expected.estimated_fair_share_memory_usage = FairShare(tree);
+
+ expected.node_count = 1 + leaf_count * (1 + 3 + 1 + flat3_count);
+ expected.node_counts.flat = leaf_count * (2 + flat3_count);
+ expected.node_counts.flat_128 = leaf_count * flat3_count;
+ expected.node_counts.flat_256 = leaf_count;
+ expected.node_counts.external = leaf_count;
+ expected.node_counts.substring = leaf_count;
+ expected.node_counts.btree = 1 + leaf_count;
+
+ EXPECT_THAT(SampleCord(tree), EqStatistics(expected));
+}
+
TEST(CordzInfoStatisticsTest, ThreadSafety) {
Notification stop;
static constexpr int kNumThreads = 8;
@@ -471,9 +582,15 @@ TEST(CordzInfoStatisticsTest, ThreadSafety) {
CordRep::Unref(cord.as_tree());
cord.set_inline_size(0);
} else {
- // 50/50 Ring or Flat coin toss
+ // Coin toss to 25% ring, 25% btree, and 50% flat.
CordRep* rep = Flat(256);
- rep = (coin_toss(gen) != 0) ? CordRepRing::Create(rep) : rep;
+ if (coin_toss(gen) != 0) {
+ if (coin_toss(gen) != 0) {
+ rep = CordRepRing::Create(rep);
+ } else {
+ rep = CordRepBtree::Create(rep);
+ }
+ }
cord.make_tree(rep);
// 50/50 sample
diff --git a/absl/strings/internal/cordz_statistics.h b/absl/strings/internal/cordz_statistics.h
index e03c651e..da4c7dbb 100644
--- a/absl/strings/internal/cordz_statistics.h
+++ b/absl/strings/internal/cordz_statistics.h
@@ -40,6 +40,7 @@ struct CordzStatistics {
size_t substring = 0; // #substring reps
size_t concat = 0; // #concat reps
size_t ring = 0; // #ring buffer reps
+ size_t btree = 0; // #btree reps
};
// The size of the cord in bytes. This matches the result of Cord::size().
@@ -61,6 +62,8 @@ struct CordzStatistics {
// The total number of nodes referenced by this cord.
// For ring buffer Cords, this includes the 'ring buffer' node.
+ // For btree Cords, this includes all 'CordRepBtree' tree nodes as well as all
+ // the substring, flat and external nodes referenced by the tree.
// A value of 0 implies the property has not been recorded.
int64_t node_count = 0;