Merge pull request #6579 from stejbac/further-speed-up-transactions-view-load

Further speed up transactions view load
This commit is contained in:
Alejandro García 2023-02-12 14:05:36 +00:00 committed by GitHub
commit 581fbd1d2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 222 additions and 100 deletions

View file

@ -7,6 +7,7 @@ import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Monetary;
@ -33,12 +34,15 @@ import org.jetbrains.annotations.NotNull;
public class FormattingUtils {
public static final String BTC_FORMATTER_KEY = "BTC";
public final static String RANGE_SEPARATOR = " - ";
public static final String RANGE_SEPARATOR = " - ";
private static final MonetaryFormat fiatPriceFormat = new MonetaryFormat().shift(0).minDecimals(4).repeatOptionalDecimals(0, 0);
private static final MonetaryFormat altcoinFormat = new MonetaryFormat().shift(0).minDecimals(8).repeatOptionalDecimals(0, 0);
private static final DecimalFormat decimalFormat = new DecimalFormat("#.#");
private static final ThreadLocal<Tuple3<Locale, DateFormat, DateFormat>> cachedUtcDateTimeFormatters = new ThreadLocal<>();
private static final ThreadLocal<Tuple3<Locale, DateFormat, DateFormat>> cachedLocalDateTimeFormatters = new ThreadLocal<>();
public static String formatCoinWithCode(long value, MonetaryFormat coinFormat) {
return formatCoinWithCode(Coin.valueOf(value), coinFormat);
}
@ -183,12 +187,25 @@ public class FormattingUtils {
public static String formatDateTime(Date date, boolean useLocaleAndLocalTimezone) {
Locale locale = useLocaleAndLocalTimezone ? GlobalSettings.getLocale() : Locale.US;
DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
DateFormat timeInstance = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
if (!useLocaleAndLocalTimezone) {
dateInstance.setTimeZone(TimeZone.getTimeZone("UTC"));
timeInstance.setTimeZone(TimeZone.getTimeZone("UTC"));
var formatterTuple = (useLocaleAndLocalTimezone ?
cachedLocalDateTimeFormatters : cachedUtcDateTimeFormatters).get();
if (formatterTuple == null || !formatterTuple.first.equals(locale)) {
formatterTuple = new Tuple3<>(locale,
DateFormat.getDateInstance(DateFormat.DEFAULT, locale),
DateFormat.getTimeInstance(DateFormat.DEFAULT, locale));
if (useLocaleAndLocalTimezone) {
cachedLocalDateTimeFormatters.set(formatterTuple);
} else {
formatterTuple.second.setTimeZone(TimeZone.getTimeZone("UTC"));
formatterTuple.third.setTimeZone(TimeZone.getTimeZone("UTC"));
cachedUtcDateTimeFormatters.set(formatterTuple);
}
}
DateFormat dateInstance = formatterTuple.second;
DateFormat timeInstance = formatterTuple.third;
return formatDateTime(date, dateInstance, timeInstance);
}
@ -288,7 +305,8 @@ public class FormattingUtils {
}
@NotNull
public static String fillUpPlacesWithEmptyStrings(String formattedNumber, @SuppressWarnings("unused") int maxNumberOfDigits) {
public static String fillUpPlacesWithEmptyStrings(String formattedNumber,
@SuppressWarnings("unused") int maxNumberOfDigits) {
//FIXME: temporary deactivate adding spaces in front of numbers as we don't use a monospace font right now.
/*int numberOfPlacesToFill = maxNumberOfDigits - formattedNumber.length();
for (int i = 0; i < numberOfPlacesToFill; i++) {

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;
@ -34,12 +36,18 @@ class TransactionAwareOpenOffer implements TransactionAwareTradable {
Offer offer = delegate.getOffer();
String paymentTxId = offer.getOfferFeePaymentTxId();
String txId = transaction.getTxId().toString();
return paymentTxId != null && paymentTxId.equals(txId);
return paymentTxId != null && paymentTxId.equals(transaction.getTxId().toString());
}
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

@ -29,18 +29,23 @@ import bisq.core.trade.model.bisq_v1.Trade;
import bisq.core.trade.model.bsq_swap.BsqSwapTrade;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import com.google.common.collect.ImmutableSet;
import javafx.collections.ObservableList;
import java.util.Optional;
import java.util.Set;
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
@ -51,6 +56,11 @@ class TransactionAwareTrade implements TransactionAwareTradable {
private final BtcWalletService btcWalletService;
private final PubKeyRing pubKeyRing;
// As Sha256Hash.toString() is expensive, cache the last result, which will usually be next one needed.
private static Tuple2<Sha256Hash, String> lastTxIdTuple;
// Similarly, cache the last computed set of tx receiver addresses, to speed up 'isRefundPayoutTx'.
private static Tuple2<String, Set<String>> lastReceiverAddressStringsTuple;
TransactionAwareTrade(TradeModel tradeModel,
ArbitrationManager arbitrationManager,
RefundManager refundManager,
@ -66,15 +76,19 @@ class TransactionAwareTrade implements TransactionAwareTradable {
@Override
public boolean isRelatedToTransaction(Transaction transaction) {
Sha256Hash hash = transaction.getTxId();
String txId = hash.toString();
var txIdTuple = lastTxIdTuple;
if (txIdTuple == null || !txIdTuple.first.equals(hash)) {
lastTxIdTuple = txIdTuple = new Tuple2<>(hash, hash.toString());
}
String txId = txIdTuple.second;
boolean tradeRelated = false;
if (tradeModel instanceof Trade) {
Trade trade = (Trade) tradeModel;
boolean isTakerOfferFeeTx = txId.equals(trade.getTakerFeeTxId());
boolean isOfferFeeTx = isOfferFeeTx(txId);
boolean isDepositTx = isDepositTx(hash);
boolean isPayoutTx = isPayoutTx(hash);
boolean isDepositTx = isDepositTx(txId);
boolean isPayoutTx = isPayoutTx(txId);
boolean isDisputedPayoutTx = isDisputedPayoutTx(txId);
boolean isDelayedPayoutTx = transaction.getLockTime() != 0 && isDelayedPayoutTx(txId);
boolean isRefundPayoutTx = isRefundPayoutTx(trade, txId);
@ -91,36 +105,28 @@ class TransactionAwareTrade implements TransactionAwareTradable {
return tradeRelated || isBsqSwapTrade;
}
private boolean isPayoutTx(Sha256Hash txId) {
private boolean isPayoutTx(String txId) {
if (isBsqSwapTrade())
return false;
Trade trade = (Trade) tradeModel;
return Optional.ofNullable(trade.getPayoutTx())
.map(Transaction::getTxId)
.map(hash -> hash.equals(txId))
.orElse(false);
return txId.equals(trade.getPayoutTxId());
}
private boolean isDepositTx(Sha256Hash txId) {
private boolean isDepositTx(String txId) {
if (isBsqSwapTrade())
return false;
Trade trade = (Trade) tradeModel;
return Optional.ofNullable(trade.getDepositTx())
.map(Transaction::getTxId)
.map(hash -> hash.equals(txId))
.orElse(false);
return txId.equals(trade.getDepositTxId());
}
private boolean isOfferFeeTx(String txId) {
if (isBsqSwapTrade())
return false;
return Optional.ofNullable(tradeModel.getOffer())
.map(Offer::getOfferFeePaymentTxId)
.map(paymentTxId -> paymentTxId.equals(txId))
.orElse(false);
Offer offer = tradeModel.getOffer();
return offer != null && txId.equals(offer.getOfferFeePaymentTxId());
}
private boolean isDisputedPayoutTx(String txId) {
@ -130,7 +136,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 -> {
@ -155,7 +161,7 @@ class TransactionAwareTrade implements TransactionAwareTradable {
if (transaction.getLockTime() == 0)
return false;
if (transaction.getInputs() == null)
if (transaction.getInputs() == null || transaction.getInputs().size() != 1)
return false;
return transaction.getInputs().stream()
@ -168,7 +174,7 @@ class TransactionAwareTrade implements TransactionAwareTradable {
if (parentTransaction == null) {
return false;
}
return isDepositTx(parentTransaction.getTxId());
return isDepositTx(parentTransaction.getTxId().toString());
});
}
@ -177,33 +183,45 @@ class TransactionAwareTrade implements TransactionAwareTradable {
return false;
String tradeId = tradeModel.getId();
ObservableList<Dispute> disputes = refundManager.getDisputesAsObservableList();
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) {
}
}
}
try {
Contract contract = checkNotNull(trade.getContract());
String myPayoutAddressString = contract.isMyRoleBuyer(pubKeyRing) ?
contract.getBuyerPayoutAddressString() :
contract.getSellerPayoutAddressString();
return getReceiverAddressStrings(txId).contains(myPayoutAddressString);
} catch (RuntimeException ignore) {
}
}
return false;
}
private Set<String> getReceiverAddressStrings(String txId) {
var tuple = lastReceiverAddressStringsTuple;
if (tuple == null || !tuple.first.equals(txId)) {
lastReceiverAddressStringsTuple = tuple = computeReceiverAddressStringsTuple(txId);
}
return tuple != null ? tuple.second : ImmutableSet.of();
}
private Tuple2<String, Set<String>> computeReceiverAddressStringsTuple(String txId) {
Transaction tx = btcWalletService.getTransaction(txId);
if (tx == null) {
// Clear cache if the tx isn't found, as theoretically it could be added to the wallet later.
return null;
}
Set<String> addressStrings = tx.getOutputs().stream()
.filter(btcWalletService::isTransactionOutputMine)
.map(txo -> txo.getScriptPubKey().getToAddress(btcWalletService.getParams()))
.map(Address::toString)
.collect(ImmutableSet.toImmutableSet());
return new Tuple2<>(txId, addressStrings);
}
private boolean isBsqSwapTrade() {
return tradeModel instanceof BsqSwapTrade;
}
@ -219,4 +237,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

@ -17,12 +17,11 @@
package bisq.desktop.main.funds.transactions;
import bisq.desktop.util.filtering.FilterableListItem;
import bisq.desktop.components.indicator.TxConfidenceIndicator;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.filtering.FilterableListItem;
import bisq.core.btc.listeners.TxConfidenceListener;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.WalletService;
@ -59,7 +58,6 @@ import javax.annotation.Nullable;
@Slf4j
class TransactionsListItem implements FilterableListItem {
private final BtcWalletService btcWalletService;
private final CoinFormatter formatter;
private String dateString;
private final Date date;
@ -69,7 +67,6 @@ class TransactionsListItem implements FilterableListItem {
private String details = "";
private String addressString = "";
private String direction = "";
private TxConfidenceListener txConfidenceListener;
private boolean received;
private Coin amountAsCoin = Coin.ZERO;
private String memo = "";
@ -91,7 +88,6 @@ class TransactionsListItem implements FilterableListItem {
// used at exportCSV
TransactionsListItem() {
date = null;
btcWalletService = null;
txId = null;
formatter = null;
isDustAttackTx = false;
@ -105,7 +101,6 @@ class TransactionsListItem implements FilterableListItem {
DaoFacade daoFacade,
CoinFormatter formatter,
long ignoreDustThreshold) {
this.btcWalletService = btcWalletService;
this.formatter = formatter;
this.memo = transaction.getMemo();
@ -197,7 +192,6 @@ class TransactionsListItem implements FilterableListItem {
if (optionalTradable.isPresent()) {
tradable = optionalTradable.get();
String tradeId = tradable.getShortId();
if (tradable instanceof OpenOffer) {
details = Res.get("funds.tx.createOfferFee");
} else if (tradable instanceof Trade) {
@ -304,19 +298,6 @@ class TransactionsListItem implements FilterableListItem {
GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator);
confirmations = confidence.getDepthInBlocks();
}});
txConfidenceListener = new TxConfidenceListener(txId) {
@Override
public void onTransactionConfidenceChanged(TransactionConfidence confidence) {
GUIUtil.updateConfidence(confidence, lazy().tooltip, lazy().txConfidenceIndicator);
confirmations = confidence.getDepthInBlocks();
}
};
btcWalletService.addTxConfidenceListener(txConfidenceListener);
}
public void cleanup() {
btcWalletService.removeTxConfidenceListener(txConfidenceListener);
}

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;
@ -201,7 +202,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
dateColumn.setComparator(Comparator.comparing(TransactionsListItem::getDate));
tradeIdColumn.setComparator(Comparator.comparing(o -> o.getTradable() != null ? o.getTradable().getId() : ""));
detailsColumn.setComparator(Comparator.comparing(o -> o.getDetails()));
detailsColumn.setComparator(Comparator.comparing(TransactionsListItem::getDetails));
addressColumn.setComparator(Comparator.comparing(item -> item.getDirection() + item.getAddressString()));
transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId));
amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmountAsCoin));
@ -211,9 +212,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
walletChangeEventListener = wallet -> {
updateList();
};
walletChangeEventListener = wallet -> updateList();
keyEventEventHandler = event -> {
// Not intended to be public to users as the feature is not well tested
@ -280,7 +279,6 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
protected void deactivate() {
filterBox.deactivate();
sortedList.comparatorProperty().unbind();
observableList.forEach(TransactionsListItem::cleanup);
btcWalletService.removeChangeEventListener(walletChangeEventListener);
if (scene != null)
@ -290,28 +288,30 @@ 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 -> {
Set<Tradable> tradables = tradableRepository.getAll();
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);
@ -327,7 +327,6 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
})
.collect(Collectors.toList());
// are sorted by getRecentTransactions
transactionsListItems.forEach(TransactionsListItem::cleanup);
observableList.setAll(transactionsListItems);
}
@ -419,15 +418,13 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
TransactionsListItem> column) {
return new TableCell<>() {
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(final TransactionsListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (item.isDustAttackTx()) {
hyperlinkWithIcon = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.WARNING_SIGN);
var hyperlinkWithIcon = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.WARNING_SIGN);
hyperlinkWithIcon.setOnAction(event -> new Popup().warning(Res.get("funds.tx.dustAttackTx.popup")).show());
setGraphic(hyperlinkWithIcon);
} else {

View file

@ -66,13 +66,13 @@ public class TransactionAwareTradeTest {
@Test
public void testIsRelatedToTransactionWhenPayoutTx() {
when(delegate.getPayoutTx().getTxId()).thenReturn(XID);
when(delegate.getPayoutTxId()).thenReturn(XID.toString());
assertTrue(trade.isRelatedToTransaction(transaction));
}
@Test
public void testIsRelatedToTransactionWhenDepositTx() {
when(delegate.getDepositTx().getTxId()).thenReturn(XID);
when(delegate.getDepositTxId()).thenReturn(XID.toString());
assertTrue(trade.isRelatedToTransaction(transaction));
}