Skip to content

fix: Unity tools no longer hang while waiting for editor frames#1333

Merged
hatayama merged 5 commits into
v3-betafrom
feature/fix-screenshot
Jun 14, 2026
Merged

fix: Unity tools no longer hang while waiting for editor frames#1333
hatayama merged 5 commits into
v3-betafrom
feature/fix-screenshot

Conversation

@hatayama

@hatayama hatayama commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Unity tools now return timeout responses instead of silently hanging when Editor frame progress stalls.
  • Input and screenshot cleanup paths now stay safe when timeout continuations resume away from the editor thread.

User Impact

  • Before this change, screenshot capture or input simulation could appear frozen if Unity stopped advancing editor frames or the main thread was stalled.
  • After this change, affected commands can report a timeout, preserve cleanup work, and avoid turning handled timeouts into internal failures.

Changes

  • Replace raw editor-frame waits with timeout-aware waits across screenshot capture, input simulation, dynamic code reload detection, and recording countdowns.
  • Keep timeout continuations from touching Unity APIs off the editor thread.
  • Preserve timeout details in screenshot responses and add focused regression guards for the high-risk paths.

Verification

  • git diff --check
  • cli/dist/darwin-arm64/uloop compile --project-path $(git rev-parse --show-toplevel)
  • cli/dist/darwin-arm64/uloop run-tests --project-path $(git rev-parse --show-toplevel) --test-mode EditMode --filter-type regex --filter-value .StaticFacadeStateGuardTests.
  • cli/dist/darwin-arm64/uloop run-tests --project-path $(git rev-parse --show-toplevel) --test-mode PlayMode --filter-type regex --filter-value .SimulateMouseUiTests.
  • codex-review v3-beta: clean, no accepted or actionable findings

Review in cubic

hatayama added 3 commits June 14, 2026 06:36
Race screenshot frame waits against a timer-based timeout so stalled editor frames return a clear timeout response instead of leaving the CLI call busy.

Keep typed tool continuations and execution-slot cleanup off captured Unity synchronization contexts, then switch screenshot continuations back through the main-thread dispatcher before touching Unity APIs.
Expose only timeout-guarded editor frame waits so CLI tools cannot wait forever when Unity frames stop advancing.

Move remaining production callers to WaitFramesOrTimeoutAsync and switch back to the Unity main thread before touching Unity APIs where needed.
Keep timeout and cleanup paths from recapturing stalled editor continuations or touching Unity APIs off the main thread.
- marshal dynamic-code reload signal cleanup, delayed recording cleanup, and mouse UI cleanup back to the editor thread
- make debug logging safe when timeout continuations resume off-thread
- add focused guards for delayed recording, screenshot cleanup, mouse UI timeout state, and logger environment collection
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@hatayama, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 20 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d186181b-7cc9-490a-8893-4622c968555d

📥 Commits

Reviewing files that changed from the base of the PR and between 99ae2a2 and cf9d261.

📒 Files selected for processing (6)
  • Assets/Editor/EditorDelayManualTests.cs
  • Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs
  • Packages/src/Editor/FirstPartyTools/RecordInput/Application/RecordingsApplicationFacade.cs
  • Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputUseCase.cs
  • Packages/src/Editor/ToolContracts/TimerDelay.cs
📝 Walkthrough

Walkthrough

Replaces EditorFrameWaiter.WaitFramesAsync with a new timeout-returning WaitFramesOrTimeoutAsync API, rewrites TimerDelay.WaitThenExecuteOnMainThread to post actions via SynchronizationContext with timeout guard, propagates ConfigureAwait(false) across all tool layers, adds TimedOut/Message fields to screenshot results, refactors SimulateMouseUi and RecordInput flows for safe off-thread cleanup, hardens DynamicCode disposal with atomic guards and VibeLogger with thread-aware initialization, and extends test coverage.

Changes

Timeout-aware frame waiting and thread-safe tool execution

