Skip to content

perf: avoid blocking update checks in editor window#954

Merged
Scriptwonder merged 3 commits intoCoplayDev:betafrom
jiajunfeng:fix/editor-window-initial-open-lag
Mar 23, 2026
Merged

perf: avoid blocking update checks in editor window#954
Scriptwonder merged 3 commits intoCoplayDev:betafrom
jiajunfeng:fix/editor-window-initial-open-lag

Conversation

@jiajunfeng
Copy link
Copy Markdown

@jiajunfeng jiajunfeng commented Mar 19, 2026

Summary

  • move the editor window package update check off the Unity main thread
  • add explicit network timeouts for GitHub/Asset Store version fetches
  • prevent repeated update-check queueing while a background check is already running

Changes

  • run CheckForUpdate(...) on a background task and marshal UI updates back with EditorApplication.delayCall
  • add a timeout-configured WebClient wrapper to cap remote version fetch latency at 3 seconds
  • keep the visible update banner behavior unchanged when results are available

Validation

  • rebuilt MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
  • rebuilt MCPForUnity/Editor/Services/PackageUpdateService.cs

Summary by Sourcery

Run Unity editor package update checks asynchronously with bounded network latency while preserving existing update banner behavior.

Bug Fixes:

  • Prevent repeated queuing of editor package update checks while a previous check is still in flight.
  • Avoid UI updates from background update checks when the editor window or its UI elements have been disposed.

Enhancements:

  • Move package update checks off the Unity main thread and marshal results back via delayed UI callbacks.
  • Introduce a timeout-configured WebClient wrapper to cap GitHub and Asset Store version fetches to a short, configurable duration.

Summary by CodeRabbit

  • New Features
    • Per-day cached update results to show instant status when available
    • Background-only network fetches with safe main-thread UI updates and conditional caching
  • Bug Fixes
    • Centralized HTTP request creation with configurable timeout to improve reliability
    • Prevents overlapping update checks and avoids editor UI freezes; failures are handled without throwing into the UI

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 19, 2026

Reviewer's Guide

Runs package update checks for the Unity editor window on a background task instead of the main thread, adds a timeout-configured WebClient for GitHub/Asset Store version fetches, and guards against re-queuing update checks while one is already in flight, without changing visible update banner behavior when results are available.

Sequence diagram for asynchronous package update check in editor window

sequenceDiagram
    actor User
    participant MCPForUnityEditorWindow
    participant EditorApplication
    participant TaskScheduler
    participant PackageUpdateService

    User->>MCPForUnityEditorWindow: Trigger QueueUpdateCheck
    MCPForUnityEditorWindow->>MCPForUnityEditorWindow: QueueUpdateCheck()
    alt updateCheckQueued or updateCheckInFlight is true
        MCPForUnityEditorWindow-->>User: Return (no new check queued)
    else
        MCPForUnityEditorWindow->>EditorApplication: delayCall += CheckForPackageUpdates
    end

    EditorApplication->>MCPForUnityEditorWindow: Invoke CheckForPackageUpdates
    MCPForUnityEditorWindow->>MCPForUnityEditorWindow: updateCheckQueued = false
    MCPForUnityEditorWindow->>MCPForUnityEditorWindow: Get currentVersion
    alt currentVersion invalid
        MCPForUnityEditorWindow->>MCPForUnityEditorWindow: Hide updateNotification
        MCPForUnityEditorWindow-->>User: No update check
    else
        MCPForUnityEditorWindow->>MCPForUnityEditorWindow: updateCheckInFlight = true
        MCPForUnityEditorWindow->>TaskScheduler: Task.Run(CheckForUpdate)
        TaskScheduler->>PackageUpdateService: CheckForUpdate(currentVersion)
        PackageUpdateService-->>TaskScheduler: UpdateCheckResult or null
        TaskScheduler->>EditorApplication: ContinueWith -> delayCall callback

        EditorApplication->>MCPForUnityEditorWindow: Invoke UI update callback
        MCPForUnityEditorWindow->>MCPForUnityEditorWindow: updateCheckInFlight = false
        alt window or UI elements destroyed
            MCPForUnityEditorWindow-->>User: Return (no UI update)
        else
            alt result indicates update available
                MCPForUnityEditorWindow->>MCPForUnityEditorWindow: Set updateNotificationText
                MCPForUnityEditorWindow->>MCPForUnityEditorWindow: Show updateNotification
            else
                MCPForUnityEditorWindow->>MCPForUnityEditorWindow: Hide updateNotification
            end
        end
    end
