Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

BSProfiler

BSProfiler records Beat Saber 1.45.2 performance data under UserData/BSProfiler/<UTC start>-<process ID>/ in the selected game instance. It starts with the game and closes files on exit. It uses no UI or hotkeys.

Default capture (schema 7)

Default capture skips mass callback discovery and Harmony rewriting. Frames, native CPU/GPU/GC markers, process/managed memory trends, scene/log events, startup phase timing, and the existing eight memory/parser hooks remain active. Default callback counts are zero; method/assembly callback attribution, callback allocations and callback receiver retention are unavailable. Callback CSV files can be empty or contain only native/frame evidence; this does not prove that mods did no work.

For a deliberate full callback investigation, create UserData/BSProfiler/full-callbacks.enabled before launch. Remove the marker before the next launch to return to passive capture. Full mode uses the existing incremental installer, coverage limits and method catalog. It can introduce large CPU/GC spikes during installation and adds runtime sampling overhead. In capture 20260930-145834-43476, installation was still incomplete after 37.7 seconds and 2,438 hooks; completed assembly patch phases alone cost 11.4 seconds and native recording observed 31 collections. A soft two-millisecond slice cannot interrupt an individual method rewrite or its collection. Full mode is unsuitable as a default capture of the game's ordinary performance.

The following callback/retention sections describe full mode. Startup phase timing, native frames and memory/parser spans also work in passive mode.

At startup, BSProfiler checks whether the allocation API actually counts a known 4,096-byte allocation. An API that exists but returns zero is marked unavailable, with the reason in session.txt and the game log; allocation columns stay empty instead of reporting false zeroes. callback-allocations.csv then contains only its header. Other memory, collection, and timing records still work.

The callback catalog also includes Unity lifecycle and focus/pause methods, installed-mod focus-event subscribers found at startup, and Chroma's environment object discovery and ID lookup methods. Focus callbacks write individual detail rows even below 8 ms, subject to the same 32-row-per-second cap, so a fast handler can be distinguished from missing coverage. Focus subscribers are not rediscovered during the run; delegate targets already discovered from IL remain hooked when subscribed later. Background work remains outside main-thread callback timing, but hooked background receivers can be observed for retention.

Callback and GC evidence (schema 4)

General delegate/UI coverage applies to installed plugin assemblies. Existing lifecycle/tick/Harmony coverage for mod libraries remains; generic library delegates are excluded. Harmony, MonoMod and Cecil internals are excluded from all callback hooks to protect the patching machinery and avoid consuming mod coverage budgets. Startup reads plugin managed IL for delegate targets (including UI listeners, named event subscribers and generated lambdas); it also considers Handle*, On* and Refresh* methods. It does not invoke mod code during discovery. Existing lifecycle, coroutine/async, tick and Harmony targets keep their coverage. Extra targets are limited to 256 per assembly and 2,048 overall; catalog rows mark omitted targets, and session metadata reports discovery failures. Generic types, native methods, background timing, reflection-only callbacks, assemblies loaded later, and targets outside these limits can remain uncovered. Patching more methods increases startup and runtime overhead; measured_hook_ms and omission counts help assess that cost.

  • callback-spikes.csv adds self_ms (inclusive time minus hooked descendants and their measured diagnostic work), no_observed_gc_ms and gc_overlap_ms. Self time still contains unhooked/native descendants and any overlapping collection. The GC columns separate whole calls; they do not estimate or subtract collection duration. Assembly inclusive totals still double-count nested callbacks; self totals exclude those hooked descendants.
  • slow-calls.csv adds call/parent IDs, parent callback, depth, start time, self time, GC evidence and cached method signatures. Parent IDs can point to omitted rows because short calls and capped details are not all emitted. Compare elapsed time intervals when frame IDs differ across callback and recorder rows.
  • frame-evidence.csv joins slow Update intervals to root callback totals, measured prefix/finalizer overhead, root GC crossings, observed collection count changes and the current native GC marker. Recorder markers can lag; inspect neighboring frames.csv rows before assigning a pause. Root time includes nested profiler overhead, so it must not be added to hook overhead. Long unaccounted intervals can be game/native/background/wait work; lack of a hooked callback does not clear a mod.