Layer / File(s) Summary
Core frame-wait API, timeout constant, and TimerDelay
Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs, Packages/src/Editor/ToolContracts/EditorFrameWaiter.cs, Packages/src/Editor/ToolContracts/TimerDelay.cs
Adds EDITOR_FRAME_WAIT_TIMEOUT_MS = 5000; replaces WaitFramesAsync with WaitFramesOrTimeoutAsync(frameCount, timeoutMs, ct) returning bool; makes EditorFrameWaiterService internal; rewrites WaitThenExecuteOnMainThread to post action via captured SynchronizationContext using TaskCompletionSource, interlocked completion guard, and timeout exception handling.
ConfigureAwait(false) propagation and timeout migration
Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs, Packages/src/Editor/Application/UnityCliLoopToolExecutionService.cs, Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs, Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs, Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputTool.cs, Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiTool.cs, Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemUpdateHelper.cs, Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs
Adds .ConfigureAwait(false) to all tool ExecuteAsync awaits; replaces hard-coded 5000 timeouts with EDITOR_FRAME_WAIT_TIMEOUT_MS; updates WaitOneRuntimeFrameOrTimeout and FinalizePressOverlay to use WaitFramesOrTimeoutAsync for timeout-aware waiting.
Screenshot timeout pipeline
Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs, Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs, Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs, Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs, Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotTool.cs, Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs
Adds TimedOut and Message to internal and response types; updates CaptureWindowAsync/CaptureGameRenderingAsync to return (texture, timedOut) tuples with frameWaitTimeoutMilliseconds parameter and CapturedEditorSynchronizationContext switching; refactors ScreenshotUseCase for overlay timeout early-return and thread-safe DestroyAnnotationOverlay posting; makes ScreenshotTool.ToResponse internal and maps new fields.
SimulateMouseUi: timeout-aware animations and frame-by-frame main-thread marshaling
Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
Captures main-thread SynchronizationContext at entry and uses it to post cleanup actions; all Execute* methods await with ConfigureAwait(false) and use per-frame WaitForEditorFrameAndSwitchToMainThreadAsync helper that returns early on timeout; PlayExpandAnimation/PlayDissipateAnimation/InterpolateDragPosition return bool for success/failure; pointer/drag cleanup via ExecuteCleanupOnMainThread in finally blocks; adds result factory helpers for consistent timeout/success payloads.
RecordInput: delayed countdown with cancellation-safe main-thread cleanup
Packages/src/Editor/FirstPartyTools/RecordInput/Application/RecordingsApplicationFacade.cs, Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputUseCase.cs, Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputTool.cs
Replaces fire-and-forget TimerDelay calls with StartDelayedRecordingAsync/ExecuteDelayedStartAsync helpers that track RecordInputDelayedStartOutcome; uses ConfigureAwait(false) and explicit MainThreadSwitcher.SwitchToMainThread for thread affinity; queues CleanupCountdownOnMainThreadAsync when delay is interrupted off-thread, matching generation to prevent stale cleanups.
DynamicCodeDomainReloadWaitSignal: atomic disposal and volatile signal observation
Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeDomainReloadWaitSignal.cs
Introduces _isDisposed atomic flag via Interlocked.Exchange to prevent double unsubscription; Dispose() schedules UnsubscribeFromEditorEventsOnMainThreadAsync when off the editor thread; ShouldWaitAsync uses Volatile.Read for signal, re-checks ShouldRequestWait after main-thread switch, and uses WaitFramesOrTimeoutAsync with post-timeout re-evaluation; event handlers use Volatile.Write for visibility.
VibeLogger: thread-aware initialization, domain-reload state, and lazy log-directory
Packages/src/Editor/ToolContracts/VibeLogger.cs
Adds [InitializeOnLoadMethod] hook → InitializeForEditorLoad() to cache editor main-thread ID and pre-populate log directory; makes GetEnvironmentInfo() thread-aware to report "unavailable off main thread" when invoked off the cached editor thread; refactors SaveLogToFile/CleanupOldLogFiles to use lazily resolved GetLogDirectory() with per-thread fallback.
Tests: manual frame-wait routing, DynamicCode disposal, static source guards, execution service
Assets/Editor/EditorDelayManualTests.cs, Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs, Assets/Tests/Editor/StaticFacadeStateGuardTests.cs, Assets/Tests/Editor/UnityCliLoopToolExecutionServiceTests.cs
Routes manual tests through WaitFramesForManualTestAsync helper; adds off-main-thread disposal test with QueuedMainThreadDispatcher scaffolding; adds six static source-text tests asserting delayed-recording cleanup, screenshot reference-null checks, window-capture timeout preservation, dynamic-code main-thread switching, mouse-UI captured-state, and VibeLogger thread-gating; adds ExecuteToolAsync test under BlockingSynchronizationContext and screenshot timeout-to-response mapping.

