Skip to content

Docs: exceptions lost on Mono ThreadPool work items, and late Task exceptions - #270

Open
krassx wants to merge 5 commits into
mainfrom
docs/mono-pool-task-exceptions
Open

krassx wants to merge 5 commits into
mainfrom
docs/mono-pool-task-exceptions

Conversation

@krassx

@krassx krassx commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

What

Docs only (Documentation~/index.md, CHANGELOG.md). No code changes.

  • Lists the managed-exception hooks the SDK registers (ILogHandler wrapper, logMessageReceivedThreaded fallback with dedup, AppDomain.UnhandledException, TaskScheduler.UnobservedTaskException), checked against Runtime/Internal/ExceptionPipeline.cs.
  • Documents that an exception thrown in a ThreadPool work item on Mono is lost with no SDK involvement; IL2CPP raises AppDomain.UnhandledException and is reported. Workaround: catch in the work item and call Bugsee.LogException.
  • Documents that Task.Run / a dropped async Task is reported only when the faulted task is finalized, so it can be late or missing.

Evidence

Unity 2021.3.45f1; physical Honor WOD-LX1 (Android 15, Mono ARMv7 and IL2CPP ARM64) and iPhone XS (iOS 18.7.9, IL2CPP). The pool loss was reproduced without the Bugsee package on Mono Release and Development. With forced GC, 4 of 18 task cases still did not finalize. Not measured: Windows, other Unity versions. The Mono "why" is from Mono runtime source knowledge, not verified on symbols of this build, and is marked as such in the docs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Summary

Summary by CodeRabbit

  • Documentation
    • Added guidance on managed exceptions that may not be captured, including ThreadPool exceptions and unobserved task faults.
    • Clarified that reporting behavior varies by runtime: some Mono Android ThreadPool exceptions may be missed, while IL2CPP reports them. Faults from dropped tasks may be reported late or not at all.
    • Documented SDK exception hooks, measurement limitations, and the recommendation to catch exceptions or await tasks explicitly.

The documentation describes managed exception capture limits for ThreadPool work items and faults stored in unobserved tasks. It records observed behavior on tested Mono Android and IL2CPP configurations, explains when task faults may be reported, and provides handling guidance. A changelog entry summarizes these points. No public declarations changed.

Change: Other

Merge Risk: Minimal · up to 95801

The documentation clarifies exception-reporting limitations and accurately scopes the observed task results. No merge-blocking issue remains.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title names the main change: documentation of exceptions lost on Mono ThreadPool work items and late Task exceptions. It matches the Documentation~/index.md and CHANGELOG.md changes and the PR obj…
Description check Passed The description states the change is docs only (Documentation~/index.md and CHANGELOG.md) with no code changes. It lists the managed-exception hooks, the Mono ThreadPool loss and the Task.Run/dropped …
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title names the main change: documentation of exceptions lost on Mono ThreadPool work items and late Task exceptions. It matches the Documentation~/index.md and CHANGELOG.md changes and the PR objectives. It is specific and concise.

Full details: Description check

Explanation

The description states the change is docs only (Documentation~/index.md and CHANGELOG.md) with no code changes. It lists the managed-exception hooks, the Mono ThreadPool loss and the Task.Run/dropped async Task reporting behavior, the workaround, and the test evidence. It is directly related to the changeset.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 0 files. (2 skipped: 2 unsupported.)

Finishing Touches
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e9fe1ef9-8f5f-4fbb-a29e-71a727ddddc8
Commits

Reviewing files that changed from the base of the PR and between e16ce2e and 95801f1.

Files selected for processing: 2
  • CHANGELOG.md
  • Documentation~/index.md
Files skipped from review as they are similar to previous changes: 1
  • CHANGELOG.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @krassx's task in 42s —— View job


Review of PR #270: docs on lost ThreadPool exceptions on Mono and late Task exceptions

This is a docs-only change (Documentation~/index.md, CHANGELOG.md). I read it against Runtime/Internal/ExceptionPipeline.cs and the surrounding docs. I did not run any tests or device scenarios. The device measurements in the PR are taken as given.

