[CoreCLR] Boost GC bridge thread CPU frequency via ADPF - #12269
[CoreCLR] Boost GC bridge thread CPU frequency via ADPF#12269jonathanpeppers wants to merge 1 commit into
Conversation
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Android PR Review — ⚠️ Minor suggestions (draft PR)
Reviewed the ADPF GC-bridge boost change. Overall this is a clean, well-scoped, and unusually well-documented change: the native path fails soft, every ADPF call is correctly gated behind a runtime android_get_device_api_level() >= __ANDROID_API_T__ check with weak-linked symbols, the session is bound to the calling bridge thread via gettid(), and timing is only done when a session exists.
Verified correct:
- New
gc_bridge_thread_boost_enabledfield is appended to the CoreCLRApplicationConfigstruct, mirrored inApplicationConfigCLR.cs, andApplicationConfigFieldCount_CoreCLRwas bumped20 → 21with a matchingcase 20inEnvironmentHelper.cs. Field ordering is consistent across the native struct, the LLVM-IR generator, and the parser. - MSBuild plumbing (
_AndroidGCBridgeThreadBoostdefaulttrue→ taskGCBridgeThreadBoost→GCBridgeThreadBoostEnabled) is wired only into the CLR generator, which is correct since this is a CoreCLR-only concern. - ADPF usage (create session with target duration, report actual duration per round) matches the documented API contract, and reporting only happens on API 33+.
Suggestions (non-blocking, both posted inline):
- 💡 Document the intentionally process-lifetime ADPF session that is never
APerformanceHint_closeSession-d, per the repo leak-documentation convention. - 💡 Add a regression test asserting the
_AndroidGCBridgeThreadBoosttoggle propagates into the generated config (default-true and explicit-false).
No correctness or safety issues found. Note this is a draft PR (mergeable_state: blocked) with open TODOs in the description (broader device coverage, idle-power confirmation, switch documentation), so it is not yet ready to merge on its own terms.
Prioritized: bugs > safety > performance > tests > docs. Nothing above the suggestion tier surfaced.
Generated by Android PR Reviewer for #12269 · 100.2 AIC · ⌖ 18.8 AIC · ⊞ 6.9K
Comment /review to run again
99c2c0c to
0f47780
Compare
The CoreCLR GC bridge runs the explicit ART GC (Runtime.gc()) synchronously on a dedicated pthread that is idle >99% of the time. When it wakes, Android's schedutil DVFS governor sees a cold thread and keeps its core near the bottom of the frequency table for the whole short GC burst, so the GC runs ~2x slower than under MonoVM (which drives the same GC from the always-hot render thread). This surfaces as periodic frame-time hitches / FPS dips. See dotnet/runtime#131370 and #12263. Fix: create an ADPF (Android Dynamic Performance Framework) APerformanceHint session bound to the bridge thread and report each bridge-GC duration, so the platform clocks the carrier core up during the rare, latency-sensitive collection. ADPF is Google's sanctioned mechanism and works without root, RT priority, or kernel CONFIG_UCLAMP_TASK (unlike a direct sched_setattr() hint). ADPF overview: https://developer.android.com/games/optimize/adpf APerformanceHint (NDK): https://developer.android.com/ndk/reference/group/a-performance-hint android_get_device_api_level: https://developer.android.com/ndk/reference/group/apilevels The APerformanceHint_* API is API 33+, so the newer symbols are weak-linked and every call is guarded by a runtime android_get_device_api_level() >= __ANDROID_API_T__ check; the whole path fails soft (a scheduling hint must never abort the process). The weak ADPF symbol itself is also null-checked before the first call, so on runtimes/devices where libandroid does not provide it the path skips instead of calling through a null pointer. Gated by a new private MSBuild property _AndroidGCBridgeThreadBoost (default true), plumbed through to the CoreCLR ApplicationConfig as gc_bridge_thread_boost_enabled. gc-bridge.cc is shared with the NativeAOT host, which does not link the generated application_config symbol; under XA_HOST_NATIVEAOT the boost is enabled unconditionally (and still fails soft) so the shared source links without dragging in application_config, which is not provided at NativeAOT app-link time. On-device (Pixel 10, Tensor G5, API 36; Perfetto, screen-on, 45s per arm, 6-7 GC events each): median carrier-core frequency during the explicit GC rose 1000 -> 1751 MHz (+75%) and explicit-GC wall-time fell 18.8 -> 13.2 ms mean (-30%), median 19.4 -> 12.9 ms (-34%). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea778d78-47ba-4349-8857-9dd3c91be5ca
0f47780 to
26dffcf
Compare
Problem
Under CoreCLR, MAUI/SkiaSharp apps show periodic frame-time hitches / 60→54 FPS dips that do not happen under MonoVM. Root cause (measured with Perfetto, see #12263 and dotnet/runtime#131370):
The CoreCLR GC bridge runs the explicit ART GC (
Runtime.gc()) synchronously on a dedicated pthread (GCBridge::start_bridge_processing_thread) that is idle >99% of the time (it sleeps ~7–8 s on a semaphore between bridge rounds). When it wakes, Android'sschedutilDVFS governor sees a cold thread and keeps its core near the bottom of the frequency table for the whole short GC burst — so the same ART GC, on the same heap, runs at roughly half the clock and is ~2× slower than under MonoVM (which drives the same GC from the always-hot render thread already pinned near max clock). The stretched stop-the-world sub-phases suspend the mutator threads (including the SkiaSharp GL render thread), producing the hitch.Fix
Create an ADPF (Android Dynamic Performance Framework)
APerformanceHintsession bound to the GC-bridge thread and report each bridge-GC duration. The platform then clocks the carrier core up while that thread runs, during the rare, latency-sensitive collection. ADPF is Google's sanctioned mechanism for exactly this and works without root, without RT priority, and independently of the kernel'sCONFIG_UCLAMP_TASKsetting — unlike a directsched_setattr()util-clamp hint (which is disabled on many shipping kernels). Because the bridge thread sleeps off-CPU >99% of the time, the boost only applies during the GC, so the power cost is negligible.Implementation notes:
src/native/clr/host/gc-bridge.cc: create the session once at bridge-thread start, then timeprocess()and callAPerformanceHint_reportActualWorkDurationeach round.APerformanceHint_*API is API 33+ (Android 13). Newer symbols are weak-linked (__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__) and every call is guarded by a runtimeandroid_get_device_api_level() >= __ANDROID_API_T__check — the native equivalent of aBuild.VERSION.SDK_INTcheck._AndroidGCBridgeThreadBoost(default true), plumbed through to the CoreCLRApplicationConfigasgc_bridge_thread_boost_enabled.On-device validation
Measured on a Pixel 10 (Tensor G5, API 36) with Perfetto (
cpu_frequencyftrace +dalvikatrace), screen verified on, 45 s per arm, all three arms captured back-to-back under identical conditions on the same benchmark. The metric is the ART "Explicit concurrent mark compact GC" slice's wall-time and the time-weighted CPU frequency its carrier core runs at during the slice.Reading the numbers:
Follow-ups / TODO
Caveats
dumpsys performance_hintreportsHAL Support: false) the session is declined and the code falls back to a no-op — the regression persists there, but that hardware also cannot be helped viauclampor RT priority, which are likewise platform-disabled.NativeAOT compatibility
src/native/clr/host/gc-bridge.ccis compiled into both the CoreCLR host (libnet-android) and the NativeAOT host (libnaot-android). NativeAOT does not link the generatedapplication_configsymbol (it has its own host and never pulls in the CoreCLR config), so referencingapplication_config.gc_bridge_thread_boost_enabledunconditionally broke every NativeAOT app link (ld: error: undefined symbol: application_config). UnderXA_HOST_NATIVEAOTthe boost is now enabled unconditionally (still fails soft) and theapplication_configread is compiled out, so the shared source links cleanly. Verified locally by rebuilding bothlibnet-android.release.soandlibnaot-android.release.soforandroid-arm64(both link with-Wl,--no-undefined; the weak ADPF symbols do not trip it).