Sequence Diagram(s)

sequenceDiagram
  participant Tool as ScreenshotTool
  participant UseCase as ScreenshotUseCase
  participant EditorFrameWaiter
  participant Utility as EditorWindowCaptureUtility
  participant EditorContext as Editor SynchronizationContext

  Tool->>UseCase: CaptureAsync(request).ConfigureAwait(false)
  UseCase->>UseCase: capture SynchronizationContext
  UseCase->>EditorFrameWaiter: WaitFramesOrTimeoutAsync(2, 5000ms, ct)
  alt frames observed within timeout
    EditorFrameWaiter-->>UseCase: true
    UseCase->>EditorContext: SwitchToMainThread
    UseCase->>Utility: CaptureWindowAsync(window, scale, 5000, ct)
    Utility-->>UseCase: (texture, timedOut=false)
    UseCase-->>Tool: Screenshots result
  else timeout elapsed
    EditorFrameWaiter-->>UseCase: false
    UseCase->>EditorContext: Post(DestroyAnnotationOverlay)
    UseCase-->>Tool: TimedOut=true, Message, partial Screenshots
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • hatayama/unity-cli-loop#1306: Both PRs overhaul EditorFrameWaiter.WaitFramesOrTimeoutAsync and TimerDelay.WaitThenExecuteOnMainThread to prevent indefinite hangs when editor updates or async continuations stall.
  • hatayama/unity-cli-loop#1164: Both PRs modify UnityCliLoopToolExecutionService.ExecuteToolAsync—the main PR adds ConfigureAwait(false) while the retrieved PR adds execution gating—affecting the same method signature and execution flow.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing hanging behavior in Unity tools by implementing timeout-aware frame waits instead of silent hangs.
