diff --git a/core/src/main/cpp/src/indexdetails.cpp b/core/src/main/cpp/src/indexdetails.cpp index fed13bb..f8bca8f 100644 --- a/core/src/main/cpp/src/indexdetails.cpp +++ b/core/src/main/cpp/src/indexdetails.cpp @@ -66,7 +66,8 @@ void IndexDetails::initializeDurableStore(const std::string& data_dir, b try { // Create the durable runtime which owns all persistence components // read_only mode: skip WAL replay for fast serverless reader startup - runtime_ = persist::DurableRuntime::open(paths, policy, false, read_only); + // Pass field_name for per-index memory tracking + runtime_ = persist::DurableRuntime::open(paths, policy, false, read_only, field_name_); // Create the durable store context (must outlive DurableStore) durable_context_ = std::make_unique(persist::DurableContext{ diff --git a/core/src/main/cpp/src/indexdetails.hpp b/core/src/main/cpp/src/indexdetails.hpp index 47902e1..98b1c0c 100644 --- a/core/src/main/cpp/src/indexdetails.hpp +++ b/core/src/main/cpp/src/indexdetails.hpp @@ -36,6 +36,7 @@ #include #include #include +#include namespace xtree { @@ -598,6 +599,148 @@ namespace xtree { return field_name_; } + // ========== Per-Index Memory Statistics ========== + + // Comprehensive memory stats for this index + struct IndexMemoryStats { + std::string field_name; + + // MMap memory (from MappingManager) + size_t mmap_bytes = 0; + size_t mmap_extents = 0; + size_t mmap_pins = 0; + + // Cache memory (from ShardedLRUCache) + size_t cache_bytes = 0; + size_t cache_entries = 0; + + // Segment allocator stats + size_t segment_live_bytes = 0; + size_t segment_dead_bytes = 0; + double segment_fragmentation_pct = 0.0; + + // Total memory for this index + size_t total_bytes() const { + return mmap_bytes + cache_bytes; + } + }; + + // Get memory stats for this specific index + IndexMemoryStats getMemoryStats() const { + IndexMemoryStats stats; + stats.field_name = field_name_; + + // Get per-field mmap stats from MappingManager + auto mmap_stats = persist::MappingManager::global().getPerFieldStats(); + auto it = mmap_stats.find(field_name_); + if (it != mmap_stats.end()) { + stats.mmap_bytes = it->second.mmap_bytes; + stats.mmap_extents = it->second.extent_count; + stats.mmap_pins = it->second.pin_count; + } + + // Get per-field cache stats from ShardedLRUCache + auto cache_stats = getCache().getPerFieldMemory(); + auto cache_it = cache_stats.find(field_name_); + if (cache_it != cache_stats.end()) { + stats.cache_bytes = cache_it->second; + } + + // Get segment stats if we have a durable runtime + if (runtime_) { + auto seg_stats = runtime_->allocator().get_total_stats(); + stats.segment_live_bytes = seg_stats.live_bytes; + stats.segment_dead_bytes = seg_stats.dead_bytes; + stats.segment_fragmentation_pct = seg_stats.fragmentation() * 100.0; + } + + return stats; + } + + // Get memory stats for all indexes + static std::vector getAllIndexStats() { + std::vector all_stats; + all_stats.reserve(indexes.size()); + + for (auto* idx : indexes) { + if (idx) { + all_stats.push_back(idx->getMemoryStats()); + } + } + + return all_stats; + } + + // Print per-index memory breakdown (for debugging/monitoring) + static void printMemoryBreakdown() { + std::cout << "\n=== Per-Index Memory Breakdown ===" << std::endl; + + // Get raw per-field mmap stats directly from MappingManager + auto raw_mmap_stats = persist::MappingManager::global().getPerFieldStats(); + + std::cout << "| Field | MMap (MB) | Cache (MB) | Segments (MB) | Total (MB) |" << std::endl; + std::cout << "|-------|-----------|------------|---------------|------------|" << std::endl; + + auto all_stats = getAllIndexStats(); + size_t total_mmap = 0, total_cache = 0, total_seg = 0; + size_t fields_with_zero = 0; + size_t fields_with_memory = 0; + + for (const auto& stats : all_stats) { + // Look up mmap stats directly from raw_mmap_stats for this field + size_t mmap_bytes = 0; + auto it = raw_mmap_stats.find(stats.field_name); + if (it != raw_mmap_stats.end()) { + mmap_bytes = it->second.mmap_bytes; + } + + size_t mmap_mb = mmap_bytes / (1024 * 1024); + size_t cache_mb = stats.cache_bytes / (1024 * 1024); + size_t seg_mb = stats.segment_live_bytes / (1024 * 1024); + size_t total_mb = (mmap_bytes + stats.cache_bytes) / (1024 * 1024); + + // Only show fields with non-zero memory (in MB) to reduce noise + if (mmap_mb > 0 || cache_mb > 0 || seg_mb > 0) { + std::cout << "| " << stats.field_name + << " | " << mmap_mb + << " | " << cache_mb + << " | " << seg_mb + << " | " << total_mb + << " |" << std::endl; + fields_with_memory++; + } else { + fields_with_zero++; + } + + total_mmap += mmap_bytes; + total_cache += stats.cache_bytes; + total_seg += stats.segment_live_bytes; + } + + // Check for unregistered memory + auto unreg_it = raw_mmap_stats.find("_unregistered_"); + if (unreg_it != raw_mmap_stats.end() && unreg_it->second.mmap_bytes > 0) { + std::cout << "| _unregistered_ | " + << (unreg_it->second.mmap_bytes / (1024 * 1024)) + << " | 0 | 0 | " + << (unreg_it->second.mmap_bytes / (1024 * 1024)) + << " |" << std::endl; + total_mmap += unreg_it->second.mmap_bytes; + } + + std::cout << "|-------|-----------|------------|---------------|------------|" << std::endl; + std::cout << "| Total | " + << (total_mmap / (1024 * 1024)) + << " | " << (total_cache / (1024 * 1024)) + << " | " << (total_seg / (1024 * 1024)) + << " | " << ((total_mmap + total_cache) / (1024 * 1024)) + << " |" << std::endl; + + if (fields_with_zero > 0) { + std::cout << "(" << fields_with_zero << " fields with evicted/zero memory not shown)" << std::endl; + } + } + // IRecord* getCachedNode( UniqueId recordAddress ) { return NULL; } // COW management methods diff --git a/core/src/main/cpp/src/lru_sharded.h b/core/src/main/cpp/src/lru_sharded.h index 3a6cffa..b67f861 100644 --- a/core/src/main/cpp/src/lru_sharded.h +++ b/core/src/main/cpp/src/lru_sharded.h @@ -132,7 +132,7 @@ class ShardedLRUCache { // use-after-free when parent nodes are temporarily unpinned. Node* add(const Id& id, T* object) { // Default: cache owns the object and will delete it on eviction/clear - return add(id, object, true); + return add(id, object, true, ""); } // O(1) add with explicit ownership control @@ -140,14 +140,22 @@ class ShardedLRUCache { // owns_object=false: cache does NOT own object, won't delete (mmap'd memory) // NOTE: Does NOT automatically evict - see add() comment for rationale Node* add(const Id& id, T* object, bool owns_object) { + return add(id, object, owns_object, ""); + } + + // O(1) add with ownership control and optional field_name for per-index tracking + // field_name: Optional field/index name for per-field memory attribution + Node* add(const Id& id, T* object, bool owns_object, const std::string& field_name) { size_t shardIdx = getShardIndex(id); auto& shard = *_shards[shardIdx]; Node* node = shard.add(id, object, owns_object); if (node) { + size_t objSize = 0; + // Track memory usage (only if budget is enabled) if (_maxMemory.load(std::memory_order_relaxed) > 0) { - size_t objSize = _memorySizer(object); + objSize = _memorySizer(object); _currentMemory.fetch_add(objSize, std::memory_order_relaxed); } @@ -157,6 +165,14 @@ class ShardedLRUCache { _globalObjMap[object] = shardIdx; } + // Track per-field memory if field_name is specified + if (!field_name.empty()) { + if (objSize == 0) objSize = _memorySizer(object); // Calculate if not already done + std::lock_guard lock(_fieldMemoryMtx); + _fieldMemory[field_name] += objSize; + _objToField[object] = field_name; + } + // NOTE: Do NOT evict here - it's unsafe during tree traversal // Caller should call evictToMemoryBudget() explicitly at safe points } @@ -169,15 +185,22 @@ class ShardedLRUCache { // If id doesn't exist, creates new node with objIfAbsent, pins it, and returns it // NOTE: Does NOT automatically evict - see add() comment for rationale AcquireResult acquirePinned(const Id& id, T* objIfAbsent) { + return acquirePinned(id, objIfAbsent, ""); + } + + // O(1) atomic get-or-create with field_name for per-index tracking + AcquireResult acquirePinned(const Id& id, T* objIfAbsent, const std::string& field_name) { size_t shardIdx = getShardIndex(id); auto& shard = *_shards[shardIdx]; AcquireResult result = shard.acquirePinned(id, objIfAbsent); // Track memory and global map if newly created if (result.created && result.node) { + size_t objSize = 0; + // Track memory usage (only if budget is enabled) if (_maxMemory.load(std::memory_order_relaxed) > 0) { - size_t objSize = _memorySizer(result.node->object); + objSize = _memorySizer(result.node->object); _currentMemory.fetch_add(objSize, std::memory_order_relaxed); } @@ -185,6 +208,14 @@ class ShardedLRUCache { std::lock_guard lock(_globalMapMtx); _globalObjMap[result.node->object] = shardIdx; } + + // Track per-field memory if field_name is specified + if (!field_name.empty()) { + if (objSize == 0) objSize = _memorySizer(result.node->object); + std::lock_guard lock(_fieldMemoryMtx); + _fieldMemory[field_name] += objSize; + _objToField[result.node->object] = field_name; + } } return result; @@ -237,9 +268,11 @@ class ShardedLRUCache { T* object = shard.removeById(id); if (object) { + size_t objSize = 0; + // Decrement memory usage (only if budget is enabled) if (_maxMemory.load(std::memory_order_relaxed) > 0) { - size_t objSize = _memorySizer(object); + objSize = _memorySizer(object); _currentMemory.fetch_sub(objSize, std::memory_order_relaxed); } @@ -248,6 +281,24 @@ class ShardedLRUCache { std::lock_guard lock(_globalMapMtx); _globalObjMap.erase(object); } + + // Decrement per-field memory if tracked + { + std::lock_guard lock(_fieldMemoryMtx); + auto it = _objToField.find(object); + if (it != _objToField.end()) { + if (objSize == 0) objSize = _memorySizer(object); + auto field_it = _fieldMemory.find(it->second); + if (field_it != _fieldMemory.end()) { + if (field_it->second >= objSize) { + field_it->second -= objSize; + } else { + field_it->second = 0; + } + } + _objToField.erase(it); + } + } } return object; // Transfer ownership to caller @@ -260,6 +311,24 @@ class ShardedLRUCache { const bool trackMemory = _maxMemory.load(std::memory_order_relaxed) > 0; size_t objSize = trackMemory ? _memorySizer(object) : 0; + // Helper lambda to decrement per-field memory + auto decrementFieldMemory = [this, object, &objSize]() { + std::lock_guard lock(_fieldMemoryMtx); + auto it = _objToField.find(object); + if (it != _objToField.end()) { + if (objSize == 0) objSize = _memorySizer(object); + auto field_it = _fieldMemory.find(it->second); + if (field_it != _fieldMemory.end()) { + if (field_it->second >= objSize) { + field_it->second -= objSize; + } else { + field_it->second = 0; + } + } + _objToField.erase(it); + } + }; + if (_useGlobalObjMap) { // 1) Read shard index under the global map lock size_t shardIdx; @@ -280,6 +349,9 @@ class ShardedLRUCache { } std::lock_guard g(_globalMapMtx); _globalObjMap.erase(object); + + // Decrement per-field memory + decrementFieldMemory(); } return removed; } else { @@ -289,6 +361,8 @@ class ShardedLRUCache { if (trackMemory) { _currentMemory.fetch_sub(objSize, std::memory_order_relaxed); } + // Decrement per-field memory + decrementFieldMemory(); return true; } } @@ -316,8 +390,9 @@ class ShardedLRUCache { if (evicted) { // Decrement memory usage (only if budget is enabled) if (evicted->object) { + size_t objSize = 0; if (trackMemory) { - size_t objSize = _memorySizer(evicted->object); + objSize = _memorySizer(evicted->object); _currentMemory.fetch_sub(objSize, std::memory_order_relaxed); } @@ -326,6 +401,24 @@ class ShardedLRUCache { std::lock_guard lock(_globalMapMtx); _globalObjMap.erase(evicted->object); } + + // Decrement per-field memory if tracked + { + std::lock_guard lock(_fieldMemoryMtx); + auto it = _objToField.find(evicted->object); + if (it != _objToField.end()) { + if (objSize == 0) objSize = _memorySizer(evicted->object); + auto field_it = _fieldMemory.find(it->second); + if (field_it != _fieldMemory.end()) { + if (field_it->second >= objSize) { + field_it->second -= objSize; + } else { + field_it->second = 0; + } + } + _objToField.erase(it); + } + } } return evicted; } @@ -402,6 +495,13 @@ class ShardedLRUCache { std::lock_guard lock(_globalMapMtx); _globalObjMap.clear(); } + + // Reset per-field memory tracking + { + std::lock_guard lock(_fieldMemoryMtx); + _fieldMemory.clear(); + _objToField.clear(); + } } // Stats for monitoring @@ -431,6 +531,13 @@ class ShardedLRUCache { return stats; } + // Get per-field cache memory breakdown + // Returns map of field_name -> memory bytes used by that field's cache entries + std::unordered_map getPerFieldMemory() const { + std::lock_guard lock(_fieldMemoryMtx); + return _fieldMemory; // Return a copy + } + // Detailed stats showing type breakdown and pin counts struct DetailedStats { size_t dataRecords = 0; @@ -519,6 +626,11 @@ class ShardedLRUCache { std::unordered_map _globalObjMap; mutable std::mutex _globalMapMtx; + // Per-field memory tracking for multi-index memory attribution + std::unordered_map _fieldMemory; // field_name -> bytes + std::unordered_map _objToField; // object -> field_name + mutable std::mutex _fieldMemoryMtx; + // Get shard index for an ID size_t getShardIndex(const Id& id) const { size_t hash = std::hash{}(id); diff --git a/core/src/main/cpp/src/persistence/durable_runtime.cpp b/core/src/main/cpp/src/persistence/durable_runtime.cpp index 8599627..51980ca 100644 --- a/core/src/main/cpp/src/persistence/durable_runtime.cpp +++ b/core/src/main/cpp/src/persistence/durable_runtime.cpp @@ -34,8 +34,9 @@ namespace xtree { std::unique_ptr DurableRuntime::open(const Paths& paths, const CheckpointPolicy& policy, - bool use_payload_recovery, bool read_only) { - auto rt = std::unique_ptr(new DurableRuntime(paths, policy, read_only)); + bool use_payload_recovery, bool read_only, + const std::string& field_name) { + auto rt = std::unique_ptr(new DurableRuntime(paths, policy, read_only, field_name)); // Recovery: build OT from latest checkpoint + optionally replay deltas // Create recovery helper @@ -82,14 +83,15 @@ namespace xtree { return rt; } - DurableRuntime::DurableRuntime(Paths p, CheckpointPolicy pol, bool read_only) - : paths_(std::move(p)), policy_(pol), read_only_(read_only) { + DurableRuntime::DurableRuntime(Paths p, CheckpointPolicy pol, bool read_only, + const std::string& field_name) + : paths_(std::move(p)), policy_(pol), read_only_(read_only), field_name_(field_name) { manifest_ = std::make_unique(paths_.data_dir); mvcc_ = std::make_unique(); // Use sharded ObjectTable for concurrent allocation // Starts with 1 active shard, grows as needed ot_sharded_ = std::make_unique(100000, ObjectTableSharded::DEFAULT_NUM_SHARDS); - alloc_ = std::make_unique(paths_.data_dir); + alloc_ = std::make_unique(paths_.data_dir, field_name_); // Set read-only mode on allocator for serverless readers if (read_only_) { diff --git a/core/src/main/cpp/src/persistence/durable_runtime.h b/core/src/main/cpp/src/persistence/durable_runtime.h index e1cb012..7da795f 100644 --- a/core/src/main/cpp/src/persistence/durable_runtime.h +++ b/core/src/main/cpp/src/persistence/durable_runtime.h @@ -49,10 +49,12 @@ namespace xtree { // Open a durable runtime // read_only: If true, skip WAL replay and open in checkpoint-only mode // for fast serverless startup. Writes are blocked. + // field_name: Optional field name for per-index memory tracking static std::unique_ptr open(const Paths& paths, const CheckpointPolicy& policy, bool use_payload_recovery = false, - bool read_only = false); + bool read_only = false, + const std::string& field_name = ""); ~DurableRuntime(); // stops coordinator @@ -76,7 +78,8 @@ namespace xtree { bool is_catalog_dirty() const { return catalog_dirty_.load(std::memory_order_acquire); } private: - DurableRuntime(Paths, CheckpointPolicy, bool read_only = false); + DurableRuntime(Paths, CheckpointPolicy, bool read_only = false, + const std::string& field_name = ""); void start(); void stop(); @@ -84,6 +87,7 @@ namespace xtree { Paths paths_; CheckpointPolicy policy_; bool read_only_ = false; // True = checkpoint-only mode, no WAL replay + std::string field_name_; // Field name for per-index memory tracking std::unique_ptr manifest_; std::unique_ptr mvcc_; diff --git a/core/src/main/cpp/src/persistence/mapping_manager.cpp b/core/src/main/cpp/src/persistence/mapping_manager.cpp index a495a43..c63265f 100644 --- a/core/src/main/cpp/src/persistence/mapping_manager.cpp +++ b/core/src/main/cpp/src/persistence/mapping_manager.cpp @@ -603,5 +603,54 @@ std::unique_ptr MappingManager::create_extent( return std::make_unique(static_cast(addr), len, file_off); } +void MappingManager::register_file_for_field(const std::string& path, + const std::string& field_name) { + // Canonicalize the path first + std::string cpath = fhr_.canonicalize_path(path); + + std::lock_guard lock(field_map_mutex_); + path_to_field_[cpath] = field_name; +} + +void MappingManager::unregister_file(const std::string& path) { + std::string cpath = fhr_.canonicalize_path(path); + + std::lock_guard lock(field_map_mutex_); + path_to_field_.erase(cpath); +} + +std::unordered_map +MappingManager::getPerFieldStats() const { + std::unordered_map result; + + // Lock both mutexes - field_map first, then main mutex + std::lock_guard field_lock(field_map_mutex_); + std::lock_guard map_lock(mu_); + + // Iterate over all file mappings and aggregate by field + for (const auto& [path, fmap] : by_file_) { + if (!fmap) continue; + + // Look up field name for this path + auto field_it = path_to_field_.find(path); + std::string field_name = (field_it != path_to_field_.end()) + ? field_it->second + : "_unregistered_"; // Default for files not registered to a field + + FieldMemoryStats& stats = result[field_name]; + + // Sum up extents for this file + for (const auto& ext : fmap->extents) { + if (ext) { + stats.mmap_bytes += ext->length; + stats.pin_count += ext->pins; + stats.extent_count++; + } + } + } + + return result; +} + } // namespace persist } // namespace xtree \ No newline at end of file diff --git a/core/src/main/cpp/src/persistence/mapping_manager.h b/core/src/main/cpp/src/persistence/mapping_manager.h index e1b15d2..03d0a89 100644 --- a/core/src/main/cpp/src/persistence/mapping_manager.h +++ b/core/src/main/cpp/src/persistence/mapping_manager.h @@ -74,6 +74,23 @@ class MappingManager { // Global singleton accessor - lazy initialization, thread-safe static MappingManager& global(); + // Per-field memory statistics + struct FieldMemoryStats { + size_t mmap_bytes = 0; // Total bytes mapped for this field + size_t pin_count = 0; // Total pins active for this field + size_t extent_count = 0; // Number of extents for this field + }; + + // Register a file as belonging to a specific field/index + // Thread-safe, can be called multiple times (idempotent) + void register_file_for_field(const std::string& path, const std::string& field_name); + + // Unregister a file when it's no longer needed + void unregister_file(const std::string& path); + + // Get memory breakdown by field + std::unordered_map getPerFieldStats() const; + // Pin is a RAII handle for mapped memory class Pin { public: @@ -214,10 +231,14 @@ class MappingManager { std::vector> find_eviction_candidates(size_t count); // Create a new mmap window - std::unique_ptr create_extent(const FileHandle& fh, - size_t file_off, - size_t len, + std::unique_ptr create_extent(const FileHandle& fh, + size_t file_off, + size_t len, bool writable); + + // Per-field tracking (path -> field_name) + mutable std::mutex field_map_mutex_; + std::unordered_map path_to_field_; }; } // namespace persist diff --git a/core/src/main/cpp/src/persistence/segment_allocator.cpp b/core/src/main/cpp/src/persistence/segment_allocator.cpp index 46eab09..e7ed951 100644 --- a/core/src/main/cpp/src/persistence/segment_allocator.cpp +++ b/core/src/main/cpp/src/persistence/segment_allocator.cpp @@ -48,8 +48,9 @@ namespace xtree { std::atomic g_segment_lock_count{0}; #endif - SegmentAllocator::SegmentAllocator(const std::string& data_dir) - : data_dir_(data_dir), config_(StorageConfig::defaults()) { + SegmentAllocator::SegmentAllocator(const std::string& data_dir, + const std::string& field_name) + : data_dir_(data_dir), field_name_(field_name), config_(StorageConfig::defaults()) { // Use global registries if configured (default), otherwise create owned ones if (config_.use_global_registries) { file_registry_ = &FileHandleRegistry::global(); @@ -81,8 +82,9 @@ namespace xtree { "Minimum size class must satisfy max alignment requirements"); // New constructor with explicit configuration - SegmentAllocator::SegmentAllocator(const std::string& data_dir, const StorageConfig& config) - : data_dir_(data_dir), config_(config) { + SegmentAllocator::SegmentAllocator(const std::string& data_dir, const StorageConfig& config, + const std::string& field_name) + : data_dir_(data_dir), field_name_(field_name), config_(config) { if (!config_.validate()) { throw std::invalid_argument("Invalid storage configuration"); } @@ -113,8 +115,9 @@ namespace xtree { // Constructor that takes registries (for DurableStore) SegmentAllocator::SegmentAllocator(const std::string& data_dir, FileHandleRegistry& fhr, - MappingManager& mm) - : data_dir_(data_dir), file_registry_(&fhr), mapping_manager_(&mm), + MappingManager& mm, + const std::string& field_name) + : data_dir_(data_dir), field_name_(field_name), file_registry_(&fhr), mapping_manager_(&mm), config_(StorageConfig::defaults()) { // Ensure data directory exists FSResult dir_result = PlatformFS::ensure_directory(data_dir); @@ -127,8 +130,9 @@ namespace xtree { SegmentAllocator::SegmentAllocator(const std::string& data_dir, FileHandleRegistry& fhr, MappingManager& mm, - const StorageConfig& config) - : data_dir_(data_dir), file_registry_(&fhr), mapping_manager_(&mm), + const StorageConfig& config, + const std::string& field_name) + : data_dir_(data_dir), field_name_(field_name), file_registry_(&fhr), mapping_manager_(&mm), config_(config) { if (!config_.validate()) { throw std::invalid_argument("Invalid storage configuration"); @@ -553,7 +557,12 @@ namespace xtree { // Store stable virtual address seg->base_vaddr = seg->pin.get(); - + + // Register this file with the field for per-index memory tracking + if (!field_name_.empty()) { + mapping_manager_->register_file_for_field(data_path, field_name_); + } + } catch (const std::exception& e) { // Failed to map segment - this is critical allocator.bytes_in_current_file -= aligned_segment_size; @@ -856,6 +865,11 @@ namespace xtree { seg->bm.resize(bm_words, ~0ull); // All blocks initially free seg->free_count = seg->blocks; seg->max_allocated = 0; // Will be updated during WAL replay + + // Register this file with the field for per-index memory tracking + if (!field_name_.empty()) { + mapping_manager_->register_file_for_field(file_path, field_name_); + } } catch (const std::exception& e) { // Failed to map segment - file doesn't exist return nullptr; diff --git a/core/src/main/cpp/src/persistence/segment_allocator.h b/core/src/main/cpp/src/persistence/segment_allocator.h index fd6f79c..f821d66 100644 --- a/core/src/main/cpp/src/persistence/segment_allocator.h +++ b/core/src/main/cpp/src/persistence/segment_allocator.h @@ -96,19 +96,23 @@ namespace xtree { // NEW: Constructor that takes the registries for windowed mmap SegmentAllocator(const std::string& data_dir, FileHandleRegistry& fhr, - MappingManager& mm); - + MappingManager& mm, + const std::string& field_name = ""); + // Constructor with explicit configuration SegmentAllocator(const std::string& data_dir, FileHandleRegistry& fhr, MappingManager& mm, - const StorageConfig& config); - + const StorageConfig& config, + const std::string& field_name = ""); + // Legacy constructor for backward compatibility (creates internal registries) - explicit SegmentAllocator(const std::string& data_dir); - + explicit SegmentAllocator(const std::string& data_dir, + const std::string& field_name = ""); + // Constructor with config (creates internal registries) - SegmentAllocator(const std::string& data_dir, const StorageConfig& config); + SegmentAllocator(const std::string& data_dir, const StorageConfig& config, + const std::string& field_name = ""); // Destructor - ensures all pins are released before destruction ~SegmentAllocator(); @@ -144,6 +148,9 @@ namespace xtree { size_t get_segment_size() const { return DEFAULT_SEGMENT_SIZE; } MappingManager& get_mapping_manager() { return *mapping_manager_; } + // Get the field name this allocator is associated with + const std::string& get_field_name() const { return field_name_; } + struct Stats { size_t live_bytes = 0; size_t dead_bytes = 0; @@ -336,6 +343,7 @@ namespace xtree { }; std::string data_dir_; + std::string field_name_; // Field/index name for per-field memory tracking ClassAllocator allocators_[NUM_CLASSES]; std::atomic next_segment_id_{0}; std::atomic global_file_seq_{0}; // Only used when !kFilePerSizeClass diff --git a/core/src/main/cpp/src/xtree.h b/core/src/main/cpp/src/xtree.h index 3591b90..e9a86a3 100644 --- a/core/src/main/cpp/src/xtree.h +++ b/core/src/main/cpp/src/xtree.h @@ -397,10 +397,14 @@ namespace xtree { // If a durable child is already set, do not touch flags or key/aliasing. // Durable attach is final for the key/NodeID; use clearDurableChild() first if you need to replace. if (_node_id.valid() && _owns_key && _recordKey) { + std::cerr << "[setRecord] Early return due to durable state: kn=" << (void*)this + << " nid=" << _node_id.raw() << std::endl << std::flush; return; } if (!record || !record->object) { + std::cerr << "[setRecord] Null record/object, clearing cache: kn=" << (void*)this + << " record=" << (void*)record << std::endl << std::flush; _cache_ptr = nullptr; return; // durable path must have set _recordKey/_node_id already } @@ -1505,6 +1509,11 @@ namespace xtree { child->setDataRecord(false); child->setLeaf(bucket->isLeaf()); // only meaningful for bucket children + // CRITICAL FIX: Wire the bucket's _parent pointer to this KN immediately. + // This ensures that after splits/sorts, bucket->_parent always points to + // the correct KN in the parent's _children array. + bucket->setParent(child); + if (durable) { // Bucket must have NodeID so it can be loaded when cache is cold persist::NodeID bucketId = bucket->getNodeID(); @@ -1694,6 +1703,34 @@ namespace xtree { } else { // Deep copy the key to avoid aliasing a transient pointer child->setChildFromKeyCopy(*key, src.isDataRecord(), src.getLeaf()); + +#ifndef NDEBUG + // In IN_MEMORY mode, bucket children should ALWAYS have cache records. + // If we're here for a bucket child, something went wrong earlier (likely + // propagateMBRUpdate clearing the cache via setKeyOwned). + if (!src.isDataRecord()) { + assert(false && "Bucket child has null cache record in IN_MEMORY mode - " + "check if propagateMBRUpdate preserves cache pointers"); + } +#endif + } + + // CRITICAL FIX: In IN_MEMORY mode, when adopting a bucket child via kn_from_entry, + // the bucket's _parent might be stale (pointing to an old KN that no longer + // represents the correct parent relationship after sorting or previous splits). + // We UNCONDITIONALLY update _parent to point to the new KN (child) to ensure + // the bucket can find its correct parent bucket via _parent->_owner. + if (!src.isDataRecord()) { + // Get the bucket from either source KN's cache record or child's cache record + auto* src_cn = const_cast<_MBRKeyNode&>(src).getCacheRecord(); + auto* child_cn = child->getCacheRecord(); + CacheNode* cn = child_cn ? child_cn : src_cn; + if (cn && cn->object) { + auto* bucket = static_cast*>(cn->object); + bucket->setParent(child); + } + // Note: if cn is null, we can't update _parent. The fix for this is to ensure + // setKeyOwned() in propagateMBRUpdate preserves cache pointers. } #ifndef NDEBUG @@ -2418,7 +2455,13 @@ namespace xtree { // CRITICAL: Use setKeyOwned to maintain MBR ownership for bucket children. // Using setKey() would alias the child's _key and set _owns_key=false, // causing use-after-free when the child bucket is evicted. + // ALSO CRITICAL: Preserve the cache pointer - setKeyOwned clears it, but + // in IN_MEMORY mode we need the cache pointer for parent rewiring during splits. + auto* saved_cache = cur->_parent->getCacheRecord(); cur->_parent->setKeyOwned(new KeyMBR(*cur->_key)); + if (saved_cache) { + cur->_parent->setCacheAlias(saved_cache); + } // If nothing changed here, no need to climb further if (!curChanged) { @@ -2434,11 +2477,13 @@ namespace xtree { // Sanity checks to catch wiring bugs early if (!parentBucket) { assert(false && "kn->_owner is null; owner must be set when wiring children"); - } else { - // The parent should actually reference this kn + } else if (_idx && _idx->hasDurableStore()) { + // Wiring validation is only reliable in DURABLE mode where structural + // changes are tracked. In IN_MEMORY mode, the _children vector may + // contain pre-allocated but unused KNs that don't match _n. bool found = false; - for (auto* kn : parentBucket->_children) { - if (kn == cur->_parent) { found = true; break; } + for (unsigned i = 0; i < parentBucket->_n; ++i) { + if (parentBucket->_children[i] == cur->_parent) { found = true; break; } } assert(found && "parent->_children does not contain cur->_parent (wiring mismatch)"); } diff --git a/core/src/main/cpp/src/xtree.hpp b/core/src/main/cpp/src/xtree.hpp index dcc68d8..1862857 100644 --- a/core/src/main/cpp/src/xtree.hpp +++ b/core/src/main/cpp/src/xtree.hpp @@ -58,6 +58,12 @@ namespace xtree { // thisCacheNode must always correspond to subTree, not the original root CacheNode* currentCacheNode = thisCacheNode; + // OPTIMIZATION: Check if we can trust _cache_ptr directly + // When eviction is disabled (getMaxMemory() == 0), pointers stored in _MBRKeyNode + // cannot become stale because no entries are ever evicted from the cache. + // This allows us to skip the O(1) cache lookup at each level of descent. + const bool can_trust_cache_ptrs = (this->_idx->getCache().getMaxMemory() == 0); + // traverse to a leaf level while(!subTree->isLeaf()) { subTree = subTree->chooseSubtree(cachedRecord); @@ -68,6 +74,18 @@ namespace xtree { // CRITICAL FIX: Update currentCacheNode to track the cache node for subTree // subTree->_parent is the _MBRKeyNode in the parent bucket that references subTree. // Its getCacheRecord() returns the cache node for subTree (set by setCacheAlias during load). + + // FAST PATH: When eviction disabled, trust _cache_ptr directly + // This eliminates O(depth) cache lookups per insertion when memory budget is unlimited. + if (can_trust_cache_ptrs && subTree->_parent) { + auto* parentKN = subTree->_parent; + if (parentKN->getCacheRecord()) { + currentCacheNode = parentKN->getCacheRecord(); + continue; // Skip the slow cache lookup + } + } + + // SLOW PATH: With eviction enabled, validate via cache lookup // SAFETY: When eviction is possible, getCacheRecord() may return dangling pointer. // Always use find() to get a validated cache node. if (subTree->_parent && subTree->hasNodeID()) { @@ -117,6 +135,17 @@ namespace xtree { assert(this->_idx && "insertHere requires valid index context"); #endif + // OPTIMIZATION: Persist data record ONCE before any bucket logic + // This unifies the persistence path that was previously duplicated in + // basicInsert() and split(). persist_data_record() is idempotent via + // hasNodeID() check, so safe to call unconditionally here. + if (cachedRecord && cachedRecord->object && cachedRecord->object->isDataNode()) { + using Alloc = XAlloc; + if constexpr (Alloc::template has_wire_methods::value) { + Alloc::persist_data_record(this->_idx, static_cast(cachedRecord->object)); + } + } + // Try to insert locally first if (this->basicInsert(cachedRecord)) { // May publish & relocate this bucket @@ -166,7 +195,10 @@ namespace xtree { kn->setDurableBucketChild(stable_mbr, current_bucket->getNodeID(), current_bucket->_leaf); if (thisCacheNode) kn->setCacheAlias(thisCacheNode); #ifndef NDEBUG - assert(kn->getNodeID() == current_bucket->getNodeID()); + // Verify KN→NodeID parity (only in DURABLE mode) + if (this->_idx->hasDurableStore()) { + assert(kn->getNodeID() == current_bucket->getNodeID()); + } #endif } else if (!current_bucket->_parent && current_bucket->_idx) { // Root case: update root identity using canonical cache key @@ -190,8 +222,8 @@ namespace xtree { #endif #ifndef NDEBUG - // Sibling NodeID dup detection - if (parent_after) { + // Sibling NodeID dup detection (only in DURABLE mode) + if (this->_idx->hasDurableStore() && parent_after) { unsigned dups = 0; for (unsigned i = 0; i < parent_after->_n; ++i) { auto* pkn = parent_after->_kn(i); @@ -202,7 +234,10 @@ namespace xtree { } assert(dups == 1 && "Sibling NodeID collision detected under parent"); } - assert(!current_bucket->_parent || current_bucket->_parent->getNodeID() == current_bucket->getNodeID()); + // Verify KN→NodeID parity (only in DURABLE mode) + if (this->_idx->hasDurableStore()) { + assert(!current_bucket->_parent || current_bucket->_parent->getNodeID() == current_bucket->getNodeID()); + } #endif } @@ -248,18 +283,9 @@ namespace xtree { // otherwise just insert the record (can be a DataRecord or another XTreeBucket (internal node) - // For DURABLE mode: persist DataRecords to .xd files before inserting - // This ensures they have a NodeID that can be stored in the leaf bucket - // Only do this for types that support persistence (have wire_size, to_wire, etc.) - if (cachedRecord && cachedRecord->object && cachedRecord->object->isDataNode()) { - using Alloc = XAlloc; - // Extra safety: compile-time check for wire methods - if constexpr (Alloc::template has_wire_methods::value) { - Alloc::persist_data_record(this->_idx, static_cast(cachedRecord->object)); - } - // SFINAE already makes persist_data_record a no-op for types without wire methods, - // but if constexpr provides an extra layer of safety and clarity - } + // NOTE: Data record persistence is now handled in insertHere() before calling basicInsert(). + // This unifies the persistence path and ensures records are persisted exactly once, + // regardless of whether the bucket has room (basicInsert succeeds) or needs split. #ifndef NDEBUG // Debug assertions to catch invalid cachedRecord before kn() call @@ -584,16 +610,10 @@ namespace xtree { } #endif - // CRITICAL FIX: Ensure durable DataRecords get a NodeID before we wire them in - // Without this, split path inserts have invalid NodeIDs (0) and disappear after recovery - if (insertingCN && insertingCN->object && insertingCN->object->isDataNode()) { - using Alloc = XAlloc; - // Only persist for types that support persistence (have wire methods) - if constexpr (Alloc::template has_wire_methods::value) { - Alloc::persist_data_record(this->_idx, static_cast(insertingCN->object)); - } - } - + // NOTE: Data record persistence is now handled in insertHere() before calling split(). + // The record already has a valid NodeID by the time we reach this point. + // This unifies persistence in one place and eliminates the duplicated code path. + // add the new key to this bucket (so it will be included when we split, or when // we turn into a supernode) #ifndef NDEBUG @@ -1185,11 +1205,14 @@ namespace xtree { // Step 4: Insert both children into the new root #ifndef NDEBUG - // Sanity check: Both children must have valid NodeIDs before insertion - assert(this->hasNodeID() && this->getNodeID().valid() && - "Left child (original root) must have valid NodeID during splitRoot"); - assert(splitBucket->hasNodeID() && splitBucket->getNodeID().valid() && - "Right child (split bucket) must have valid NodeID during splitRoot"); + // Sanity check: In DURABLE mode, both children must have valid NodeIDs before insertion + // In IN_MEMORY mode, NodeIDs are not used (buckets are identified by pointer) + if (this->_idx->hasDurableStore()) { + assert(this->hasNodeID() && this->getNodeID().valid() && + "Left child (original root) must have valid NodeID during splitRoot"); + assert(splitBucket->hasNodeID() && splitBucket->getNodeID().valid() && + "Right child (split bucket) must have valid NodeID during splitRoot"); + } // Both children (original root and split bucket) must agree on leaf-ness // They can be leaf buckets (first root split) or internal buckets (cascade split) @@ -1233,10 +1256,19 @@ namespace xtree { << std::hex << rootBucket->_key->debug_area_value() << std::dec << std::endl; } - // Use a stable copy of the MBR to avoid aliasing/UAF - KeyMBR stable_mbr = *this->_key; - left_kn->setDurableBucketChild(stable_mbr, this->getNodeID(), this->_leaf); - left_kn->setCacheAlias(thisCacheNode); + // Initialize left child KN based on persistence mode + const bool is_durable = this->_idx && this->_idx->hasDurableStore(); + if (is_durable) { + // DURABLE mode: use setDurableBucketChild which copies MBR and stores NodeID + KeyMBR stable_mbr = *this->_key; + left_kn->setDurableBucketChild(stable_mbr, this->getNodeID(), this->_leaf); + left_kn->setCacheAlias(thisCacheNode); + } else { + // IN_MEMORY mode: use setRecord to alias the cache node, then set type flags + left_kn->setRecord(thisCacheNode); + left_kn->setDataRecord(false); // This is a bucket, not data + left_kn->setLeaf(this->_leaf); + } left_kn->_owner = rootBucket; this->setParent(left_kn); @@ -1253,8 +1285,17 @@ namespace xtree { << std::hex << rootBucket->_key->debug_area_value() << std::dec << " valid=" << rootBucket->_key->debug_check_area() << std::endl; - right_kn->setDurableBucketChild(*splitBucket->_key, splitBucket->getNodeID(), splitBucket->_leaf); - right_kn->setCacheAlias(cachedSplitBucket); + // Initialize right child KN based on persistence mode + if (is_durable) { + // DURABLE mode: use setDurableBucketChild which copies MBR and stores NodeID + right_kn->setDurableBucketChild(*splitBucket->_key, splitBucket->getNodeID(), splitBucket->_leaf); + right_kn->setCacheAlias(cachedSplitBucket); + } else { + // IN_MEMORY mode: use setRecord to alias the cache node, then set type flags + right_kn->setRecord(cachedSplitBucket); + right_kn->setDataRecord(false); // This is a bucket, not data + right_kn->setLeaf(splitBucket->_leaf); + } right_kn->_owner = rootBucket; splitBucket->setParent(right_kn); @@ -1275,9 +1316,11 @@ namespace xtree { assert(this->_parent == left_kn && "Left bucket parent must be left_kn"); assert(splitBucket->_parent == right_kn && "Right bucket parent must be right_kn"); - // Verify KN→NodeID parity - assert(left_kn->hasNodeID() && left_kn->getNodeID() == this->getNodeID()); - assert(right_kn->hasNodeID() && right_kn->getNodeID() == splitBucket->getNodeID()); + // Verify KN→NodeID parity (only in DURABLE mode where NodeIDs are used) + if (this->_idx->hasDurableStore()) { + assert(left_kn->hasNodeID() && left_kn->getNodeID() == this->getNodeID()); + assert(right_kn->hasNodeID() && right_kn->getNodeID() == splitBucket->getNodeID()); + } #endif // Debug: Check rootBucket->_key state before recalculateMBR @@ -1397,28 +1440,44 @@ namespace xtree { splitBucket->markDirty(); // Step 3: Wire right sibling into parent (structural, no insertHere) - // Use NodeID-based matching instead of pointer-based (post-reallocation safe) + // Use NodeID-based matching in DURABLE mode, or cache record matching in IN_MEMORY mode int left_idx = -1; + const bool use_nodeid_matching = this->_idx->hasDurableStore(); + for (unsigned i = 0; i < parent->_n; ++i) { auto* kn = parent->_kn(i); if (!kn || kn->isDataRecord()) continue; - if (kn->hasNodeID()) { - if (kn->getNodeID() == this->getNodeID()) { + + if (use_nodeid_matching) { + // DURABLE mode: match by NodeID + if (kn->hasNodeID() && kn->getNodeID() == this->getNodeID()) { + left_idx = static_cast(i); + break; + } + } else { + // IN_MEMORY mode: match by cache record pointer or parent pointer + if (kn == this->_parent) { + left_idx = static_cast(i); + break; + } + // Fallback: match by cache record pointing to this bucket + auto* cn = kn->getCacheRecord(); + if (cn && cn->object == reinterpret_cast(this)) { left_idx = static_cast(i); break; } - } else if (this->_parent && !kn->hasNodeID() && kn == this->_parent) { // secondary fallback for mixed/staged states - left_idx = static_cast(i); - break; } } + assert(left_idx >= 0 && "Left child's KN not found in parent"); #ifndef NDEBUG - // Assert uniqueness: no other child should have the same NodeID - for (unsigned j = left_idx + 1; j < parent->_n; ++j) { - assert(!(parent->_kn(j)->hasNodeID() && parent->_kn(j)->getNodeID() == this->getNodeID()) && - "duplicate child NodeID under same parent"); + // Assert uniqueness: no other child should have the same NodeID (only in DURABLE mode) + if (use_nodeid_matching) { + for (unsigned j = left_idx + 1; j < parent->_n; ++j) { + assert(!(parent->_kn(j)->hasNodeID() && parent->_kn(j)->getNodeID() == this->getNodeID()) && + "duplicate child NodeID under same parent"); + } } #endif @@ -1477,25 +1536,28 @@ namespace xtree { #endif #ifndef NDEBUG - // Detect NodeID collision between left child and parent (allocator bug) - if (parent_after && curLeft->getNodeID() == parent_after->getNodeID()) { - trace() << "[ID_COLLISION] left child NodeID matches parent after split publish: " - << curLeft->getNodeID().raw() << "\n"; - assert(false && "allocator/id-publish must never collide with parent NodeID"); - } + // Detect NodeID collision (only in DURABLE mode where NodeIDs are meaningful) + if (this->_idx->hasDurableStore()) { + // Detect NodeID collision between left child and parent (allocator bug) + if (parent_after && curLeft->getNodeID() == parent_after->getNodeID()) { + trace() << "[ID_COLLISION] left child NodeID matches parent after split publish: " + << curLeft->getNodeID().raw() << "\n"; + assert(false && "allocator/id-publish must never collide with parent NodeID"); + } - // Detect NodeID collision between right child and parent (allocator bug) - if (parent_after && curRight->getNodeID() == parent_after->getNodeID()) { - trace() << "[ID_COLLISION] right child NodeID matches parent after split publish: " - << curRight->getNodeID().raw() << "\n"; - assert(false && "allocator/id-publish must never collide with parent NodeID"); - } + // Detect NodeID collision between right child and parent (allocator bug) + if (parent_after && curRight->getNodeID() == parent_after->getNodeID()) { + trace() << "[ID_COLLISION] right child NodeID matches parent after split publish: " + << curRight->getNodeID().raw() << "\n"; + assert(false && "allocator/id-publish must never collide with parent NodeID"); + } - // Detect NodeID collision between siblings (allocator bug) - if (curLeft->getNodeID() == curRight->getNodeID()) { - trace() << "[ID_COLLISION] left and right siblings have identical NodeIDs: " - << curLeft->getNodeID().raw() << "\n"; - assert(false && "allocator/id-publish must assign unique NodeIDs to siblings"); + // Detect NodeID collision between siblings (allocator bug) + if (curLeft->getNodeID() == curRight->getNodeID()) { + trace() << "[ID_COLLISION] left and right siblings have identical NodeIDs: " + << curLeft->getNodeID().raw() << "\n"; + assert(false && "allocator/id-publish must assign unique NodeIDs to siblings"); + } } #endif @@ -1523,7 +1585,10 @@ namespace xtree { #endif #ifndef NDEBUG - assert(left_kn->getNodeID() == curLeft->getNodeID()); + // Verify NodeID parity (only in DURABLE mode) + if (this->_idx->hasDurableStore()) { + assert(left_kn->getNodeID() == curLeft->getNodeID()); + } #endif } @@ -1549,11 +1614,23 @@ namespace xtree { parent->kn(cachedSplitBucket); // Find the KN that was just created for the sibling + // In DURABLE mode, match by NodeID. In IN_MEMORY mode, match by cache pointer. + // (use_nodeid_matching already defined above) _MBRKeyNode* right_kn = nullptr; for (unsigned i = 0; i < parent->_n; ++i) { auto* kn = parent->_kn(i); - if (kn && !kn->isDataRecord() && kn->hasNodeID()) { - if (kn->getNodeID() == curRight->getNodeID()) { + if (!kn || kn->isDataRecord()) continue; + + if (use_nodeid_matching) { + // DURABLE mode: match by NodeID + if (kn->hasNodeID() && kn->getNodeID() == curRight->getNodeID()) { + right_kn = kn; + break; + } + } else { + // IN_MEMORY mode: match by cache record pointer + auto* cn = kn->getCacheRecord(); + if (cn && cn->object == reinterpret_cast(curRight)) { right_kn = kn; break; } @@ -1566,7 +1643,9 @@ namespace xtree { #ifndef NDEBUG // Verify wiring - assert(right_kn->getNodeID() == curRight->getNodeID()); + if (this->_idx->hasDurableStore()) { + assert(right_kn->getNodeID() == curRight->getNodeID()); + } assert(curRight->_parent == right_kn); // Verify no self-alias corruption @@ -1577,15 +1656,17 @@ namespace xtree { } } - // Sibling NodeID uniqueness check - unsigned dups_right = 0; - for (unsigned i = 0; i < parent->_n; ++i) { - auto* pkn = parent->_kn(i); - if (pkn && !pkn->isDataRecord() && pkn->hasNodeID()) { - if (pkn->getNodeID() == curRight->getNodeID()) ++dups_right; + // Sibling NodeID uniqueness check (only in DURABLE mode) + if (this->_idx->hasDurableStore()) { + unsigned dups_right = 0; + for (unsigned i = 0; i < parent->_n; ++i) { + auto* pkn = parent->_kn(i); + if (pkn && !pkn->isDataRecord() && pkn->hasNodeID()) { + if (pkn->getNodeID() == curRight->getNodeID()) ++dups_right; + } } + assert(dups_right == 1 && "Right sibling NodeID collision detected under parent"); } - assert(dups_right == 1 && "Right sibling NodeID collision detected under parent"); // Parent is internal assert(!parent->_leaf); diff --git a/core/src/main/cpp/test/test_performance.cpp b/core/src/main/cpp/test/test_performance.cpp index 385d543..310b8e7 100644 --- a/core/src/main/cpp/test/test_performance.cpp +++ b/core/src/main/cpp/test/test_performance.cpp @@ -179,11 +179,13 @@ TEST_F(XTreePerformanceTest, BulkInsertions) { << " (" << (NUM_POINTS * 1000.0 / duration.count()) << " inserts/second)" << endl; EXPECT_GT(root->n(), 0); - - // Clear the static cache to prevent interference with subsequent tests - // For performance tests, we can just clear without deleting since the process will exit - IndexDetails::clearCache(); - + + // NOTE: We do NOT call clearCache() before delete idx because: + // - clearCache() deletes cached objects (including the root bucket) + // - Then ~IndexDetails() tries to unpin the freed root → use-after-free + // The cache is global and shared across tests; each test is responsible for + // cleaning up its own index via delete, not clearing the shared cache. + delete idx; delete dimLabels; @@ -256,11 +258,11 @@ TEST_F(XTreePerformanceTest, SpatialQueries) { for (auto query : queries) { delete query; } - - // Clear the static cache to prevent interference with subsequent tests - // For performance tests, we can just clear without deleting since the process will exit - IndexDetails::clearCache(); - + + // NOTE: We do NOT call clearCache() before delete idx because: + // - clearCache() deletes cached objects (including the root bucket) + // - Then ~IndexDetails() tries to unpin the freed root → use-after-free + delete idx; delete dimLabels; diff --git a/core/src/main/cpp/test/test_xtree.cpp b/core/src/main/cpp/test/test_xtree.cpp index d5d11ad..dfaf1ab 100644 --- a/core/src/main/cpp/test/test_xtree.cpp +++ b/core/src/main/cpp/test/test_xtree.cpp @@ -141,11 +141,11 @@ TEST_F(XTreeBucketTest, MockBucketInsertion) { // Verify insertion EXPECT_EQ(root->n(), 1); - - // Clean up: cache manages the nodes now - // Just clean up the mock objects themselves + + // Clean up: mockKey is NOT managed by cache (we own it) + // But mock is in the cache - cache will delete it in TearDown via clearCache() delete mockKey; - delete mock; + // NOTE: Do NOT delete mock - it's owned by the cache now } TEST_F(XTreeBucketTest, MockMultipleInsertions) { @@ -190,14 +190,12 @@ TEST_F(XTreeBucketTest, MockMultipleInsertions) { EXPECT_EQ(root->n(), NUM_RECORDS); - // Clean up - // Cache manages the nodes - no need to delete them + // Clean up: mockKeys are NOT managed by cache (we own them) + // But mocks are in the cache - cache will delete them in TearDown via clearCache() for (auto* mockKey : mockKeys) { delete mockKey; } - for (auto* mock : mocks) { - delete mock; - } + // NOTE: Do NOT delete mocks - they're owned by the cache now } TEST_F(XTreeBucketTest, MockInsertionWithSplitScenario) { @@ -242,13 +240,11 @@ TEST_F(XTreeBucketTest, MockInsertionWithSplitScenario) { // After many insertions, the tree structure should have grown EXPECT_GE(root->n(), 1); // Root should have at least one child EXPECT_GT(root->memoryUsage(), sizeof(XTreeBucket)); - - // Clean up - // Cache manages the nodes - no need to delete them + + // Clean up: mockKeys are NOT managed by cache (we own them) + // But mocks are in the cache - cache will delete them in TearDown via clearCache() for (auto* mockKey : mockKeys) { delete mockKey; } - for (auto* mock : mocks) { - delete mock; - } + // NOTE: Do NOT delete mocks - they're owned by the cache now } \ No newline at end of file diff --git a/core/src/main/cpp/test/test_xtree_durability_stress.cpp b/core/src/main/cpp/test/test_xtree_durability_stress.cpp index c447b1a..ac2d73d 100644 --- a/core/src/main/cpp/test/test_xtree_durability_stress.cpp +++ b/core/src/main/cpp/test/test_xtree_durability_stress.cpp @@ -1318,6 +1318,9 @@ TEST_F(XTreeDurabilityStressTest, ServerlessFieldScaling) { } } + // Print per-field memory breakdown + IndexDetails::printMemoryBreakdown(); + // Final stats size_t final_rss = getMemoryUsage(); auto final_mmap = persist::MappingManager::global().getStats();