Skip to content

Add profile module — sessions, vitals, assert, remote (Android verified)#44

Draft
Niaobu wants to merge 1 commit intomainfrom
mv/profile-module
Draft

Add profile module — sessions, vitals, assert, remote (Android verified)#44
Niaobu wants to merge 1 commit intomainfrom
mv/profile-module

Conversation

@Niaobu
Copy link
Copy Markdown
Contributor

@Niaobu Niaobu commented Apr 30, 2026

Summary

A new unityctl profile command family for capturing Unity Editor and connected-player performance data. Designed around two shapes: structured JSON for agents (vitals, assert) and shareable artifacts for humans (.data for the Profiler window, .snap for Memory Profiler).

Commands

profile list-stats        Enumerate Unity's built-in counters (~3,790 in test project)
profile start / stop      Session lifecycle, with optional --max-duration safety cap
profile status            List active sessions
profile capture           One-shot sugar: start → wait → stop
profile vitals            Curated 5-number report (avg/p99 frame, GC, draws, GPU)
profile assert            CI/regression gate — exit 1 if thresholds breached
profile snapshot          Memory snapshot via com.unity.memoryprofiler
profile targets           List editor + connected players
profile connect <url>     Direct-URL connect (e.g. adb-forwarded Android)

Stat name aliases (main, render, gpu, drawcalls, gc-alloc, …) give agents stable handles independent of Unity's slightly inconsistent counter naming.

Design decisions

  • Sessions, not commands. Session-based start/stop with optional --max-duration cap — same shape as xctrace, dotnet-trace, Puppeteer's tracing.start/stop. Lets agents compose: start → run scenario via other unityctl commands → stop → read summary.
  • JSON-first. Agents are the primary user. Default output is structured (avg/p50/p95/p99/max + hitches per stat); --save produces a Profiler-window-openable .data for humans to look at.
  • Phase 1: floor — list-stats, start/stop, capture, vitals, assert. Pure ProfilerRecorder sampling, no .data file.
  • Phase 2: artifacts--save flag drives ProfilerDriver.SaveProfile. profile snapshot for memory.
  • Phase 3: remoteprofile targets enumerates connections, --target <id> selects, profile connect <host:port> for explicit URL connect (Android via adb forward).
  • Hitch detection: two modes. Default is median × 2 (robust against jitter from warmup frames); override with --hitch-ms 33.3 for absolute thresholds. Returned as {frameIndex, frameTimeMs} array on stop.

Local vs remote sampling — the interesting bit

Unity.Profiling.ProfilerRecorder only samples the local editor process. When ProfilerDriver.connectedProfiler points at a remote target (e.g. an Android dev build with autoconnect), the recorder returns zeros.

The fix: detect remote at session start, switch code paths.

  • Local: create a ProfilerRecorder per requested stat, sample once per EditorApplication.update tick.
  • Remote: enable ProfilerDriver to buffer streamed remote frames, snapshot the start frame index, and on Stop() walk every frame in the buffer with RawFrameDataView.GetCounterValueAsLong(), mapping by stat name. Units are looked up locally (built-in counter names share units across editor and player). Same summary JSON comes back, populated.

The human-readable output now also prints target: REMOTE — <connection name> so you can't confuse local vs device data.

Verified end-to-end on Android

Built a Development + AutoConnect Profiler APK (unity-project/Assets/Editor/AndroidProfileBuild.cs, IL2CPP/ARM64), installed on a connected Samsung SM-A266B, and ran:

$ uc profile vitals --duration 3
Vitals  (85 frames, 3.1s)
  Frame time:      avg 10.02 ms   p99 11.03 ms   max 13.35 ms
  Render thread:   avg 0.69 ms
  GPU frame:       avg 5.64 ms
  Draw calls:      avg 2.00   max 2.00

— real device numbers, ~30 fps cap (Android default), reasonable GPU headroom.

profile assert PASS / FAIL exit codes verified on the device. Saved .data opens cleanly in the Profiler window with the connection dropdown showing the device.