A GC crossing means a collection occurred while the callback was active. It does not identify whose earlier allocation triggered it. This Unity build's per-thread allocation API still fails calibration, so those bytes remain unavailable. The profiler does not force GC, change collection policy, or claim to identify every allocation source.

  • frames.csv: one row per Unity Update call, with wall-clock frame time, approximate FPS, display budget, Unity CPU main-thread time, Unity GPU frame span, XR app/compositor GPU time, dropped/presented frame counts, motion-to-photon latency, memory, GC counters, and available Unity profiler markers. It also records main-thread allocated bytes since the previous Update, the previous BSProfiler Update's time and allocated bytes, intervals from the previous Update to LateUpdate and from LateUpdate to this Update, and the cumulative number of omitted slow-call details. Empty cells mean the counter was unavailable. Counter names and units are in the header.
  • incidents.csv: runs of frames exceeding max(12 ms, 1.5 × display frame budget). A gap of one second without a slow frame closes a run. The peak frame number links to frames.csv.
  • summaries.csv: ten-second frame-time percentiles and GC collection deltas. The percentile sample keeps up to 4,096 frames per interval; the mean and worst use every frame.
  • events.csv: active-scene changes, scene loads and unloads, application focus changes, observed GC count changes, and Unity warnings, errors, and exceptions with capture time. IPA's own log remains the source for messages that bypass Unity logging.
  • session.txt: hardware, graphics settings, recorder and allocation-counter availability, hook counts, capture schema, profiler module MVID, and loaded plugin versions. UTC start now matches the origin of elapsed timestamps.
  • available-markers.csv: Unity's runtime profiler marker catalog, including markers that BSProfiler did not record.
  • callback-spikes.csv: on slow frames, the top eight installed-mod assembly totals and top twenty individual callbacks, with call counts, inclusive total time, longest call, allocated bytes, largest call allocation, and the number of calls during which GC counts changed. It times Unity update callbacks, Zenject ticks, plugin DLL coroutine and async steps, and Harmony callbacks on the main thread. Match frame with frames.csv; assembly names the mod DLL. Rows outside the top eight assemblies or top twenty callbacks are omitted.
  • callback-catalog.csv: every callback BSProfiler attempted to time, its DLL path, and whether the hook installed. Check this file when the slow frame has no matching callback.
  • callback-allocations.csv: approximately once per second, the top eight assemblies and top twenty methods by allocated bytes, including normal frames. interval_ms is the actual reporting interval. Use this file to find sustained allocation sources before a collection.
  • slow-calls.csv: individual calls lasting at least 8 ms, crossing a GC count change, or throwing an exception, with the actual Unity frame at return, elapsed time, allocated bytes, GC deltas, exception type, and caller stack. Details are capped at 32 rows per second and stacks at 12,000 characters. slow_call_details_dropped in frames.csv counts rows omitted by that cap. Finalizers record throwing callbacks without suppressing their exceptions.
  • memory-operations.csv: caller stacks, thread IDs, duration, and GC deltas for successfully hooked managed GC.Collect overloads and Resources.UnloadUnusedAssets. Nested overload calls produce one outer operation record. For asynchronous resource unloading, duration measures the request call, not completion. Background-thread frame and scene values are the most recent main-thread sample.
  • memory-hooks.csv: installed and failed memory-operation hooks. An empty operations file does not exclude GC if a hook failed or collection began automatically or in native code.
  • memory-trend.csv: one-second samples plus samples when a GC count change is observed. Records managed used/reserved memory, Unity allocated/reserved memory, Windows resident RAM, private committed memory, peak resident RAM, counter status/age, deltas, and growth since capture start. A settled-menu checkpoint after ten seconds in each MainMenu visit compares memory with the previous such visit. The name describes the sampling rule; it does not guarantee that background loading finished. OS values are sampled on a worker through Windows GetProcessMemoryInfo, replacing unusable Mono process working-set zeroes. Failures are blank with their reason. The earliest observation after GC is not a measurement of the exact live heap: allocations may already have resumed.
  • mod-memory.csv: approximately every thirty seconds, counts surviving observed callback receivers by DLL and type, including destroyed Unity wrappers still alive in managed memory. alive_after_observed_gc means a collection happened since the instance was first observed. Type counts have deltas from the previous scan; scans are spread across frames and are not atomic heap snapshots. The tracked receivers use weak references and do not stay alive solely because they were observed. The 8,192-instance limit, 32-reference-field limit, omitted fields, failed field reads, and dropped observation attempts are explicit.
  • mod-fields.csv: direct fields of those observed instances: string lengths, array lengths, selected framework collection counts and their changes, primitive-array/string payload sizes, and direct Unity-object native memory when the runtime supplies it. The DLL, owner type, field, and observed instance ID identify a growing cache or buffer. Payload sizes omit headers/referenced objects; collection capacity and nested contents are not measured. Native references can be shared; do not add them into a per-mod total. Static fields, value-type fields, unobserved instances, arbitrary object graphs, and arbitrary property getters are excluded. These rows suggest where to investigate, not which mod has conclusively leaked memory.
  • memory-snapshots.csv and requested .snap files: request/completion/failure records for Unity managed/native object snapshots. Full snapshots provide the retained-object/reference evidence needed for static roots and deeper graphs. Snapshot support in this release player is unconfirmed until a nonempty capture succeeds; a failed capture supplies no ownership evidence.

Investigating growth

Repeat the same song/menu cycle several times. Compare settled-menu checkpoints, natural-GC samples, surviving type counts, and the same owner's field counts across visits. Initial cache loading, reserved heap expansion, and high allocation throughput alone are not proof of a leak. A growing retained baseline and the references keeping those objects alive are stronger evidence. OS private-memory growth with flat managed/Unity counters can indicate external/native allocations that these managed observations do not attribute.

