Add new tx-by-id cache to speed up BsqWalletService.isWalletTransaction

Reduce a hotspot searching the BSQ wallet for the user's votes, upon
selecting a list item of the Vote Result view, by optimising the method
'BsqWalletService.isWalletTransaction(String)'. Do this by adding a
lazily initialised Map field, 'walletTransactionsById', kept in sync
with the existing 'walletTransactions' List field, similar to the tx-by-
id cache removed from the base class in the previous commit, so that a
linear scan of that list can be avoided. Don't bother to make the cache
thread safe, however, since 'isWalletTransaction' is only called from
the user thread and wasn't thread safe to begin with -- access to
'walletTransactions' isn't synchronised, and it is updated only on the
user thread, after a 100 ms delay upon any changes to the BSQ wallet.

Also remove the unused methods 'getUnverifiedBsqTransactions()' and
'getBsqWalletTransactions()' from the class.
This commit is contained in:
Steven Barclay 2024-05-05 23:57:12 +02:00
parent d803a67d0d
commit 3a70213d1b
No known key found for this signature in database
GPG Key ID: 9FED6BF1176D500B

View File

@ -48,7 +48,6 @@ import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutput;
@ -60,14 +59,12 @@ import org.bitcoinj.wallet.SendRequest;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -95,6 +92,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
private final DaoStateService daoStateService; private final DaoStateService daoStateService;
private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService; private final UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService;
private final List<Transaction> walletTransactions = new ArrayList<>(); private final List<Transaction> walletTransactions = new ArrayList<>();
private Map<String, Transaction> walletTransactionsById;
private final CopyOnWriteArraySet<BsqBalanceListener> bsqBalanceListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<BsqBalanceListener> bsqBalanceListeners = new CopyOnWriteArraySet<>();
private final List<WalletTransactionsChangeListener> walletTransactionsChangeListeners = new ArrayList<>(); private final List<WalletTransactionsChangeListener> walletTransactionsChangeListeners = new ArrayList<>();
private boolean updateBsqWalletTransactionsPending; private boolean updateBsqWalletTransactionsPending;
@ -233,7 +231,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
private void updateBsqBalance() { private void updateBsqBalance() {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
unverifiedBalance = Coin.valueOf( unverifiedBalance = Coin.valueOf(
getTransactions(false).stream() walletTransactions.stream()
.filter(tx -> tx.getConfidence().getConfidenceType() == PENDING) .filter(tx -> tx.getConfidence().getConfidenceType() == PENDING)
.mapToLong(tx -> { .mapToLong(tx -> {
// Sum up outputs into BSQ wallet and subtract the inputs using lockup or unlocking // Sum up outputs into BSQ wallet and subtract the inputs using lockup or unlocking
@ -270,7 +268,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
.sum() .sum()
); );
Set<String> confirmedTxIdSet = getTransactions(false).stream() Set<String> confirmedTxIdSet = walletTransactions.stream()
.filter(tx -> tx.getConfidence().getConfidenceType() == BUILDING) .filter(tx -> tx.getConfidence().getConfidenceType() == BUILDING)
.map(Transaction::getTxId) .map(Transaction::getTxId)
.map(Sha256Hash::toString) .map(Sha256Hash::toString)
@ -343,13 +341,15 @@ public class BsqWalletService extends WalletService implements DaoStateListener
// BSQ TransactionOutputs and Transactions // BSQ TransactionOutputs and Transactions
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// not thread safe - call only from user thread
public List<Transaction> getClonedWalletTransactions() { public List<Transaction> getClonedWalletTransactions() {
return new ArrayList<>(walletTransactions); return new ArrayList<>(walletTransactions);
} }
// not thread safe - call only from user thread
public Stream<Transaction> getPendingWalletTransactionsStream() { public Stream<Transaction> getPendingWalletTransactionsStream() {
return walletTransactions.stream() return walletTransactions.stream()
.filter(transaction -> transaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING); .filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING);
} }
private void updateBsqWalletTransactions() { private void updateBsqWalletTransactions() {
@ -363,6 +363,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
walletTransactions.clear(); walletTransactions.clear();
walletTransactions.addAll(getTransactions(false)); walletTransactions.addAll(getTransactions(false));
walletTransactionsById = null;
walletTransactionsChangeListeners.forEach(WalletTransactionsChangeListener::onWalletTransactionsChange); walletTransactionsChangeListeners.forEach(WalletTransactionsChangeListener::onWalletTransactionsChange);
updateBsqBalance(); updateBsqBalance();
updateBsqWalletTransactionsPending = false; updateBsqWalletTransactionsPending = false;
@ -371,37 +372,6 @@ public class BsqWalletService extends WalletService implements DaoStateListener
} }
} }
private Set<Transaction> getBsqWalletTransactions() {
return getTransactions(false).stream()
.filter(transaction -> transaction.getConfidence().getConfidenceType() == PENDING ||
daoStateService.containsTx(transaction.getTxId().toString()))
.collect(Collectors.toSet());
}
public Set<Transaction> getUnverifiedBsqTransactions() {
Set<Transaction> bsqWalletTransactions = getBsqWalletTransactions();
Set<Transaction> walletTxs = new HashSet<>(getTransactions(false));
checkArgument(walletTxs.size() >= bsqWalletTransactions.size(),
"We cannot have more txsWithOutputsFoundInBsqTxo than walletTxs");
if (walletTxs.size() == bsqWalletTransactions.size()) {
// As expected
return new HashSet<>();
} else {
Map<String, Transaction> map = walletTxs.stream()
.collect(Collectors.toMap(t -> t.getTxId().toString(), Function.identity()));
Set<String> walletTxIds = walletTxs.stream()
.map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet());
Set<String> bsqTxIds = bsqWalletTransactions.stream()
.map(Transaction::getTxId).map(Sha256Hash::toString).collect(Collectors.toSet());
walletTxIds.stream()
.filter(bsqTxIds::contains)
.forEach(map::remove);
return new HashSet<>(map.values());
}
}
@Override @Override
public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException { public Coin getValueSentFromMeForTransaction(Transaction transaction) throws ScriptException {
Coin result = Coin.ZERO; Coin result = Coin.ZERO;
@ -414,7 +384,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
// We grab the parent tx of the connected output // We grab the parent tx of the connected output
final Transaction parentTransaction = connectedOutput.getParentTransaction(); final Transaction parentTransaction = connectedOutput.getParentTransaction();
final boolean isConfirmed = parentTransaction != null && final boolean isConfirmed = parentTransaction != null &&
parentTransaction.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING; parentTransaction.getConfidence().getConfidenceType() == BUILDING;
if (connectedOutput.isMineOrWatched(wallet)) { if (connectedOutput.isMineOrWatched(wallet)) {
if (isConfirmed) { if (isConfirmed) {
// We lookup if we have a BSQ tx matching the parent tx // We lookup if we have a BSQ tx matching the parent tx
@ -455,7 +425,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
for (int i = 0; i < transaction.getOutputs().size(); i++) { for (int i = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput output = transaction.getOutputs().get(i); TransactionOutput output = transaction.getOutputs().get(i);
final boolean isConfirmed = output.getParentTransaction() != null && final boolean isConfirmed = output.getParentTransaction() != null &&
output.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING; output.getParentTransaction().getConfidence().getConfidenceType() == BUILDING;
if (output.isMineOrWatched(wallet)) { if (output.isMineOrWatched(wallet)) {
if (isConfirmed) { if (isConfirmed) {
if (txOptional.isPresent()) { if (txOptional.isPresent()) {
@ -484,8 +454,13 @@ public class BsqWalletService extends WalletService implements DaoStateListener
return result; return result;
} }
// not thread safe - call only from user thread
public Optional<Transaction> isWalletTransaction(String txId) { public Optional<Transaction> isWalletTransaction(String txId) {
return walletTransactions.stream().filter(e -> e.getTxId().toString().equals(txId)).findAny(); if (walletTransactionsById == null) {
walletTransactionsById = walletTransactions.stream()
.collect(Collectors.toMap(tx -> tx.getTxId().toString(), tx -> tx));
}
return Optional.ofNullable(walletTransactionsById.get(txId));
} }