Introduce a Tracy/UE-Insights-style scoped-zone profiler that instruments the
engine's core per-frame systems and reports per-module/per-zone self vs total
time plus GPU frame timing.
- common/Profile.* : core zone collector (eve::prof). EV_PROFILE_* RAII macros
push {name,module,thread,parent,depth,start,end} into per-thread buffers;
frameMark() aggregates the frame into a call tree with self/total per module
and zone, sorted by self time. Cheap single-branch no-op when disabled.
Lives in common/ so any module can instrument without layering violations.
- profiler module (SLOT profiler): script API setEnabled()/beginFrame()/
endFrame()/begin()/end()/capture()/textReport()/frameMs()/gpuFrameMs().
Installs itself as the IRenderTracer so render passes become graphics zones.
- Instrument core hot systems: physics (World/World3D), animation, particles,
audio pump, ui render, scene transforms, map tiles, crowd, event pump.
- GPU timing: Vulkan timestamp queries around the present command buffer
(whole-frame), exposed via common/GpuTimer.h IGpuTimer capability provided by
graphics; read after the synchronous submit waits the in-flight fence.
- load.nut drives frameBegin()/endFrame() + section markers, no-op when absent.
- Tests: C++ unit (core aggregation self<total, render-pass zones) + script API.
Verified: all changed TUs compile against real VKBuilder/ECS/zeroerr headers;
module_depgraph.py --check / --check-layers pass. Full-link build blocked in
this env by a prebuilt third-party Squirrel version mismatch (unrelated).
76 lines
2.3 KiB
C++
76 lines
2.3 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Profile.h"
|
|
#include "profiler/Profiler.h"
|
|
|
|
#include <string>
|
|
|
|
using eve::profiler::Profiler;
|
|
|
|
TEST_CASE("profiler.core.disabled.isNoop") {
|
|
eve::prof::Profiler::reset();
|
|
eve::prof::Profiler::setEnabled(false);
|
|
eve::prof::Profiler::zoneBegin("a");
|
|
eve::prof::Profiler::zoneEnd();
|
|
eve::prof::Profiler::frameMark();
|
|
CHECK_NOT(eve::prof::Profiler::hasFrame());
|
|
eve::prof::Profiler::reset();
|
|
}
|
|
|
|
TEST_CASE("profiler.core.aggregatesSelfAndTotal") {
|
|
eve::prof::Profiler::reset();
|
|
eve::prof::Profiler::setEnabled(true);
|
|
eve::prof::Profiler::zoneBegin("parent", "physics");
|
|
eve::prof::Profiler::zoneBegin("child", "physics");
|
|
eve::prof::Profiler::zoneEnd();
|
|
eve::prof::Profiler::zoneEnd();
|
|
const double frameMs = eve::prof::Profiler::frameMark();
|
|
CHECK_GT(frameMs, 0.0);
|
|
CHECK(eve::prof::Profiler::hasFrame());
|
|
const auto& samples = eve::prof::Profiler::lastFrame();
|
|
bool foundParent = false, foundChild = false;
|
|
for (const auto& s : samples) {
|
|
if (s.name == "parent" && s.module == "physics") {
|
|
foundParent = true;
|
|
// parent self time must be < its total time (a child ran inside it).
|
|
CHECK_LT(s.selfMs, s.totalMs);
|
|
}
|
|
if (s.name == "child") foundChild = true;
|
|
}
|
|
CHECK(foundParent);
|
|
CHECK(foundChild);
|
|
CHECK(eve::prof::Profiler::textReport().find("parent") != std::string::npos);
|
|
eve::prof::Profiler::setEnabled(false);
|
|
eve::prof::Profiler::reset();
|
|
}
|
|
|
|
TEST_CASE("profiler.module.frameCapture") {
|
|
auto* p = Profiler::create();
|
|
p->reset();
|
|
p->setEnabled(true);
|
|
p->beginFrame();
|
|
p->begin("moduleScope");
|
|
p->end();
|
|
p->endFrame();
|
|
CHECK(p->hasFrame());
|
|
CHECK(p->frameMs() >= 0.f);
|
|
CHECK(p->textReport().find("moduleScope") != std::string::npos);
|
|
p->setEnabled(false);
|
|
p->reset();
|
|
}
|
|
|
|
TEST_CASE("profiler.module.renderPassZones") {
|
|
auto* p = Profiler::create();
|
|
p->reset();
|
|
p->setEnabled(true);
|
|
p->beginFrame();
|
|
p->passBegin("ShadowPass"); // IRenderTracer pass -> graphics zone
|
|
p->passEnd("ShadowPass");
|
|
p->endFrame();
|
|
CHECK(p->hasFrame());
|
|
CHECK(p->textReport().find("ShadowPass") != std::string::npos);
|
|
p->setEnabled(false);
|
|
p->reset();
|
|
}
|