# 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1757972021750/4d5c977c-c75b-4920-9193-1700efddfa63.png align="center")

## 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](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.

```csharp
/// <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.

```csharp
// 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.

```csharp
/// <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.

```csharp
/// <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](https://beniaminlenarcik.pl/series/uzyteczne-algorytmy-w-systemach-opartych-na-zdarzeniach). Przykładową implementację debounce oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: [useful-async-algorithms](https://github.com/L3mur1/useful-async-algorithms)