Loading

Class diagram for PackageUpdateService and TimeoutWebClient changes

classDiagram
    class MCPForUnityEditorWindow {
        -bool toolsLoaded
        -bool resourcesLoaded
        -double lastRefreshTime
        -const double RefreshDebounceSeconds
        -bool updateCheckQueued
        -bool updateCheckInFlight
        +void QueueUpdateCheck()
        +void CheckForPackageUpdates()
    }

    class PackageUpdateService {
        -const int DefaultRequestTimeoutMs
        -string LastCheckDateKey
        -string CachedVersionKey
        -string LastBetaCheckDateKey
        +string FetchLatestVersionFromGitHub(string branch)
        +string FetchLatestVersionFromAssetStoreJson()
        +WebClient CreateWebClient()
        +int GetRequestTimeoutMs()
    }

    class TimeoutWebClient {
        -int _timeoutMs
        +TimeoutWebClient(int timeoutMs)
        +WebRequest GetWebRequest(Uri address)
    }

    MCPForUnityEditorWindow --> PackageUpdateService : uses
    PackageUpdateService o-- TimeoutWebClient : creates
    TimeoutWebClient --|> WebClient
Loading

Flow diagram for update check queuing and in-flight guard

flowchart TD
    A[QueueUpdateCheck called] --> B{updateCheckQueued<br/>or updateCheckInFlight}

    B -- yes --> C[Return without queuing]
    B -- no --> D[Set updateCheckQueued = true]
    D --> E[EditorApplication.delayCall += CheckForPackageUpdates]

    E --> F[CheckForPackageUpdates invoked]
    F --> G[Set updateCheckQueued = false]
    G --> H{currentVersion is null<br/>empty or unknown}

    H -- yes --> I[Hide updateNotification]
    I --> J[Return]

    H -- no --> K[Set updateCheckInFlight = true]
    K --> L["Task.Run(CheckForUpdate)"]
    L --> M[Background CheckForUpdate completes]
    M --> N[EditorApplication.delayCall UI callback]

    N --> O[Set updateCheckInFlight = false]
    O --> P{window or UI destroyed}

    P -- yes --> Q[Return]
    P -- no --> R{Update available<br/>and check succeeded}

    R -- yes --> S[Set updateNotificationText<br/>and tooltip]
    S --> T[Show updateNotification]
    R -- no --> U[Hide updateNotification]

    T --> V[End]
    U --> V[End]
Loading

File-Level Changes

Change Details Files
Move editor window package update check off the Unity main thread and prevent concurrent/duplicate checks while preserving UI behavior.
  • Add updateCheckInFlight flag to track active background update checks and extend QueueUpdateCheck guard to skip when a check is already running.
  • Refactor CheckForPackageUpdates to start MCPServiceLocator.Updates.CheckForUpdate in a Task.Run with try/catch, logging and treating failures as no-update cases.
  • Use ContinueWith plus EditorApplication.delayCall to marshal completion back to the main thread, clear updateCheckInFlight, validate window/UI elements are still valid, and then update or hide the notification banner based on the result.
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Introduce a timeout-configured WebClient for remote version fetches to bound network latency for GitHub and Asset Store update checks.
  • Add DefaultRequestTimeoutMs constant and a GetRequestTimeoutMs virtual method to centralize timeout configuration.
  • Replace direct WebClient instantiations in FetchLatestVersionFromGitHub and FetchLatestVersionFromAssetStoreJson with a new CreateWebClient factory method.
  • Implement TimeoutWebClient subclass that sets Timeout and ReadWriteTimeout on the underlying WebRequest/HttpWebRequest to enforce the configured request timeout.
