Optimise TransactionAwareTrade.isRelatedToTransaction

Attempt to remove a bottleneck during the transactions view load, as
revealed by JProfiler, by optimising the code to determine if any given
transaction and trade are related. Since both these sets will tend to
grow linearly with time, causing quadratic slowdown of TransactionsView,
try to alleviate (without completely fixing) the problem.

To do this, add a cached set of disputed trade IDs to DisputeListService
so that TransactionAwareTradable.is(Dispute|RefundPayout)Tx can be made
O(1) in the common case that the given trade is not involved in any
dispute. Also avoid calling Sha256Hash::toString by passing tx IDs as
Sha256Hash objects directly to is(Deposit|Payout)Tx, and short circuit
an expensive call BtcWalletService.getTransaction in isDelayedPayoutTx,
in the common case, by pre-checking the transaction locktime.

This also fixes a bug in isRefundPayoutTx whereby it incorrectly returns
false if there are >1 disputes in the list returned by RefundManager but
the given trade is not involved in the last one.
This commit is contained in:
Steven Barclay 2021-01-26 20:10:47 +00:00
parent 97779f10d8
commit db26a1fe2d
No known key found for this signature in database
GPG key ID: 9FED6BF1176D500B
4 changed files with 47 additions and 42 deletions

View file

@ -34,8 +34,10 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ObservableList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -53,6 +55,8 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
private final Map<String, Subscription> disputeIsClosedSubscriptionsMap = new HashMap<>();
@Getter
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
@Getter
private final Set<String> disputedTradeIds = new HashSet<>();
///////////////////////////////////////////////////////////////////////////////////////////
@ -154,6 +158,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
disputeIsClosedSubscriptionsMap.get(id).unsubscribe();
disputeIsClosedSubscriptionsMap.remove(id);
}
disputedTradeIds.remove(dispute.getTradeId());
});
}
addedList.forEach(dispute -> {
@ -168,6 +173,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
});
});
disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription);
disputedTradeIds.add(dispute.getTradeId());
});
}

View file

@ -69,6 +69,7 @@ import java.security.KeyPair;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -231,6 +232,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return disputeListService.getDisputeList();
}
public Set<String> getDisputedTradeIds() {
return disputeListService.getDisputedTradeIds();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API

View file

@ -36,13 +36,11 @@ import org.bitcoinj.core.TransactionOutput;
import javafx.collections.ObservableList;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
class TransactionAwareTrade implements TransactionAwareTradable {
private final Trade trade;
@ -65,32 +63,31 @@ class TransactionAwareTrade implements TransactionAwareTradable {
@Override
public boolean isRelatedToTransaction(Transaction transaction) {
String txId = transaction.getTxId().toString();
Sha256Hash hash = transaction.getTxId();
String txId = hash.toString();
boolean isTakerOfferFeeTx = txId.equals(trade.getTakerFeeTxId());
boolean isOfferFeeTx = isOfferFeeTx(txId);
boolean isDepositTx = isDepositTx(txId);
boolean isPayoutTx = isPayoutTx(txId);
boolean isDepositTx = isDepositTx(hash);
boolean isPayoutTx = isPayoutTx(hash);
boolean isDisputedPayoutTx = isDisputedPayoutTx(txId);
boolean isDelayedPayoutTx = isDelayedPayoutTx(txId);
boolean isDelayedPayoutTx = transaction.getLockTime() != 0 && isDelayedPayoutTx(txId);
boolean isRefundPayoutTx = isRefundPayoutTx(txId);
return isTakerOfferFeeTx || isOfferFeeTx || isDepositTx || isPayoutTx ||
isDisputedPayoutTx || isDelayedPayoutTx || isRefundPayoutTx;
}
private boolean isPayoutTx(String txId) {
private boolean isPayoutTx(Sha256Hash txId) {
return Optional.ofNullable(trade.getPayoutTx())
.map(Transaction::getTxId)
.map(Sha256Hash::toString)
.map(hash -> hash.equals(txId))
.orElse(false);
}
private boolean isDepositTx(String txId) {
private boolean isDepositTx(Sha256Hash txId) {
return Optional.ofNullable(trade.getDepositTx())
.map(Transaction::getTxId)
.map(Sha256Hash::toString)
.map(hash -> hash.equals(txId))
.orElse(false);
}
@ -104,9 +101,11 @@ class TransactionAwareTrade implements TransactionAwareTradable {
private boolean isDisputedPayoutTx(String txId) {
String delegateId = trade.getId();
ObservableList<Dispute> disputes = arbitrationManager.getDisputesAsObservableList();
return disputes.stream()
boolean isAnyDisputeRelatedToThis = arbitrationManager.getDisputedTradeIds().contains(trade.getId());
return isAnyDisputeRelatedToThis && disputes.stream()
.anyMatch(dispute -> {
String disputePayoutTxId = dispute.getDisputePayoutTxId();
boolean isDisputePayoutTx = txId.equals(disputePayoutTxId);
@ -139,42 +138,37 @@ class TransactionAwareTrade implements TransactionAwareTradable {
if (parentTransaction == null) {
return false;
}
return isDepositTx(parentTransaction.getTxId().toString());
return isDepositTx(parentTransaction.getTxId());
});
}
private boolean isRefundPayoutTx(String txId) {
String tradeId = trade.getId();
ObservableList<Dispute> disputes = refundManager.getDisputesAsObservableList();
AtomicBoolean isRefundTx = new AtomicBoolean(false);
AtomicBoolean isDisputeRelatedToThis = new AtomicBoolean(false);
disputes.forEach(dispute -> {
String disputeTradeId = dispute.getTradeId();
isDisputeRelatedToThis.set(tradeId.equals(disputeTradeId));
if (isDisputeRelatedToThis.get()) {
Transaction tx = btcWalletService.getTransaction(txId);
if (tx != null) {
tx.getOutputs().forEach(txo -> {
if (btcWalletService.isTransactionOutputMine(txo)) {
try {
Address receiverAddress = txo.getScriptPubKey().getToAddress(btcWalletService.getParams());
Contract contract = checkNotNull(trade.getContract());
String myPayoutAddressString = contract.isMyRoleBuyer(pubKeyRing) ?
contract.getBuyerPayoutAddressString() :
contract.getSellerPayoutAddressString();
if (receiverAddress != null && myPayoutAddressString.equals(receiverAddress.toString())) {
isRefundTx.set(true);
}
} catch (Throwable ignore) {
}
boolean isAnyDisputeRelatedToThis = refundManager.getDisputedTradeIds().contains(tradeId);
if (isAnyDisputeRelatedToThis) {
Transaction tx = btcWalletService.getTransaction(txId);
if (tx != null) {
for (TransactionOutput txo : tx.getOutputs()) {
if (btcWalletService.isTransactionOutputMine(txo)) {
try {
Address receiverAddress = txo.getScriptPubKey().getToAddress(btcWalletService.getParams());
Contract contract = checkNotNull(trade.getContract());
String myPayoutAddressString = contract.isMyRoleBuyer(pubKeyRing) ?
contract.getBuyerPayoutAddressString() :
contract.getSellerPayoutAddressString();
if (receiverAddress != null && myPayoutAddressString.equals(receiverAddress.toString())) {
return true;
}
} catch (RuntimeException ignore) {
}
});
}
}
}
});
return isRefundTx.get() && isDisputeRelatedToThis.get();
}
return false;
}
@Override

View file

@ -28,7 +28,7 @@ import org.bitcoinj.core.Transaction;
import javafx.collections.FXCollections;
import java.util.Collections;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
@ -41,8 +41,6 @@ import static org.mockito.Mockito.when;
public class TransactionAwareTradeTest {
private static final Sha256Hash XID = Sha256Hash.wrap("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef");
private Transaction transaction;
private ArbitrationManager arbitrationManager;
private Trade delegate;
@ -95,7 +93,10 @@ public class TransactionAwareTradeTest {
when(dispute.getTradeId()).thenReturn(tradeId);
when(arbitrationManager.getDisputesAsObservableList())
.thenReturn(FXCollections.observableArrayList(Collections.singleton(dispute)));
.thenReturn(FXCollections.observableArrayList(Set.of(dispute)));
when(arbitrationManager.getDisputedTradeIds())
.thenReturn(Set.of(tradeId));
when(delegate.getId()).thenReturn(tradeId);