Skip to content

Load application favicons without blocking the UI - #4889

Open
mewclouds wants to merge 10 commits into
ChrisTitusTech:mainfrom
mewclouds:perf/favicon-loading
Open

Load application favicons without blocking the UI#4889
mewclouds wants to merge 10 commits into
ChrisTitusTech:mainfrom
mewclouds:perf/favicon-loading

Conversation

@mewclouds

@mewclouds mewclouds commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Type of Change

  • New feature
  • Bug fix
  • Documentation update
  • Refactor
  • UI/UX improvement

Description

This changes how application favicons are loaded on the Install tab. Previously, favicon loading started while application entries were being created, causing favicon setup and request submission to run synchronously, blocking the app for some time.

Application entries now render completely with fallback letters before favicon work begins. Once rendering finishes, a lightweight pending queue feeds a dedicated favicon runspace pool. Only enough operations to fill the pool are active at once, and available capacity is refilled as requests complete.

The dedicated pool uses the machine's available logical processor count, with a minimum of one worker. It remains separate from WinUtil's general runspace pool, so favicon downloads do not compete with installs or other background operations, while preserving the concurrency and safety guarantees built around the dedicated pool.

A thread-safe circuit breaker stops additional requests after 8 consecutive failures. This prevents machines that block Google or have network problems from continuing through the remaining queue. Fallback letters remain visible when requests fail or the circuit opens.

Pending requests, active operations, the polling timer, runspace pool, and circuit breaker are cleaned up when WinUtil closes. This does not add local caching or change applications.json.

Demo

load_favicons_async.mp4

Verification

Manual verification confirmed that the application list renders without hanging before favicon loading begins.

Rough worker-count benchmarking on a 32-logical-processor machine:

  • 8 workers: 9.1 seconds
  • 16 workers: 4.6 seconds
  • 32 workers: 2.5 seconds

Full Pester 5.8.0 suite passes.

Issue related to PR

N/A

@github-actions github-actions Bot added new feature New feature or request ui update UI/UX improvements labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Improved favicon loading with background processing and UI-safe result updates.
    • Favicon URLs are derived from app links with proper escaping.
    • App fallback text remains visible while logos load.
    • Favicon loading begins after app entries finish rendering.
  • Bug Fixes

    • Added cancellation, timeouts, fallback handling, and failure protection for downloads.
    • Improved cleanup and shutdown when the window closes.
    • Improved recovery when favicon loading setup or downloads fail.
  • Tests

    • Expanded coverage for URL generation, loading outcomes, UI updates, cancellation, and cleanup.
    • Updated validation for favicon-related application state.

Walkthrough

The change adds asynchronous favicon loading for app entries. It includes URL construction, queueing, bounded runspace execution, circuit-breaker handling, dispatcher-based WPF updates, fallback rendering, and shutdown cleanup.

Changes

Favicon loading

Layer / File(s) Summary
Favicon URL and queue integration
functions/private/Get-WinUtilFaviconUrl.ps1, functions/private/Initialize-InstallAppEntry.ps1, functions/private/Initialize-InstallCategoryAppList.ps1, functions/private/Start-WinUtilInstallAppRendering.ps1, pester/install-rendering.Tests.ps1
App entries create escaped favicon URLs, keep fallback controls visible, queue requests, and start favicon loading after app rendering.
Asynchronous favicon fetch pipeline
functions/private/Invoke-WinUtilFaviconFetch.ps1, pester/favicon-loading.Tests.ps1
Adds bounded asynchronous downloads, circuit-breaker checks, dispatcher polling, bitmap conversion, fallback updates, operation cleanup, and coverage for scheduling and failure paths.
Pool lifecycle and shutdown cleanup
functions/private/Initialize-WinUtilFaviconRunspacePool.ps1, functions/private/Close-WinUtilFaviconRunspacePool.ps1, scripts/main.ps1, pester/favicon-loading.Tests.ps1
Replaces stale pools, cancels queued and active favicon work, disposes shared resources, and wires cleanup into form closing.
Dynamic state validation
pester/xaml.Tests.ps1, pester/favicon-loading.Tests.ps1
Allows favicon state identifiers and validates URL generation, pool sizing, fetch results, scheduling, cleanup, and shutdown behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: christitustech