For full snapshots, write a short label such as baseline to <game>/UserData/BSProfiler/memory-snapshot.request. The request waits until MainMenu has been active for ten seconds, then is consumed and the snapshot is written inside the current capture folder. Repeat with after-cycles after reproducing growth. Inspect/compare successful .snap files in Unity's Memory Profiler to follow retained references back to mod instances/static fields. Snapshots can pause the game and create large files; they are not taken automatically. No forced GC or global GC policy change is performed. Requests during gameplay wait for the menu. The snapshot index records failure if the player does not support the API.

The retention scan processes at most four instances per frame, with a soft 0.5 ms budget. A single reflection/native query can exceed that budget. Its overhead appears in the existing profiler Update timings, but observer registrations in callback finalizers are additional hook overhead. The bounded observer metadata, file writer, and generated rows also use memory. Account for these diagnostic costs when comparing runs.

The first schema-3 build (1e5565e) had avoidable observer overhead: an untracked receiver called Assembly.GetName() before checking the instance limit, allocating metadata again on every rejected callback. Capture 20260930-073524-33696 recorded over ten million rejected observations and substantial managed-heap growth, so its growth cannot be assigned entirely to other mods. The observer now compares a cached assembly reference and rejects saturated observations before locking. Reprofile before drawing retention conclusions from that run; registration, rows, and other hook overhead still exist.

Compare an incident's peak frame with nearby frame, slow-call, allocation, and memory-operation rows. Recorder values can lag the wall-clock Update sample, so inspect neighboring frames. Callback totals cover the interval between BSProfiler Update samples and may straddle Unity frames; slow-calls.csv gives the Unity frame at the individual call's return. The two phase intervals sum to the wall-clock Update interval and help locate a stall before or after the previous LateUpdate. They do not identify a native method.

Time and allocation totals are inclusive: nested callbacks can count the same work twice, and nested diagnostic work can appear in a caller's totals. Do not sum methods or assemblies to estimate the application's total allocations. gc_crossing_calls identifies callbacks active during a collection, not the code that caused it; another thread can trigger GC. Automatic or native collections have no managed caller stack from these hooks. The hooks do not cover game-owned methods, native work, arbitrary mod methods outside the catalog, or background callback allocations. The GPU frame span includes waiting and does not measure GPU utilization.

The capture writes a row every frame and can grow by hundreds of megabytes over a long session. The writer uses a bounded queue and writes on a background thread; queue_dropped in summaries.csv reveals lost rows. Callback samples are reused instead of allocated each frame, and the OS memory query runs on a worker. Hooks and stack capture still add overhead. The recorded profiler Update cost excludes callback-hook overhead and the writer thread; compare runs with the same capture setup. BSProfiler attempts to remove its hooks on exit and records any cleanup error without interrupting file closure.

Build for the selected instance with dotnet build BSProfiler.csproj -c Release -p:BeatSaberDir=<instance> -p:DisableCopyToPlugins=true. Install only bin/Release/net48/BSProfiler.dll into the instance's Plugins directory, or IPA/Pending/Plugins while the game runs.

Shutdown

On application quit, timing and memory hooks become inactive, without synchronously unpatching thousands of methods during process teardown. Normal live capture disposal still unpatches. Stop begin/end are logged; writer draining waits at most two seconds, then the background writer can continue. A timeout warning means remaining records may be incomplete when the process exits. This prevents an unlimited writer join from holding shutdown; it does not prove every other mod exits promptly.

Worker parser evidence (schema 5)

memory-operations.csv also times CustomJSONData top-level v2/v3 Deserialize methods on their actual thread. session.txt reports the main thread ID; rows add kind, start elapsed time, and start frame/scene. End frame/scene values are the most recent observations from the main thread; worker frame IDs are approximate, so correlate elapsed bounds with frame and GC records. Nested hooks on one thread are suppressed; ordinary callback timing remains main-thread only. Parsing time is inclusive wall time, not total beatmap conversion time. This does not measure heap ownership or prove which worker allocation triggered a collection. Hook status is in memory-hooks.csv; methods unavailable at startup cannot be timed.

Startup capture (schema 6)

events.csv records bootstrap phase duration, GC counts and managed heap change, callback discovery duration per assembly, patch duration per assembly, and final callback-install completion with active work versus elapsed wall time. session.txt includes process start UTC/capture delay, live/final hook counts, and whether installation completed. When full mode is explicitly enabled, callback discovery and installation run as a main-thread coroutine with a soft two-millisecond work slice; an indivisible reflection scan or method rewrite can exceed it. The profiler suppresses its own callback sampling while it advances installation, then captures installed callbacks between slices. Startup callback coverage is partial until installation completes; a closed partial capture retains its catalog. Memory/parser hooks and native frame/GC recorders start before callback discovery. Earlier process/IPA startup is outside this capture and needs the game log or external tracing. Incremental installation removes the old single synchronous mass-hook setup block; runtime capture confirmed ongoing installation spikes. Schema 7 therefore disables this installer by default.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages