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).
This commit is contained in:
Steven Barclay 2023-02-11 01:03:52 +08:00
parent 19a80d19bb
commit 97ef9c1308
No known key found for this signature in database
GPG Key ID: 9FED6BF1176D500B
5 changed files with 134 additions and 18 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<TransactionAwareTradable> tradables;
private final BitSet[] filterSlices;
public RelatedTransactionFilterSlices(Collection<? extends TransactionAwareTradable> 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<TransactionAwareTradable> getAllRelatedTradables(Transaction tx) {
int i = TransactionAwareTradable.bucketIndex(tx);
return filterSlices[i].stream()
.mapToObj(tradables::get)
.filter(t -> t.isRelatedToTransaction(tx));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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<Dispute> 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);
}
}
}

View File

@ -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<VBox, Void> {
private void updateList() {
Set<Tradable> 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<TransactionsListItem> 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);