Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
<AndroidCodegenTarget Condition=" '$(AndroidCodegenTarget)' == '' ">XAJavaInterop1</AndroidCodegenTarget>
<_AndroidJcwCodegenTarget Condition=" '$(_AndroidJcwCodegenTarget)' == '' and '$(_AndroidRuntime)' != 'NativeAOT' ">XAJavaInterop1</_AndroidJcwCodegenTarget>
<_AndroidTypeMapImplementation Condition=" '$(_AndroidTypeMapImplementation)' == '' and '$(_AndroidRuntime)' != 'NativeAOT' ">llvm-ir</_AndroidTypeMapImplementation>
<!-- Boost the CoreCLR GC bridge thread's CPU frequency during the rare, latency-sensitive bridge
GC (via an ADPF performance-hint session) so the DVFS governor does not underclock its core.
Private/unsupported; default on. See dotnet/runtime#131370. -->
<_AndroidGCBridgeThreadBoost Condition=" '$(_AndroidGCBridgeThreadBoost)' == '' ">true</_AndroidGCBridgeThreadBoost>
<AndroidBoundInterfacesContainStaticAndDefaultInterfaceMethods Condition=" '$(AndroidBoundInterfacesContainStaticAndDefaultInterfaceMethods)' == '' ">true</AndroidBoundInterfacesContainStaticAndDefaultInterfaceMethods>
<AndroidBoundInterfacesContainTypes Condition=" '$(AndroidBoundInterfacesContainTypes)' == '' ">true</AndroidBoundInterfacesContainTypes>
<AndroidBoundInterfacesContainConstants Condition=" '$(AndroidBoundInterfacesContainConstants)' == '' ">true</AndroidBoundInterfacesContainConstants>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class GenerateNativeApplicationConfigSources : AndroidTask

public bool EnableMarshalMethods { get; set; }
public bool AndroidEnableAssemblyStoreDecompressionCache { get; set; }
public bool GCBridgeThreadBoost { get; set; } = true;
public string? RuntimeConfigBinFilePath { get; set; }
public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty;
public string? BoundExceptionType { get; set; }
Expand Down Expand Up @@ -286,6 +287,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly)
IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
HaveAssemblyStore = UseAssemblyStore,
AssemblyStoreDecompressionCacheEnabled = AndroidEnableAssemblyStoreDecompressionCache,
GCBridgeThreadBoostEnabled = GCBridgeThreadBoost,
};
} else {
appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, Log) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,36 @@ public void AssemblyStoreDecompressionCacheSettingIsEmitted (bool enabled)
var config = (EnvironmentHelper.ApplicationConfig_CoreCLR)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR);
Assert.AreEqual (enabled, config.assembly_store_decompression_cache_enabled);
}

[Test]
public void GCBridgeThreadBoostSettingIsEmitted ()
{
string outputRoot = Path.Combine (Root, "temp", nameof (GCBridgeThreadBoostSettingIsEmitted));
string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll");
FileAssert.Exists (monoAndroidPath);

var task = new GenerateNativeApplicationConfigSources {
BuildEngine = new MockBuildEngine (TestContext.Out),
ResolvedAssemblies = [new TaskItem (monoAndroidPath)],
EnvironmentOutputDirectory = Path.Combine (outputRoot, "android"),
SupportedAbis = ["arm64-v8a"],
AndroidPackageName = "com.microsoft.android.gcbridgeboosttest",
EnablePreloadAssembliesDefault = false,
TargetsCLR = true,
AndroidRuntime = "CoreCLR",
// Defaults to true; set false to prove the toggle actually propagates and the field is not hard-coded.
GCBridgeThreadBoost = false,
};

Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed.");

var environmentFiles = EnvironmentHelper.GatherEnvironmentFiles (
outputRoot,
"arm64-v8a",
required: true,
runtime: AndroidRuntime.CoreCLR
);
var config = (EnvironmentHelper.ApplicationConfig_CoreCLR)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR);
Assert.IsFalse (config.gc_bridge_thread_boost_enabled, "GCBridgeThreadBoost=false should emit gc_bridge_thread_boost_enabled=false.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ public sealed class ApplicationConfig_CoreCLR : IApplicationConfig
public string android_package_name = String.Empty;
public bool have_assembly_store;
public bool assembly_store_decompression_cache_enabled;
public bool gc_bridge_thread_boost_enabled;
}