Things worth flagging

  • Memory counters (System Used Memory, GC Allocated In Frame) come back as 0 from Android. Markers exist in the streamed frames, values aren't populated — Unity's player doesn't ship the Memory profiler module by default. CPU/GPU/draw counters work fine. Likely fixable via Profiler.SetAreaEnabled(ProfilerArea.Memory, true) at runtime in the player or a build flag.
  • EditorApplication.delayCall did not fire reliably in our test session. Worked around by calling BuildPipeline.BuildPlayer synchronously inside script eval with a long timeout. Worth investigating whether it's a Unity quirk or interaction with the script-eval wrapper.
  • Initial Android build hung on a hidden "Unsupported Input Handling" dialog for ~10 minutes until I ran uc dialog list and dismissed it. UX improvement: the build helper could auto-dismiss known-safe dialogs, or profile capture could proactively check for blocking dialogs.
  • Switching unity-project to Android target updates URP/GraphicsSettings/ProjectSettings (productName, applicationIdentifier, preloadedAssets entry, default URP changes per platform). Kept in this PR — they're tied to the Android test infrastructure.
  • Locale fix: set CultureInfo.InvariantCulture in Program.cs so threshold flags like --p99-frame-ms 16.7 parse correctly on non-en locales (was hitting Norwegian , decimal separator).

Next steps (intentionally not in this PR)

  • Chrome Trace JSON export — flatten the .data content to the {name, ph, ts, dur, tid} event format. Unlocks drag-drop into ui.perfetto.dev / speedscope.app, zero-install shareable viewers.
  • profile watch — live counter dashboard streaming ProfilerRecorder.CurrentValue at 2-Hz, like dotnet-counters monitor. Cheap to build.
  • Memory module on Android — investigate enabling streaming so Android vitals also shows GC alloc / system memory.
  • --target UX — when no --target is passed but a remote is currently connected, currently we silently profile the remote. The target: REMOTE line makes this visible, but explicit --target editor to force-local might be worth adding.

Test plan

  • profile list-stats — 3,790 counters enumerated
  • profile capture --duration N in editor play mode — real numbers, hitch detection
  • profile capture --save run.data — opens cleanly in Profiler window
  • profile vitals — curated report
  • profile assert — pass + fail with correct exit codes (local and remote)
  • profile targets — editor + Android device enumerated
  • Android remote capture — full summary populated from Samsung SM-A266B
  • --json output — agent-friendly shape
  • Locale fix — --p99-frame-ms 16.7 parses on Norwegian locale
  • Memory snapshot — requires com.unity.memoryprofiler install (not in this PR's test project)
  • Chrome Trace export — deferred

…emote

New `unityctl profile` family for capturing Unity Editor and remote-player
performance data from the CLI:

  profile list-stats        Enumerate built-in counters (~3,790 in test project)
  profile start/stop/status Session lifecycle with --max-duration auto-stop cap
  profile capture           One-shot start+wait+stop, with optional --save .data
  profile vitals            Curated 5-number report (avg/p99 frame, GC, draws, GPU)
  profile assert            CI/regression gate: exit 1 if thresholds breached
  profile snapshot          Memory snapshot via com.unity.memoryprofiler
  profile targets           List editor + connected players
  profile connect <url>     Direct-URL connect (e.g. adb-forwarded Android)

Local mode samples Unity.Profiling.ProfilerRecorder per editor tick. Remote
mode (autoconnect-profiler builds, live device) drives the editor's profiler
buffer and walks frames with RawFrameDataView.GetCounterValueAsLong on stop —
same JSON shape, real device numbers. Verified end-to-end on a connected
Samsung SM-A266B running an IL2CPP+ARM64 dev build.

Stat name aliases (main, render, gpu, drawcalls, gc-alloc, ...) give agents
stable handles independent of Unity's slightly inconsistent counter naming.

Side effects:
  - Switching unity-project to Android target updates URP/GraphicsSettings/
    ProjectSettings (productName, applicationIdentifier, etc.) — kept.
  - Set System.Globalization invariant culture in CLI Program.cs so threshold
    flags like --p99-frame-ms 16.7 parse correctly on non-en locales.
  - unity-project/Assets/Editor/AndroidProfileBuild.cs is a reusable helper
    that produces a Dev + AutoConnect Profiler APK from BuildPipeline.

Known limitation: Memory counters (System Used Memory, GC Allocated In Frame)
return 0 from Android — Unity's player doesn't stream the Memory profiler
module by default. Markers exist; values aren't populated. CPU/GPU/draw
counters work end-to-end.
@Niaobu Niaobu self-assigned this Apr 30, 2026
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