Skip to content

fix: Cancelled test runs no longer hang, and log retrieval responds faster#1321

Merged
hatayama merged 9 commits into
v3-betafrom
feature/hatayama/fix-perf-and-memory-leaks
Jun 12, 2026
Merged

fix: Cancelled test runs no longer hang, and log retrieval responds faster#1321
hatayama merged 9 commits into
v3-betafrom
feature/hatayama/fix-perf-and-memory-leaks

Conversation

@hatayama

@hatayama hatayama commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Cancelling a test run mid-execution (e.g. a dropped CLI connection) no longer leaves the request hanging forever.
  • get-logs responds faster on large consoles, and every tool request does less redundant work per call.

User Impact

  • Before: if the CLI cancelled while Unity tests were still running, the request never completed and the test-runner callback stayed subscribed until the Editor restarted. Request teardown and server shutdown also always waited out an extra polling interval.
  • After: cancellation completes the request immediately and cleans up the callback, and teardown stops without the extra delay.
  • get-logs on consoles with thousands of entries is noticeably cheaper: per-entry reflection lookups, triple array copies, and per-call regex rebuilds are gone.

Changes

  • Register the cancellation token so the test-execution await resolves on cancellation.
  • Resolve Unity-internal reflection handles once in the console log retriever instead of per entry.
  • Reuse a shared camelCase serializer and cache the generated parameter schema per tool type; unify the five identical JSON-RPC response serializer settings into one shared instance.
  • Rewrite log filtering as a single reverse pass and keep the last user search pattern compiled.
  • Pass the request token to the disconnect monitor delay, and guard the package-removal event subscription against double registration.
  • Go CLI: use one ticker per wait loop instead of a timer per iteration, read the tool catalog once per readiness probe sequence (was three filesystem walks per tick), and compile the completion-block regex at package level.

Notes for reviewers

  • The ticker change slightly alters retry pacing: time.After restarted the timer after each loop body, while a ticker fires on a fixed schedule. If a loop body (e.g. a slow connection-retry send) runs longer than the poll interval, the next iteration starts immediately from the buffered tick instead of waiting one more interval. Timeout termination conditions are unchanged.

Verification

  • uloop compile after each change: 0 errors, 0 warnings.
  • Full EditMode suite: 1225/1227 passed; the 2 failures are pre-existing source-scan tests on files untouched by this PR (identical bytes to origin/v3-beta).
  • New direct unit tests for log filtering (ordering, limiting, stack-trace handling, argument validation) pass before and after the rewrite.
  • scripts/check-go-cli.sh: formatting, vet, lint, tests, and dist rebuild all pass.
  • Smoke-tested uloop get-logs against a running Editor with the rebuilt dev binary.

hatayama added 6 commits June 12, 2026 23:23
ExecuteTestWithEventNotification awaited a TaskCompletionSource that was
only resolved by the test-completion callback. When the CancellationToken
fired while tests were still running (e.g. dropped CLI connection), the
await never completed, keeping the TestRunnerApi callback subscription
alive indefinitely. Register the token so cancellation resolves the TCS,
letting the using-scoped callback dispose and unsubscribe.
…lookups

GetLogEntryAt resolved the LogEntry type, the GetEntryInternal method, and
three FieldInfo handles from scratch for every console entry, and the mask
helpers re-resolved the consoleFlags property on every call. With thousands
of console entries these repeated reflection lookups dominated get-logs
latency. All handles are now resolved once in the constructor, which also
states the Unity-internal members this class depends on in one place.
Every tool invocation allocated a fresh JsonSerializerSettings plus
CamelCasePropertyNamesContractResolver, throwing away the resolver's
per-type contract cache each time, and every ParameterSchema access
regenerated the schema through full reflection. JsonRpcProcessor likewise
allocated identical serializer settings on all five response paths.

Hold one shared camelCase serializer for parameter conversion, cache the
generated schema per closed TSchema type, and unify the response settings
into a single static readonly instance.
FilterAndLimitLogs copied the entries into three intermediate arrays via
Skip/Reverse/Select; it now fills a single pre-sized array in one reverse
pass. SearchConsoleLogs rebuilt an interpreted Regex from the user pattern
on every call even though AI clients poll with the same pattern; the last
pattern is now kept compiled and reused. Behavior is pinned by new direct
unit tests for LogFilteringService (ordering, limiting, stack-trace
handling, argument validation).
…e-subscription