Sequence Diagram(s)

sequenceDiagram
  participant AppEntry
  participant InstallRendering
  participant FaviconFetch
  participant RunspacePool
  participant CircuitBreaker
  participant Dispatcher
  participant WPFControls
  AppEntry->>InstallRendering: queue favicon request after app rendering
  InstallRendering->>FaviconFetch: start favicon loading
  FaviconFetch->>CircuitBreaker: check request eligibility
  FaviconFetch->>RunspacePool: submit asynchronous HTTP fetch
  RunspacePool->>CircuitBreaker: report fetch result
  Dispatcher->>FaviconFetch: complete finished operation
  FaviconFetch->>WPFControls: show bitmap or fallback
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description check ✅ Passed The description clearly explains the asynchronous favicon loading changes, dedicated runspace pool, circuit breaker, cleanup, testing, and performance verification.
Title check ✅ Passed The title clearly summarizes the main change: loading application favicons without blocking the user interface.
✨ 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.

@mewclouds
mewclouds marked this pull request as ready for review July 30, 2026 03:15
@coderabbitai coderabbitai Bot added the bug Something isn't working label Jul 30, 2026

@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: 3

🤖 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 `@functions/private/Close-WinUtilFaviconRunspacePool.ps1`:
- Around line 19-31: The Close-WinUtilFaviconRunspacePool cleanup must not call
PowerShell.Stop() for each pending favicon operation, since it can block the
form’s Closing path. Remove the per-operation Stop invocation while retaining
disposal and operation collection cleanup, and ensure the circuit breaker or
runspace pool is closed through the existing non-blocking cleanup path.

In `@functions/private/Initialize-WinUtilFaviconRunspacePool.ps1`:
- Around line 10-16: Update the stale-pool branch in
Initialize-WinUtilFaviconRunspacePool so it disposes and clears only
$sync.FaviconRunspace instead of calling Close-WinUtilFaviconRunspacePool.
Preserve the shared $sync.FaviconCircuitBreaker and $sync.FaviconOperations
entries while recreating the runspace pool.

In `@functions/private/Invoke-WinUtilFaviconFetch.ps1`:
- Around line 1-275: Split the four functions into separate files named after
their primary functions: Initialize-WinUtilFaviconCircuitBreaker,
Complete-WinUtilFaviconFetch, Start-WinUtilFaviconPolling, and
Invoke-WinUtilFaviconFetch. Preserve each function’s implementation and ensure
the project loads or includes all four files wherever this file was previously
referenced.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 32039875-3a3a-4b0a-808e-3db49ddefd2b

📥 Commits

Reviewing files that changed from the base of the PR and between 5f37ef4 and b6b6a52.

📒 Files selected for processing (8)
  • functions/private/Close-WinUtilFaviconRunspacePool.ps1
  • functions/private/Get-WinUtilFaviconUrl.ps1
  • functions/private/Initialize-InstallAppEntry.ps1
  • functions/private/Initialize-WinUtilFaviconRunspacePool.ps1
  • functions/private/Invoke-WinUtilFaviconFetch.ps1
  • pester/favicon-loading.Tests.ps1
  • pester/xaml.Tests.ps1
  • scripts/main.ps1

Comment thread functions/private/Close-WinUtilFaviconRunspacePool.ps1
Comment thread functions/private/Initialize-WinUtilFaviconRunspacePool.ps1
Comment thread functions/private/Invoke-WinUtilFaviconFetch.ps1
@coderabbitai coderabbitai Bot removed the bug Something isn't working label Jul 30, 2026

@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)
pester/favicon-loading.Tests.ps1 (1)

147-158: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the cancellation path this test claims to cover.

The test reports failures after a success but never requests cancellation, so it cannot verify “ignores cancellations.” Rename the test to describe reset behavior or explicitly invoke cancellation and assert post-cancellation behavior.

