Files
EVEngine/test/profiler.cpp
T
西风逍遥游 3ac1411d85 feat(profiler): add built-in engine-wide profiler
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).
2026-08-26 00:44:26 +08:00

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();
}