From 9dd011afc887e71436434dba6da78c405460f184 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 31 May 2025 09:10:52 -0400 Subject: [PATCH] poll key images in batches --- .../core/xmr/wallet/XmrKeyImagePoller.java | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 73332dc4..606ca297 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,12 +43,15 @@ public class XmrKeyImagePoller { private MoneroDaemon daemon; private long refreshPeriodMs; + private Object lock = new Object(); private Map> keyImageGroups = new HashMap>(); + private LinkedHashSet keyImagePollQueue = new LinkedHashSet<>(); private Set listeners = new HashSet(); private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; private Long lastLogPollErrorTimestamp; + private static final int MAX_POLL_SIZE = 200; /** * Construct the listener. @@ -74,8 +78,10 @@ public class XmrKeyImagePoller { * @param listener - the listener to add */ public void addListener(XmrKeyImageListener listener) { - listeners.add(listener); - refreshPolling(); + synchronized (lock) { + listeners.add(listener); + refreshPolling(); + } } /** @@ -84,9 +90,11 @@ public class XmrKeyImagePoller { * @param listener - the listener to remove */ public void removeListener(XmrKeyImageListener listener) { - if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); - listeners.remove(listener); - refreshPolling(); + synchronized (lock) { + if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered"); + listeners.remove(listener); + refreshPolling(); + } } /** @@ -140,10 +148,11 @@ public class XmrKeyImagePoller { * @param keyImages - key images to listen to */ public void addKeyImages(Collection keyImages, String groupId) { - synchronized (this.keyImageGroups) { + synchronized (lock) { if (!keyImageGroups.containsKey(groupId)) keyImageGroups.put(groupId, new HashSet()); Set keyImagesGroup = keyImageGroups.get(groupId); keyImagesGroup.addAll(keyImages); + keyImagePollQueue.addAll(keyImages); refreshPolling(); } } @@ -154,17 +163,16 @@ public class XmrKeyImagePoller { * @param keyImages - key images to unlisten to */ public void removeKeyImages(Collection keyImages, String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImagesGroup.removeAll(keyImages); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); - synchronized (lastStatuses) { - for (String keyImage : keyImages) { - if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { - lastStatuses.remove(keyImage); - } + for (String keyImage : keyImages) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); } } refreshPolling(); @@ -172,16 +180,15 @@ public class XmrKeyImagePoller { } public void removeKeyImages(String groupId) { - synchronized (keyImageGroups) { + synchronized (lock) { Set keyImagesGroup = keyImageGroups.get(groupId); if (keyImagesGroup == null) return; keyImageGroups.remove(groupId); Set allKeyImages = getKeyImages(); - synchronized (lastStatuses) { - for (String keyImage : keyImagesGroup) { - if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) { - lastStatuses.remove(keyImage); - } + for (String keyImage : keyImagesGroup) { + if (!allKeyImages.contains(keyImage)) { + keyImagePollQueue.remove(keyImage); + lastStatuses.remove(keyImage); } } refreshPolling(); @@ -192,11 +199,10 @@ public class XmrKeyImagePoller { * Clear the key images which stops polling. */ public void clearKeyImages() { - synchronized (keyImageGroups) { + synchronized (lock) { keyImageGroups.clear(); - synchronized (lastStatuses) { - lastStatuses.clear(); - } + keyImagePollQueue.clear(); + lastStatuses.clear(); refreshPolling(); } } @@ -208,7 +214,7 @@ public class XmrKeyImagePoller { * @return true if the key is spent, false if unspent, null if unknown */ public Boolean isSpent(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { if (!lastStatuses.containsKey(keyImage)) return null; return XmrKeyImagePoller.isSpent(lastStatuses.get(keyImage)); } @@ -231,7 +237,7 @@ public class XmrKeyImagePoller { * @return the last known spent status of the key image */ public MoneroKeyImageSpentStatus getLastSpentStatus(String keyImage) { - synchronized (lastStatuses) { + synchronized (lock) { return lastStatuses.get(keyImage); } } @@ -244,7 +250,7 @@ public class XmrKeyImagePoller { // fetch spent statuses List spentStatuses = null; - List keyImages = new ArrayList(getKeyImages()); + List keyImages = new ArrayList(getNextKeyImageBatch()); try { spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } catch (Exception e) { @@ -257,10 +263,20 @@ public class XmrKeyImagePoller { return; } - // collect changed statuses + // process spent statuses Map changedStatuses = new HashMap(); - synchronized (lastStatuses) { - for (int i = 0; i < spentStatuses.size(); i++) { + synchronized (lock) { + Set allKeyImages = getKeyImages(); + for (int i = 0; i < keyImages.size(); i++) { + + // skip if key image is removed + if (!allKeyImages.contains(keyImages.get(i))) continue; + + // move key image to the end of the queue + keyImagePollQueue.remove(keyImages.get(i)); + keyImagePollQueue.add(keyImages.get(i)); + + // update spent status if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); @@ -270,14 +286,18 @@ public class XmrKeyImagePoller { // announce changes if (!changedStatuses.isEmpty()) { - for (XmrKeyImageListener listener : new ArrayList(listeners)) { + List listeners; + synchronized (lock) { + listeners = new ArrayList(this.listeners); + } + for (XmrKeyImageListener listener : listeners) { listener.onSpentStatusChanged(changedStatuses); } } } private void refreshPolling() { - synchronized (keyImageGroups) { + synchronized (lock) { setIsPolling(!getKeyImages().isEmpty() && listeners.size() > 0); } } @@ -296,11 +316,24 @@ public class XmrKeyImagePoller { private Set getKeyImages() { Set allKeyImages = new HashSet(); - synchronized (keyImageGroups) { + synchronized (lock) { for (Set keyImagesGroup : keyImageGroups.values()) { allKeyImages.addAll(keyImagesGroup); } } return allKeyImages; } + + private List getNextKeyImageBatch() { + synchronized (lock) { + List keyImageBatch = new ArrayList<>(); + int count = 0; + for (String keyImage : keyImagePollQueue) { + if (count >= MAX_POLL_SIZE) break; + keyImageBatch.add(keyImage); + count++; + } + return keyImageBatch; + } + } }