Skip to content

[ios-clr] Deduplicate CreateThread event when a thread runs both R2R and interpreted code#130127

Open
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-debugger-createthread-idempotent
Open

[ios-clr] Deduplicate CreateThread event when a thread runs both R2R and interpreted code#130127
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-debugger-createthread-idempotent

Conversation

@kotlarmilos

@kotlarmilos kotlarmilos commented Jul 2, 2026

Copy link
Copy Markdown
Member

Description

PR #129389 added a second debugger CreateThread announcement for interpreted threads, because the trace-call mechanism that normally announces a thread never fires for purely interpreted code. When corelib is compiled as R2R, for example in Debug on Apple mobile, both announcements happen for the same thread. The precompiled corelib code announces the thread once during startup, and the interpreter announces it again when it reaches interpreted user code. This produces a duplicate thread-attach event and an extra debuggee pause. This change makes the announcement happen at most once per thread using a new per-thread flag named TSNC_DebuggerThreadStartSent.

ThreadStarted checks and sets the flag under the ThreadStore lock, so whichever path arrives first sends the event and the second one does nothing. SendCreateThreadAtInterpreterEntry also checks the flag before it sets up the send-event context, which lets the redundant interpreter announcement skip the unnecessary synchronization pause. The flag is only written from ThreadStarted, which always runs on the thread being announced, so it follows the existing single-writer rule for these flags.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to prevent duplicate managed debugger CreateThread / thread-attach announcements when a thread executes both ReadyToRun (R2R) and interpreted code, by introducing a per-thread “already sent” flag and short-circuiting the redundant path. It also includes an additional (seemingly separate) change that updates the ReadyToRun stripped-IL sentinel and adds a runtime fail-fast with a clearer message when that sentinel is encountered.

Changes:

  • Add Thread::TSNC_DebuggerThreadStartSent and use it to deduplicate Debugger::ThreadStarted and avoid redundant interpreter-side synchronization.
  • Change the stripped-IL sentinel body emitted by Crossgen2 from ldnull; throw to an invalid opcode (0xFE 0x24) and update tests accordingly.
  • Detect the stripped-IL sentinel at runtime and fail-fast with a specific diagnostic message instead of crashing later with less actionable failures.
Show a summary per file
File Description
src/coreclr/vm/threads.h Introduces a new ThreadStateNoConcurrency bit to record whether the debugger thread-start event was already sent.
src/coreclr/debug/ee/debugger.cpp Uses the new per-thread bit to prevent duplicate thread-attach IPC events and to skip redundant interpreter entry work.
src/coreclr/vm/prestub.cpp Adds runtime detection/fail-fast for the stripped-IL sentinel body when IL is unexpectedly required.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILNode.cs Changes the Crossgen2 stripped-IL sentinel body to an invalid opcode sequence.
src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCasesRunner/R2RResultChecker.cs Updates test logic/messages to match the new stripped-IL sentinel sequence.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

Comment thread src/coreclr/vm/prestub.cpp Outdated
…h R2R and interpreted code

PR dotnet#129389 added a second debugger CreateThread announcement for interpreted threads, because the trace-call mechanism that normally announces a thread never fires for purely interpreted code. When corelib is compiled as R2R, for example in Debug on Apple mobile, both announcements happen for the same thread. The precompiled corelib code announces the thread once during startup, and the interpreter announces it again when it reaches interpreted user code. This produces a duplicate thread-attach event and an extra debuggee pause. This change makes the announcement happen at most once per thread using a new per-thread flag named TSNC_DebuggerThreadStartSent. ThreadStarted checks and sets the flag under the ThreadStore lock, so whichever path arrives first sends the event and the second one does nothing. SendCreateThreadAtInterpreterEntry also checks the flag before it sets up the send-event context, which lets the redundant interpreter announcement skip the unnecessary synchronization pause. The flag is only written from ThreadStarted, which always runs on the thread being announced, so it follows the existing single-writer rule for these flags. Reusing the free 0x00000001 slot is safe because no DAC or cDAC contract depends on these flag values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilos force-pushed the interp-debugger-createthread-idempotent branch from 71e59f0 to 7a34f60 Compare July 2, 2026 14:53
@kotlarmilos kotlarmilos changed the title [interp][debug] Deduplicate CreateThread event when a thread runs both R2R and interpreted code [ios-clr] Deduplicate CreateThread event when a thread runs both R2R and interpreted code Jul 2, 2026
@kotlarmilos kotlarmilos added this to the 11.0.0 milestone Jul 2, 2026
Copilot AI review requested due to automatic review settings July 2, 2026 22:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/debug/ee/debugger.cpp
@thaystg