Checked against the code

  • The four hooks match ExceptionPipeline.Install: the logHandler wrapper, logMessageReceivedThreaded with dedup (IsDuplicate and LogExceptionEcho), AppDomain.UnhandledException, and UnobservedTaskException.
  • UnobservedTaskException is gated by !NET_LEGACY and CaptureUnobservedTaskExceptions, as documented.
  • There is no FirstChanceException hook.
  • The "handled error" and "crash-type" outcomes match the code. Report(..., handled: true) is used for the unobserved-task path and handled: false, syncFlush: true for AppDomain.
  • The "twice on IL2CPP" claim is plausible. The handler echo and the AppDomain path use different dedup keys (DuplicateKey(exception, source)).
  • The "4 of 18" list adds up: one Mono, two Android IL2CPP, one iOS.
  • The existing notes at lines 76–78 use Task-based thread-pool cases, so they do not contradict the new QueueUserWorkItem finding.

Findings (both P3, inline)

  1. Unmeasured claims stated as fact (index.md:203, also line 213). "new Thread ... reported on both backends" and "async void ... reported normally" are not in the measured list. Measure them or mark them as expected.
  2. Scope of the Mono claim (index.md:217). The bold lead sentence generalizes to "the Mono backend", but it was measured only on Unity 2021.3.45f1 (Android ARMv7). Qualify it in the sentence itself, not only in the "Not measured" paragraph.

Minor, not posted inline

  • The CHANGELOG entry is very long for one bullet. It repeats most of the doc section and could be trimmed to a pointer.

Overall risk: Low. There are no code changes, and the only risk is documentation accuracy.

Merge recommendation: Approve once the two wording points are addressed. Neither blocks the merge.

Most important to fix

  1. Mark or measure the new Thread and async void statements.
  2. Scope the Mono ThreadPool claim to the measured Unity version and device.

Positives

  • Measured results, unmeasured items, and the speculative Mono explanation are clearly separated.
  • The reproduction without the Bugsee package shows the loss is not caused by the SDK.
  • The try/catch plus Bugsee.LogException workaround is concrete and usable.

Comment thread Documentation~/index.md Resolved
Comment thread Documentation~/index.md Resolved

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread CHANGELOG.md Resolved
@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @krassx's task in 20s —— View job


Re-review of PR #270 after commit 54955b5

Both findings from my earlier review are fixed. I found nothing new. This is a docs-only change, and I did not run tests or device scenarios.

Earlier findings

  • Unmeasured new Thread and async void claims: fixed. The "What was measured" paragraph now says the same probe also threw from a bare new Thread, an async void method and a coroutine. It states where each was logged and reported.
  • Scope of the Mono claim: fixed. The bold lead sentence in Documentation~/index.md is now qualified with "measured on Unity 2021.3.45f1, Android ARMv7; other Unity versions not measured".

Checked again against the code

  • The four hooks match Runtime/Internal/ExceptionPipeline.cs: the logHandler wrapper, logMessageReceivedThreaded, AppDomain.UnhandledException and UnobservedTaskException. The task hook is gated by !NET_LEGACY and CaptureUnobservedTaskExceptions.
  • Bugsee.LogException and Bugsee.LogUnhandledException are public static methods (Runtime/Bugsee.cs:467 and :474), so the workaround snippet compiles.
  • The CaptureManagedExceptions and CaptureUnobservedTaskExceptions option names exist.

No inline comments this round.

Optional nit: the CHANGELOG bullet is long. It repeats most of the doc section, and could be shortened to a pointer.

  1. Overall risk: Low.
  2. Merge recommendation: Approve. Nothing blocks the merge.
  3. Most important issues to fix: none.
  4. Positives:
    • Measured results, unmeasured items and the unverified Mono explanation are kept clearly separate.
    • The loss was reproduced without the SDK.
    • The try/catch plus Bugsee.LogException workaround is concrete.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Docs-only review of e16ce2e (Documentation~/index.md, CHANGELOG.md). No production change. Checked the new claims against Runtime/Internal/ExceptionPipeline.cs, ManagedExceptionPayload.FromException, Android StartLaunch/Settle vs iOS LaunchNow, the existing unobserved-task notes, and the sample unobserved harness. Did not run tests; CI was still pending at review time.

