1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 03:10:06 +01:00

♻️ Improve debouncing mechanism

This commit is contained in:
2025-11-29 14:35:36 +01:00
parent 04046e1ded
commit 4b41704fdf
6 changed files with 64 additions and 42 deletions

View File

@@ -75,6 +75,10 @@
0379C4A62ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */; };
037F44182EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44172EDB27B7002EBF75 /* Debouncer.swift */; };
0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B52ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B62ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
@@ -1052,6 +1056,7 @@
0379C49E2ED71CFC0035D7EA /* Startup+Launch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Startup+Launch.swift"; sourceTree = "<group>"; };
0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+DetectApps.swift"; sourceTree = "<group>"; };
037F44152EDB0AA8002EBF75 /* FSNotifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifierTest.swift; sourceTree = "<group>"; };
037F44172EDB27B7002EBF75 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
0386B0B32ED36C3D00CA6795 /* Locked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locked.swift; sourceTree = "<group>"; };
0386B0B82ED36DF800CA6795 /* LockedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedTests.swift; sourceTree = "<group>"; };
0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
@@ -2331,6 +2336,7 @@
C4C8E81D276F5686003AC782 /* Watcher */ = {
isa = PBXGroup;
children = (
037F44172EDB27B7002EBF75 /* Debouncer.swift */,
C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */,
C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */,
C41ADCE72970CCC700120423 /* FSNotifier.swift */,
@@ -2850,6 +2856,7 @@
C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */,
039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */,
C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */,
C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */,
@@ -3106,6 +3113,7 @@
C471E86328F9BB650021E251 /* PMTableView.swift in Sources */,
C471E86428F9BB650021E251 /* Warning.swift in Sources */,
03C29A772EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */,
037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */,
C43931C729C4BD610069165B /* PhpVersionManagerView.swift in Sources */,
0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */,
@@ -3427,6 +3435,7 @@
C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */,
C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */,
C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */,
037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */,
032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */,
C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */,
C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */,
@@ -3671,6 +3680,7 @@
C485707828BF456300539B36 /* Warning.swift in Sources */,
033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */,
C4513F8F2B13E2E5001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
037F44182EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C415938027A1B54F00D2E1B7 /* ProjectTypeDetection.swift in Sources */,
C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */,

View File

@@ -139,6 +139,9 @@ class App {
/** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
var watchers: [String: FSNotifier] = [:]
/** Individual debouncers for filesystem watchers. */
var debouncers: [String: Debouncer] = [:]
/**
The `ConfigWatchManager` is responsible for watching the `.ini` files and the `.conf.d` folder.
This manager object can immediately start or stop all watchers (or pause them) all at once.

View File

@@ -17,27 +17,22 @@ extension App {
onChange: { Task { await self.onHomebrewPhpModification() } }
)
App.shared.watchers["homebrewBinaries"] = notifier
}
public func destroyHomebrewWatchers() {
// Removing requires termination and then removing reference
self.watchers["homebrewBinaries"]?.terminate()
self.watchers["homebrewBinaries"] = nil
self.watchers["homebrewBinaries"] = notifier
self.debouncers["homebrewBinaries"] = Debouncer()
}
public func onHomebrewPhpModification() async {
// let previous = App.shared.container.phpEnvs.currentInstall?.version.text
Log.info("Something changed in the Homebrew binary directory...")
await container.phpEnvs.reloadPhpVersions()
await MainMenu.shared.refreshActiveInstallation()
if let debouncer = self.debouncers["homebrewBinaries"] {
await debouncer.debounce(for: 5.0) {
Log.info("No changes in `\(self.container.paths.binPath)` occurred for 5 seconds. Reloading now.")
//
// TODO: PHP Guard 2.0
// Check if the new and previous version of PHP are different
// if so, we can show a notification if needed or alert the user
//
// let new = App.shared.container.phpEnvs.currentInstall?.version.text
//
// We reload the PHP versions in the background
await self.container.phpEnvs.reloadPhpVersions()
// Finally, refresh the active installation
await MainMenu.shared.refreshActiveInstallation()
}
}
}
}

View File

@@ -0,0 +1,26 @@
//
// Debouncer.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/11/2025.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
actor Debouncer {
private var task: Task<Void, Never>?
func debounce(for duration: TimeInterval, action: @escaping () async -> Void) {
task?.cancel()
task = Task {
try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
guard !Task.isCancelled else { return }
await action()
}
}
func cancel() {
task?.cancel()
}
}

View File

@@ -16,7 +16,6 @@ class FSNotifier {
let queue = DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_notifier")
let url: URL
var lastUpdate: TimeInterval?
// MARK: Private variables
@@ -43,23 +42,7 @@ class FSNotifier {
dispatchSource?.setEventHandler(handler: {
self.queue.async {
// See how long ago our last handled event was
let distance = self.lastUpdate?
.distance(to: Date().timeIntervalSince1970)
// Add a debounce of 1 second
if let distance, distance <= 1.00 {
Log.perf("FSNotifier debounce active for \(url.path).")
return
}
// Synchronize the last update property
self.lastUpdate = Date().timeIntervalSince1970
// Dispatch the async task we set for when the filesystem event occurs
Task {
onChange()
}
Task { onChange() }
}
})

View File

@@ -25,13 +25,19 @@ struct FSNotifierTest {
}
let eventFired = Locked<Int>(0)
let debouncer = Debouncer()
// Create notifier
let notifier = FSNotifier(
for: testFile,
eventMask: .write,
onChange: {
eventFired.value += 1
Task {
// Debouncer is an actor so this is allowed
await debouncer.debounce(for: 1.0) {
eventFired.value += 1
}
}
}
)
@@ -43,16 +49,15 @@ struct FSNotifierTest {
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
// Wait for the event to fire, verify it fired ONCE (not TWICE)
try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
// Wait for the event to fire, verify it fired ONCE after 1 second debounce
await delay(seconds: 1.2)
#expect(eventFired.value == 1)
// Try to write again (after debounce timing)
try await Task.sleep(nanoseconds: 2_000_000_000)
try "hello".write(to: testFile, atomically: false, encoding: .utf8)
// Verify our event fired AGAIN after 0.2 seconds
try await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds
// Verify after another second, our second write is actually noted
await delay(seconds: 1.2)
#expect(eventFired.value == 2)
}
}