Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ The `RenderBackend` trait abstracts the GPU backend. Both 2D (SpriteBatch) and 3
| `net-tcp` | (empty) |
| `net-webrtc` | (empty) |
| `net-ws` | `tungstenite`, `rustls` |
| `profiling-puffin` | `profiling`, `puffin` |
| `profiling-tracy` | `profiling`, `profiling/profile-with-tracy` |
| `wgpu-backend` | `wgpu`, `winit`, `naga`, `pollster` |
| `web` | `wgpu-backend`, `wasm-bindgen`, `wasm-bindgen-futures`, `web-sys`, `js-sys` |
| `rapier2d` | `rapier2d` |
Expand Down
145 changes: 141 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [Engineering Constitution](development/constitution.md)
- [Governance and Enforcement](development/governance.md)
- [Performance Roadmap](development/performance-roadmap.md)
- [Profiling](development/profiling.md)

# v2 Rebuild Runbook (ENG2)

Expand Down
45 changes: 45 additions & 0 deletions docs/src/development/profiling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Profiling

GoudEngine has optional Tracy and Puffin instrumentation for local engine profiling. It is off by default and is not part of the release build surface unless a profiling feature is enabled.

## Run With Profiler Spans

Start a local engine process with one profiler backend enabled:

```bash
cargo run -p sandbox --features goud_engine/profiling-tracy
cargo run -p sandbox --features goud_engine/profiling-puffin
```

Use one backend feature per profiling run. The CI all-features path prefers Tracy when both feature flags are present, but local captures should choose one backend so the capture is unambiguous.

For Tracy, open the Tracy viewer while the sandbox is running and connect to the process. For Puffin, attach `puffin_viewer` through an app-side `puffin_http` sink, or use an in-app `puffin_egui` view. Puffin scope sites turn capture on, and WGPU `end_frame` advances Puffin frames; headless tools that profile ECS without a rendered frame should advance the app-side Puffin frame sink themselves. The viewer transport stays outside engine core so the engine does not bind a TCP port.

Both backends emit spans for the wgpu frame path and ECS schedule execution, including:

- `wgpu.begin_frame`
- `wgpu.end_frame`
- `wgpu.uniform_upload`
- `wgpu.shadow_pass`
- `wgpu.render_pass`
- `wgpu.gpu_submit`
- `ecs.run_system`
- `ecs.system_stage`
- `ecs.system`
- `ecs.parallel_stage`
- `ecs.parallel_system`

The feature is intentionally separate from the fixed frame phase counters. Use Tracy to investigate local call paths and flamegraphs; use the ENG2 phase counters and `scripts/perf-capture` reports for gate evidence.

For compile-only validation, run both feature paths:

```bash
cargo check -p goud-engine-core --features profiling-tracy
cargo check -p sandbox --features goud_engine/profiling-tracy
cargo check -p goud-engine-core --features profiling-puffin
cargo check -p sandbox --features goud_engine/profiling-puffin
```

## Default Build Cost

Without a profiling feature, the span sites are removed at compile time and the default feature graph does not activate the Tracy backend (`tracy-client` or `tracy-client-sys`) or the Puffin backend (`puffin`). The lockfile can still contain the generic `profiling` crate because existing transitive dependencies use it.
4 changes: 4 additions & 0 deletions goud_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ net-udp = []
net-tcp = []
net-webrtc = []
net-ws = ["dep:tungstenite", "dep:rustls"]
profiling-puffin = ["dep:profiling", "dep:puffin"]
profiling-tracy = ["dep:profiling", "profiling/profile-with-tracy"]
wgpu-backend = ["dep:wgpu", "dep:winit", "dep:naga", "dep:pollster"]
web = ["wgpu-backend", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:web-sys", "dep:js-sys"]
rapier2d = ["dep:rapier2d"]
Expand Down Expand Up @@ -71,6 +73,8 @@ wgpu = { version = "29", optional = true }
winit = { version = "0.30", optional = true, features = ["android-native-activity"] }
naga = { version = "29", optional = true, features = ["glsl-in", "wgsl-out"] }
pollster = { version = "0.4", optional = true }
profiling = { version = "1.0.18", optional = true, default-features = false }
puffin = { version = "0.20", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
wasm-bindgen-futures = { version = "0.4", optional = true }
js-sys = { version = "0.3", optional = true }
Expand Down
17 changes: 17 additions & 0 deletions goud_engine/src/ecs/schedule/parallel/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use super::ParallelSystemStage;
impl ParallelSystemStage {
/// Runs all systems using parallel execution.
pub fn run_parallel(&mut self, world: &mut World) {
crate::libs::profiling::profile_scope!(ECS_PARALLEL_STAGE, self.name.as_str());

let profiler_route = debugger::current_route().filter(debugger::profiler_enabled_for_route);

if (self.dirty || self.batches.is_empty()) && self.config.auto_rebuild {
Expand Down Expand Up @@ -52,6 +54,11 @@ impl ParallelSystemStage {
continue;
}
if runnable.len() == 1 {
crate::libs::profiling::profile_scope!(
ECS_PARALLEL_SYSTEM,
self.systems[runnable[0]].name()
);

if let Some(route_id) = profiler_route.as_ref() {
let system_name = self.systems[runnable[0]].name();
let started_at = Instant::now();
Expand Down Expand Up @@ -87,6 +94,11 @@ impl ParallelSystemStage {
let profiler_route = profiler_route.clone();
let stage_name = stage_name.clone();
s.spawn(move |_| {
crate::libs::profiling::profile_scope!(
ECS_PARALLEL_SYSTEM,
system_name
);

let started_at = Instant::now();
// SAFETY: Each system accesses disjoint data.
unsafe {
Expand All @@ -109,6 +121,11 @@ impl ParallelSystemStage {
#[cfg(not(feature = "native"))]
{
for &idx in &runnable {
crate::libs::profiling::profile_scope!(
ECS_PARALLEL_SYSTEM,
self.systems[idx].name()
);

if let Some(route_id) = profiler_route.as_ref() {
let system_name = self.systems[idx].name();
let started_at = Instant::now();
Expand Down
8 changes: 8 additions & 0 deletions goud_engine/src/ecs/schedule/system_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,13 @@ impl SystemStage {

/// Runs a single system by ID on the given world.
pub fn run_system(&mut self, id: SystemId, world: &mut World) -> Option<bool> {
crate::libs::profiling::profile_scope!(ECS_RUN_SYSTEM);

if let Some(&index) = self.system_indices.get(&id) {
let system = &mut self.systems[index];
if system.should_run(world) {
crate::libs::profiling::profile_scope!(ECS_SYSTEM, system.name());

system.run(world);
Some(true)
} else {
Expand Down Expand Up @@ -371,6 +375,8 @@ impl Stage for SystemStage {
}

fn run(&mut self, world: &mut World) {
crate::libs::profiling::profile_scope!(ECS_SYSTEM_STAGE, self.name.as_str());

let profiler_route = debugger::current_route().filter(debugger::profiler_enabled_for_route);

if self.order_dirty {
Expand All @@ -396,6 +402,8 @@ impl Stage for SystemStage {
}
for system in &mut self.systems {
if system.should_run(world) {
crate::libs::profiling::profile_scope!(ECS_SYSTEM, system.name());

if let Some(route_id) = profiler_route.as_ref() {
let started_at = Instant::now();
system.run(world);
Expand Down
Loading
Loading