Skip to main content

Command Palette

Search for a command to run...

Debounce w .NET – jak zatrzymać lawinę zdarzeń

Updated
3 min read
Debounce w .NET – jak zatrzymać lawinę zdarzeń

Wprowadzenie

Czasem w systemie zaczyna pojawiać się zbyt dużo zdarzeń. Mogą być one powtarzalne, nadmiernie szczegółowe albo po prostu nieistotne w dużej liczbie. Taka lawina informacji przeciąża nasze rozwiązanie. Potrzebujemy mechanizmu, który ograniczy częstość ich występowania. Jest nim debounce.

Debounce redukuje wiele powtarzalnych zdarzeń do jednego sygnału

Skąd pochodzi debounce

Termin „debounce” pochodzi z elektroniki. Mechaniczne przyciski po naciśnięciu potrafią kilkukrotnie „odbijać” styki, generując serię krótkich impulsów zamiast jednego sygnału. Aby zapobiec błędnemu odczytowi wielu kliknięć, stosuje się układ debouncing, który czeka, aż sygnał się ustabilizuje. Ta sama koncepcja została zaadaptowana w programowaniu zdarzeń.

Redukcja powtarzalnych zdarzeń przy monitorowaniu zmian w plikach

Wyobraźmy sobie proces monitorowania zmian na dysku za pomocą FileSystemWatcher. Z jednej operacji potrafi pojawić się kilka różnych zdarzeń dotyczących tego samego zasobu. Sygnały mogą dotyczyć modyfikacji, zmiany rozmiaru, pojawienia się pliku itd. Wszystkie występują w bliskich odstępach czasowych. Reakcja na każde z nich osobno oznaczałaby kilkukrotne przeprocesowanie tych samych informacji. Zastosowanie mechanizmu debounce rozwiązuje ten problem.

Implementacja

Pełną wersję przykładu wraz z innymi algorytmami asynchronicznymi znajdziesz w repozytorium:

https://github.com/L3mur1/useful-async-algorithms

Tworząc debounce określamy tzw. debouncing window – przedział czasu, w którym kolejne zdarzenia uznajemy za duplikaty.

/// <summary>
/// Events with the same path within this time span are ignored.
/// </summary>,
private readonly TimeSpan debounceWindow;

Do podstawowej implementacji wystarczy pamiętać ostatni czas obsłużonego zdarzenia i porównać go z aktualnym. W przypadku plików musimy dodatkowo wziąć pod uwagę ścieżkę, ponieważ każde zdarzenie dotyczy innego zasobu. Dlatego używamy słownika ConcurrentDictionary, w którym trzymamy ostatni czas publikacji dla każdego zasobu osobno.

// Stores the last event time for each file path to support debouncing.
private readonly ConcurrentDictionary<string, DateTime> lastEventTimes = new();

Gdy pojawiają się kolejne sygnały, porównujemy ich czas wystąpienia z ostatnim zarejestrowanym dla danej ścieżki. Pomijamy zdarzenia, które wystąpiły wewnątrz deboucing window dla tego samego zasobu. Jeśli zdarzenie występuje poza oknem czasowym, uzupełniamy słownik i publikujemy.

/// <summary>
/// Handles incoming events and applies debouncing based on the window and path.
/// </summary>
private void OnNext(FileEvent fileEvent)
{
    if (lastEventTimes.TryGetValue(fileEvent.Path, out var lastTime))
    {
        // Check if the event is within the debounce window
        if (fileEvent.PublishTime - lastTime < debounceWindow)
        {
            // Ignore event within deboucing window
            return;
        }
    }

    // Publish event and update last event time for path
    lastEventTimes[fileEvent.Path] = fileEvent.PublishTime;
    subject.OnNext(fileEvent);
}

Warto pamiętać, że w przypadku monitorowania wielu ścieżek, np. całych folderów, słownik może rosnąć w nieskończoność i niepotrzebnie zabierać pamięć systemu. Warto wprowadzić mechanizm okresowego czyszczenia zasobów.

/// <summary>
/// lastEventTimes dictionary should be periodically cleaned up
/// to avoid memory leaks from paths that are no longer active.
/// this is example clean up
/// </summary>
private void CleanUp(long obj)
{
    var threshold = DateTime.UtcNow - debounceWindow;
    foreach (var kvp in lastEventTimes)
    {
        // Remove entries older than the threshold
        if (kvp.Value < threshold)
        {
            lastEventTimes.TryRemove(kvp.Key, out _);
        }
    }
}

Dzięki zastosowaniu debounce zamiast lawiny zdarzeń dostajemy tylko jedno – reprezentatywne dla konkretnej ścieżki – w danym oknie czasowym.

Podsumowanie

Debounce to algorytm redukujący liczbę przetwarzanych zdarzeń w systemie. Jego istotą jest powiązanie różnych sygnałów w krótkich odstępach czasu. Ma to szczególne znaczenie w przetwarzaniu zdarzeń w architekturach event-driven, które z natury są asynchroniczne i nie dają gwarancji spójności. Stosując ten wzorzec tworzymy systemy mniej hałaśliwe i bardziej oszczędne w zasobach.

Ten artykuł jest częścią serii. Przykładową implementację debounce oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: useful-async-algorithms

More from this blog

B

Beniamin Lenarcik

13 posts