Skip to content

perf: scan installed programs off the main actor#117

Merged
frankea merged 1 commit into
mainfrom
perf/async-program-loading
Jun 13, 2026
Merged

perf: scan installed programs off the main actor#117
frankea merged 1 commit into
mainfrom
perf/async-program-loading

Conversation

@frankea

@frankea frankea commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Closes the last performance gap from the upstream PR review: upstream #574 "Async bottle loading."

Problem

updateInstalledPrograms() walked both Program Files trees and parsed every executable's PE header synchronously on the @MainActor-isolated Bottle. Opening or switching to a bottle with many installed programs hitched the UI while it scanned. (The audit flagged this as the one still-open item from Whisky-App#574.)

Change

Move the heavy work off the main actor, following the existing duplicate()/exportAsArchive() pattern in the same file:

  • Bottle.discoverInstalledExecutables(driveC:blocklist:) — a new nonisolated static helper (in WhiskyKit) that does the Program Files walk + filtering (exe extension, ClickOnce cache, noise executables, blocklist). Pure and unit-tested. noiseExecutableNames moved into WhiskyKit alongside it.
  • PE parsing offloaded tooPEFile is Sendable, so the detached task parses each executable and returns (URL, PEFile?). A new Program(url:bottle:peFile:) initializer builds the program on the main actor from the pre-parsed struct (the existing init(url:bottle:) is now a convenience initializer that parses then delegates — all existing call sites unchanged).
  • programsLoading @Published flag drives a progress indicator: the programs-list toolbar shows a spinner in place of the rescan button while a scan runs, and the list refreshes when results publish.
  • All callers (BottleView start-menu scan via .task, the rescan button, ContentView, ProgramMenuView, PinCreationView) updated to await the now-async scan. A re-entrancy guard coalesces redundant concurrent calls.

Tests

New BottleProgramDiscoveryTests (6 cases) covering the pure helper: cross-tree discovery, non-exe exclusion, ClickOnce-cache exclusion, case-insensitive noise exclusion, blocklist exclusion, empty result.

Verification

  • swift test --package-path WhiskyKit — new tests pass.
  • xcodebuild ... -scheme WhiskyBUILD SUCCEEDED.
  • swiftformat --lint . and swiftlint lint --strict — clean.

updateInstalledPrograms() walked the Program Files trees and parsed every
executable's PE header synchronously on the @mainactor Bottle, hitching the
UI when opening or switching to a bottle with many installed programs.

Move the heavy work off the main actor: a new nonisolated
Bottle.discoverInstalledExecutables(driveC:blocklist:) does the directory
walk, and PE parsing runs in the same detached task (PEFile is Sendable).
Programs are then built on the main actor from the pre-parsed structs via a
new Program(url:bottle:peFile:) initializer. A programsLoading flag drives a
progress indicator in the programs list while the scan runs.

Closes Whisky-App#574.
@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

@frankea frankea merged commit 72a0cce into main Jun 13, 2026
10 checks passed
@frankea frankea deleted the perf/async-program-loading branch June 13, 2026 21:53
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