Skip to content

fix: prevent UI freeze when loading large remote hosts files#242

Open
2ndalpha wants to merge 7 commits intomasterfrom
fix/large-remote-file-performance
Open

fix: prevent UI freeze when loading large remote hosts files#242
2ndalpha wants to merge 7 commits intomasterfrom
fix/large-remote-file-performance

Conversation

@2ndalpha
Copy link
Owner

@2ndalpha 2ndalpha commented Mar 1, 2026

Summary

  • Large remote hosts files (~1MB, e.g. StevenBlack/hosts) caused the app to freeze for several seconds due to synchronous syntax highlighting processing ~77K lines on the main thread
  • Refactored HostsTextView to highlight large documents (>50KB) asynchronously in ~100KB chunks, yielding to the run loop between each chunk
  • Replaced eager .draggable(hosts.contents()) in SidebarView with lazy .onDrag using NSItemProvider so sidebar rendering no longer reads full file contents
  • Dispatched all HostsDownloader delegate callbacks to the main thread to fix data races from NSURLSession background queue
  • Added O(1) NSString.length comparison before O(n) string equality check in HostsTextViewRepresentable.updateNSView

Test plan

  • Add https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts as a remote hosts file — UI should remain responsive
  • Verify syntax highlighting appears progressively (colors fill in over ~1-2 seconds)
  • Switch to a different file during async highlighting — no crash
  • Toggle syntax highlighting off/on for the large file — works correctly
  • Drag a hosts file from the sidebar — exports plain text content with name preview
  • Add a small remote or local hosts file — instant highlighting, no regression
  • Edit a small local file — incremental highlighting still works normally

2ndalpha added 7 commits March 1, 2026 13:19
Large remote hosts files (e.g. StevenBlack/hosts ~1MB, ~77K domains)
caused the app to become unresponsive due to synchronous syntax
highlighting processing the entire document on the main thread.

- Refactor HostsTextView to highlight large documents (>50KB) in async
  chunks of ~100KB, yielding to the run loop between chunks. A
  generation counter cancels stale passes on file switch or user edit.
- Replace eager .draggable(hosts.contents()) with lazy .onDrag using
  NSItemProvider so sidebar rendering no longer reads file contents.
- Dispatch all HostsDownloader delegate callbacks to the main thread
  to fix data races from NSURLSession background queue callbacks.
- Add O(1) NSString.length check before O(n) string comparison in
  HostsTextViewRepresentable.updateNSView.
Add replaceContentWith: method that bypasses the expensive synchronous
textStorageDidProcessEditing: callback during bulk text replacement.
This avoids the O(n) lineRangeForRange: computation that blocked the
main thread on every file switch. Highlighting is instead triggered
manually after replacement — async for large files, batched sync for
small files.
Add tests proving text view layer is fast:
- Small file switching: ~5ms per switch
- replaceContentWith: no regression vs direct assignment
- No notification cascade during selection changes
- No HostsNodeNeedsUpdate posted during selection
The updateNSView guard was comparing the full text content (O(n)) on
every @published property change, not just selection changes. For a
16K-line file, each comparison took ~22ms, causing visible lag when
multiple re-renders occurred per click.

Fix: decouple HostsTextViewRepresentable from the monolithic store
and use a two-tier guard:
- O(1) pointer check for selection changes (always replace)
- Token-based check for external content updates (compare only when
  rowRefreshToken changes)
- Skip entirely when neither selection nor token changed

Also adds integration tests for the full HostsDataStore → updateNSView
pipeline, objectWillChange publication counting, and pointer-based
guard verification.
…cking

When switching to a large hosts file (>50K chars), replaceContentWith:
called highlightAsyncFrom:0 synchronously, blocking the main thread
for ~20ms on a 1.38MB file. Dispatch the first chunk via
dispatch_async like subsequent chunks, reducing switch time to ~1.5ms.
Three targeted fixes for UI lockup when switching between local files
while a large remote file (e.g. StevenBlack ~1MB) is configured:

- Coalesce rowRefreshToken updates in HostsDataStore so 9-12 rapid
  notifications from a download lifecycle produce 1 SwiftUI re-render
  instead of 9-12
- Remove duplicate HostsFileSavedNotification in RemoteHostsManager
  (hostsController saveHosts: already posts it)
- Make dscacheutil -flushcache non-blocking using terminationHandler
  instead of [task waitUntilExit] (~9ms saved per call)
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