Skip to content

Commit

Permalink
[Microsoft.Android.Runtime.NativeAOT] Parse debug.dotnet.log (#9824)
Browse files Browse the repository at this point in the history
Context: https://github.com/dotnet/java-interop/blob/9dea87dc1f3052ed0f9499c9858b27c83e7d139e/samples/Hello-NativeAOTFromAndroid/README.md

The NativeAOT sample and related `Microsoft.Android.Runtime.NativeAOT`
assembly is based in part on the `Hello-NativeAOTFromAndroid` sample
in dotnet/java-interop, which contains this comment about Logging:

> The (very!) extensive logging around JNI Global and Local
> references mean that this sample should not be used as-is for
> startup timing comparison.

…because `adb logcat` is absolutely *spammed* by LREF and GREF logs.

The "solution" in .NET for Android "proper" is to use the
`debug.mono.log` system property to control logging.  By default,
LREF and GREF logs are disabled, but can be enabled by setting
the `debug.mono.log` system property property:

	adb shell setprop debug.mono.log gref       # only grefs
	adb shell setprop debug.mono.log lref,gref  # lrefs & grefs
	adb shell setprop debug.mono.log all        # EVERYTHING

Begin plumbing similar support for a new `debug.dotnet.log` system
property into `JavaInteropRuntime` via a new `DiagnosticSettings`
type.  This allows us to e.g. have LREF and GREF logging
*disabled by default*, allowing for (somewhat) more representative
startup timing comparisons, while allowing these logs to be enabled
when needed.

	adb shell setprop debug.dotnet.log gref         # only grefs
	adb shell setprop debug.dotnet.log lref,gref    # only grefs
	adb shell setprop debug.dotnet.log lref,gref    # both, right now

Additionally, `=PATH` can be appended to redirect gref or lref logs
to the file `PATH`.  Note that `PATH` must be writable by the app,
and that the value of `debug.dotnet.log` must be shorter than 92 bytes:

	adb shell mkdir -p /sdcard/Documents/jonp
	adb shell chmod 777 /sdcard/Documents/jonp
	adb shell setprop debug.dotnet.log gref=/sdcard/Documents/jonp/jni.txt,lref=/sdcard/Documents/jonp/jni.txt

Note that in the above example on a Pixel 8 with Android 15,
`/sdcard/Documents/jonp/jni.txt` can only be written to *once*;
subsequent attempts to write to the file will error out, so we'll
fallback to writing to `adb logcat`:

	E NativeAot: Failed to open log file `/sdcard/Documents/jonp/jni.txt`: System.UnauthorizedAccessException: UnauthorizedAccess_IODenied_Path, /sdcard/Documents/jonp/jni.txt
	E NativeAot:  ---> System.IO.IOException: Permission denied
	…
	D NativeAot:LREF: +l+ lrefc 1 handle 0x723ff2501d/L from thread ''(1)
	…
  • Loading branch information
jonpryor authored Feb 24, 2025
1 parent bae53be commit b217dca
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.IO;
using System.Text;
using System.Runtime.InteropServices;

using Java.Interop;

namespace Microsoft.Android.Runtime;

struct DiagnosticSettings {

public bool LogJniLocalReferences;
private string? LrefPath;

public bool LogJniGlobalReferences;
private string? GrefPath;

private TextWriter? GrefLrefLog;


public TextWriter? GrefLog {
get {
if (!LogJniGlobalReferences) {
return null;
}
return ((LrefPath != null && LrefPath == GrefPath)
? GrefLrefLog ??= CreateWriter (LrefPath)
: null)
??
((GrefPath != null)
? CreateWriter (GrefPath)
: null)
??
new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:GREF");
}
}

public TextWriter? LrefLog {
get {
if (!LogJniLocalReferences) {
return null;
}
return ((LrefPath != null && LrefPath == GrefPath)
? GrefLrefLog ??= CreateWriter (LrefPath)
: null)
??
((LrefPath != null)
? CreateWriter (LrefPath)
: null)
??
new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:LREF");
}
}

TextWriter? CreateWriter (string path)
{
try {
return File.CreateText (path);
}
catch (Exception e) {
AndroidLog.Print (AndroidLogLevel.Error, "NativeAot", $"Failed to open log file `{path}`: {e}");
return null;
}
}

public void AddDebugDotnetLog ()
{
Span<byte> value = stackalloc byte [RuntimeNativeMethods.PROP_VALUE_MAX];
if (!RuntimeNativeMethods.TryGetSystemProperty ("debug.dotnet.log"u8, ref value)) {
return;
}
AddParse (value);
}

void AddParse (ReadOnlySpan<byte> value)
{
while (TryGetNextValue (ref value, out var v)) {
if (v.SequenceEqual ("lref"u8)) {
LogJniLocalReferences = true;
}
else if (v.StartsWith ("lref="u8)) {
LogJniLocalReferences = true;
var path = v.Slice ("lref=".Length);
LrefPath = Encoding.UTF8.GetString (path);
}
else if (v.SequenceEqual ("gref"u8)) {
LogJniGlobalReferences = true;
}
else if (v.StartsWith ("gref="u8)) {
LogJniGlobalReferences = true;
var path = v.Slice ("gref=".Length);
GrefPath = Encoding.UTF8.GetString (path);
}
else if (v.SequenceEqual ("all"u8)) {
LogJniLocalReferences = true;
LogJniGlobalReferences = true;
}
}

bool TryGetNextValue (ref ReadOnlySpan<byte> value, out ReadOnlySpan<byte> next)
{
if (value.Length == 0) {
next = default;
return false;
}
int c = value.IndexOf ((byte) ',');
if (c >= 0) {
next = value.Slice (0, c);
value = value.Slice (c + 1);
}
else {
next = value;
value = default;
}
return true;
}
}
}

static partial class RuntimeNativeMethods {

[LibraryImport ("c", EntryPoint="__system_property_get")]
static private partial int system_property_get (ReadOnlySpan<byte> name, Span<byte> value);

internal const int PROP_VALUE_MAX = 92;

internal static bool TryGetSystemProperty (ReadOnlySpan<byte> name, ref Span<byte> value)
{
int len = system_property_get (name, value);
if (len <= 0) {
return false;
}

value = value.Slice (0, len);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Microsoft.Android.Runtime;

static class JavaInteropRuntime
static partial class JavaInteropRuntime
{
static JniRuntime? runtime;

Expand Down Expand Up @@ -34,14 +34,17 @@ static void JNI_OnUnload (IntPtr vm, IntPtr reserved)
static void init (IntPtr jnienv, IntPtr klass)
{
try {
var settings = new DiagnosticSettings ();
settings.AddDebugDotnetLog ();

var typeManager = new NativeAotTypeManager ();
var options = new NativeAotRuntimeOptions {
EnvironmentPointer = jnienv,
TypeManager = typeManager,
ValueManager = new NativeAotValueManager (typeManager),
UseMarshalMemberBuilder = false,
JniGlobalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:GREF"),
JniLocalReferenceLogWriter = new LogcatTextWriter (AndroidLogLevel.Debug, "NativeAot:LREF"),
JniGlobalReferenceLogWriter = settings.GrefLog,
JniLocalReferenceLogWriter = settings.LrefLog,
};
runtime = options.CreateJreVM ();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<SignAssembly>true</SignAssembly>
<OutputPath>$(_MonoAndroidNETDefaultOutDir)</OutputPath>
<RootNamespace>Microsoft.Android.Runtime</RootNamespace>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down

0 comments on commit b217dca

Please sign in to comment.