Step 564: add memory snapshot model
This commit is contained in:
@@ -3955,4 +3955,13 @@ target_link_libraries(step563_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step564_test tests/step564_test.cpp)
|
||||
target_include_directories(step564_test PRIVATE src)
|
||||
target_link_libraries(step564_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
149
editor/src/MemorySnapshotModel.h
Normal file
149
editor/src/MemorySnapshotModel.h
Normal file
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
// Step 564: Memory Snapshot Model
|
||||
|
||||
#include "DebugValidationUtil.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class MemoryRegionKind {
|
||||
Stack,
|
||||
Heap,
|
||||
Global
|
||||
};
|
||||
|
||||
struct MemoryRegionRecord {
|
||||
std::string id;
|
||||
MemoryRegionKind kind = MemoryRegionKind::Heap;
|
||||
std::uint64_t startAddress = 0;
|
||||
std::uint64_t sizeBytes = 0;
|
||||
bool readOnly = false;
|
||||
};
|
||||
|
||||
struct MemoryReferenceRecord {
|
||||
std::uint64_t fromAddress = 0;
|
||||
std::uint64_t toAddress = 0;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
struct MemorySnapshot {
|
||||
std::string snapshotId;
|
||||
std::string sessionId;
|
||||
std::string threadId;
|
||||
std::uint64_t sequence = 0;
|
||||
std::vector<MemoryRegionRecord> regions;
|
||||
std::vector<MemoryReferenceRecord> references;
|
||||
};
|
||||
|
||||
class MemorySnapshotModel {
|
||||
public:
|
||||
static bool createSnapshot(const std::string& snapshotId,
|
||||
const std::string& sessionId,
|
||||
const std::string& threadId,
|
||||
std::uint64_t sequence,
|
||||
MemorySnapshot* outSnapshot,
|
||||
std::string* error) {
|
||||
if (!outSnapshot || !error) return false;
|
||||
error->clear();
|
||||
if (!hasRequiredDebugIds(snapshotId, sessionId)) {
|
||||
*error = "snapshot_id_or_session_id_missing";
|
||||
return false;
|
||||
}
|
||||
if (threadId.empty()) {
|
||||
*error = "thread_id_missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
MemorySnapshot snap;
|
||||
snap.snapshotId = snapshotId;
|
||||
snap.sessionId = sessionId;
|
||||
snap.threadId = threadId;
|
||||
snap.sequence = sequence;
|
||||
*outSnapshot = snap;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addRegion(MemorySnapshot* snapshot,
|
||||
const MemoryRegionRecord& region,
|
||||
std::string* error) {
|
||||
if (!snapshot || !error) return false;
|
||||
error->clear();
|
||||
if (region.id.empty()) {
|
||||
*error = "region_id_missing";
|
||||
return false;
|
||||
}
|
||||
if (region.startAddress == 0) {
|
||||
*error = "region_start_address_invalid";
|
||||
return false;
|
||||
}
|
||||
if (region.sizeBytes == 0) {
|
||||
*error = "region_size_invalid";
|
||||
return false;
|
||||
}
|
||||
if (findRegionIndexById(*snapshot, region.id) != -1) {
|
||||
*error = "region_id_duplicate";
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot->regions.push_back(region);
|
||||
stableSortRegions(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addReference(MemorySnapshot* snapshot,
|
||||
const MemoryReferenceRecord& reference,
|
||||
std::string* error) {
|
||||
if (!snapshot || !error) return false;
|
||||
error->clear();
|
||||
if (reference.fromAddress == 0 || reference.toAddress == 0) {
|
||||
*error = "reference_address_invalid";
|
||||
return false;
|
||||
}
|
||||
if (reference.label.empty()) {
|
||||
*error = "reference_label_missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot->references.push_back(reference);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const MemoryRegionRecord* findRegionByAddress(const MemorySnapshot& snapshot,
|
||||
std::uint64_t address) {
|
||||
if (address == 0) return nullptr;
|
||||
for (const auto& region : snapshot.regions) {
|
||||
const std::uint64_t regionEnd = region.startAddress + region.sizeBytes;
|
||||
if (address >= region.startAddress && address < regionEnd) return ®ion;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static std::vector<MemoryReferenceRecord> outgoingReferences(const MemorySnapshot& snapshot,
|
||||
std::uint64_t fromAddress) {
|
||||
std::vector<MemoryReferenceRecord> filtered;
|
||||
if (fromAddress == 0) return filtered;
|
||||
for (const auto& reference : snapshot.references) {
|
||||
if (reference.fromAddress == fromAddress) filtered.push_back(reference);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private:
|
||||
static int findRegionIndexById(const MemorySnapshot& snapshot, const std::string& id) {
|
||||
for (size_t i = 0; i < snapshot.regions.size(); ++i) {
|
||||
if (snapshot.regions[i].id == id) return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void stableSortRegions(MemorySnapshot* snapshot) {
|
||||
std::stable_sort(snapshot->regions.begin(),
|
||||
snapshot->regions.end(),
|
||||
[](const MemoryRegionRecord& a, const MemoryRegionRecord& b) {
|
||||
if (a.startAddress != b.startAddress) return a.startAddress < b.startAddress;
|
||||
return a.id < b.id;
|
||||
});
|
||||
}
|
||||
};
|
||||
179
editor/tests/step564_test.cpp
Normal file
179
editor/tests/step564_test.cpp
Normal file
@@ -0,0 +1,179 @@
|
||||
// Step 564: Memory Snapshot Model (12 tests)
|
||||
|
||||
#include "MemorySnapshotModel.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
void test_create_snapshot_success() {
|
||||
TEST(create_snapshot_success);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
const bool ok = MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 7, &snapshot, &error);
|
||||
CHECK(ok, "snapshot creation should succeed");
|
||||
CHECK(snapshot.snapshotId == "snap-1", "snapshot id mismatch");
|
||||
CHECK(snapshot.sessionId == "sess-1", "session id mismatch");
|
||||
CHECK(snapshot.threadId == "thread-1", "thread id mismatch");
|
||||
CHECK(snapshot.sequence == 7, "sequence mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_create_snapshot_fails_with_missing_ids() {
|
||||
TEST(create_snapshot_fails_with_missing_ids);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
const bool ok = MemorySnapshotModel::createSnapshot("", "sess-1", "thread-1", 1, &snapshot, &error);
|
||||
CHECK(!ok, "creation should fail with missing snapshot id");
|
||||
CHECK(error == "snapshot_id_or_session_id_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_create_snapshot_fails_with_missing_thread() {
|
||||
TEST(create_snapshot_fails_with_missing_thread);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
const bool ok = MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "", 1, &snapshot, &error);
|
||||
CHECK(!ok, "creation should fail with missing thread id");
|
||||
CHECK(error == "thread_id_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_region_success() {
|
||||
TEST(add_region_success);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
MemoryRegionRecord stackRegion{"r-stack", MemoryRegionKind::Stack, 0x1000, 64, false};
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, stackRegion, &error), "add region failed");
|
||||
CHECK(snapshot.regions.size() == 1, "region count should be one");
|
||||
CHECK(snapshot.regions[0].id == "r-stack", "region id mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_region_rejects_duplicate_id() {
|
||||
TEST(add_region_rejects_duplicate_id);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
MemoryRegionRecord first{"dup", MemoryRegionKind::Heap, 0x2000, 32, false};
|
||||
MemoryRegionRecord second{"dup", MemoryRegionKind::Heap, 0x3000, 32, false};
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, first, &error), "first add failed");
|
||||
CHECK(!MemorySnapshotModel::addRegion(&snapshot, second, &error), "duplicate id should fail");
|
||||
CHECK(error == "region_id_duplicate", "wrong duplicate error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_region_rejects_invalid_bounds() {
|
||||
TEST(add_region_rejects_invalid_bounds);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
MemoryRegionRecord badStart{"bad-start", MemoryRegionKind::Heap, 0, 32, false};
|
||||
CHECK(!MemorySnapshotModel::addRegion(&snapshot, badStart, &error), "zero start should fail");
|
||||
CHECK(error == "region_start_address_invalid", "wrong start-address error");
|
||||
MemoryRegionRecord badSize{"bad-size", MemoryRegionKind::Heap, 0x2000, 0, false};
|
||||
CHECK(!MemorySnapshotModel::addRegion(&snapshot, badSize, &error), "zero size should fail");
|
||||
CHECK(error == "region_size_invalid", "wrong size error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_regions_are_sorted_by_start_address() {
|
||||
TEST(regions_are_sorted_by_start_address);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r2", MemoryRegionKind::Heap, 0x3000, 16, false}, &error), "add r2 failed");
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r1", MemoryRegionKind::Stack, 0x1000, 16, false}, &error), "add r1 failed");
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"r3", MemoryRegionKind::Global, 0x2000, 16, true}, &error), "add r3 failed");
|
||||
CHECK(snapshot.regions[0].id == "r1", "first region should be r1");
|
||||
CHECK(snapshot.regions[1].id == "r3", "second region should be r3");
|
||||
CHECK(snapshot.regions[2].id == "r2", "third region should be r2");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_find_region_by_address_within_bounds() {
|
||||
TEST(find_region_by_address_within_bounds);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-a", MemoryRegionKind::Heap, 0x5000, 128, false}, &error), "add failed");
|
||||
const MemoryRegionRecord* found = MemorySnapshotModel::findRegionByAddress(snapshot, 0x507F);
|
||||
CHECK(found != nullptr, "region should be found");
|
||||
CHECK(found->id == "heap-a", "wrong region resolved");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_find_region_by_address_at_exclusive_end_returns_null() {
|
||||
TEST(find_region_by_address_at_exclusive_end_returns_null);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
CHECK(MemorySnapshotModel::addRegion(&snapshot, {"heap-a", MemoryRegionKind::Heap, 0x5000, 128, false}, &error), "add failed");
|
||||
const MemoryRegionRecord* found = MemorySnapshotModel::findRegionByAddress(snapshot, 0x5080);
|
||||
CHECK(found == nullptr, "exclusive upper-bound should not match");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_reference_success() {
|
||||
TEST(add_reference_success);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
MemoryReferenceRecord ref{0x4000, 0x7000, "next"};
|
||||
CHECK(MemorySnapshotModel::addReference(&snapshot, ref, &error), "add reference failed");
|
||||
CHECK(snapshot.references.size() == 1, "reference count should be one");
|
||||
CHECK(snapshot.references[0].label == "next", "label mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_add_reference_rejects_invalid_fields() {
|
||||
TEST(add_reference_rejects_invalid_fields);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
CHECK(!MemorySnapshotModel::addReference(&snapshot, {0, 0x7000, "next"}, &error), "zero source should fail");
|
||||
CHECK(error == "reference_address_invalid", "wrong address error");
|
||||
CHECK(!MemorySnapshotModel::addReference(&snapshot, {0x4000, 0x7000, ""}, &error), "empty label should fail");
|
||||
CHECK(error == "reference_label_missing", "wrong label error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_outgoing_references_filter_by_source_address() {
|
||||
TEST(outgoing_references_filter_by_source_address);
|
||||
MemorySnapshot snapshot;
|
||||
std::string error;
|
||||
CHECK(MemorySnapshotModel::createSnapshot("snap-1", "sess-1", "thread-1", 1, &snapshot, &error), "create failed");
|
||||
CHECK(MemorySnapshotModel::addReference(&snapshot, {0x1000, 0x2000, "a"}, &error), "add ref a failed");
|
||||
CHECK(MemorySnapshotModel::addReference(&snapshot, {0x1000, 0x3000, "b"}, &error), "add ref b failed");
|
||||
CHECK(MemorySnapshotModel::addReference(&snapshot, {0x5000, 0x6000, "c"}, &error), "add ref c failed");
|
||||
const auto refs = MemorySnapshotModel::outgoingReferences(snapshot, 0x1000);
|
||||
CHECK(refs.size() == 2, "expected two outgoing refs");
|
||||
CHECK(refs[0].label == "a", "first outgoing label mismatch");
|
||||
CHECK(refs[1].label == "b", "second outgoing label mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 564: Memory Snapshot Model\n";
|
||||
|
||||
test_create_snapshot_success(); // 1
|
||||
test_create_snapshot_fails_with_missing_ids(); // 2
|
||||
test_create_snapshot_fails_with_missing_thread(); // 3
|
||||
test_add_region_success(); // 4
|
||||
test_add_region_rejects_duplicate_id(); // 5
|
||||
test_add_region_rejects_invalid_bounds(); // 6
|
||||
test_regions_are_sorted_by_start_address(); // 7
|
||||
test_find_region_by_address_within_bounds(); // 8
|
||||
test_find_region_by_address_at_exclusive_end_returns_null(); // 9
|
||||
test_add_reference_success(); // 10
|
||||
test_add_reference_rejects_invalid_fields(); // 11
|
||||
test_outgoing_references_filter_by_source_address(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
35
progress.md
35
progress.md
@@ -10155,3 +10155,38 @@ header-only design and deterministic behavior.
|
||||
- `editor/src/BreakpointSystem.h` within header-size limit (`120` <= `600`)
|
||||
- `editor/src/ExceptionStackTraceCapture.h` within header-size limit (`73` <= `600`)
|
||||
- Shared validation centralization reduces duplicated guard logic and keeps module boundaries aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 564: Memory Snapshot Model
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements a structured memory snapshot schema with region/reference capture,
|
||||
validation, and query helpers for stack/heap/global memory reflection.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/MemorySnapshotModel.h` - memory snapshot module:
|
||||
- snapshot/session/thread identity schema
|
||||
- typed memory region records (stack/heap/global)
|
||||
- pointer/reference record schema
|
||||
- region insertion validation + deterministic ordering
|
||||
- address-to-region and outgoing-reference query helpers
|
||||
- `editor/tests/step564_test.cpp` - 12 tests covering:
|
||||
- snapshot identity validation behavior
|
||||
- region add/duplicate/bounds guard behavior
|
||||
- deterministic region ordering behavior
|
||||
- inclusive/exclusive address lookup behavior
|
||||
- reference add/validation behavior
|
||||
- outgoing reference filtering behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` - `step564_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step564_test step563_test` - PASS
|
||||
- `./editor/build-native/step564_test` - PASS (12/12)
|
||||
- `./editor/build-native/step563_test` - PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/MemorySnapshotModel.h` within header-size limit (`149` <= `600`)
|
||||
- `editor/tests/step564_test.cpp` within test-file size guidance (`179` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user