Skip to content

[TrimmableTypeMap] Fix thread safety in TrimmableTypeMap.Initialize#11049

Open
Copilot wants to merge 3 commits intomainfrom
copilot/fix-thread-safety-in-trimmabletypemap
Open

[TrimmableTypeMap] Fix thread safety in TrimmableTypeMap.Initialize#11049
Copilot wants to merge 3 commits intomainfrom
copilot/fix-thread-safety-in-trimmabletypemap

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.


This section details on the original issue you should resolve

<issue_title>[TrimmableTypeMap] Fix thread safety in TrimmableTypeMap.Initialize()</issue_title>
<issue_description>Part of #10788

Problem

TrimmableTypeMap.Initialize() has a race condition. The Interlocked.CompareExchange correctly ensures only one instance wins the singleton slot, but the losing thread still proceeds to call RegisterBootstrapNativeMethod():

// TrimmableTypeMap.cs:41-48
internal static void Initialize()
{
    var instance = new TrimmableTypeMap();
    var previous = Interlocked.CompareExchange(ref s_instance, instance, null);
    Debug.Assert(previous is null, "TrimmableTypeMap must only be created once.");
    instance.RegisterBootstrapNativeMethod();  // runs even if CompareExchange lost
}

If two threads race on Initialize():

  1. Both create a TrimmableTypeMap instance (expensive — scans type map attributes)
  2. Only one wins the CompareExchange
  3. Both call RegisterBootstrapNativeMethod() — the losing thread registers on its discarded instance, which is a no-op, but it still creates JNI types and calls RegisterNatives unnecessarily

Fix

Add an early return guard after the CompareExchange:

internal static void Initialize()
{
    var instance = new TrimmableTypeMap();
    var previous = Interlocked.CompareExchange(ref s_instance, instance, null);
    if (previous is not null)
        return;
    instance.RegisterBootstrapNativeMethod();
}

Impact

Low — Initialize() is typically called once during startup. But the fix is trivial and prevents unnecessary work + potential double JNI registration.</issue_description>

Comments on the Issue (you are @copilot in this section)

@simonrozsival
Copy link
Copy Markdown
Member

@copilot try again

Agent-Logs-Url: https://github.com/dotnet/android/sessions/cb7028a6-408a-410f-9e71-a8d53aa9c2ec

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member

@copilot continue

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot continue

The fix has been applied in d0513d7. The Initialize() method now returns early if another thread already won the CompareExchange, preventing the losing thread from calling RegisterNatives() unnecessarily.

@simonrozsival simonrozsival changed the title [WIP] Fix thread safety in TrimmableTypeMap.Initialize [TrimmableTypeMap] Fix thread safety in TrimmableTypeMap.Initialize Mar 31, 2026
@simonrozsival simonrozsival marked this pull request as ready for review March 31, 2026 13:34
Copilot AI review requested due to automatic review settings March 31, 2026 13:34
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Mar 31, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts TrimmableTypeMap.Initialize() in Mono.Android to avoid redundant JNI native registration work when multiple threads race to initialize the singleton, supporting the Trimmable Type Map runtime path.

Changes:

  • Add an early-return guard after Interlocked.CompareExchange so only the winning initializer registers JNI natives.
  • Remove the prior debug-only “must only be created once” assertion behavior.

Comment on lines 41 to +46
internal static void Initialize ()
{
var instance = new TrimmableTypeMap ();
var previous = Interlocked.CompareExchange (ref s_instance, instance, null);
Debug.Assert (previous is null, "TrimmableTypeMap must only be created once.");
if (previous is not null)
return;
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert was removed, so using System.Diagnostics; in this file is now unused. Please remove the unused using to avoid analyzer/warning failures in builds that treat warnings as errors.

Copilot uses AI. Check for mistakes.
Comment on lines 41 to 49
internal static void Initialize ()
{
var instance = new TrimmableTypeMap ();
var previous = Interlocked.CompareExchange (ref s_instance, instance, null);
Debug.Assert (previous is null, "TrimmableTypeMap must only be created once.");
if (previous is not null)
return;

instance.RegisterNatives ();
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the early-return, a concurrent caller can return from Initialize() while the winning thread is still in the middle of instance.RegisterNatives(). Since the method’s contract is “initialize and register natives”, consider ensuring other threads either wait for registration to complete (e.g., via Lazy/LazyInitializer or an init-state gate) or only publish s_instance after native registration has succeeded.

Copilot uses AI. Check for mistakes.
@simonrozsival
Copy link
Copy Markdown
Member

@copilot address the review comments. Use a static Lock object instead of Interlocked.

Agent-Logs-Url: https://github.com/dotnet/android/sessions/fa7fcbcb-4960-4629-a4a2-627b90a610ec

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot address the review comments. Use a static Lock object instead of Interlocked.

Done in 5a8136b. The implementation now uses a static Lock with double-checked locking. s_instance is only assigned after RegisterNatives() completes, so concurrent callers either wait inside the lock or see the fully-initialized instance. Also removed the now-unused using System.Diagnostics;.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TrimmableTypeMap] Fix thread safety in TrimmableTypeMap.Initialize()

3 participants