1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-01 09:10:08 +02: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 */; }; 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 */; }; 0379C4A72ED720220035D7EA /* App+DetectApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0379C4A32ED7201D0035D7EA /* App+DetectApps.swift */; };
037F44162EDB0AAA002EBF75 /* FSNotifierTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037F44152EDB0AA8002EBF75 /* FSNotifierTest.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 */; }; 0386B0B42ED36C3D00CA6795 /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0386B0B32ED36C3D00CA6795 /* Locked.swift */; };
0386B0B52ED36C3D00CA6795 /* 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 0392CDE52EB23B8F009176DA /* CertificateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidator.swift; sourceTree = "<group>"; };
@@ -2331,6 +2336,7 @@
C4C8E81D276F5686003AC782 /* Watcher */ = { C4C8E81D276F5686003AC782 /* Watcher */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
037F44172EDB27B7002EBF75 /* Debouncer.swift */,
C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */, C4C8E81A276F54E5003AC782 /* ConfigWatchManager.swift */,
C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */, C441CC552AE8249400DDFACD /* ConfigFSNotifier.swift */,
C41ADCE72970CCC700120423 /* FSNotifier.swift */, C41ADCE72970CCC700120423 /* FSNotifier.swift */,
@@ -2850,6 +2856,7 @@
C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */, C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */,
039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */, 039616102E74A61E002DD7F6 /* LoggableEvent.swift in Sources */,
C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, C4C0E8DF27F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */,
037F44192EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */, C44B3A4628E5C70100718CB1 /* TimeIntervalExtension.swift in Sources */,
03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */, 03C099452EA15C8E00B76D43 /* Container+Real.swift in Sources */,
C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */, C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */,
@@ -3106,6 +3113,7 @@
C471E86328F9BB650021E251 /* PMTableView.swift in Sources */, C471E86328F9BB650021E251 /* PMTableView.swift in Sources */,
C471E86428F9BB650021E251 /* Warning.swift in Sources */, C471E86428F9BB650021E251 /* Warning.swift in Sources */,
03C29A772EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */, 03C29A772EC88E3100FBA25E /* ValetServicesDataManager.swift in Sources */,
037F441B2EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */, C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */,
C43931C729C4BD610069165B /* PhpVersionManagerView.swift in Sources */, C43931C729C4BD610069165B /* PhpVersionManagerView.swift in Sources */,
0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */, 0379C4A52ED720220035D7EA /* App+DetectApps.swift in Sources */,
@@ -3427,6 +3435,7 @@
C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */, C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */,
C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */, C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */,
C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */, C471E82128F9BB2E0021E251 /* ProjectTypeDetection.swift in Sources */,
037F441A2EDB27BA002EBF75 /* Debouncer.swift in Sources */,
032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */, 032DAC282E8BEB5B0018E01C /* RealWebApi.swift in Sources */,
C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */, C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */,
C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */, C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */,
@@ -3671,6 +3680,7 @@
C485707828BF456300539B36 /* Warning.swift in Sources */, C485707828BF456300539B36 /* Warning.swift in Sources */,
033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */, 033D459F2B0D513900070080 /* RemovePhpExtensionCommand.swift in Sources */,
C4513F8F2B13E2E5001AD760 /* PhpExtensionManagerWindowController.swift in Sources */, C4513F8F2B13E2E5001AD760 /* PhpExtensionManagerWindowController.swift in Sources */,
037F44182EDB27BA002EBF75 /* Debouncer.swift in Sources */,
C415938027A1B54F00D2E1B7 /* ProjectTypeDetection.swift in Sources */, C415938027A1B54F00D2E1B7 /* ProjectTypeDetection.swift in Sources */,
C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */, C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */,
C4D9ADC9277611A0007277F4 /* InternalSwitcher.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. */ /** Individual filesystem watchers, which are, i.e. responsible for watching the Homebrew folders. */
var watchers: [String: FSNotifier] = [:] 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. 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. 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() } } onChange: { Task { await self.onHomebrewPhpModification() } }
) )
App.shared.watchers["homebrewBinaries"] = notifier self.watchers["homebrewBinaries"] = notifier
} self.debouncers["homebrewBinaries"] = Debouncer()
public func destroyHomebrewWatchers() {
// Removing requires termination and then removing reference
self.watchers["homebrewBinaries"]?.terminate()
self.watchers["homebrewBinaries"] = nil
} }
public func onHomebrewPhpModification() async { public func onHomebrewPhpModification() async {
// let previous = App.shared.container.phpEnvs.currentInstall?.version.text if let debouncer = self.debouncers["homebrewBinaries"] {
Log.info("Something changed in the Homebrew binary directory...") await debouncer.debounce(for: 5.0) {
await container.phpEnvs.reloadPhpVersions() Log.info("No changes in `\(self.container.paths.binPath)` occurred for 5 seconds. Reloading now.")
await MainMenu.shared.refreshActiveInstallation()
// // We reload the PHP versions in the background
// TODO: PHP Guard 2.0 await self.container.phpEnvs.reloadPhpVersions()
// Check if the new and previous version of PHP are different
// if so, we can show a notification if needed or alert the user // Finally, refresh the active installation
// await MainMenu.shared.refreshActiveInstallation()
// let new = App.shared.container.phpEnvs.currentInstall?.version.text }
// }
} }
} }

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 queue = DispatchQueue(label: "com.nicoverbruggen.phpmon.fs_notifier")
let url: URL let url: URL
var lastUpdate: TimeInterval?
// MARK: Private variables // MARK: Private variables
@@ -43,23 +42,7 @@ class FSNotifier {
dispatchSource?.setEventHandler(handler: { dispatchSource?.setEventHandler(handler: {
self.queue.async { self.queue.async {
// See how long ago our last handled event was Task { onChange() }
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()
}
} }
}) })

View File

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