The headline is right. On Mono, ThreadPool.QueueUserWorkItem(_ => throw ...) is invisible to every hook this pipeline registers (Unity logs nothing, AppDomain.UnhandledException is not raised, process keeps running). That matches ExceptionPipeline (those four subscriptions only) and Unity's own split (staff repro: Mono/Editor ThreadPool does not raise UnhandledException; IL2CPP does; IN-49334, will not fix). IL2CPP reporting twice as handled + crash-type also follows the code: Unity logs via ILogHandler (Report(..., source: LogException), non-fatal unless TreatUnityLogExceptionsAsFatal) and the pipeline also files OnUnhandledException (source: AppDomain, fatal). LogExceptionEcho.DuplicateKey includes source, so those two are not collapsed. UnobservedTaskException reporting as a handled error with the throw stack matches OnUnobservedTaskException plus the single-inner AggregateException unwrap in FromException.

Two P2s in the new section would send integrators to the wrong workaround or the wrong conclusion about UnobservedTaskException.


P2: async void is not reported normally on the Mono pool path this section is about

  • Location: Documentation~/index.md:213
  • Problem: The sentence treats async void like a coroutine: Unity logs it and the SDK reports it. That is true for async void that runs under UnitySynchronizationContext (a typical Start / UI / main-thread callback). It is not true for async void invoked from a ThreadPool thread on Mono. There SynchronizationContext.Current is null, AsyncVoidMethodBuilder.SetException does not post back to Unity, and the throw lands on the same native-invoke + legacy unhandled-exception path this section documents as silent.
  • Impact: This paragraph is the contrast table for background work. Readers who take async void as the safe alternative to QueueUserWorkItem / a dropped Task will still lose the exception on Android Mono, with no SDK involvement.
  • Scenario: ThreadPool.QueueUserWorkItem(_ => Fire()); async void Fire() { throw ...; } or an async void handler subscribed to an event raised from a pool thread. On the measured Mono ARMv7 player this is the pool-loss case, not the coroutine case.
  • Fix: Qualify it: an async void method that runs on the Unity main thread (or otherwise under Unity's synchronization context), and coroutines, are logged and reported. async void called from a ThreadPool work item on Mono is lost the same way as a synchronous throw in that work item; catch and Bugsee.LogException (or do not use async void there).

P2: 4 of 18 did not raise is judged at WaitForPendingFinalizers, which this package already says is not when Unity raises the event

  • Location: Documentation~/index.md:215 (CHANGELOG bullet compresses the same 4 of 18 / never)
  • Problem: The protocol given is GC.Collect() + GC.WaitForPendingFinalizers() twice. This repo already records that UnobservedTaskException follows that by an unpredictable interval: CHANGELOG for CaptureUnobservedTaskExceptions (seen: 14 to 50 s after a forced GC); sample AttachmentTaskE2E.UnobservedRoutine comments the same and waits up to 90 s after Collect/Wait/Collect.
  • Impact: As written, 4/18 cannot be distinguished from still in the well-known delay window. That over-states or not at all beyond process exit / a still-rooted Task, and contradicts the earlier arrives-late contract on line 50.
  • Scenario: Reproducing with only the two GC+finalizer cycles will look like a miss for cases that would have fired at 14-50 s, which is exactly why the sample waits.
  • Fix: If the 4/18 already included a post-finalizer wait, say so (duration, same 90 s cap as the sample). If it did not, either re-judge after that wait or keep late and reserve never for process exit / the Task remaining reachable.

Overall risk: Low

Docs only. The Mono pool-loss claim, the IL2CPP UnhandledException path, the hook list, and the Task finalizer dependency are accurate against the code and against Unity's documented Mono/IL2CPP split.

Merge recommendation

Request changes. The two P2s are wording in a new public limitation section; they will cause people to pick the wrong background pattern or to treat UnobservedTaskException as more hopeless than this package has already measured.

Most important to fix

  1. Qualify async void as main-thread / Unity sync-context only.
  2. Put the post-finalizer wait into the 4/18 protocol, or drop never except process exit / still-rooted Task.

Positives

  • Hook list matches ExceptionPipeline.Install (ILogHandler, logMessageReceivedThreaded + echo drop, UnhandledException, UnobservedTaskException behind !NET_LEGACY and CaptureUnobservedTaskExceptions). No FirstChanceException is correct.
  • Mono pool silence is framed as a runtime fact (reproduced without the package), not an SDK miss, and the why is marked as Mono-source knowledge not verified on this build's symbols.
  • IL2CPP twice, handled + crash-type is the real AppDomain + Unity-log pair (DuplicateKey includes source); worth documenting. Milestone C1's appears-once only covers the ILogHandler/echo pair.
  • Measurement matrix (Unity 2021.3.45f1, Honor WOD-LX1 Mono ARMv7 / IL2CPP ARM64, iPhone XS IL2CPP) and the not-measured list are explicit.

Residual, not blocking: after the native launch and again when it succeeds is not how the bridges call Install. Android installs hooks before Launch and the second Install on success only re-syncs the UnobservedTask subscription; iOS installs once immediately after _bugsee_launch and OnNativeStarted does not install again. The LogUnhandledException alternative in the snippet files a crash-type report for a caught exception (the table above already distinguishes it from LogException).

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread Documentation~/index.md Resolved
Comment thread Documentation~/index.md Resolved

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Deep review of docs-only PR #270 at 54955b5 (Documentation~/index.md, CHANGELOG.md). No production code. Intent: document that Mono ThreadPool.QueueUserWorkItem throws never reach any SDK hook, and that dropped Task.Run / async Task faults are reported only via UnobservedTaskException (late, and claimed sometimes never).

Checked against Runtime/Internal/ExceptionPipeline.cs (ILogHandler wrapper, logMessageReceivedThreaded echo drop, AppDomain.UnhandledException → fatal, TaskScheduler.UnobservedTaskException → handled, no FirstChanceException), LogExceptionEcho.DuplicateKey (source is part of the key, so Unity-log + AppDomain is two reports), Android InstallHooks before launch and again on success, iOS Install once after _bugsee_launch, sample unobserved (AttachmentTaskE2E) and the existing CaptureUnobservedTaskExceptions note.

Prior review threads (e16ce2e) are addressed in 54955b5: new Thread / async void / coroutine are now in the measured paragraph; the Mono lead sentence is scoped to Unity 2021.3.45f1 Android ARMv7; changelog “did not finalize” is now “did not raise UnobservedTaskException”.

New P2: the 4/18 “never” rate is measured with a GC pulse this package already knows is not the observation window for UnobservedTaskException.

Overall risk: Low (docs only)
Merge recommendation: Request changes
Most important to fix: State the post-finalizer wait used for the 4 misses, or drop 4/18 as evidence of “never” (keep “late, or missing if the process exits first”).

Positive: Mono pool loss is reproduced without the package, so it is not an SDK hole. IL2CPP dual-report matches DuplicateKey(..., source) treating LogException and AppDomain as distinct. Workaround snippet is usable. Unverified Mono-runtime “why” is labelled as such.

Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread Documentation~/index.md Resolved
@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @krassx's task in 24s —— View job


Re-review of PR #270 at 0301df3

Docs-only change (Documentation~/index.md, CHANGELOG.md). I read the full diff against origin/main, and checked the hook list and its timing against Runtime/Internal/ExceptionPipeline.cs. I did not run tests or device scenarios. The device measurements are taken as given.

Earlier findings, now fixed by the latest commits

  • Cursor P2 on async void: fixed in 74d9366. The doc now limits "reported normally" to async void on the Unity main thread and to coroutines. An async void started from a Mono pool work item is marked "not measured; expect it to be lost".
  • Cursor P2 on the 4/18 observation window: mostly fixed in 74d9366 and 0301df3. The doc now gives the window (GC at 8 s and 11 s, observed until 16 s). It also says the event can arrive tens of seconds after a collection.
  • Hook timing wording: now says "during launch", which matches the single Install call path.
  • The LogUnhandledException comment was dropped from the workaround snippet.
  • Earlier points on new Thread, async void measurements, the Mono scope, and "did not finalize" were fixed in 54955b5.

New finding (inline, P3)

  • CHANGELOG.md:12: the bullet says "within about 8 s" and "late or never". The doc says the GC pulse ran at 8 s and 11 s and the event was observed until 16 s. A 16 s window cannot show "never", and the doc already says the event can arrive tens of seconds after a collection. Align the changelog with the doc: use "within 16 s", and say "late, or missing if the process exits first" instead of "late or never".

Minor, not inline

  • Documentation~/index.md:213 still says "or not at all if ... no collection finalizes the task". That is fair as a general statement, but it sits next to a 4/18 figure that was measured over a window of only about 16 s.
  1. Overall risk: Low. It is docs only, and the claims are scoped to what was measured.
  2. Merge recommendation: Approve once the changelog wording is aligned. Nothing blocks the merge.
  3. Most important to fix: make the changelog's observation window and its "never" wording match the doc.
  4. Positives:
    • Measured results, unmeasured items and the unverified Mono explanation are kept clearly separate.
    • The pool loss was reproduced without the Bugsee package, so it is not an SDK gap.
    • The try/catch plus Bugsee.LogException workaround is concrete.
    • The hook list matches the pipeline code.

Comment thread CHANGELOG.md Resolved

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Deep review of docs-only PR #270 at 0301df3 (Documentation~/index.md, CHANGELOG.md). No production code. Intent: Mono ThreadPool.QueueUserWorkItem throws never reach any SDK hook; dropped Task.Run / async Task faults are reported only via UnobservedTaskException.

Checked against ExceptionPipeline.Install (ILogHandler wrapper, logMessageReceivedThreaded echo drop, AppDomain.UnhandledException → fatal, TaskScheduler.UnobservedTaskException → handled, no FirstChanceException), LogExceptionEcho.DuplicateKey (source is part of the key), Android InstallHooks before launch / iOS Install after _bugsee_launch, sample AttachmentTaskE2E.UnobservedRoutine (90 s player-loop wait), and the existing CaptureUnobservedTaskExceptions note (CHANGELOG.md:69: 14–50 s after a forced GC). Did not run tests locally. CI at review: Unity C# compile, wrapper-policy, Android wrapper, Windows Editor, and CodeQL passed; iOS bridge tests still pending.

Prior findings addressed in 74d9366 / 0301df3: async void is limited to the Unity main thread, with Mono pool async void marked unmeasured / expect-lost. The docs now state the GC protocol (8 s and 11 s after the throw, observed until 16 s) and that the event can arrive tens of seconds after a collection. Hook timing is “during launch”. LogUnhandledException was dropped from the snippet.

Remaining P2: 0301df3 still presents 4/18 as “late or never” after “about 8 s”, which is not this package’s UnobservedTaskException window.


Overall risk: Low

Docs only. Headline Mono pool-loss, IL2CPP dual-report, hook list, and Task finalizer dependency remain accurate against the pipeline and against Unity IN-49334.

Merge recommendation

Request changes. One P2 left, in the changelog bullet this commit was supposed to fix.

Most important to fix

  1. Stop using 4/18-in-~8 s as evidence of “never”. Match Documentation~/index.md:215 (16 s of the throw, GC at 8 s and 11 s) and this file’s own 14–50 s delay; reserve missing for process exit / a task that is never collected.

Positives

  • Mono pool silence is still framed as a runtime fact (reproduced without the package), with the “why” labelled as Mono-source knowledge not verified on this build’s symbols.
  • IL2CPP twice (handled + crash-type) matches DuplicateKey(..., source) treating LogException and AppDomain as distinct.
  • Measurement matrix and the not-measured list (Windows, other Unity versions, pool async void) are explicit.
  • Workaround snippet now files a handled error (LogException), matching the table above it.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread CHANGELOG.md Resolved
@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @krassx's task in 15s —— View job


Re-review of PR #270 at 027deee

Docs-only change (Documentation~/index.md, CHANGELOG.md). I read the full diff against origin/main. I did not run tests or device scenarios, and I did not re-check the code paths already verified in earlier rounds (hook list against ExceptionPipeline.cs, public Bugsee.LogException). The device measurements are taken as given.

Earlier findings, now fixed

  • Changelog window and "never" (my P3 and Cursor's P2 on CHANGELOG.md:12): fixed in 027deee. The bullet now says "late, or missing if the process exits first". It also gives "4 of 18 did not raise UnobservedTaskException within 16 s of the throw, with forced GC at 8 s and 11 s; the event can arrive tens of seconds after a collection". This matches Documentation~/index.md.
  • async void scope: the doc limits "reported normally" to main-thread async void and coroutines. async void from a Mono pool work item is marked "not measured; expect it to be lost".
  • GC observation window: the doc states the pulses at 8 s and 11 s, observation until 16 s, and that the event can arrive tens of seconds later.
  • Smaller points fixed in earlier commits:
    • The hook timing now says "during launch".
    • The LogUnhandledException comment is gone from the snippet.
    • The new Thread and async void measurements are listed.
    • The Mono claim is scoped to Unity 2021.3.45f1 on Android ARMv7.
    • "did not finalize" became "did not raise UnobservedTaskException".

New findings: none, so no inline comments this round.

Optional nits, not blocking

  • The doc still says the report can be missed "if ... no collection finalizes the task". That is true in general, and it no longer sits beside an over-strong 4/18 claim.
  • The CHANGELOG bullet is long and mostly repeats the doc section. A short pointer would do.
  1. Overall risk: Low. It is docs only, and every claim is scoped to what was measured.
  2. Merge recommendation: Approve. Nothing blocks the merge.
  3. Most important issues to fix: none.
  4. Positives:
    • Measured results, unmeasured items and the unverified Mono explanation are kept clearly separate.
    • The ThreadPool loss was reproduced without the Bugsee package, so it is not an SDK gap.
    • The changelog and the doc now state the same observation window.
    • The try/catch plus Bugsee.LogException workaround is concrete and matches the handled-error semantics described above it.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Deep review of docs-only PR #270 at 027deee (Documentation~/index.md, CHANGELOG.md). No production code. Intent: Mono ThreadPool.QueueUserWorkItem throws never reach any SDK hook; dropped Task.Run / async Task faults are reported only via UnobservedTaskException.

Checked against ExceptionPipeline.Install (ILogHandler wrapper, logMessageReceivedThreaded echo drop, AppDomain.UnhandledException → fatal, TaskScheduler.UnobservedTaskException → handled, no FirstChanceException), LogExceptionEcho.DuplicateKey (source is part of the key, so Unity-log + AppDomain is two reports), Android InstallHooks before launch / again on success vs iOS Install after _bugsee_launch, ManagedExceptionPayload.FromException (single-inner AggregateException unwrap), sample AttachmentTaskE2E.UnobservedRoutine (90 s player-loop wait), and the existing CaptureUnobservedTaskExceptions note (CHANGELOG.md:69: 14–50 s after a forced GC). NullRefE2E’s “thread-pool” case is Task.Run with the Task kept, so it does not contradict the QueueUserWorkItem silence claim. Did not run tests locally (docs-only). CI at review: Unity C# compile, wrapper-policy, Android wrapper, Windows Editor, and CodeQL passed; iOS bridge tests still pending.

Prior findings addressed in 027deee: the remaining changelog P2 is gone. CHANGELOG.md:12 now matches Documentation~/index.md:215: “late, or missing if the process exits first (4 of 18 did not raise UnobservedTaskException within 16 s of the throw, with forced GC at 8 s and 11 s; the event can arrive tens of seconds after a collection)”. Earlier async void / GC-window / hook-timing P2s stay addressed.

No remaining P0–P3.


Overall risk: Low

Docs only. Headline Mono pool-loss, IL2CPP dual-report, hook list, and Task finalizer dependency remain accurate against the pipeline and against Unity IN-49334.

Merge recommendation

Approve.

Most important issues to fix

None remaining.

Positives

  • Mono pool silence is framed as a runtime fact (reproduced without the package), with the “why” labelled as Mono-source knowledge not verified on this build’s symbols.
  • IL2CPP twice (handled + crash-type) matches DuplicateKey(..., source) treating LogException and AppDomain as distinct.
  • Measurement matrix and the not-measured list (Windows, other Unity versions, pool async void) are explicit.
  • Workaround snippet files a handled error (LogException), matching the table above it and the fact that the Mono pool throw does not kill the process.
  • Changelog 4/18 is now a 16 s observation, not a “never” rate, which matches this package’s own 14–50 s / 90 s UnobservedTaskException window.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

krassx and others added 5 commits November 3, 2026 15:24
…ceptions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x
…ements, fix changelog wording

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x
…x hook timing wording

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfQe6YfjwEeceEeuf3jS6x
@krassx
krassx force-pushed the docs/mono-pool-task-exceptions branch from 027deee to 95801f1 Compare November 3, 2026 10:24
@claude

claude Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Claude finished @krassx's task in 15s —— View job


Re-review of PR #270 at 95801f1

Docs-only change (Documentation~/index.md, CHANGELOG.md). I read the full diff against origin/main. I spot-checked that ExceptionPipeline.Install is called from the iOS and Android bridges and that Bugsee.LogException is public (Runtime/Bugsee.cs:467). I did not run tests or device scenarios, and I took the device measurements as given.

Earlier findings: all addressed by the commits on this branch

  • new Thread, async void and coroutine measurements are listed.
  • The Mono claim is scoped to Unity 2021.3.45f1 on Android ARMv7.
  • async void is limited to the Unity main thread; async void from a Mono pool work item is marked "not measured; expect it to be lost".
  • The GC window is stated (GC at 8 s and 11 s, observed until 16 s), and the doc says the event can arrive tens of seconds after a collection.
  • The changelog matches the doc: "late, or missing if the process exits first", 16 s window, no "never".
  • The hook timing now says "during launch".
  • The workaround snippet uses only Bugsee.LogException.

New findings: none, so no inline comments this round.

Optional nits, not blocking

  • The doc still says the report can be missed "if ... no collection finalizes the task". That is true in general, and it is no longer next to an over-strong 4/18 claim.
  • The CHANGELOG bullet is long and mostly repeats the doc section. A short pointer would do.
  1. Overall risk: Low. Docs only, and every claim is scoped to what was measured.
  2. Merge recommendation: Approve. Nothing blocks the merge.
  3. Most important issues to fix: none.
  4. Positives:
    • Measured results, unmeasured items and the unverified Mono explanation are kept clearly separate.
    • The Mono ThreadPool loss was reproduced without the Bugsee package, so it is not an SDK gap.
    • The changelog and the doc state the same observation window.
    • The hook list matches ExceptionPipeline.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review of docs-only PR #270 at 95801f1 (Documentation~/index.md, CHANGELOG.md). No production code. Intent: Mono ThreadPool.QueueUserWorkItem throws never reach any SDK hook; dropped Task.Run / async Task faults are reported only via UnobservedTaskException.

Checked against ExceptionPipeline.Install (ILogHandler wrapper, logMessageReceivedThreaded echo drop, AppDomain.UnhandledException → fatal, TaskScheduler.UnobservedTaskException → handled, no FirstChanceException), LogExceptionEcho.DuplicateKey (source is part of the key, so Unity-log + AppDomain is two reports), AndroidBridge.SendManagedPayload (JNI with no AndroidJniScope), ExceptionPipeline.DispatchPayload (handled reports hop to the main thread), sample SignatureThreadsE2E (attaches JNI on a worker before Bugsee.LogException), and the existing CaptureUnobservedTaskExceptions note (CHANGELOG.md:71: 14–50 s after a forced GC). NullRefE2E’s “thread-pool” case is Task.Run with the Task kept, so it does not contradict the QueueUserWorkItem silence claim. Did not run tests locally (docs-only). CI at review: Unity C# compile, Android wrapper, wrapper-build-id, and CodeQL (actions) passed; wrapper-policy, iOS bridge, Windows Editor, and CodeQL csharp/java still in progress.

Prior findings addressed (rebased as 7ef86a4 / 0110060 / a097978 / 95801f1): async void is limited to the Unity main thread, with Mono pool async void marked unmeasured / expect-lost. Docs and changelog both state the GC protocol (8 s and 11 s after the throw, observed until 16 s) and that the event can arrive tens of seconds after a collection. Changelog says “late, or missing if the process exits first”, not “never”. Hook timing is “during launch”. Workaround snippet files a handled error (LogException).

New P2: the documented Android Mono recovery calls Bugsee.LogException on the pool thread itself. That path does JNI without an attach; this same file already says that aborts the process.


Overall risk: Low

Docs only. Headline Mono pool-loss, IL2CPP dual-report, hook list, and Task finalizer dependency remain accurate against the pipeline and against Unity IN-49334. The new issue is the workaround snippet on the measured Android player, not the limitation itself.

Merge recommendation

Request changes. One P2: do not tell people to call Bugsee.LogException from QueueUserWorkItem on Android without an attach or a hop to the Unity main thread.

Most important issues to fix

  1. Make the pool-thread workaround JNI-safe on Android (Debug.LogException, main-thread hop, or AndroidJNI.AttachCurrentThread / wrap SendManagedPayload in AndroidJniScope).

Positives

  • Mono pool silence is framed as a runtime fact (reproduced without the package), with the “why” labelled as Mono-source knowledge not verified on this build’s symbols.
  • IL2CPP twice (handled + crash-type) matches DuplicateKey(..., source) treating LogException and AppDomain as distinct.
  • Measurement matrix and the not-measured list (Windows, other Unity versions, pool async void) are explicit.
  • Changelog 4/18 is a 16 s observation, not a “never” rate, which matches this package’s own 14–50 s / 90 s UnobservedTaskException window.
Open in Web View Automation 

Sent by Cursor Automation: Bugsee code review

Comment thread Documentation~/index.md
ThreadPool.QueueUserWorkItem(_ =>
{
try { DoWork(); }
catch (Exception e) { Bugsee.LogException(e); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: P2 Medium
Location: Documentation~/index.md:209 (same call in the ContinueWith(..., OnlyOnFaulted) sentence at :213)
Problem: The workaround this section exists to teach is catch + Bugsee.LogException inside ThreadPool.QueueUserWorkItem. On Android that is a public JNI call with no attach. AndroidBridge.SendManagedPayload builds a UnityManagedException and calls Bugsee.logException on the calling thread and does not enter AndroidJniScope. Span APIs and GetLaunched do, because this same file already states the JVM “aborts the process on a JNI call from an unattached thread” (Documentation~/index.md:683; JniAttachPolicy / AndroidJniScope comments: CheckJNI “use of deleted local reference”, Galaxy Tab A 8.0). ExceptionPipeline.DispatchPayload hops handled auto-capture onto the Unity main thread before the same JNI; the public Bugsee.LogException path does not. Sample SignatureThreadsE2E only calls Bugsee.LogException from a worker after AndroidJNI.AttachCurrentThread().
Impact: On the configuration this paragraph measured (Android Mono ARMv7), following the snippet can replace a silently lost exception (process keeps running) with a hard abort. ContinueWith(..., OnlyOnFaulted) plus Bugsee.LogException is the same pool-thread JNI call.
Scenario: Copy the snippet onto the Honor / Unity 2021.3.45f1 Mono player. DoWork() throws, catch runs on a CLR thread-pool thread (IsThreadPoolThread true, the case JniAttachPolicy treats as AttachDetach). Bugsee.LogException → SendManagedPayload → new AndroidJavaObject(...) with no JNIEnv. Auto-capture of UnobservedTaskException would have been safe because it is handled and MainThreadDispatcher.Runs the send; this snippet bypasses that hop.
Fix: Point the snippet at a path that is already safe from a background thread: Debug.LogException(e) (CHANGELOG: background Debug.LogException now reports; the pipeline hops handled sends to main). Or keep Bugsee.LogException and say it must run on the Unity main thread, or after AndroidJNI.AttachCurrentThread() (and detach a pool thread), matching sigthreads. The durable product fix is using (AndroidJniScope.Enter()) around SendManagedPayload, which this docs-only PR does not have to include.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant