diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 1fc9f84..662df72 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -4027,4 +4027,13 @@ target_link_libraries(step571_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step572_test tests/step572_test.cpp) +target_include_directories(step572_test PRIVATE src) +target_link_libraries(step572_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) diff --git a/editor/src/PerformanceProbeOverlay.h b/editor/src/PerformanceProbeOverlay.h new file mode 100644 index 0000000..7d89116 --- /dev/null +++ b/editor/src/PerformanceProbeOverlay.h @@ -0,0 +1,110 @@ +#pragma once +// Step 572: Performance Probe Overlay + +#include "DebugValidationUtil.h" + +#include +#include +#include +#include +#include + +struct ProbeSample { + std::string sampleId; + std::string sessionId; + std::string bufferId; + int line = 0; + std::uint64_t durationMicros = 0; + std::uint64_t timestamp = 0; +}; + +struct ProbeAggregate { + int line = 0; + std::uint64_t totalDurationMicros = 0; + std::uint64_t sampleCount = 0; +}; + +class PerformanceProbeOverlay { +public: + bool addSample(const ProbeSample& sample, std::string* error) { + if (!error) return false; + error->clear(); + if (!hasRequiredDebugIds(sample.sampleId, sample.sessionId)) return fail(error, "sample_or_session_missing"); + if (sample.bufferId.empty()) return fail(error, "sample_buffer_missing"); + if (!isPositiveLine(sample.line)) return fail(error, "sample_line_invalid"); + if (sample.durationMicros == 0) return fail(error, "sample_duration_invalid"); + if (sample.timestamp == 0) return fail(error, "sample_timestamp_invalid"); + if (containsSample(sample.sampleId)) return fail(error, "sample_duplicate"); + + samples_.push_back(sample); + return true; + } + + std::vector aggregatesFor(const std::string& sessionId, + const std::string& bufferId) const { + std::map grouped; + for (const auto& sample : samples_) { + if (!sessionId.empty() && sample.sessionId != sessionId) continue; + if (!bufferId.empty() && sample.bufferId != bufferId) continue; + auto& entry = grouped[sample.line]; + entry.line = sample.line; + entry.totalDurationMicros += sample.durationMicros; + ++entry.sampleCount; + } + + std::vector out; + for (const auto& kv : grouped) out.push_back(kv.second); + std::stable_sort(out.begin(), out.end(), [](const ProbeAggregate& a, const ProbeAggregate& b) { + return a.line < b.line; + }); + return out; + } + + std::vector topHotspots(const std::string& sessionId, + const std::string& bufferId, + std::size_t limit, + std::uint64_t minTotalDurationMicros) const { + auto aggregates = aggregatesFor(sessionId, bufferId); + std::vector filtered; + for (const auto& aggregate : aggregates) { + if (aggregate.totalDurationMicros >= minTotalDurationMicros) filtered.push_back(aggregate); + } + + std::stable_sort(filtered.begin(), filtered.end(), [](const ProbeAggregate& a, const ProbeAggregate& b) { + if (a.totalDurationMicros != b.totalDurationMicros) return a.totalDurationMicros > b.totalDurationMicros; + return a.line < b.line; + }); + if (filtered.size() > limit) filtered.resize(limit); + return filtered; + } + + std::vector overlayWindow(const std::string& sessionId, + const std::string& bufferId, + int pauseLine, + int radius) const { + std::vector window; + if (!isPositiveLine(pauseLine) || radius < 0) return window; + + const int minLine = pauseLine - radius; + const int maxLine = pauseLine + radius; + for (const auto& aggregate : aggregatesFor(sessionId, bufferId)) { + if (aggregate.line >= minLine && aggregate.line <= maxLine) window.push_back(aggregate); + } + return window; + } + +private: + std::vector samples_; + + static bool fail(std::string* error, const char* code) { + *error = code; + return false; + } + + bool containsSample(const std::string& sampleId) const { + for (const auto& sample : samples_) { + if (sample.sampleId == sampleId) return true; + } + return false; + } +}; diff --git a/editor/tests/step572_test.cpp b/editor/tests/step572_test.cpp new file mode 100644 index 0000000..2942b6c --- /dev/null +++ b/editor/tests/step572_test.cpp @@ -0,0 +1,175 @@ +// Step 572: Performance Probe Overlay (12 tests) + +#include "PerformanceProbeOverlay.h" + +#include + +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 {} + +static ProbeSample sample(const std::string& id, + const std::string& session, + const std::string& buffer, + int line, + std::uint64_t duration, + std::uint64_t ts) { + ProbeSample s; + s.sampleId = id; + s.sessionId = session; + s.bufferId = buffer; + s.line = line; + s.durationMicros = duration; + s.timestamp = ts; + return s; +} + +void test_add_sample_success() { + TEST(add_sample_success); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 500, 1), &error), "sample should add"); + PASS(); +} + +void test_add_sample_rejects_missing_ids() { + TEST(add_sample_rejects_missing_ids); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(!overlay.addSample(sample("", "sess-1", "buf-1", 10, 500, 1), &error), "sample should fail"); + CHECK(error == "sample_or_session_missing", "wrong error"); + PASS(); +} + +void test_add_sample_rejects_invalid_line() { + TEST(add_sample_rejects_invalid_line); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(!overlay.addSample(sample("s1", "sess-1", "buf-1", 0, 500, 1), &error), "sample should fail"); + CHECK(error == "sample_line_invalid", "wrong error"); + PASS(); +} + +void test_add_sample_rejects_invalid_duration() { + TEST(add_sample_rejects_invalid_duration); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(!overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 0, 1), &error), "sample should fail"); + CHECK(error == "sample_duration_invalid", "wrong error"); + PASS(); +} + +void test_add_sample_rejects_duplicate_id() { + TEST(add_sample_rejects_duplicate_id); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 500, 1), &error), "first sample failed"); + CHECK(!overlay.addSample(sample("s1", "sess-1", "buf-1", 11, 400, 2), &error), "duplicate should fail"); + CHECK(error == "sample_duplicate", "wrong error"); + PASS(); +} + +void test_aggregates_group_by_line() { + TEST(aggregates_group_by_line); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 100, 1), &error), "s1 failed"); + CHECK(overlay.addSample(sample("s2", "sess-1", "buf-1", 10, 300, 2), &error), "s2 failed"); + CHECK(overlay.addSample(sample("s3", "sess-1", "buf-1", 20, 500, 3), &error), "s3 failed"); + const auto aggs = overlay.aggregatesFor("sess-1", "buf-1"); + CHECK(aggs.size() == 2, "aggregate count mismatch"); + CHECK(aggs[0].line == 10 && aggs[0].totalDurationMicros == 400, "line 10 aggregate mismatch"); + CHECK(aggs[1].line == 20 && aggs[1].totalDurationMicros == 500, "line 20 aggregate mismatch"); + PASS(); +} + +void test_aggregates_filter_by_session_and_buffer() { + TEST(aggregates_filter_by_session_and_buffer); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 100, 1), &error), "s1 failed"); + CHECK(overlay.addSample(sample("s2", "sess-2", "buf-1", 10, 300, 2), &error), "s2 failed"); + CHECK(overlay.addSample(sample("s3", "sess-1", "buf-2", 20, 500, 3), &error), "s3 failed"); + const auto aggs = overlay.aggregatesFor("sess-1", "buf-1"); + CHECK(aggs.size() == 1, "filtered aggregate count mismatch"); + CHECK(aggs[0].line == 10, "line mismatch"); + PASS(); +} + +void test_top_hotspots_order_by_total_duration() { + TEST(top_hotspots_order_by_total_duration); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 100, 1), &error), "s1 failed"); + CHECK(overlay.addSample(sample("s2", "sess-1", "buf-1", 20, 600, 2), &error), "s2 failed"); + CHECK(overlay.addSample(sample("s3", "sess-1", "buf-1", 30, 300, 3), &error), "s3 failed"); + const auto top = overlay.topHotspots("sess-1", "buf-1", 2, 0); + CHECK(top.size() == 2, "top hotspot count mismatch"); + CHECK(top[0].line == 20, "highest hotspot should be line 20"); + CHECK(top[1].line == 30, "second hotspot should be line 30"); + PASS(); +} + +void test_top_hotspots_applies_threshold() { + TEST(top_hotspots_applies_threshold); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 10, 100, 1), &error), "s1 failed"); + CHECK(overlay.addSample(sample("s2", "sess-1", "buf-1", 20, 600, 2), &error), "s2 failed"); + const auto top = overlay.topHotspots("sess-1", "buf-1", 5, 500); + CHECK(top.size() == 1, "threshold should filter to one hotspot"); + CHECK(top[0].line == 20, "threshold hotspot mismatch"); + PASS(); +} + +void test_overlay_window_returns_pause_neighborhood() { + TEST(overlay_window_returns_pause_neighborhood); + PerformanceProbeOverlay overlay; + std::string error; + CHECK(overlay.addSample(sample("s1", "sess-1", "buf-1", 8, 100, 1), &error), "s1 failed"); + CHECK(overlay.addSample(sample("s2", "sess-1", "buf-1", 10, 200, 2), &error), "s2 failed"); + CHECK(overlay.addSample(sample("s3", "sess-1", "buf-1", 13, 300, 3), &error), "s3 failed"); + const auto window = overlay.overlayWindow("sess-1", "buf-1", 10, 2); + CHECK(window.size() == 2, "window count mismatch"); + CHECK(window[0].line == 8, "window first line mismatch"); + CHECK(window[1].line == 10, "window second line mismatch"); + PASS(); +} + +void test_overlay_window_invalid_pause_line_is_empty() { + TEST(overlay_window_invalid_pause_line_is_empty); + PerformanceProbeOverlay overlay; + const auto window = overlay.overlayWindow("sess-1", "buf-1", 0, 2); + CHECK(window.empty(), "window should be empty"); + PASS(); +} + +void test_overlay_window_negative_radius_is_empty() { + TEST(overlay_window_negative_radius_is_empty); + PerformanceProbeOverlay overlay; + const auto window = overlay.overlayWindow("sess-1", "buf-1", 10, -1); + CHECK(window.empty(), "window should be empty"); + PASS(); +} + +int main() { + std::cout << "Step 572: Performance Probe Overlay\n"; + + test_add_sample_success(); // 1 + test_add_sample_rejects_missing_ids(); // 2 + test_add_sample_rejects_invalid_line(); // 3 + test_add_sample_rejects_invalid_duration(); // 4 + test_add_sample_rejects_duplicate_id(); // 5 + test_aggregates_group_by_line(); // 6 + test_aggregates_filter_by_session_and_buffer(); // 7 + test_top_hotspots_order_by_total_duration(); // 8 + test_top_hotspots_applies_threshold(); // 9 + test_overlay_window_returns_pause_neighborhood(); // 10 + test_overlay_window_invalid_pause_line_is_empty(); // 11 + test_overlay_window_negative_radius_is_empty(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 3a9444e..c1f8c78 100644 --- a/progress.md +++ b/progress.md @@ -10440,3 +10440,36 @@ replay with deterministic eviction and session/window query helpers. - `editor/src/TimeTravelDebugEventBuffer.h` within header-size limit (`86` <= `600`) - `editor/tests/step571_test.cpp` within test-file size guidance (`174` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 572: Performance Probe Overlay +**Status:** PASS (12/12 tests) + +Implements a pause-time performance probe overlay model that aggregates per-line +runtime costs and exposes hotspot-ranked metadata for debug surfaces. + +**Files added:** +- `editor/src/PerformanceProbeOverlay.h` - probe overlay module: + - per-sample validation and duplicate-id guards + - per-line duration/count aggregation by session + buffer + - hotspot ranking with threshold and limit controls + - pause-line neighborhood overlay window extraction +- `editor/tests/step572_test.cpp` - 12 tests covering: + - sample ingestion success/failure behavior + - duplicate sample rejection behavior + - aggregate grouping/filtering behavior + - hotspot ranking/threshold behavior + - pause overlay window boundary behavior + +**Files modified:** +- `editor/CMakeLists.txt` - `step572_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step572_test step571_test` - PASS +- `./editor/build-native/step572_test` - PASS (12/12) +- `./editor/build-native/step571_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/PerformanceProbeOverlay.h` within header-size limit (`110` <= `600`) +- `editor/tests/step572_test.cpp` within test-file size guidance (`175` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`