Step 572: add performance probe overlay
This commit is contained in:
@@ -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)
|
||||
|
||||
110
editor/src/PerformanceProbeOverlay.h
Normal file
110
editor/src/PerformanceProbeOverlay.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
// Step 572: Performance Probe Overlay
|
||||
|
||||
#include "DebugValidationUtil.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<ProbeAggregate> aggregatesFor(const std::string& sessionId,
|
||||
const std::string& bufferId) const {
|
||||
std::map<int, ProbeAggregate> 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<ProbeAggregate> 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<ProbeAggregate> 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<ProbeAggregate> 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<ProbeAggregate> overlayWindow(const std::string& sessionId,
|
||||
const std::string& bufferId,
|
||||
int pauseLine,
|
||||
int radius) const {
|
||||
std::vector<ProbeAggregate> 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<ProbeSample> 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;
|
||||
}
|
||||
};
|
||||
175
editor/tests/step572_test.cpp
Normal file
175
editor/tests/step572_test.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
// Step 572: Performance Probe Overlay (12 tests)
|
||||
|
||||
#include "PerformanceProbeOverlay.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 {}
|
||||
|
||||
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;
|
||||
}
|
||||
33
progress.md
33
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`
|
||||
|
||||
Reference in New Issue
Block a user