From f27f3c46c1f2cfb75d85f75cb946aa047926872b Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 19 Mar 2025 11:55:29 +0100 Subject: [PATCH] decouple eventMap insertion from event happening (to not spam FX thread) --- .../org/cryptomator/common/vaults/Vault.java | 5 +- .../org/cryptomator/event/VaultEventsMap.java | 219 +++++++++++++----- 2 files changed, 161 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 37fb53f10..bac33151f 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -34,7 +34,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; -import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; @@ -260,9 +259,7 @@ public class Vault { private void consumeVaultEvent(FilesystemEvent e) { - Platform.runLater(() -> { - vaultEventsMap.put(this, e); - }); + vaultEventsMap.enque(this, e); } // ****************************************************************************** diff --git a/src/main/java/org/cryptomator/event/VaultEventsMap.java b/src/main/java/org/cryptomator/event/VaultEventsMap.java index bbaa5475b..da46f3c86 100644 --- a/src/main/java/org/cryptomator/event/VaultEventsMap.java +++ b/src/main/java/org/cryptomator/event/VaultEventsMap.java @@ -1,6 +1,5 @@ package org.cryptomator.event; -import org.cryptomator.common.ObservableMapDecorator; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.event.BrokenDirFileEvent; import org.cryptomator.cryptofs.event.BrokenFileNodeEvent; @@ -11,45 +10,177 @@ import org.cryptomator.cryptofs.event.FilesystemEvent; import javax.inject.Inject; import javax.inject.Singleton; +import javafx.application.Platform; import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableMap; import java.nio.file.Path; import java.util.List; import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; -/** - * Map containing {@link VaultEvent}s. - * The map is keyed by three elements: - * - * - *

- * Use {@link VaultEventsMap#put(Vault, FilesystemEvent)} to add an element and {@link VaultEventsMap#remove(Vault, FilesystemEvent)} to remove it. - *

- * The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed. - */ @Singleton -public class VaultEventsMap extends ObservableMapDecorator { +public class VaultEventsMap { - private static final int MAX_SIZE = 300; + private static final int MAX_MAP_SIZE = 400; public record Key(Vault vault, Path idPath, Class c) {} public record Value(FilesystemEvent mostRecentEvent, int count) {} /** - * Internal least-recently-used cache. + * Queue of elements to be inserted into the map + */ + private final ConcurrentMap queue; + /** + * Least-recently-used cache. */ private final TreeSet lruCache; + /** + * Actual map + */ + private final ObservableMap map; + + private final ScheduledExecutorService scheduler; + + private final AtomicBoolean queueHasElements; @Inject - public VaultEventsMap() { - super(FXCollections.observableHashMap()); - this.lruCache = new TreeSet<>(this::compareToMapKeys); + public VaultEventsMap(ScheduledExecutorService scheduledExecutorService) { + this.queue = new ConcurrentHashMap<>(); + this.lruCache = new TreeSet<>(this::compareKeys); + this.map = FXCollections.observableHashMap(); + this.scheduler = scheduledExecutorService; + this.queueHasElements = new AtomicBoolean(false); + scheduler.scheduleWithFixedDelay(() -> { + if (queueHasElements.get()) { + flush(); + } + }, 1000, 1000, TimeUnit.MILLISECONDS); } + /** + * Enques the given event to be inserted into the map. + * + * @param v Vault where the event occurred + * @param e Actual {@link FilesystemEvent} + */ + public synchronized void enque(Vault v, FilesystemEvent e) { + var key = computeKey(v, e); + queue.compute(key, (k, val) -> { + if (val == null) { + return new Value(e, 1); + } else { + return new Value(e, val.count() + 1); + } + }); + + queueHasElements.set(true); + } + + + /** + * Lists all entries in this map as {@link VaultEvent}. The list is sorted ascending by the timestamp of event occurral (and more if it is the same timestamp). + * Must be executed on the JavaFX application thread + * + * @return a list of vault events, mainly sorted ascending by the event timestamp + */ + public List listAll() { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException("Listing map entries must be performed on JavaFX application thread"); + } + return lruCache.stream().map(key -> { + var value = map.get(key); + return new VaultEvent(key.vault(), value.mostRecentEvent(), value.count()); + }).toList(); + } + + /** + * Removes an event from the map. + *

+ * To identify the event, a similar event (in the sense of map key) is given. + * Must be executed on the JavaFX application thread + * + * @param v Vault where the event occurred + * @param similar A similar {@link FilesystemEvent} (same class, same idPath) + * @return the removed {@link Value} + */ + public Value remove(Vault v, FilesystemEvent similar) { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException("Map removal must be performed on JavaFX application thread"); + } + var key = computeKey(v, similar); + lruCache.remove(key); + return map.remove(key); + } + + public void clear() { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException("Map removal must be performed on JavaFX application thread"); + } + lruCache.clear(); + map.clear(); + } + + /** + * Flushes all changes from the queue into the map + */ + private synchronized void flush() { + //Lock queue + var latch = new CountDownLatch(1); + Platform.runLater(() -> { + queue.forEach(this::updateMap); + queue.clear(); + latch.countDown(); + }); + try { + latch.await(); + queueHasElements.set(false); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** + * Updates a single map entry + * + * @param k Key of the entry to update + * @param v Value of the entry to update + */ + private void updateMap(Key k, Value v) { + var entry = map.get(k); + if (entry == null) { + if (map.size() == MAX_MAP_SIZE) { + var toRemove = lruCache.first(); + lruCache.remove(toRemove); + map.remove(toRemove); + } + map.put(k, v); + lruCache.add(k); + } else { + lruCache.remove(k); + map.put(k, new Value(v.mostRecentEvent, entry.count + v.count)); + lruCache.add(k); //correct, because cache-sorting uses the map in comparsionMethod + } + } + + /* Observability */ + + public void addListener(MapChangeListener mapChangeListener) { + map.addListener(mapChangeListener); + } + + public void removeListener(MapChangeListener mapChangeListener) { + map.removeListener(mapChangeListener); + } + + + /* Internal stuff */ /** * Comparsion method for the lru cache. During comparsion the map is accessed. @@ -59,9 +190,9 @@ public class VaultEventsMap extends ObservableMapDecorator listAll() { - return lruCache.stream().map(key -> { - var value = delegate.get(key); - return new VaultEvent(key.vault(), value.mostRecentEvent(), value.count()); - }).toList(); - } - - public synchronized void put(Vault v, FilesystemEvent e) { - var key = computeKey(v, e); - //if-else - var entry = delegate.get(key); - if (entry == null) { - if (size() == MAX_SIZE) { - var toRemove = lruCache.first(); - lruCache.remove(toRemove); - delegate.remove(toRemove); - } - delegate.put(key, new Value(e, 1)); - lruCache.add(key); - } else { - lruCache.remove(key); - delegate.put(key, new Value(e, entry.count() + 1)); - lruCache.add(key); //correct, because cache-sorting uses the map in comparsionMethod - } - } - - public synchronized Value remove(Vault v, FilesystemEvent similar) { - var key = computeKey(v, similar); - lruCache.remove(key); - return delegate.remove(key); - } - private Key computeKey(Vault v, FilesystemEvent event) { var p = switch (event) { case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath; @@ -125,4 +225,5 @@ public class VaultEventsMap extends ObservableMapDecorator