From 97ef9c13080afff81041e97c5dd6a7ad1e85a99e Mon Sep 17 00:00:00 2001 From: Steven Barclay Date: Sat, 11 Feb 2023 01:03:52 +0800 Subject: [PATCH] Place filter in front of 'isRelatedToTransaction' for speedup Use a crude Bloom filter (of sorts) to cut down the quadratic number of calls to 'TransactionAwareTradable.isRelatedToTransaction' (that is, one for each tx-tradable pair) during the Transactions view load. In this way, we may reduce the number of calls roughly 40-fold, for a Bisq instance with similar numbers of BSQ swap trades and escrow trades. (Sadly, profiling does not show a 40-fold reduction in the size of the 'isRelatedToTransaction' hotspot, likely due to the remaining calls being expensive ones involving disputed trades or unusual txs with nonzero locktime, e.g. dust attacks or funds from Electrum wallets.) To this end, partition the wallet transactions into 64 pseudo-randomly chosen buckets (with a dedicated bucket for txs which might be delayed payouts, namely those with nonzero locktime). Add an interface method, 'TransactionAwareTradable.getRelatedTransactionFilter', which returns an IntStream of all the indices of buckets where a related tx may plausibly be found. Where this is unclear, e.g. for trades involved in a dispute, just return everything (that is, the range 0..63 inclusive). Add a class, 'RelatedTransactionFilterSlices', that holds a provided list of TransactionAwareTradable instances and 64 bitsets of all the slices through their respective filters (each realised as 64-bit word instead of a streams of integers). In this way, a list of tradables plausibly related to any given tx may be quickly found by simply selecting the appropriate bitset of the 64 (by the tx bucket index). --- .../RelatedTransactionFilterSlices.java | 52 +++++++++++++++++++ .../TransactionAwareOpenOffer.java | 10 ++++ .../TransactionAwareTradable.java | 25 +++++++++ .../transactions/TransactionAwareTrade.java | 28 +++++++++- .../funds/transactions/TransactionsView.java | 37 +++++++------ 5 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/funds/transactions/RelatedTransactionFilterSlices.java diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/RelatedTransactionFilterSlices.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/RelatedTransactionFilterSlices.java new file mode 100644 index 0000000000..08f168dc2e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/RelatedTransactionFilterSlices.java @@ -0,0 +1,52 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.funds.transactions; + +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static bisq.desktop.main.funds.transactions.TransactionAwareTradable.TX_FILTER_SIZE; + +public class RelatedTransactionFilterSlices { + private final List tradables; + private final BitSet[] filterSlices; + + public RelatedTransactionFilterSlices(Collection tradables) { + this.tradables = List.copyOf(tradables); + + filterSlices = new BitSet[TX_FILTER_SIZE]; + Arrays.setAll(filterSlices, i -> new BitSet(this.tradables.size())); + + IntStream.range(0, this.tradables.size()) + .forEach(j -> this.tradables.get(j).getRelatedTransactionFilter() + .forEach(i -> filterSlices[i].set(j))); + } + + public Stream getAllRelatedTradables(Transaction tx) { + int i = TransactionAwareTradable.bucketIndex(tx); + return filterSlices[i].stream() + .mapToObj(tradables::get) + .filter(t -> t.isRelatedToTransaction(tx)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareOpenOffer.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareOpenOffer.java index a355e2f849..b9f14f1e8a 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareOpenOffer.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareOpenOffer.java @@ -23,6 +23,8 @@ import bisq.core.trade.model.Tradable; import org.bitcoinj.core.Transaction; +import java.util.stream.IntStream; + class TransactionAwareOpenOffer implements TransactionAwareTradable { private final OpenOffer delegate; @@ -40,4 +42,12 @@ class TransactionAwareOpenOffer implements TransactionAwareTradable { public Tradable asTradable() { return delegate; } + + @Override + public IntStream getRelatedTransactionFilter() { + Offer offer = delegate.getOffer(); + String paymentTxId = offer.getOfferFeePaymentTxId(); + return IntStream.of(TransactionAwareTradable.bucketIndex(paymentTxId)) + .filter(i -> i >= 0); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradable.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradable.java index b00c283d96..99d05de3ac 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradable.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradable.java @@ -19,10 +19,35 @@ package bisq.desktop.main.funds.transactions; import bisq.core.trade.model.Tradable; +import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; +import java.util.stream.IntStream; + +import javax.annotation.Nullable; + interface TransactionAwareTradable { + int TX_FILTER_SIZE = 64; + int DELAYED_PAYOUT_TX_BUCKET_INDEX = TX_FILTER_SIZE - 1; + boolean isRelatedToTransaction(Transaction transaction); Tradable asTradable(); + + /** Returns a list of bucket indices of all transactions which might be related to this Tradable. */ + IntStream getRelatedTransactionFilter(); + + static int bucketIndex(Transaction tx) { + return tx.getLockTime() == 0 ? bucketIndex(tx.getTxId()) : DELAYED_PAYOUT_TX_BUCKET_INDEX; + } + + static int bucketIndex(Sha256Hash hash) { + int i = hash.getBytes()[31] & 255; + return i % TX_FILTER_SIZE != DELAYED_PAYOUT_TX_BUCKET_INDEX ? + i % TX_FILTER_SIZE : i / TX_FILTER_SIZE; + } + + static int bucketIndex(@Nullable String txId) { + return txId != null ? bucketIndex(Sha256Hash.wrap(txId)) : -1; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java index faca5917c1..4ffe513cf2 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java @@ -38,8 +38,11 @@ import org.bitcoinj.core.TransactionOutput; import javafx.collections.ObservableList; +import java.util.stream.IntStream; + import lombok.extern.slf4j.Slf4j; +import static bisq.desktop.main.funds.transactions.TransactionAwareTradable.bucketIndex; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -128,7 +131,7 @@ class TransactionAwareTrade implements TransactionAwareTradable { String delegateId = tradeModel.getId(); ObservableList disputes = arbitrationManager.getDisputesAsObservableList(); - boolean isAnyDisputeRelatedToThis = arbitrationManager.getDisputedTradeIds().contains(tradeModel.getId()); + boolean isAnyDisputeRelatedToThis = arbitrationManager.getDisputedTradeIds().contains(delegateId); return isAnyDisputeRelatedToThis && disputes.stream() .anyMatch(dispute -> { @@ -216,4 +219,27 @@ class TransactionAwareTrade implements TransactionAwareTradable { public Tradable asTradable() { return tradeModel; } + + @Override + public IntStream getRelatedTransactionFilter() { + if (tradeModel instanceof Trade && !arbitrationManager.getDisputedTradeIds().contains(tradeModel.getId()) && + !refundManager.getDisputedTradeIds().contains(tradeModel.getId())) { + Trade trade = (Trade) tradeModel; + String takerFeeTxId = trade.getTakerFeeTxId(); + String offerFeeTxId = trade.getOffer() != null ? trade.getOffer().getOfferFeePaymentTxId() : null; + String depositTxId = trade.getDepositTxId(); + String payoutTxId = trade.getPayoutTxId(); + return IntStream.of(DELAYED_PAYOUT_TX_BUCKET_INDEX, bucketIndex(takerFeeTxId), bucketIndex(offerFeeTxId), + bucketIndex(depositTxId), bucketIndex(payoutTxId)) + .filter(i -> i >= 0); + } else if (tradeModel instanceof BsqSwapTrade) { + BsqSwapTrade trade = (BsqSwapTrade) tradeModel; + String swapTxId = trade.getTxId(); + return IntStream.of(bucketIndex(swapTxId)) + .filter(i -> i >= 0); + } else { + // We are involved in a dispute (rare) - don't do any initial tx filtering. + return IntStream.range(0, TX_FILTER_SIZE); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index 2fc2e86876..3fa3bdb401 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -95,6 +95,7 @@ import javafx.util.Callback; import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -289,27 +290,29 @@ public class TransactionsView extends ActivatableView { private void updateList() { Set tradables = tradableRepository.getAll(); + var filterSlices = new RelatedTransactionFilterSlices(tradables.stream() + .map(tradable -> { + if (tradable instanceof OpenOffer) { + return new TransactionAwareOpenOffer((OpenOffer) tradable); + } else if (tradable instanceof TradeModel) { + return new TransactionAwareTrade( + (TradeModel) tradable, + arbitrationManager, + refundManager, + btcWalletService, + pubKeyRing + ); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList())); List transactionsListItems = btcWalletService.getTransactions(false) .stream() .map(transaction -> { - TransactionAwareTradable maybeTradable = tradables.stream() - .map(tradable -> { - if (tradable instanceof OpenOffer) { - return new TransactionAwareOpenOffer((OpenOffer) tradable); - } else if (tradable instanceof TradeModel) { - return new TransactionAwareTrade( - (TradeModel) tradable, - arbitrationManager, - refundManager, - btcWalletService, - pubKeyRing - ); - } else { - return null; - } - }) - .filter(tradable -> tradable != null && tradable.isRelatedToTransaction(transaction)) + TransactionAwareTradable maybeTradable = filterSlices.getAllRelatedTradables(transaction) .findAny() .orElse(null);