MCPForUnity/Editor/Services/PackageUpdateService.cs

Possibly linked issues

  • #(none provided): PR makes CheckForPackageUpdates non-blocking with timeouts and in-flight guards, directly mitigating the frequent-freeze issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Adds per-day caching and background-safe fetch APIs to package update checks, centralizes WebClient/timeout creation, and updates the editor window to use cached results first and perform network fetches on a background task, marshaling UI updates back to the main thread.

Changes

Cohort / File(s) Summary
PackageUpdateService
MCPForUnity/Editor/Services/PackageUpdateService.cs
Adds TryGetCachedResult, FetchAndCompare, and CacheFetchResult; centralizes HTTP creation via CreateWebClient() and GetRequestTimeoutMs(); introduces TimeoutWebClient and a 3000ms default timeout; CheckForUpdate now uses CreateWebClient().
Editor window / update flow
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Prevents concurrent checks with updateCheckInFlight/queue gating; new flow: use TryGetCachedResult to apply cached result, otherwise run FetchAndCompare on Task.Run, then CacheFetchResult and ApplyUpdateCheckResult via EditorApplication.delayCall.
Interface
MCPForUnity/Editor/Services/IPackageUpdateService.cs
Exposes new methods TryGetCachedResult(string), FetchAndCompare(string), and CacheFetchResult(string) to separate main-thread prefs access from background network fetch.

Sequence Diagram

sequenceDiagram
    actor User
    participant EditorWindow as Editor Window
    participant UpdateService as PackageUpdateService
    participant WebClient as TimeoutWebClient
    participant Prefs as EditorPrefs

    User->>EditorWindow: QueueUpdateCheck()
    EditorWindow->>EditorWindow: if cached today -> TryGetCachedResult(currentVersion)
    alt cached result exists
        EditorWindow->>EditorWindow: ApplyUpdateCheckResult(cached)
    else no cached result
        EditorWindow->>EditorWindow: start Task.Run -> FetchAndCompare(currentVersion)
        UpdateService->>WebClient: CreateWebClient() / Fetch latest (GitHub or AssetStore)
        WebClient->>UpdateService: HTTP response (version or error)
        UpdateService->>UpdateService: compare versions -> UpdateCheckResult
        UpdateService-->>EditorWindow: result (via Task continuation)
        EditorWindow->>Prefs: CacheFetchResult(currentVersion, fetchedVersion)  rgba(0,128,0,0.5)
        EditorWindow->>EditorWindow: ApplyUpdateCheckResult(fetched)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I sniffed the cache beneath the hay,

Fetched the latest on a quiet day,
Timeouts set, no wild delay,
I hop back to the UI to say:
"New version?" — maybe — "Or not today!" 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request description covers key aspects (summary, changes, validation) but lacks formal alignment with the template's Type of Change, Testing, Documentation Updates, and Related Issues sections. Add missing template sections: explicitly select a Type of Change (likely 'Refactoring' or 'Performance improvement'), include Testing section, confirm documentation updates status, and add Related Issues if applicable.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf: avoid blocking update checks in editor window' directly and clearly describes the main change: making update checks non-blocking by moving them off the main thread.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In CheckForPackageUpdates, updateCheckInFlight is only reset inside the delayed UI callback and after the this/updateNotification null checks, so if the window is closed or the notification elements are null, the method returns early and leaves updateCheckInFlight stuck true, preventing future update checks; move the reset before those early returns (or ensure it's done in a finally-style path) so the flag is always cleared.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `CheckForPackageUpdates`, `updateCheckInFlight` is only reset inside the delayed UI callback and after the `this/updateNotification` null checks, so if the window is closed or the notification elements are null, the method returns early and leaves `updateCheckInFlight` stuck `true`, preventing future update checks; move the reset before those early returns (or ensure it's done in a `finally`-style path) so the flag is always cleared.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs`:
- Around line 382-387: The CheckForUpdate call currently runs on a worker thread
and therefore performs unsafe EditorPrefs access and lazy initialization of
MCPServiceLocator.Updates off the main thread; instead, ensure
MCPServiceLocator.Updates is accessed on the editor thread and read any
EditorPrefs cache (EditorPrefs.GetString/SetString) on the main thread before
starting background work, then call Task.Run only to execute the network methods
(FetchLatestVersionFromGitHub / FetchLatestVersionFromAssetStoreJson) using the
cached values as parameters; finally, marshal the result back with
EditorApplication.delayCall and perform any EditorPrefs writes or
MCPServiceLocator.Updates lazy initialization there.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 446a5176-1e3f-446d-885d-6673965b9e4a

📥 Commits

Reviewing files that changed from the base of the PR and between ec25df8 and b050907.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Services/PackageUpdateService.cs
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs

Address CodeRabbit review feedback on PR CoplayDev#954: CheckForUpdate was running
entirely inside Task.Run, including EditorPrefs reads/writes and
MCPServiceLocator.Updates lazy initialization — both unsafe off the main
thread.

Split CheckForUpdate into three thread-aware methods:
- TryGetCachedResult: reads EditorPrefs cache (main thread)
- FetchAndCompare: network I/O only (background thread safe)
- CacheFetchResult: writes EditorPrefs cache (main thread)

The editor window now resolves the service and checks cache on the main
thread, runs only network I/O in Task.Run, and caches results back on the
main thread via EditorApplication.delayCall.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Services/IPackageUpdateService.cs`:
- Around line 15-19: Update the TryGetCachedResult contract to remove the
incorrect "Safe to call from any thread" claim and instead document that it must
be called on the Unity main thread because it reads EditorPrefs; change the XML
summary to state "Main-thread only" or equivalent. Also make the return type
nullable to match the implementation and comments: change the signature from
UpdateCheckResult TryGetCachedResult(string currentVersion) to
UpdateCheckResult? TryGetCachedResult(string currentVersion). Reference:
TryGetCachedResult, UpdateCheckResult, and EditorPrefs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5580160-5200-498e-bc3f-f2d667df63cc

📥 Commits

Reviewing files that changed from the base of the PR and between b050907 and 4f52b86.

📒 Files selected for processing (3)
  • MCPForUnity/Editor/Services/IPackageUpdateService.cs
  • MCPForUnity/Editor/Services/PackageUpdateService.cs
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
  • MCPForUnity/Editor/Services/PackageUpdateService.cs

Comment on lines +15 to +19
/// <summary>
/// Returns a cached update result if one exists for today, or null if a network fetch is needed.
/// Safe to call from any thread context but designed for main-thread use (reads EditorPrefs).
/// </summary>
UpdateCheckResult TryGetCachedResult(string currentVersion);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is UnityEditor.EditorPrefs documented as safe to access from background threads, or should it be treated as main-thread-only in Unity Editor code?

💡 Result:

UnityEditor.EditorPrefs is not documented as safe to access from background threads. It should be treated as main-thread-only in Unity Editor code. Official Unity documentation for EditorPrefs (across multiple versions, including 2023.1 and current as of 2026) makes no mention of thread safety or background thread usage. All examples show usage in main-thread contexts like EditorWindow.OnGUI methods. Unity's general threading guidelines state that nearly all UnityEngine and UnityEditor APIs are not thread-safe unless explicitly documented otherwise (e.g., NativeContainers for jobs). Editor APIs, being stateful and tied to editor UI/operations, follow this rule. Forum discussions confirm EditorPrefs fails silently or behaves unexpectedly when called from callbacks like server responses (likely background/async), and community consensus lists it implicitly as main-thread-only alongside other Unity APIs. No sources document EditorPrefs as thread-safe, and Unity's pattern requires explicit main-thread dispatchers (e.g., Search.Dispatcher) for other editor APIs when needed.

Citations:


🏁 Script executed:

# Find and read the IPackageUpdateService.cs file to see full implementation
fd "IPackageUpdateService.cs" --type f

Repository: CoplayDev/unity-mcp

Length of output: 173


🏁 Script executed:

# Check project file for NRT (nullable reference types) settings
fd "\.csproj$|\.sln$" --type f | head -5

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Read the full interface file to see the actual signatures and implementation details
cat -n MCPForUnity/Editor/Services/IPackageUpdateService.cs

Repository: CoplayDev/unity-mcp

Length of output: 3515


🏁 Script executed:

# Check for csproj files more carefully
find . -name "*.csproj" -type f | head -5

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Check for csproj file in the repository
find . -type f -name "*.csproj" | head -10

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Also check for any assembly.cs files that might have NRT settings
find . -type f -name "AssemblyInfo.cs"

Repository: CoplayDev/unity-mcp

Length of output: 192


🏁 Script executed:

# Check the AssemblyInfo.cs to see if NRT is enabled
cat -n ./MCPForUnity/Editor/AssemblyInfo.cs

Repository: CoplayDev/unity-mcp

Length of output: 183


🏁 Script executed:

# Also check for an .editorconfig or Directory.Build.props file that might configure NRT
find . -type f \( -name ".editorconfig" -o -name "Directory.Build.props" -o -name "csproj" \) | head -10

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

# Check the implementation file to see if TryGetCachedResult actually reads EditorPrefs
find . -type f -name "*PackageUpdateService.cs" | grep -v ".meta"

Repository: CoplayDev/unity-mcp

Length of output: 171


🏁 Script executed:

# Read the implementation to confirm EditorPrefs usage
fd "PackageUpdateService\.cs" -t f --type f | grep -v ".meta" | grep -v "Interface"

Repository: CoplayDev/unity-mcp

Length of output: 167


🏁 Script executed:

# Read the implementation file
cat -n MCPForUnity/Editor/Services/PackageUpdateService.cs

Repository: CoplayDev/unity-mcp

Length of output: 20656


Fix contradictory thread-safety contract in TryGetCachedResult.

Line 17 says this method is safe on any thread, but it reads EditorPrefs (lines 99–100 of the implementation), which Unity documents as main-thread-only. This direct contradiction can reintroduce off-main-thread bugs. Revise the contract to explicitly state main-thread-only usage.

Additionally, the method returns null (line 113) but the interface signature declares a non-nullable return type. If nullable reference types are enabled, update the signature to UpdateCheckResult? to match the documented behavior ("or null if a network fetch is needed").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Services/IPackageUpdateService.cs` around lines 15 - 19,
Update the TryGetCachedResult contract to remove the incorrect "Safe to call
from any thread" claim and instead document that it must be called on the Unity
main thread because it reads EditorPrefs; change the XML summary to state
"Main-thread only" or equivalent. Also make the return type nullable to match
the implementation and comments: change the signature from UpdateCheckResult
TryGetCachedResult(string currentVersion) to UpdateCheckResult?
TryGetCachedResult(string currentVersion). Reference: TryGetCachedResult,
UpdateCheckResult, and EditorPrefs.

- Remove incorrect "Safe to call from any thread" claim; method reads
  EditorPrefs which requires the Unity main thread
- Add nullable return type annotation to match the null-on-cache-miss behavior
@Scriptwonder Scriptwonder merged commit 56e1e14 into CoplayDev:beta Mar 23, 2026
@Scriptwonder
Copy link
Copy Markdown
Collaborator

Thanks for the feedback!

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.

2 participants