thaystg commented Jul 8, 2026

Copy link
Copy Markdown
Member

LGTM

@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
  "version": 5,
  "last_dispatched_commit": "ec5bcd7dfc9dda2ba6263e54d73e97ba65b9a3ce",
  "last_dispatched_base_ref": "main",
  "last_dispatched_base_sha": "57fc9dc8acd841ecffb733faf8def2cd96fadfd0",
  "last_reviewed_commit": "ec5bcd7dfc9dda2ba6263e54d73e97ba65b9a3ce",
  "last_reviewed_base_ref": "main",
  "last_reviewed_base_sha": "57fc9dc8acd841ecffb733faf8def2cd96fadfd0",
  "last_recorded_worker_run_id": "29680702935",
  "review_attempt_commit": "",
  "review_attempt_base_ref": "",
  "review_attempt_count": 0,
  "max_review_attempts": 5,
  "review_history_format": "holistic-review-disclosure-v1",
  "review_history": [
    {
      "commit": "ec5bcd7dfc9dda2ba6263e54d73e97ba65b9a3ce",
      "review_id": 4730522898
    }
  ]
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: PR #129389 added a second debugger CreateThread announcement for interpreted threads because the trace-call mechanism that normally announces a thread never fires for purely interpreted code. When corelib is R2R-compiled (e.g. Debug on Apple mobile), a thread that runs both precompiled and interpreted code is announced twice: once at startup via the DebuggerThreadStarter patch and again when the interpreter reaches user code. This produces a duplicate DB_IPCE_THREAD_ATTACH event and an extra debuggee pause. The motivation is clear and legitimate.

Approach: A previously-unused Thread non-cacheable state bit is repurposed as TSNC_DebuggerThreadStartSent (0x00000001). Debugger::ThreadStarted now returns early if the bit is set, and otherwise sets it before sending the attach event; Debugger::SendCreateThreadAtInterpreterEntry checks the same bit up front so the redundant interpreter announcement also skips the synchronization pause (PollWaitingForHelper / SENDIPCEVENT_BEGIN). The approach is minimal and idiomatic for this file.

Summary: The change is correct and low-risk. Both paths that call ThreadStarted execute on the thread being announced: the DebuggerThreadStarter::SendEvent patch path (controller.cpp) and SendCreateThreadAtInterpreterEntry (invoked from Thread::GetOrCreateInterpThreadContext on this). The bit is written only inside ThreadStarted, which asserts ThreadHoldsLock(), so the read-then-set is serialized under the ThreadStore lock and respects the existing single-writer convention for TSNC flags. The early check in SendCreateThreadAtInterpreterEntry is an unsynchronized read, but a benign one: the only cost of a stale false read is falling through to ThreadStarted, which re-checks authoritatively under the lock. Repurposing the comment-marked unused 0x00000001 bit is appropriate and the XML-free comment accurately documents its meaning.

No actionable issues were found. One non-blocking observation for the author's awareness: the flag is never cleared, so it persists for the lifetime of the Thread. This is correct for the debugger-attached lifetime, and CoreCLR reattach re-enumerates existing threads through DAC rather than replaying ThreadStarted, so a persistently-set bit does not suppress a needed announcement on reattach. No change is required.

Verdict: LGTM.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 61.4 AIC · ⌖ 10.5 AIC · ⊞ 10K

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants