Skip to content

Profile build: timelines, simulation trace summary, instrumentation - #594

Merged
freezy merged 8 commits into
masterfrom
feat/profile-build
Oct 17, 2026
Merged

freezy merged 8 commits into
masterfrom
feat/profile-build

Conversation

@freezy

@freezy freezy commented Oct 17, 2026

Copy link
Copy Markdown
Owner

The engine side of the profile build: a version of the player that finds and explains performance problems (stutters, frame pacing, physics stalls, CPU/GPU limits, slow start-up and table loads) on testers' machines and reports them to Sentry. Plan: PROFILE_BUILD_PLAN.md.

For every build

  • Start-up timeline: from the process' creation to the menu's first frame, with wall and CPU time per step. It produces one Startup: log line and a Startup diagnostics section.
  • Table load timeline: the package reader's steps and the load phases, plus the texture cache result and the load kind. Playable is the first frame after the gamelogic engine started. Settled is the first 3 s window where no frame took over 1.5× its median. The load ends as settled, timeout, failed or cancelled, and logs a line for each.
  • SimulationTrace:
    • Players keep a 60 s summary instead of 10 minutes of CSVs. The summary is refreshed every minute on a worker, saved next to the session log, and attached to reports.
    • The rings are per session and are read through a per-slot seqlock.
    • The editor toggle and --simulation-trace still write the CSVs.
  • Fixes:
    • The rendered snapshot no longer steps back to an older one.
    • Microsecond timestamps no longer overflow after days of uptime.

For the profile build only (VPE_PROFILE)

  • VpeProfiler: an API with conditional methods, so its calls compile out of normal builds. A test checks that, and checks the IL.
  • Recorders: per-frame PlayerLoop phase stamps, sections, Unity frame timings matched to frames, and the simulation ticks and lock waits.
  • Detectors: stutter, hitch, pacing, simulation stall and slowdown, each with the most likely cause.
  • Traces: Perfetto traces of the worst incidents (opens in ui.perfetto.dev), metrics windows, and self-tests (--vpe-test-perf=<mode>|all).

Testing

  • 102 EditMode tests pass in the HDRP test project, including new seqlock, timeline and gating tests.
  • Both flavors compile.
  • A profile build of the HDRP player ran a table with all self-tests. Traces, logs and events went to Sentry.

…trace.

Changes for all builds, from the profile build plan (phase 1):

- Start-up timeline: from the process' creation (so the time before the
  first managed code shows) over Unity's init stages and the host's
  steps to the first frame after the menu is ready. One log line and a
  Startup diagnostics section.
- Table load timeline: the reader's steps and the load phases as
  milestones with wall and CPU time, texture cache result and load kind.
  Playable is the first frame after the table loaded and its gamelogic
  engine said it started; settled is the first 3 s window without a frame
  over 1.5x its median. The load ends as settled, timeout, failed or
  cancelled, with a log line each and a Finished event.
- SimulationTrace: a summary mode for players, with a 60 s ring instead of
  10 minutes and no CSVs, summarized every minute on a worker. The rings
  belong to a session and are read with a per-slot seqlock, so a summary
  never reads a record being written or freed rings. A new run supersedes
  the cached summary and its file. The summary goes next to the session
  log, for reports. The editor toggle and --simulation-trace keep the
  10-minute CSVs.
- The rendered snapshot no longer steps back to an older one when nothing
  new was published.
- Microsecond timestamps no longer overflow after days of uptime.
VpeProfiler exists in every build, but its methods are conditional on
VPE_PROFILE, which only the profile build defines, so their calls and
arguments compile out of the normal player. A test checks the API's
methods, that profiling code stays inside the define, that nothing sends
performance data to Sentry outside of it, and that the engine's IL has
no calls to the conditional methods. The build info says which flavor a
build is.
…file build.

All in VPE_PROFILE code, so normal builds are unchanged:

- Recorders: a stamp at the start of each top-level PlayerLoop system,
  the VPE sections per frame (and calls of 0.5 ms or more), Unity's frame
  timings matched to the frames they belong to (Unity's timer is the
  platform's monotonic clock, Mono's stopwatch starts with the process),
  events like focus, display changes and first uses, and the simulation's
  ticks and lock waits through the trace's seqlock. Two minutes each.