const uint ApplicationConfigFieldCount_CoreCLR = 20;
const uint ApplicationConfigFieldCount_CoreCLR = 21;

// This must be identical to the ApplicationConfig structure in src/native/mono/xamarin-app-stub/xamarin-app.hh
public sealed class ApplicationConfig_MonoVM : IApplicationConfig
Expand Down Expand Up @@ -406,6 +407,11 @@ static IApplicationConfig ReadApplicationConfig_CoreCLR (EnvironmentFile envFile
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.assembly_store_decompression_cache_enabled = ConvertFieldToBool ("assembly_store_decompression_cache_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;

case 20: // gc_bridge_thread_boost_enabled: bool / .byte
Comment thread
jonathanpeppers marked this conversation as resolved.
AssertFieldType (envFile.Path, parser.SourceFilePath, ".byte", field [0], item.LineNumber);
ret.gc_bridge_thread_boost_enabled = ConvertFieldToBool ("gc_bridge_thread_boost_enabled", envFile.Path, parser.SourceFilePath, item.LineNumber, field [1]);
break;
}
fieldCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ sealed class ApplicationConfigCLR
public string android_package_name = String.Empty;
public bool have_assembly_store;
public bool assembly_store_decompression_cache_enabled;
public bool gc_bridge_thread_boost_enabled;
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ sealed class DsoCacheState
public bool IgnoreSplitConfigs { get; set; }
public bool HaveAssemblyStore { get; set; }
public bool AssemblyStoreDecompressionCacheEnabled { get; set; }
public bool GCBridgeThreadBoostEnabled { get; set; }

public ApplicationConfigNativeAssemblyGeneratorCLR (IDictionary<string, string> environmentVariables, IDictionary<string, string> systemProperties,
IDictionary<string, string>? runtimeProperties, TaskLoggingHelper log)
Expand Down Expand Up @@ -284,6 +285,7 @@ protected override void Construct (LlvmIrModule module)
android_package_name = AndroidPackageName,
have_assembly_store = HaveAssemblyStore,
assembly_store_decompression_cache_enabled = AssemblyStoreDecompressionCacheEnabled,
gc_bridge_thread_boost_enabled = GCBridgeThreadBoostEnabled,
};
application_config = new StructureInstance<ApplicationConfigCLR> (applicationConfigStructureInfo, app_cfg);
module.AddGlobalVariable ("application_config", application_config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,7 @@ because xbuild doesn't support framework reference assemblies.
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
UseAssemblyStore="$(_AndroidUseAssemblyStore)"
AndroidEnableAssemblyStoreDecompressionCache="$(AndroidEnableAssemblyStoreDecompressionCache)"
GCBridgeThreadBoost="$(_AndroidGCBridgeThreadBoost)"
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
CustomBundleConfigFile="$(AndroidBundleConfigurationFile)"
TargetsCLR="$(_AndroidUseCLR)"
Expand Down
156 changes: 155 additions & 1 deletion src/native/clr/host/gc-bridge.cc
Original file line number Diff line number Diff line change
@@ -1,17 +1,142 @@
#include <cerrno>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <ctime>
#include <dlfcn.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#include <host/gc-bridge.hh>
#include <host/bridge-processing.hh>
#include <host/os-bridge.hh>
#include <host/host.hh>
#include <runtime-base/util.hh>
#include <shared/helpers.hh>
#include <xamarin-app.hh>

using namespace xamarin::android;

namespace {
// 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 big core near the bottom of the frequency table for the whole short
// GC burst, making the GC ~2x slower than under MonoVM (which drives the same GC from the
// always-hot render thread). See dotnet/android#12263 / dotnet/runtime#131370.
//
// The Android-recommended fix is ADPF (the Android Dynamic Performance Framework): create an
// APerformanceHint session bound to the bridge thread with a target work duration, report the
// actual duration each round, and notify the framework of the imminent spike so it pre-boosts the
// carrier core. The framework then clocks the core up (via uclamp or schedtune, whichever the
// kernel/power-HAL supports) whenever the thread runs. This works without root, without RT
// priority, and independent of the kernel's CONFIG_UCLAMP_TASK setting, unlike a direct
// sched_setattr() util-clamp hint. Everything here fails soft: a scheduling hint must never take
// down the process.
//
// The APerformanceHint_* entry points live in libandroid, which neither host links directly, and
// they are API-gated (getManager/createSession/reportActualWorkDuration are API 33+;
// notifyWorkloadSpike is API 36+). We resolve them at runtime with dlopen/dlsym rather than
// hard-linking libandroid and weak-linking the symbols: that keeps the dependency optional, avoids
// forcing libandroid into every app's DT_NEEDED (which under NativeAOT would also mean
// redistributing an NDK stub), behaves identically on the CoreCLR and NativeAOT hosts, and makes a
// missing library or symbol a natural no-op. A non-null resolved pointer *is* the availability
// test -- more precise than an OS-version proxy.
//
// Android docs:
// ADPF overview: https://developer.android.com/games/optimize/adpf
// APerformanceHint (NDK): https://developer.android.com/ndk/reference/group/a-performance-hint

// Opaque ADPF handles. We deliberately do not include <android/performance_hint.h>: its
// __INTRODUCED_IN declarations would emit references the linker would have to satisfy, defeating
// the point of resolving everything through dlsym.
struct APerformanceHintManager;
struct APerformanceHintSession;

using APerformanceHint_getManager_fn = APerformanceHintManager* (*) ();
using APerformanceHint_createSession_fn =
APerformanceHintSession* (*) (APerformanceHintManager*, const int32_t*, size_t, int64_t);
using APerformanceHint_reportActualWorkDuration_fn = int (*) (APerformanceHintSession*, int64_t);
using APerformanceHint_notifyWorkloadSpike_fn = int (*) (APerformanceHintSession*, bool, bool, const char*);

// Target work duration reported to ADPF. The bridge GC should finish well inside a 60 Hz frame;
// a tight target relative to the real (multi-ms) duration keeps the framework boosting the core.
constexpr int64_t GCBridgeHintTargetDurationNs = 4'000'000; // 4 ms

// Resolved once, on the bridge thread, in create_gc_bridge_hint_session(); read on the per-GC path.
APerformanceHintSession *gc_bridge_hint_session = nullptr;
APerformanceHint_reportActualWorkDuration_fn gc_bridge_report_actual_work_duration = nullptr;
APerformanceHint_notifyWorkloadSpike_fn gc_bridge_notify_workload_spike = nullptr;

auto monotonic_now_ns () noexcept -> int64_t
{
struct timespec ts;
clock_gettime (CLOCK_MONOTONIC, &ts);
return (static_cast<int64_t> (ts.tv_sec) * 1'000'000'000) + static_cast<int64_t> (ts.tv_nsec);
}

// Create an ADPF performance-hint session bound to the calling (GC bridge) thread so the platform
// clocks its core up during the explicit GC. Must be called once, from the bridge thread itself.
void create_gc_bridge_hint_session () noexcept
{
// libandroid is a core system library present on every device; when it is already mapped into
// the process (the common case) this dlopen just bumps its refcount. RTLD_LOCAL: we only dlsym
// our own handle. Kept open for the process lifetime -- there is no teardown path (see the
// session retention note below).
void *libandroid = ::dlopen ("libandroid.so", RTLD_NOW | RTLD_LOCAL);
if (libandroid == nullptr) {
log_info (LOG_DEFAULT, "GC bridge boost: libandroid.so not available; skipping ADPF hint");
return;
}

// A non-null getManager is the availability test: libandroid only exports the APerformanceHint_*
// API on Android 13+ (API 33), so resolving these is equivalent to an SDK_INT >= 33 check, and
// works uniformly on the CoreCLR and NativeAOT hosts without either linking libandroid.
auto get_manager = reinterpret_cast<APerformanceHint_getManager_fn> (::dlsym (libandroid, "APerformanceHint_getManager"));
auto create_session = reinterpret_cast<APerformanceHint_createSession_fn> (::dlsym (libandroid, "APerformanceHint_createSession"));
auto report_actual = reinterpret_cast<APerformanceHint_reportActualWorkDuration_fn> (::dlsym (libandroid, "APerformanceHint_reportActualWorkDuration"));
if (get_manager == nullptr || create_session == nullptr || report_actual == nullptr) {
log_info (LOG_DEFAULT, "GC bridge boost: ADPF hint sessions require Android 13 (API 33); skipping");
return;
}

APerformanceHintManager *manager = get_manager ();
if (manager == nullptr) {
log_info (LOG_DEFAULT, "GC bridge boost: no ADPF hint manager on this device; skipping");
return;
}

int32_t tids[1] = { static_cast<int32_t> (gettid ()) };
APerformanceHintSession *session = create_session (manager, tids, 1, GCBridgeHintTargetDurationNs);
if (session == nullptr) {
log_info (LOG_DEFAULT, "GC bridge boost: device declined ADPF hint session; skipping");
return;
}

// Intentionally never APerformanceHint_closeSession()-d: the session lives for the lifetime of
// the GC bridge thread, which itself lives for the whole process. There is no teardown path to
// close it from, so this is a deliberate process-lifetime retention, not a leak.
gc_bridge_hint_session = session;
Comment thread
jonathanpeppers marked this conversation as resolved.
gc_bridge_report_actual_work_duration = report_actual;

// notifyWorkloadSpike was added in Android 16 (API 36); it stays null on older platforms, which
// disables only the pre-boost -- the after-the-fact reportActualWorkDuration path still runs.
gc_bridge_notify_workload_spike =
reinterpret_cast<APerformanceHint_notifyWorkloadSpike_fn> (::dlsym (libandroid, "APerformanceHint_notifyWorkloadSpike"));

log_infof (LOG_DEFAULT, "GC bridge boost: ADPF hint session created (target=%" PRId64 " ns)", GCBridgeHintTargetDurationNs);
log_infof (LOG_DEFAULT, "GC bridge boost: workload-spike pre-boost %s",
gc_bridge_notify_workload_spike != nullptr ? "available" : "unavailable");
}

// Report the just-finished bridge GC duration so ADPF keeps boosting this thread's core. Only ever
// called when a session exists, which guarantees the reporter pointer was resolved alongside it.
void report_gc_bridge_work (int64_t actual_duration_ns) noexcept
{
gc_bridge_report_actual_work_duration (gc_bridge_hint_session, actual_duration_ns);
}
}

void GCBridge::initialize_shared_args_semaphore () noexcept
{
int ret = sem_init (&shared_args_semaphore, 0, 0);
Expand Down Expand Up @@ -110,14 +235,43 @@ void GCBridge::bridge_processing () noexcept
bridge_processing_started_callback (args);

BridgeProcessing bridge_processing {args};
bridge_processing.process ();
if (gc_bridge_hint_session != nullptr) [[likely]] {
// Tell ADPF a CPU workload spike is imminent so it pre-ramps this thread's core *before*
// the GC starts, instead of the governor lagging ~200ms behind the ~10ms burst. Null (a
// no-op) on platforms below API 36; resolved in create_gc_bridge_hint_session().
if (gc_bridge_notify_workload_spike != nullptr) {
gc_bridge_notify_workload_spike (gc_bridge_hint_session, /* cpu */ true, /* gpu */ false, "gc-bridge");
}

// Time the explicit bridge GC and report it to ADPF so the platform keeps this thread's
// carrier core clocked up for the rare, latency-sensitive collection.
int64_t start_ns = monotonic_now_ns ();
bridge_processing.process ();
report_gc_bridge_work (monotonic_now_ns () - start_ns);
} else {
bridge_processing.process ();
}

bridge_processing_finished_callback (args);
}
}

auto GCBridge::bridge_processing_thread_entry ([[maybe_unused]] void *arg) noexcept -> void*
{
// Ask the platform to boost this thread's CPU frequency for the explicit ART GC, so schedutil
// does not leave the mostly-idle bridge thread's core near the bottom of the frequency table.
#if defined (XA_HOST_NATIVEAOT)
// The NativeAOT host does not link the generated application_config symbol (it has its own host and
// never pulls in the CoreCLR config), so the per-app toggle is unavailable here. Enable the boost
// unconditionally; the ADPF entry points are resolved via dlopen/dlsym, so they now work on this
// host too, and it still fails soft on devices without ADPF support.
create_gc_bridge_hint_session ();
#else
if (application_config.gc_bridge_thread_boost_enabled) {
create_gc_bridge_hint_session ();
}
#endif

bridge_processing ();
return nullptr;
}
Expand Down
1 change: 1 addition & 0 deletions src/native/clr/include/xamarin-app.hh
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ struct ApplicationConfig
const char *android_package_name;
bool have_assembly_store;
bool assembly_store_decompression_cache_enabled;
bool gc_bridge_thread_boost_enabled;
};

struct DSOCacheEntry
Expand Down
Loading