Files
Bitsy 35ffc86e46 Fix idle CPU drain from open path queries, general code cleanup
Fix
- An open window with a path query (term containing "/") re-ran the search
  over the whole index on every FSEvents update, keeping several cores busy
  while files changed. Index-driven refilters are now throttled to one per
  second, skipped while the window is closed, minimized or covered, and
  replayed once when it becomes visible again. Typing stays instant.
- Path terms no longer build and lowercase the full path of every record:
  the folded folder path is precomputed once per folder (interned like
  parentPath) and matched together with the folded name without allocating
  the joined string. Path-only queries are about 3x faster.

Cleanup
- Remove dead code: FSChange.isDir, FileWatcher.latestEventId,
  IndexState.record(atPath:), the never-set lastQuery preference,
  AccessManager.reset(), SearchField.onSubmit, an unreachable return.
- Merge duplicates: the two Settings size formatters become
  Formatters.sizeOrDash, the shellQuote wrapper is replaced by
  AdminShell.quote.
- Naming conventions: "in"-prefixed parameter names everywhere (delegate
  methods, binary helpers, initializers), camelCase impl helpers.
- Comment every function and type; fix stale comments.
- Debug and release builds are warning-free.
2026-10-02 09:29:18 +02:00

131 lines
4.3 KiB
Swift

import Foundation
import CoreServices
// FSChange describes a single path reported by FSEvents along with its flag
// bits. The flags tell us whether the kernel coalesced events past its
// history limit, in which case we must rescan the affected subdirectory.
struct FSChange {
// absolute path that changed
let path: String
// raw FSEvents flag bits for this change
let flags: FSEventStreamEventFlags
// FSEvents id of this change, persisted so the next launch resumes after it
let eventId: FSEventStreamEventId
// true for the synthetic marker FSEvents sends once the replay of
// historical events (sinceWhen) is finished - not a real file change
var isHistoryDone: Bool {
return flags & FSEventStreamEventFlags(kFSEventStreamEventFlagHistoryDone) != 0
}
// true when the kernel asks the client to rescan inside this path
// because events were dropped (history too old or buffer overflow)
var mustScanSubDirs: Bool {
return flags & FSEventStreamEventFlags(kFSEventStreamEventFlagMustScanSubDirs) != 0
}
}
// FileWatcher streams real-time filesystem change notifications using FSEvents.
// FSEvents is the macOS analogue of the NTFS USN journal: the kernel records
// every change and we receive coalesced notifications batched by latency.
//
// The watcher can be started from a specific event id, which lets us "replay"
// every change that occurred while the app was closed - the same fast-resume
// trick Everything uses on Windows.
//
// @unchecked Sendable because all mutable state is guarded by the internal
// NSLock, and the C callback handles concurrency via that same lock.
final class FileWatcher: @unchecked Sendable {
// callback signature receiving an array of change descriptors
typealias ChangeHandler = ([FSChange]) -> Void
// the active FSEvents stream, nil when stopped
private var stream: FSEventStreamRef?
// the user-provided change handler, invoked from the FSEvents callback
private var handler: ChangeHandler?
// guards stream/handler against concurrent access
private let lock = NSLock()
// stops the stream so its callback never outlives the watcher
deinit {
stop()
}
// starts watching inRoots, delivering batches of changes to inHandler
// starting from inSinceWhen (defaults to "now")
func start(inRoots: [String],
inSinceWhen: FSEventStreamEventId = FSEventStreamEventId(kFSEventStreamEventIdSinceNow),
inHandler: @escaping ChangeHandler) {
stop()
lock.lock()
handler = inHandler
lock.unlock()
var vContext = FSEventStreamContext(
version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil
)
// C callback: forwards paths + flags back to the Swift handler
let vCallback: FSEventStreamCallback = { (_, info, numEvents, eventPaths, eventFlags, eventIds) in
guard let vInfo = info else { return }
let vWatcher = Unmanaged<FileWatcher>.fromOpaque(vInfo).takeUnretainedValue()
let vPathsArray = unsafeBitCast(eventPaths, to: NSArray.self)
var vChanges: [FSChange] = []
vChanges.reserveCapacity(numEvents)
for vI in 0..<numEvents {
guard let vPath = vPathsArray[vI] as? String else { continue }
vChanges.append(FSChange(path: vPath, flags: eventFlags[vI], eventId: eventIds[vI]))
}
vWatcher.lock.lock()
let vHandler = vWatcher.handler
vWatcher.lock.unlock()
vHandler?(vChanges)
}
// kFSEventStreamCreateFlagIgnoreSelf filters out events caused by
// this same process - keeps the indexer's own cache writes from
// showing up in its event stream as phantom file activity.
let vFlags = FSEventStreamCreateFlags(
kFSEventStreamCreateFlagFileEvents |
kFSEventStreamCreateFlagNoDefer |
kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagIgnoreSelf
)
guard let vStream = FSEventStreamCreate(
nil,
vCallback,
&vContext,
inRoots as CFArray,
inSinceWhen,
0.2,
vFlags
) else { return }
FSEventStreamSetDispatchQueue(vStream, DispatchQueue.global(qos: .utility))
FSEventStreamStart(vStream)
lock.lock()
stream = vStream
lock.unlock()
}
// stops the watcher and releases the underlying stream
func stop() {
lock.lock()
let vStream = stream
stream = nil
handler = nil
lock.unlock()
if let vRealStream = vStream {
FSEventStreamStop(vRealStream)
FSEventStreamInvalidate(vRealStream)
FSEventStreamRelease(vRealStream)
}
}
}