Description check ✅ Passed The description comprehensively explains the motivation, changes, user impact, and verification steps related to the changeset's timeout-aware frame waiting mechanism.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix-screenshot

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeDomainReloadWaitSignal.cs`:
- Around line 51-54: The ShouldRequestWait method call is accessing
EditorApplication.isCompiling before switching to the main thread, which is
unsafe when the method can be entered off-thread. Move the
MainThreadSwitcher.SwitchToMainThread call to occur before the ShouldRequestWait
invocation (which includes the EditorApplication.isCompiling access) to ensure
main thread safety before any UnityEditor API access.

In
`@Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs`:
- Around line 83-86: The ExecuteAsync method has await expressions at lines 50,
51, and 53 (WarmForegroundExecutionPathIfNeededAsync and related awaits) that
lack ConfigureAwait(false), while ConfigureAwait(false) is only added later at
line 85 for ShouldWaitAsync. To ensure consistent timeout-safe behavior and
prevent capturing the editor's stalled SynchronizationContext throughout the
entire method execution, add ConfigureAwait(false) to all await expressions in
the ExecuteAsync method, starting with the early awaits at lines 50, 51, and 53,
and continuing through any other awaits in the method's execution flow.

In
`@Packages/src/Editor/FirstPartyTools/RecordInput/Application/RecordingsApplicationFacade.cs`:
- Around line 252-295: The delayed-start callbacks across
RecordingsApplicationFacade.cs (methods StartDelayedRecordingAsync,
CleanupCountdownOnMainThreadAsync, and StartRecordingAfterCountdown) and
RecordInputUseCase.cs (the delayed-start callback and cleanup path around lines
235-293) use generation checks to gate state mutations, but stale generations
can still execute cleanup or recording logic on the current overlay state. For
RecordingsApplicationFacade.cs, add an early-return guard in
CleanupCountdownOnMainThreadAsync so that if the generation is stale (not equal
to _countdownGeneration), the method returns immediately without clearing
overlay state; also ensure StartRecordingAfterCountdown checks generation before
starting recording. For RecordInputUseCase.cs, introduce an invocation-scoped
generation and cancellation token around the entire delayed-start callback and
cleanup path (similar to the facade's _countdownGeneration pattern) so that
earlier RecordInputAsync(Start) invocations cannot affect overlay state once a
newer countdown or recording path has taken control.

In `@Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs`:
- Around line 213-221: When the EditorWindow capture times out at the timedOut
check in ScreenshotUseCase.cs, the current code returns a fresh timeout result
via CreateTimedOutResult() which discards the screenshots already collected from
previous iterations. Instead, modify the timeout handling to preserve the
already-captured window metadata by including the screenshots list that has been
populated in earlier loop iterations in the result being returned, so the CLI
response accurately reflects the side effects (PNGs written to disk) even when a
later window hits the timeout.

In `@Packages/src/Editor/ToolContracts/TimerDelay.cs`:
- Around line 108-121: The timeoutTask created by the Wait method is never
canceled when completionSource.Task completes successfully, causing the
background timer to remain active for the full EDITOR_FRAME_WAIT_TIMEOUT_MS
duration. Add a cancel operation for timeoutTask on the non-timeout
path—specifically, when completedTask does not equal timeoutTask, cancel the
timeout wait so that the timer/callback is not left running unnecessarily. This
should be done before or after the final await of completionSource.Task,
mirroring the pattern used in EditorFrameWaiter.WaitFramesOrTimeoutAsync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee8adaad-512f-4bc6-b656-3751297e470e

📥 Commits

Reviewing files that changed from the base of the PR and between 148c109 and 64b9da3.

⛔ Files ignored due to path filters (4)
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/UnityCLILoop.FirstPartyTools.ExecuteDynamicCode.Editor.asmdef is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/RecordInput/UnityCLILoop.FirstPartyTools.RecordInput.Editor.asmdef is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/Screenshot/UnityCLILoop.FirstPartyTools.Screenshot.Editor.asmdef is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/UnityCLILoop.FirstPartyTools.SimulateMouseUi.Editor.asmdef is excluded by none and included by none
📒 Files selected for processing (26)
  • Assets/Editor/CaptureTest/EditorWindowCaptureTest.cs
  • Assets/Editor/EditorDelayManualTests.cs
  • Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs
  • Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
  • Assets/Tests/Editor/UnityCliLoopToolExecutionServiceTests.cs
  • Packages/src/Editor/Application/UnityCliLoopToolExecutionService.cs
  • Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputSystemUpdateHelper.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCodeDomainReloadWaitSignal.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeTool.cs
  • Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/ExecuteDynamicCodeUseCase.cs
  • Packages/src/Editor/FirstPartyTools/RecordInput/Application/RecordingsApplicationFacade.cs
  • Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputTool.cs
  • Packages/src/Editor/FirstPartyTools/RecordInput/RecordInputUseCase.cs
  • Packages/src/Editor/FirstPartyTools/Screenshot/EditorWindowCaptureUtility.cs
  • Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotResponse.cs
  • Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotTool.cs
  • Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs
  • Packages/src/Editor/FirstPartyTools/Screenshot/UnityCliLoopScreenshotTypes.cs
  • Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiTool.cs
  • Packages/src/Editor/FirstPartyTools/SimulateMouseUi/SimulateMouseUiUseCase.cs
  • Packages/src/Editor/ToolContracts/EditorFrameWaiter.cs
  • Packages/src/Editor/ToolContracts/TimerDelay.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
  • Packages/src/Editor/ToolContracts/VibeLogger.cs

Comment thread Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs
Comment thread Packages/src/Editor/ToolContracts/TimerDelay.cs

@cubic-dev-ai cubic-dev-ai 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.

5 issues found across 30 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Packages/src/Editor/ToolContracts/TimerDelay.cs Outdated
Comment thread Assets/Editor/EditorDelayManualTests.cs Outdated
Comment thread Packages/src/Editor/FirstPartyTools/Screenshot/ScreenshotUseCase.cs Outdated
hatayama added 2 commits June 14, 2026 14:39
Switch dynamic-code reload waiting back to the editor thread before reading EditorApplication state, and keep already captured screenshots in timeout responses so CLI output matches files written before a later timeout.
Add timeout-safe ConfigureAwait usage through the dynamic-code workflow, cancel the losing TimerDelay timeout after posted actions complete, and bind delayed record-input callbacks to their active countdown generation.

Manual frame-wait checks now report timeout failures instead of logging completion after a timed-out wait.
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