🤖 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 `@pester/favicon-loading.Tests.ps1` around lines 147 - 158, Update the test
around Initialize-WinUtilFaviconCircuitBreaker so it either renames the test to
cover only consecutive-failure reset or explicitly invokes the circuit breaker’s
cancellation operation and asserts the expected post-cancellation behavior. If
retaining “ignores cancellations” in the name, exercise cancellation through the
relevant FaviconCircuitBreaker member and verify its effect alongside the
existing failure-count assertion.
🤖 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 `@pester/favicon-loading.Tests.ps1`:
- Around line 147-158: Update the test around
Initialize-WinUtilFaviconCircuitBreaker so it either renames the test to cover
only consecutive-failure reset or explicitly invokes the circuit breaker’s
cancellation operation and asserts the expected post-cancellation behavior. If
retaining “ignores cancellations” in the name, exercise cancellation through the
relevant FaviconCircuitBreaker member and verify its effect alongside the
existing failure-count assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0526459-f065-4eaa-96c5-a8fc8594cfd4

📥 Commits

Reviewing files that changed from the base of the PR and between b6b6a52 and 1837a8d.

📒 Files selected for processing (2)
  • functions/private/Initialize-WinUtilFaviconRunspacePool.ps1
  • pester/favicon-loading.Tests.ps1

@ChrisTitusTech

Copy link
Copy Markdown
Owner

@codex review

Check if consolidation is possible with existing runspace init function

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1837a8dbaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread functions/private/Invoke-WinUtilFaviconFetch.ps1 Outdated
Comment thread functions/private/Initialize-InstallAppEntry.ps1 Outdated

@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 (2)
functions/private/Invoke-WinUtilFaviconFetch.ps1 (2)

108-140: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Update the circuit breaker for every favicon failure.

ReportFailure() is only called for NetworkFailure returns from the worker. Complete-WinUtilFaviconFetch also hides empty successful results and exceptions thrown by EndInvoke() without counting them as failures, so eight consecutive failures can bypass the cutoff. Count empty-success and EndInvoke() failures explicitly.

🤖 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 `@functions/private/Invoke-WinUtilFaviconFetch.ps1` around lines 108 - 140,
Update Complete-WinUtilFaviconFetch so FaviconCircuitBreaker.ReportFailure() is
called for every failed favicon attempt, including empty or invalid worker
results and exceptions from EndInvoke(). Preserve the existing fallback
visibility behavior while adding failure reporting in the outer catch and
non-success/empty-result paths, without double-counting failures already
reported by the worker.

212-275: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clean up failed PowerShell submissions.

The operation is registered before BeginInvoke() and Start-WinUtilFaviconPolling. If setup, submission, or polling startup fails, $sync.FaviconOperations[$AppKey] can keep a record with $operation.Handle = $null, and $powershell may not be disposed. Wrap the creation/assignment/submission/polling path in try/catch, remove only the failed operation, dispose $powershell, then rethrow or return a controlled failure.

🤖 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 `@functions/private/Invoke-WinUtilFaviconFetch.ps1` around lines 212 - 275,
Wrap the operation registration, Handle assignment via BeginInvoke, and
Start-WinUtilFaviconPolling call in a try/catch. On failure, remove only the
matching $sync.FaviconOperations[$AppKey] entry, dispose $powershell, and then
rethrow or return the function’s controlled failure result; preserve the
existing cancellation cleanup before registration.
🤖 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 `@functions/private/Invoke-WinUtilFaviconFetch.ps1`:
- Around line 108-140: Update Complete-WinUtilFaviconFetch so
FaviconCircuitBreaker.ReportFailure() is called for every failed favicon
attempt, including empty or invalid worker results and exceptions from
EndInvoke(). Preserve the existing fallback visibility behavior while adding
failure reporting in the outer catch and non-success/empty-result paths, without
double-counting failures already reported by the worker.
- Around line 212-275: Wrap the operation registration, Handle assignment via
BeginInvoke, and Start-WinUtilFaviconPolling call in a try/catch. On failure,
remove only the matching $sync.FaviconOperations[$AppKey] entry, dispose
$powershell, and then rethrow or return the function’s controlled failure
result; preserve the existing cancellation cleanup before registration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1fb60808-b6b9-4e34-9c0c-4ba4c2d4054d

📥 Commits

Reviewing files that changed from the base of the PR and between 1837a8d and 1968542.

📒 Files selected for processing (3)
  • functions/private/Initialize-InstallAppEntry.ps1
  • functions/private/Invoke-WinUtilFaviconFetch.ps1
  • pester/favicon-loading.Tests.ps1
🚧 Files skipped from review as they are similar to previous changes (2)
  • functions/private/Initialize-InstallAppEntry.ps1
  • pester/favicon-loading.Tests.ps1

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 196854264e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread functions/private/Invoke-WinUtilFaviconFetch.ps1
Comment thread functions/private/Invoke-WinUtilFaviconFetch.ps1 Outdated

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

🧹 Nitpick comments (1)
pester/favicon-loading.Tests.ps1 (1)

199-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace source-text checks with executable failure-path tests.

Both tests inspect Invoke-WinUtilFaviconFetch.ps1 as text. Unrelated copies of these fragments can satisfy the tests while runtime behavior is broken.

  • pester/favicon-loading.Tests.ps1#L199-L204: execute completion handling and verify unexpected failures increment the breaker once.
  • pester/favicon-loading.Tests.ps1#L206-L212: force submission failure and verify operation removal, $powershell.Dispose(), and rethrow behavior.
🤖 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 `@pester/favicon-loading.Tests.ps1` around lines 199 - 204, The tests at
pester/favicon-loading.Tests.ps1 lines 199-204 and 206-212 must replace
source-text matching with executable failure-path coverage. Exercise completion
handling to verify an unexpected failure increments FaviconCircuitBreaker
exactly once, and force submission failure to verify operation removal,
$powershell.Dispose(), and exception rethrow; update both test sites
accordingly.
🤖 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.

Nitpick comments:
In `@pester/favicon-loading.Tests.ps1`:
- Around line 199-204: The tests at pester/favicon-loading.Tests.ps1 lines
199-204 and 206-212 must replace source-text matching with executable
failure-path coverage. Exercise completion handling to verify an unexpected
failure increments FaviconCircuitBreaker exactly once, and force submission
failure to verify operation removal, $powershell.Dispose(), and exception
rethrow; update both test sites accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9fbc639b-2cfa-4fd1-be2e-5137516717c6

📥 Commits

Reviewing files that changed from the base of the PR and between 1968542 and 9f57161.

📒 Files selected for processing (2)
  • functions/private/Invoke-WinUtilFaviconFetch.ps1
  • pester/favicon-loading.Tests.ps1
🚧 Files skipped from review as they are similar to previous changes (1)
  • functions/private/Invoke-WinUtilFaviconFetch.ps1

@mewclouds

Copy link
Copy Markdown
Contributor Author

@coderabbitai The comment is technically reasonable, but replacing only these two tests with complex mocks would be inconsistent and disproportionate for this PR. Mocking this would be substantially more complicated. You’d need fake PowerShell objects, async handles, WPF controls, the synchronized operation table, and failure injection for BeginInvoke()/EndInvoke().

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mewclouds, agreed. Replacing only these tests with mocks would require a large, special-purpose test harness for the asynchronous PowerShell and WPF path. That would be inconsistent with the current coverage and disproportionate to this PR.

I withdraw the comment.

🐇


✏️ Learnings added
Learnt from: mewclouds
URL: https://github.com/ChrisTitusTech/winutil/pull/4889

Timestamp: 2026-08-03T18:44:16.508Z
Learning: For the PowerShell favicon-loading workflow, tests that exercise `BeginInvoke()`/`EndInvoke()`, WPF controls, synchronized operation state, and circuit-breaker failures should not be selectively replaced with mocks. Creating faithful fake PowerShell objects, async handles, dispatcher behavior, and failure injection is disproportionately complex and inconsistent with the existing test strategy.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@ChrisTitusTech

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 9f5716180f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@vyas-devgna

Copy link
Copy Markdown
Contributor

@mewclouds , Cool feature, the blocking favicon loads during UI generation is a real pain point. Had a thought on a lighter way to get the same result, wanted to share in case it's useful.