- Detectors on frames once their timings are in: stutters, hitches,
  pacing, simulation stalls and slowdowns, each with its most likely
  cause (physics lock, GC, a VPE section, render/GPU or the present,
  scheduling, simulation overload, first use, environment).
- A Perfetto trace of the seconds around the worst incidents (3 per
  table, and F9), copied on the main thread and written on a worker into
  a reused buffer, into logs/perf.
- Ten-second metrics windows, play statistics at the end of a table,
  log lines per incident, and self tests (--vpe-test-perf=<mode>|all)
  that make each problem in a real build.
- Section and first-use call sites in the player, lamps, balls and DMD.

Also, for all builds: start-up ends at a table launched from the command
line, and the RAM at playable is read without Process, which says 0 on
Mono.
@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The changes since the previous review appear safe to merge.

Summary

Adds startup and table-load timelines, saved simulation summaries, and profile-only frame recording, incident detection, metrics, and Perfetto traces.

  • Startup and table loads now show where their time goes.
  • Player runs keep a rolling summary of recent simulation timing.
  • Profile builds record performance events and explain likely causes.
  • Rendered snapshots stay current, and long-running timestamps keep their value.
  • Player source lines are normalized without changing game behavior.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Startup and table-load milestones] --> B[Logs and diagnostics]
  C[Simulation ticks and frames] --> D[Per-session rings]
  D --> E[Saved simulation summary]
  F[Profile-only frame and section recording] --> G[Incident detection]
  D --> G
  G --> H[Metrics and incident reports]
  G --> I[Perfetto trace capture]
Loading

Reviews (5) · Last reviewed commit: "diagnostics: Run the system lock probe o..." · Reviewed by Greptile

Comment thread VisualPinball.Unity/VisualPinball.Unity/Game/Player.cs Outdated
Comment thread VisualPinball.Unity/VisualPinball.Unity/Diagnostics/Profiling/IncidentDetector.cs Outdated
…on quit.

- The play ends at the table's unload (and when the player goes): the
  frames still waiting for their timing are handed on, nothing after
  counts as play, and a trace they asked for is written.
- When the application quits, the simulation's last summary is made on
  the main thread instead of a worker the exit may kill.
- A callback that throws no longer leaves its section open.
- A frame's incidents count in the metrics window that frame closes.
- In a profile build, --no-simulation-trace keeps the summary but not the
  CSVs; the mode test knows the profile build's modes.
… profile build.

The incident detector read Screen.currentResolution for every frame to know the vsync interval. That call runs QueryDisplayConfig, which blocks for about 21 ms while another process holds Windows' window manager (for example while a script starts a console program). The profile build then stalled 2-3 frames in a row on its own, after the frame's end stamp, so the time showed up as unexplained main-thread time outside the PlayerLoop.

The recorder now keeps the refresh rate it polls and reads it again only when the resolution, fullscreen mode or display count changes, or every 10 s.
…file build.

While a process creates a console window, Windows' window manager is locked for about 85 ms in four slices. Our display queries, Present and input polling wait for it, and the player hitches for 50-80 ms. Until now the profile build reported that as "unknown" or "render-gpu".

SystemLockProbe times GetDisplayConfigBufferSizes on a background thread every 10 ms, using a high-resolution waitable timer. It follows a stall every millisecond and merges its slices into one record, so one burst becomes one record of about 90 ms. Samples a garbage collection ran into are dropped. Incidents that overlap a recorded stall by 5 ms or more get the new cause "system-lock" and the locked time. A display mode or focus change keeps "environment" as the primary cause, since the player locks the window manager itself then. The time also appears in the log line, on a trace track of its own and in the trace metadata. --vpe-test-perf=syslock starts a few hidden consoles to check the path end to end.
Comment thread VisualPinball.Unity/VisualPinball.Unity/Diagnostics/Profiling/SystemLockProbe.cs Outdated
A Windows build target in a Mac or Linux editor compiled the probe in, and its
thread would have died on the missing DLLs. Also checks the OS before starting
and logs instead of letting an exception end the process.
@freezy
freezy merged commit 5a24481 into master Oct 17, 2026
15 checks passed
@freezy
freezy deleted the feat/profile-build branch October 17, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant