Repository navigation
Logging, diagnostics and performance tracking for error reporting - #590
Merged
Merged
Conversation
Logging is configured at SubsystemRegistration, so the first scene's Awake no longer logs into the void. All targets except the editor console sit behind a queue of our own: the logging thread only enqueues, everything else (rendering, file I/O) happens on a writer thread. NLog's AsyncTargetWrapper renders events with exceptions on the logging thread, which the simulation thread can't afford. - VpeLog: install once, begin a session per play mode/player start (new session id, cleared tail, reset throttles), host settings via Configure() (log folder, level), extra targets via AddTarget(), -vpe-log-level override. - Session log file per session, newest 10 kept, flushed per batch so a native crash loses at most 50 ms. - LogTail: the last 2000 lines in memory, for error reports. - LogThrottle / LogOncePerKey for hot paths. - UnityLogBridge forwards Debug.Log from Unity and third-party code into NLog. - CrashHooks log unhandled and unobserved exceptions. - UnityTarget renders exceptions as text instead of Debug.LogException, which would turn every logged exception into an unhandled one. - The simulation thread catches what kills it, logs it after the no-GC region ends, and SimulationThreadComponent raises SimulationFaulted.
DiagnosticsContext holds what a developer needs to reproduce a problem, in named sections that are written when something changes and logged at the same time: - System: OS, CPU with logical and physical cores, RAM, GPU with driver version and VRAM, displays, Unity version, render pipeline, scripting backend, and the command line with secrets removed. - Build: app version, the commit of every VPE repository, and the version of every VPE package. - Table: a LoadAttempt is created before a table is read. It tracks the load phase, package format and writer, metadata and a cheap fingerprint, and it keeps describing the table after a failed load. - Gamelogic: the engine, ROM and game id, plus whatever the engine reports through the new IDiagnosticsSource. Player.Start logs the gamelogic engine's start and duration. An exception from OnInit, which previously escaped the async void, is now logged as an error and fails the load attempt. Redaction removes the home folder and user name from paths. DiagnosticsWriter never writes the value of a setting whose name suggests a secret.
Every runtime UnityEngine.Debug.Log call now goes through NLog, and every log call follows the level policy in VpeLog: - Problems a table author or user can cause (unmapped coils, switches, wires and lamps, missing players, missing sounds, unknown component types) are warnings, not errors. Errors are left for VPE bugs. - Per-item, per-event, per-frame and teardown lines move to Debug or Trace. Load-phase timings are Debug; one Info line per table load remains. - Hot paths check a LogThrottle or LogOncePerKey before logging, so a broken mapping logs once instead of on every event. - Error and Fatal take message templates, so reports group by template. Exceptions are passed as exceptions. - Swallowed exceptions are logged where the user loses something. - The async void methods log their exceptions, and the texture cache's background write logs a failure. Bugs fixed on the way: a hit event for an item without a hit handler threw a KeyNotFoundException, PackagedRefs threw on a missing component, and the trough logged an error on every drain. LoggingRulesTests scans the runtime code of the packages that follow these rules for Debug.Log calls, interpolated errors and stringified exceptions.
- IPluginLog (Debug/Info/Warn/Error with exceptions) as PluginApplicationContext.Logger, next to the old text Log. The host takes an optional factory for it, and an error callback that receives exceptions from plugin hooks with their stack trace. Invalid plugin settings stay a warning. - NLogPluginLog writes a plugin's log to NLog as "Plugin.<id>". - IGamelogicStartupStatus: engines report what the user can fix (missing ROM, emulator not installed) instead of throwing from OnInit. The player logs it, fails the load attempt and keeps the table playable. - Player.RefreshGamelogicDiagnostics() for engines that learn more after OnInit. - DiagnosticsContext logs only what changed after a section's first appearance, and Retain() drops what describes a previous table. - LoggingRulesTests now covers every swept package.
…nes. PerformanceMonitor runs as a PlayerLoop system and is the only frame timing capturer. It closes 1 s windows, classifies what bounds the frame (capped, gpu, cpu-main, cpu-render) with hysteresis, and logs a line when the frame rate moves by 20 % or the bound changes, at most every 10 s with a 5 minute heartbeat. Hitches and a nearly full GPU budget warn, throttled. Process RAM comes from the OS (Unity's counter on macOS), VRAM usage and budget from DXGI on Windows. The Performance diagnostics section holds the session's totals and the latest window, and the table end logs a summary.
ReportDedupe decides which automatic reports of a session are sent: the first of each kind, at most ten, fatal ones always. Redaction.RedactJson redacts JSON string values as the text they stand for, so escaped backslashes don't hide the home folder. DiagnosticsContext raises Removed for dropped sections, VpeLog names a diagnostics snapshot next to each log and rotates it with it, and the performance section says whether the game is paused.
…kageException. A file that can't be opened as a package, has no manifest, comes from a newer format version or has no scene is a problem with the file, not with VPE. The reader now says so with its own exception, and only starts its background reads once the file passed these checks.
This was referenced Oct 15, 2026
[Medium impact] The changes appear safe to merge; both previous findings are fixed and no new actionable issues were found. SummaryAdds queued session logging, diagnostics, plugin logging APIs, performance tracking, and error-reporting helpers.
|
…ult handler, throttle per instance. A fatal report is sent even if the same kind came before as an error that wasn't sent. Update stops when a SimulationFaulted handler stopped the simulation. Once-per-id warnings of per-table players and the throttles of components that exist several at a time belong to their instance, so one table or one failing instance doesn't hide another's.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The engine part of the logging and error reporting work: a cleaned-up NLog setup, diagnostics that describe the session for bug reports, performance tracking, and the building blocks the player's Sentry integration uses. Sentry itself lives in the player (VisualPinball/VisualPinball.Engine.Player); the engine has no dependency on it.
Logging
SubsystemRegistration. All targets except the editor console sit behind our own queue, so the logging thread only enqueues and rendering and file I/O happen on a writer thread. NLog'sAsyncTargetWrapperrenders events with exceptions on the logging thread, which the simulation thread can't afford.-vpe-log-leveloverrides the level.UnityLogBridgeforwardsDebug.Logfrom Unity and third-party code into NLog.CrashHookslog unhandled and unobserved exceptions.UnityTargetrenders exceptions as text instead ofDebug.LogException.SimulationThreadComponentraisesSimulationFaulted.LogThrottle/LogOncePerKey. Errors use message templates, so reports group by template.LoggingRulesTestsscans the runtime code of every swept package and enforces this.KeyNotFoundException;PackagedRefsthrew on a missing component;Diagnostics
DiagnosticsContextholds named sections, logged when they change and attached to reports:LoadAttempttracks phase, package format, metadata and a fingerprint. It survives a failed load.IDiagnosticsSource.Redactionremoves the home folder and user name from paths, including inside JSON (RedactJson).DiagnosticsWriternever writes values of settings that look like secrets.Plugins
IPluginLog(levelled, with exceptions) is added next to the old text log. It is written to NLog asPlugin.<id>. An error callback receives exceptions from plugin hooks with their stack trace.IGamelogicStartupStatuslets engines report what the user can fix (missing ROM, emulator not installed) instead of throwing fromOnInit.Performance
PerformanceMonitoris a PlayerLoop system and the only frame timing capturer;FramePacingGraphand the player's overlay read from it.Error reporting building blocks
ReportDedupe: the first automatic report of each kind per session, at most ten, fatal ones always.DiagnosticsContext.Removed, so the report scope forgets a closed table.InvalidPackageException: a file that isn't a loadable.vpepackage is the user's problem, not a bug. The reader also only starts its background reads once the file passed these checks.Testing
VisualPinball.Unity.Project.Hdrp.Merge order
Merge this first. The plugin PRs depend on the new plugin API (
IPluginLog,IDiagnosticsSource,IGamelogicStartupStatus).