Approach: Instead of a dedicated RunspacePool, a compiled C# WinUtilFaviconCircuitBreaker, and per-operation tracking (6 new files, ~646 lines), this can be done in ~105 lines in 1 file by leaning on what already exists:

  1. Initialize-InstallAppEntry.ps1 queues each app's favicon URL, Image control, and TextBlock fallback into $sync.FaviconQueue (a List[hashtable]). Fallback letters show immediately since the Image starts Collapsed.

  2. Complete-WinUtilInstallAppRendering in Start-WinUtilInstallAppRendering.ps1 fires after all entries render. It drains the queue and calls Invoke-WinUtilFaviconBatch.

  3. Invoke-WinUtilFaviconBatch (single new file) does two things:

    • Background: Uses the existing Invoke-WPFRunspace (no new pool) with a shared HttpClient to download favicon bytes sequentially. Stores raw bytes in $sync.FaviconResults keyed by index. A plain $consecutiveFailures counter stops after 8 failures, same circuit breaker behavior without the C# class.
    • UI thread: A DispatcherTimer polls $sync.FaviconResults every 100ms, creates BitmapImage from bytes, and applies them to the Image controls. All WPF access stays on the UI thread, no cross-runspace control refs.
  4. Cleanup: Close-WinUtilFaviconWork just sets $sync.FaviconCancel = $true, the worker loop checks it and exits. Called from the Form.Add_Closing handler in main.ps1.

The key difference is that WPF control references never cross the runspace boundary, only byte arrays flow through $sync. This avoids the threading pitfalls that the Operation.TargetImage / Operation.Fallback pattern in Complete-WinUtilFaviconFetch could hit.

Happy to open a PR with this if you'd like to compare side by side!

@mewclouds

Copy link
Copy Markdown
Contributor Author

@vyas-devgna Thanks for sharing the alternative. I spent some time comparing it with the current implementation, and I do not think the two approaches are equivalent. We need to be careful here because the differences affect concurrency, cleanup, and how the rest of WinUtil behaves.

The dedicated pool is intentional. It limits favicon downloads to 2 to 8 workers and keeps them separate from installs and other work using the general WinUtil pool. Using Invoke-WPFRunspace would remove that isolation and, as proposed, replace bounded parallel loading with one sequential job that starts only after the app list finishes rendering. It stays off the UI thread, but the downloads themselves are sequential and the job still occupies one slot in the shared pool.

The C# circuit breaker is there because several favicon workers can complete at the same time. Its lock serializes failure counts and success resets, while the cancellation token provides thread-safe cancellation. A plain counter is safe in the suggested approach only because one sequential worker is updating it. If the requests were made concurrent again, shared thread-safe state would still be required.

WPF controls are also not crossing into the worker runspaces. Each worker receives only the URL, connection limit, circuit breaker, and timeout. It returns a status and downloaded byte array. TargetImage and Fallback remain in the UI-side operation record and are only updated by the dispatcher timer.

The cleanup behavior is different as well. Setting a cancellation flag can prevent the next request from starting, but it does not stop or dispose a request that is already running. The current implementation tracks each operation and cleans up the timer, active workers, pool, and circuit breaker when the window closes.

Also, 228 of the 646 added lines are tests. I appreciate the suggestion, but the shorter implementation gets there by changing the concurrency, isolation, and lifecycle behavior. For those reasons, I am going to keep the current design.

@vyas-devgna

Copy link
Copy Markdown
Contributor

@vyas-devgna Thanks for sharing the alternative. I spent some time comparing it with the current implementation, and I do not think the two approaches are equivalent. We need to be careful here because the differences affect concurrency, cleanup, and how the rest of WinUtil behaves.

The dedicated pool is intentional. It limits favicon downloads to 2 to 8 workers and keeps them separate from installs and other work using the general WinUtil pool. Using Invoke-WPFRunspace would remove that isolation and, as proposed, replace bounded parallel loading with one sequential job that starts only after the app list finishes rendering. It stays off the UI thread, but the downloads themselves are sequential and the job still occupies one slot in the shared pool.

The C# circuit breaker is there because several favicon workers can complete at the same time. Its lock serializes failure counts and success resets, while the cancellation token provides thread-safe cancellation. A plain counter is safe in the suggested approach only because one sequential worker is updating it. If the requests were made concurrent again, shared thread-safe state would still be required.

WPF controls are also not crossing into the worker runspaces. Each worker receives only the URL, connection limit, circuit breaker, and timeout. It returns a status and downloaded byte array. TargetImage and Fallback remain in the UI-side operation record and are only updated by the dispatcher timer.

The cleanup behavior is different as well. Setting a cancellation flag can prevent the next request from starting, but it does not stop or dispose a request that is already running. The current implementation tracks each operation and cleans up the timer, active workers, pool, and circuit breaker when the window closes.

Also, 228 of the 646 added lines are tests. I appreciate the suggestion, but the shorter implementation gets there by changing the concurrency, isolation, and lifecycle behavior. For those reasons, I am going to keep the current design.

fair points, and thanks for actually working through the comparison instead of waving it off, you're right that a single sequential worker gives up the bounded concurrency your pool provides, and a cancellation flag alone won't stop a request that's already in flight over the wire, that's a real gap I hadn't accounted for

to be clear the sketch wasn't meant as a like for like replacement, just a lighter starting point aimed at the same isolation goal, something like a small bounded pool of two to four runspaces reusing the Invoke-WPFRunspace pattern instead of a dedicated one, with cancellation handled through a CancellationTokenSource passed into the HTTP calls so in flight requests actually abort, that would keep real concurrency and real cancellation while trimming some of the added surface area

worth weighing that against scale too, applications json sits at 215 entries, not the thousands a heavier isolated pipeline is usually justified for, at that size the failure modes you're guarding against, several workers finishing at once, a machine blocking every request, are real but rare enough in practice that a smaller bounded pool with proper cancellation could likely absorb them without the dedicated C# circuit breaker and the six file split, so the six hundred plus lines might be buying more resilience than the current list actually calls for

your call either way, I respect the work here and the points you raised are genuinely valid and impressive, happy to open a separate PR to compare concretely if that would help, otherwise no objection to shipping as is

@mewclouds

Copy link
Copy Markdown
Contributor Author

Moving this back to draft while I make a few small changes to the favicon scheduling and initial UI rendering. The core concurrency and cleanup design will remain intact.

@mewclouds
mewclouds marked this pull request as draft August 9, 2026 18:37
Render app cards with fallback letters before starting favicon work.
Feed the dedicated pool from a bounded queue while preserving the
circuit breaker, operation tracking, and shutdown cleanup.
@mewclouds
mewclouds marked this pull request as ready for review August 9, 2026 19:08
@mewclouds

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

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

🧹 Nitpick comments (1)
functions/private/Initialize-WinUtilFaviconRunspacePool.ps1 (1)

29-40: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid failing favicon initialization on an invalid environment value.

If NUMBER_OF_PROCESSORS is non-numeric, the [int] cast throws before the pool is created. Start-WinUtilFaviconLoading then clears $sync.FaviconQueue, so favicon loading stops for the session. Use a validated value with a runtime fallback.

Proposed fix
-    $maxthreads = [Math]::Max(1, [int]$env:NUMBER_OF_PROCESSORS)
+    $processorCount = 0
+    if (-not [int]::TryParse($env:NUMBER_OF_PROCESSORS, [ref]$processorCount)) {
+        $processorCount = [System.Environment]::ProcessorCount
+    }
+    $maxThreads = [Math]::Max(1, $processorCount)
🤖 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 `@functions/private/Initialize-WinUtilFaviconRunspacePool.ps1` around lines 29
- 40, Update Initialize-WinUtilFaviconRunspacePool’s processor-count handling to
validate NUMBER_OF_PROCESSORS before converting it to an integer, and use a safe
runtime fallback when the environment value is missing or non-numeric. Ensure
favicon runspace pool creation continues with at least one thread so
Start-WinUtilFaviconLoading does not lose the queued work.
🤖 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.

Nitpick comments:
In `@functions/private/Initialize-WinUtilFaviconRunspacePool.ps1`:
- Around line 29-40: Update Initialize-WinUtilFaviconRunspacePool’s
processor-count handling to validate NUMBER_OF_PROCESSORS before converting it
to an integer, and use a safe runtime fallback when the environment value is
missing or non-numeric. Ensure favicon runspace pool creation continues with at
least one thread so Start-WinUtilFaviconLoading does not lose the queued work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 77d766cd-8ad4-4c92-9fd7-6a7e6b88afa1

📥 Commits

Reviewing files that changed from the base of the PR and between 79a1b68 and d6f5271.

📒 Files selected for processing (2)
  • functions/private/Initialize-WinUtilFaviconRunspacePool.ps1
  • pester/favicon-loading.Tests.ps1
🚧 Files skipped from review as they are similar to previous changes (1)
  • pester/favicon-loading.Tests.ps1

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

Labels

new feature New feature or request ui update UI/UX improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants