Make SHOW_MEMORY builds work: fix deferred node capacity assert and eraseTree use-after-free #91

Merged
andrew merged 4 commits from weaselbot/conflict-set:weaselbot/issue-88 into main 2026-10-27 19:32:44 +00:00
2 changed files with 74 additions and 13 deletions
Showing only changes of commit df986810ff - Show all commits

No files matched your search

+48 -13
View File
@@ -80,7 +80,10 @@ using namespace weaselab;
#define assume assert #define assume assert
#endif #endif
#if SHOW_MEMORY // The SHOW_MEMORY instrumentation. Declared unconditionally so that each of
// these has a single definition in every configuration (see the definitions at
// the end of this file), rather than a no-op stub standing in for the real
// thing.
void addNode(struct Node *); void addNode(struct Node *);
void removeNode(struct Node *); void removeNode(struct Node *);
void addKey(struct Node *); void addKey(struct Node *);
@@ -90,14 +93,6 @@ void removeKey(struct Node *);
// `parent` pointers invalid. // `parent` pointers invalid.
void removeKey(struct Node *, int64_t searchPathLength); void removeKey(struct Node *, int64_t searchPathLength);
int64_t getSearchPathLength(struct Node *); int64_t getSearchPathLength(struct Node *);
#else
constexpr void addNode(struct Node *) {}
constexpr void removeNode(struct Node *) {}
constexpr void addKey(struct Node *) {}
constexpr void removeKey(struct Node *) {}
constexpr void removeKey(struct Node *, int64_t) {}
constexpr int64_t getSearchPathLength(struct Node *) { return 0; }
#endif
// ==================== BEGIN IMPLEMENTATION ==================== // ==================== BEGIN IMPLEMENTATION ====================
1
@@ -5492,8 +5487,9 @@ void internal_addWrites(ConflictSet::Impl *impl,
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes); impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
// Testing-only cross-check of the accounting.
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort(); // GCOVR_EXCL_LINE
} }
#endif #endif
} }
@@ -5504,8 +5500,9 @@ void internal_setOldestVersion(ConflictSet::Impl *impl, int64_t oldestVersion) {
impl->totalBytes += mallocBytesDelta; impl->totalBytes += mallocBytesDelta;
impl->memory_bytes.set(impl->totalBytes); impl->memory_bytes.set(impl->totalBytes);
#if SHOW_MEMORY #if SHOW_MEMORY
// Testing-only cross-check of the accounting.
if (impl->totalBytes != mallocBytes) { if (impl->totalBytes != mallocBytes) {
abort(); abort(); // GCOVR_EXCL_LINE
} }
#endif #endif
} }
@@ -5971,6 +5968,14 @@ checkMaxVersion(Node *root, Node *node, InternalVersionT oldestVersion,
} // namespace } // namespace
// The SHOW_MEMORY instrumentation. Each of these functions is defined exactly
// once, with the accounting compiled out when SHOW_MEMORY is disabled. Defining
// them once (rather than defining no-op stubs when SHOW_MEMORY is disabled)
// keeps every configuration of this file reporting each function on the same
// line, which merging coverage from objects compiled with different options
// requires: gcovr, merging the SHOW_MEMORY test with the rest of the build,
// otherwise aborts with "Got function ... on multiple lines". The optimizer
// removes the resulting empty calls.
#if SHOW_MEMORY #if SHOW_MEMORY
int64_t nodeBytes = 0; int64_t nodeBytes = 0;
@@ -5994,12 +5999,15 @@ int64_t getNodeSize(struct Node *n) {
return sizeof(Node48); return sizeof(Node48);
case Type_Node256: case Type_Node256:
return sizeof(Node256); return sizeof(Node256);
default: default: // GCOVR_EXCL_LINE
abort(); abort(); // GCOVR_EXCL_LINE
} }
} }
#endif
int64_t getSearchPathLength(Node *n) { int64_t getSearchPathLength(Node *n) {
#if SHOW_MEMORY
assert(n != nullptr); assert(n != nullptr);
int64_t result = 0; int64_t result = 0;
for (;;) { for (;;) {
@@ -6011,9 +6019,14 @@ int64_t getSearchPathLength(Node *n) {
n = n->parent; n = n->parent;
} }
return result; return result;
#else
(void)n;
return 0;
#endif
} }
void addNode(Node *n) { void addNode(Node *n) {
#if SHOW_MEMORY
nodeBytes += getNodeSize(n); nodeBytes += getNodeSize(n);
partialCapacityBytes += n->getCapacity(); partialCapacityBytes += n->getCapacity();
if (nodeBytes > peakNodeBytes) { if (nodeBytes > peakNodeBytes) {
@@ -6022,14 +6035,22 @@ void addNode(Node *n) {
if (partialCapacityBytes > peakPartialCapacityBytes) { if (partialCapacityBytes > peakPartialCapacityBytes) {
peakPartialCapacityBytes = partialCapacityBytes; peakPartialCapacityBytes = partialCapacityBytes;
} }
#else
(void)n;
#endif
} }
void removeNode(Node *n) { void removeNode(Node *n) {
#if SHOW_MEMORY
nodeBytes -= getNodeSize(n); nodeBytes -= getNodeSize(n);
partialCapacityBytes -= n->getCapacity(); partialCapacityBytes -= n->getCapacity();
#else
(void)n;
#endif
} }
void addKey(Node *n) { void addKey(Node *n) {
#if SHOW_MEMORY
if (!n->entryPresent) { if (!n->entryPresent) {
++totalKeys; ++totalKeys;
keyBytes += getSearchPathLength(n); keyBytes += getSearchPathLength(n);
@@ -6040,22 +6061,36 @@ void addKey(Node *n) {
peakKeyBytes = keyBytes; peakKeyBytes = keyBytes;
} }
} }
#else
(void)n;
#endif
} }
void removeKey(Node *n) { void removeKey(Node *n) {
#if SHOW_MEMORY
if (n->entryPresent) { if (n->entryPresent) {
--totalKeys; --totalKeys;
keyBytes -= getSearchPathLength(n); keyBytes -= getSearchPathLength(n);
} }
#else
(void)n;
#endif
} }
void removeKey(Node *n, int64_t searchPathLength) { void removeKey(Node *n, int64_t searchPathLength) {
#if SHOW_MEMORY
if (n->entryPresent) { if (n->entryPresent) {
--totalKeys; --totalKeys;
keyBytes -= searchPathLength; keyBytes -= searchPathLength;
} }
#else
(void)n;
(void)searchPathLength;
#endif
} }
#if SHOW_MEMORY
struct __attribute__((visibility("default"))) PeakPrinter { struct __attribute__((visibility("default"))) PeakPrinter {
~PeakPrinter() { ~PeakPrinter() {
printf("--- radix_tree ---\n"); printf("--- radix_tree ---\n");
+26
View File
@@ -86,4 +86,30 @@ int main() {
assert(checkPoint(cs, "other/999", version) == ConflictSet::Conflict); assert(checkPoint(cs, "other/999", version) == ConflictSet::Conflict);
} }
checkFullyReleased(); checkFullyReleased();
// Grow a node through every node type, so that the accounting for each node
// type's size is exercised, then erase the entries one at a time so that
// nodes are released individually rather than as part of a whole tree.
{
ConflictSet cs{0};
std::string key(1, '\0');
for (int i = 0; i < 256; ++i) {
key[0] = char(i);
writePoint(cs, key, i + 1);
}
for (int i = 0; i < 256; ++i) {
key[0] = char(i);
assert(checkPoint(cs, key, i) == ConflictSet::Conflict);
}
// Garbage collection erases entries that can only fail with TooOld anyway.
// Doing it one version at a time walks the tree, erasing, downsizing and
// releasing nodes as it goes.
for (int i = 0; i < 256; ++i) {
cs.setOldestVersion(i + 1);
}
key[0] = '\0';
assert(checkPoint(cs, key, 0) == ConflictSet::TooOld);
}
checkFullyReleased();
} }