MonitorClientDisconnectAsync delayed without the request token, so every
request teardown and server shutdown waited out a full poll interval
before the monitor noticed cancellation; the delay is now cancelled
directly. UnityCliLoopPackageRemovalSettingsResetter subscribed to
Events.registeringPackages without the unsubscribe-first guard used by
the other editor-lifetime subscriptions, allowing duplicate handlers if
registration ever ran twice.
Replace per-iteration time.After timers with a single time.NewTicker per
wait loop (pause point, play mode, connection retry, tool readiness,
compile status), matching the existing waitForUnityLockfile idiom and
avoiding a timer allocation per tick. Read the tool catalog once per
readiness probe sequence instead of three times (each read walks the
project for the catalog file), and compile the completion-block regex at
package level like every other pattern in the package.
@coderabbitai

coderabbitai Bot commented Jun 12, 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 21 minutes and 56 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: b6e31cc2-34a1-4aa6-b916-b4093e2ba4f5

📥 Commits

Reviewing files that changed from the base of the PR and between a4f22e0 and 7d264cf.

📒 Files selected for processing (2)
  • Packages/src/Editor/Domain/CliConstants.cs
  • cli/contract.json
📝 Walkthrough

Walkthrough

Caches expensive runtime/serialization resources, replaces per-iteration timers with reusable tickers in CLI polling loops, and fixes cancellation and duplicate-event-handler behavior; adds tests for LogFilteringService.

Changes

Performance Optimization through Resource Caching and Cancellation Improvements

Layer / File(s) Summary
Log retrieval pipeline optimization
Assets/Tests/Editor/LogFilteringServiceTests.cs, Packages/src/Editor/FirstPartyTools/GetLogs/LogFilteringService.cs, Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs, Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs
Manual newest-first iteration replaces LINQ in FilterAndLimitLogs; ConsoleLogRetriever caches Unity LogEntry reflection members at construction and uses them for GetLogEntryAt; LogGetter caches compiled search regex; NUnit tests added for FilterAndLimitLogs.
Cancellation and event handler cleanup
Packages/src/Editor/FirstPartyTools/RunTests/TestFramework/PlayModeTestExecuter.cs, Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs, Packages/src/Editor/Infrastructure/Settings/UnityCliLoopPackageRemovalSettingsResetter.cs
PlayModeTestExecuter registers a cancellation callback to cancel the internal TaskCompletionSource; MonitorClientDisconnectAsync honors the request cancellation token and exits on cancellation; package-removal resetter unsubscribes handlers before re-subscribing.
JSON serialization configuration centralization
Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs, Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
Introduces shared ResponseSerializerSettings for JSON-RPC responses; introduces a shared camel-case JsonSerializer and per-closed-generic cached parameter schema for tool parameter deserialization.
Go CLI polling timer and probe optimization
cli/internal/cli/compile_wait.go, cli/internal/cli/completion.go, cli/internal/cli/connection_retry.go, cli/internal/cli/control_play_mode_wait.go, cli/internal/cli/pause_point_wait.go, cli/internal/cli/tool_readiness.go
Replaces time.After allocations inside loops with time.NewTicker reused across iterations; caches completion-block regex at package level; computes tool capability once per readiness sequence and conditionally skips probes when unavailable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.51% 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 accurately summarizes the two main improvements: test run cancellation no longer hangs, and log retrieval is faster.
Description check ✅ Passed The description provides comprehensive context for the changes, including user impact, technical details, and verification steps.
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/hatayama/fix-perf-and-memory-leaks

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: 2

🤖 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/Common/Console/ConsoleLogRetriever.cs`:
- Around line 44-51: ConsoleLogRetriever currently only validates
_logEntriesType/_logEntryType but lets reflection member lookups
(_consoleFlagsProperty, _getCountMethod, _getEntryInternalMethod, _messageField,
_modeField, _callstackTextStartField) be null and silently degrades
GetLogCount()/GetLogEntryAt(); update the constructor/initialization in
ConsoleLogRetriever so that after performing the reflection lookups you verify
each of those members is non-null and, if any are missing, throw an informative
InvalidOperationException (or similar) that names the missing members and the
expected Unity internal member names; also remove or avoid null-coalescing
fallbacks in GetLogCount()/GetLogEntryAt() so they assume members exist (the
constructor check guarantees this). This ensures a fast, clear failure when
Unity internal reflection contracts change.

In `@Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs`:
- Around line 288-292: GetOrCreateSearchRegex currently constructs user-supplied
Regex without a timeout which can hang SearchConsoleLogs when IsMatch is called;
change the Regex creation in GetOrCreateSearchRegex (and any cache assignment of
_cachedSearchRegex) to use the overload that accepts a TimeSpan (e.g., new
Regex(searchText, RegexOptions.Compiled, TimeSpan.FromMilliseconds(...))) so a
finite timeout is enforced, choose a sensible small timeout value, and leave the
caching logic ( _cachedSearchRegex ) and consumers like SearchConsoleLogs
unchanged except they will now receive a Regex with a timeout.
🪄 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: 3e61bd73-f420-4d68-b9ab-1bc1120754a2

📥 Commits

Reviewing files that changed from the base of the PR and between 0392ca9 and 61edd80.

⛔ Files ignored due to path filters (1)
  • Assets/Tests/Editor/LogFilteringServiceTests.cs.meta is excluded by none and included by none
📒 Files selected for processing (15)
  • Assets/Tests/Editor/LogFilteringServiceTests.cs
  • Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs
  • Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs
  • Packages/src/Editor/FirstPartyTools/GetLogs/LogFilteringService.cs
  • Packages/src/Editor/FirstPartyTools/RunTests/TestFramework/PlayModeTestExecuter.cs
  • Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs
  • Packages/src/Editor/Infrastructure/Settings/UnityCliLoopPackageRemovalSettingsResetter.cs
  • Packages/src/Editor/Infrastructure/UnityCliLoopBridgeServer.cs
  • Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
  • cli/internal/cli/compile_wait.go
  • cli/internal/cli/completion.go
  • cli/internal/cli/connection_retry.go
  • cli/internal/cli/control_play_mode_wait.go
  • cli/internal/cli/pause_point_wait.go
  • cli/internal/cli/tool_readiness.go

Comment thread Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs Outdated

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

3 issues found across 16 files

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

Re-trigger cubic

Comment thread Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs Outdated
Comment thread Packages/src/Editor/ToolContracts/UnityCliLoopTool.cs
hatayama added 2 commits June 13, 2026 00:08
Review feedback: the cached reflection members could be null after an
internal Unity API change, and the per-method null guards silently
degraded into zero counts and empty log data. Validate all members at
construction like the existing type checks, and remove the now
unreachable null guards and ConsoleWindow fallback.
Review feedback: a pathologically backtracking user pattern could stall
the Editor main thread while get-logs matches every console entry. The
cached compiled regex now carries a two-second match timeout.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs (1)

31-31: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused ConsoleWindow type lookup.

This line retrieves the ConsoleWindow type but discards the result. Based on the summary, this appears to be leftover from the removed GetMaskFromConsoleWindow fallback.

🧹 Suggested fix
             _logEntriesType = editorAssembly.GetType("UnityEditor.LogEntries");
-            editorAssembly.GetType("UnityEditor.ConsoleWindow");
 
             if (_logEntriesType == null)
🤖 Prompt for 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.

In `@Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs` at
line 31, Remove the stray unused type lookup call
editorAssembly.GetType("UnityEditor.ConsoleWindow") from ConsoleLogRetriever.cs
since its return value is discarded and the GetMaskFromConsoleWindow fallback
was removed; locate the call in the body of the ConsoleLogRetriever class (where
editorAssembly is used) and delete that single statement to clean up dead code.
🤖 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.

Outside diff comments:
In `@Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs`:
- Line 31: Remove the stray unused type lookup call
editorAssembly.GetType("UnityEditor.ConsoleWindow") from ConsoleLogRetriever.cs
since its return value is discarded and the GetMaskFromConsoleWindow fallback
was removed; locate the call in the body of the ConsoleLogRetriever class (where
editorAssembly is used) and delete that single statement to clean up dead code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7fd73692-764b-40e5-acf7-96aff30e9ddd

📥 Commits

Reviewing files that changed from the base of the PR and between 61edd80 and a4f22e0.

📒 Files selected for processing (2)
  • Packages/src/Editor/FirstPartyTools/Common/Console/ConsoleLogRetriever.cs
  • Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Packages/src/Editor/FirstPartyTools/Common/Console/LogGetter.cs

The repository convention advances cli/contract.json and
MINIMUM_REQUIRED_CLI_VERSION in lockstep whenever Go CLI sources change;
the build-cli gate enforces it. This PR's CLI changes are internal
optimizations with no new contract dependency, so this is the mechanical
one-step bump. Version gate tests reference the constant symbolically
and pass unchanged.
@hatayama
hatayama merged commit ee81068 into v3-beta Jun 12, 2026
8 checks passed
@hatayama
hatayama deleted the feature/hatayama/fix-perf-and-memory-leaks branch June 12, 2026 15:36
@github-actions github-actions Bot mentioned this pull request Jun 12, 2026
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