From c51060a6d48e156b2eb8de2dc44cc0fb1c881cf4 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 21:05:31 +0100 Subject: [PATCH 01/12] Move getAveragePrice and roundToTick to ChartCalculations Make buildUsdPricesPerTickUnit static and pass params Rename usdPriceMapsPerTickUnit to usdAveragePriceMapsPerTickUnit Rename local variable map to usdAveragePriceMap Move method calls syncPriceFeedCurrency() and setMarketPriceFeedCurrency() before other calls (those will become async later) --- .../main/market/trades/ChartCalculations.java | 68 +++++++++++++++++++ .../market/trades/TradesChartsViewModel.java | 68 +++++-------------- 2 files changed, 84 insertions(+), 52 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java new file mode 100644 index 00000000000..67f5aac09c6 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -0,0 +1,68 @@ +/* + * 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.market.trades; + +import bisq.core.trade.statistics.TradeStatistics3; + +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Coin; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; + +import java.util.Date; +import java.util.List; + +public class ChartCalculations { + static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + static long getAveragePrice(List tradeStatisticsList) { + long accumulatedAmount = 0; + long accumulatedVolume = 0; + for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { + accumulatedAmount += tradeStatistics.getAmount(); + accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + } + + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + } + + static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) { + switch (tickUnit) { + case YEAR: + return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case MONTH: + return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case WEEK: + int dayOfWeek = localDate.getDayOfWeek().getValue(); + LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); + return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case DAY: + return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case HOUR: + return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + case MINUTE_10: + return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); + default: + return Date.from(localDate.atZone(ZONE_ID).toInstant()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index b56073f48ba..2852d103f90 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -59,10 +59,6 @@ import javafx.util.Pair; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -81,7 +77,6 @@ class TradesChartsViewModel extends ActivatableViewModel { private static final int TAB_INDEX = 2; - private static final ZoneId ZONE_ID = ZoneId.systemDefault(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -116,7 +111,7 @@ public enum TickUnit { TickUnit tickUnit; final int maxTicks = 90; private int selectedTabIndex; - final Map> usdPriceMapsPerTickUnit = new HashMap<>(); + final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; /////////////////////////////////////////////////////////////////////////////////////////// @@ -156,16 +151,17 @@ protected void activate() { fillTradeCurrencies(); fillTradeCurrenciesOnActivateCalled = true; } - buildUsdPricesPerDay(); - updateSelectedTradeStatistics(getCurrencyCode()); - updateChartData(); syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); + buildUsdPricesPerTickUnit(usdAveragePriceMapsPerTickUnit, tradeStatisticsManager.getObservableTradeStatisticsSet()); + updateSelectedTradeStatistics(getCurrencyCode()); + updateChartData(); } @Override protected void deactivate() { tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); + usdAveragePriceMapsPerTickUnit.clear(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -252,18 +248,19 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - private void buildUsdPricesPerDay() { - if (usdPriceMapsPerTickUnit.isEmpty()) { + private static void buildUsdPricesPerTickUnit(Map> usdAveragePriceMapsPerTickUnit, + Set tradeStatisticsSet) { + if (usdAveragePriceMapsPerTickUnit.isEmpty()) { Map>> dateMapsPerTickUnit = new HashMap<>(); for (TickUnit tick : TickUnit.values()) { dateMapsPerTickUnit.put(tick, new HashMap<>()); } - tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + tradeStatisticsSet.stream() .filter(e -> e.getCurrency().equals("USD")) .forEach(tradeStatistics -> { for (TickUnit tick : TickUnit.values()) { - long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); + long time = ChartCalculations.roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); Map> map = dateMapsPerTickUnit.get(tick); map.putIfAbsent(time, new ArrayList<>()); map.get(time).add(tradeStatistics); @@ -272,24 +269,12 @@ private void buildUsdPricesPerDay() { dateMapsPerTickUnit.forEach((tick, map) -> { HashMap priceMap = new HashMap<>(); - map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); - usdPriceMapsPerTickUnit.put(tick, priceMap); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, ChartCalculations.getAveragePrice(tradeStatisticsList))); + usdAveragePriceMapsPerTickUnit.put(tick, priceMap); }); } } - private long getAveragePrice(List tradeStatisticsList) { - long accumulatedAmount = 0; - long accumulatedVolume = 0; - for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { - accumulatedAmount += tradeStatistics.getAmount(); - accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); - } - - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); - return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); - } - private void updateChartData() { // Generate date range and create sets for all ticks itemsPerInterval = new HashMap<>(); @@ -313,7 +298,7 @@ private void updateChartData() { } }); - Map map = usdPriceMapsPerTickUnit.get(tickUnit); + Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); AtomicLong averageUsdPrice = new AtomicLong(0); // create CandleData for defined time interval @@ -322,8 +307,8 @@ private void updateChartData() { .map(entry -> { long tickStartDate = entry.getValue().getKey().getTime(); // If we don't have a price we take the previous one - if (map.containsKey(tickStartDate)) { - averageUsdPrice.set(map.get(tickStartDate)); + if (usdAveragePriceMap.containsKey(tickStartDate)) { + averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); } return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get()); }) @@ -409,28 +394,7 @@ CandleData getCandleData(long tick, Set set, long averageUsdPr } Date roundToTick(Date time, TickUnit tickUnit) { - return roundToTick(time.toInstant().atZone(ZONE_ID).toLocalDateTime(), tickUnit); - } - - Date roundToTick(LocalDateTime localDate, TickUnit tickUnit) { - switch (tickUnit) { - case YEAR: - return Date.from(localDate.withMonth(1).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case MONTH: - return Date.from(localDate.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case WEEK: - int dayOfWeek = localDate.getDayOfWeek().getValue(); - LocalDateTime firstDayOfWeek = ChronoUnit.DAYS.addTo(localDate, 1 - dayOfWeek); - return Date.from(firstDayOfWeek.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case DAY: - return Date.from(localDate.withHour(0).withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case HOUR: - return Date.from(localDate.withMinute(0).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - case MINUTE_10: - return Date.from(localDate.withMinute(localDate.getMinute() - localDate.getMinute() % 10).withSecond(0).withNano(0).atZone(ZONE_ID).toInstant()); - default: - return Date.from(localDate.atZone(ZONE_ID).toInstant()); - } + return ChartCalculations.roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); } private long getTimeFromTick(long tick) { From deeb912f9b7178729ea2aa5cb15ed1c49100625f Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 21:07:31 +0100 Subject: [PATCH 02/12] Move buildUsdPricesPerTickUnit to ChartCalculations --- .../main/market/trades/ChartCalculations.java | 32 +++++++++++++++++++ .../market/trades/TradesChartsViewModel.java | 31 ++---------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index 67f5aac09c6..60a45582176 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -27,12 +27,44 @@ import java.time.ZoneId; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); + static void buildUsdPricesPerTickUnit(Map> usdAveragePriceMapsPerTickUnit, + Set tradeStatisticsSet) { + if (usdAveragePriceMapsPerTickUnit.isEmpty()) { + Map>> dateMapsPerTickUnit = new HashMap<>(); + for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { + dateMapsPerTickUnit.put(tick, new HashMap<>()); + } + + tradeStatisticsSet.stream() + .filter(e -> e.getCurrency().equals("USD")) + .forEach(tradeStatistics -> { + for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { + long time = roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); + Map> map = dateMapsPerTickUnit.get(tick); + map.putIfAbsent(time, new ArrayList<>()); + map.get(time).add(tradeStatistics); + } + }); + + dateMapsPerTickUnit.forEach((tick, map) -> { + HashMap priceMap = new HashMap<>(); + map.forEach((date, tradeStatisticsList) -> priceMap.put(date, getAveragePrice(tradeStatisticsList))); + usdAveragePriceMapsPerTickUnit.put(tick, priceMap); + }); + } + } + + static long getAveragePrice(List tradeStatisticsList) { long accumulatedAmount = 0; long accumulatedVolume = 0; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 2852d103f90..8c94072f9c0 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -153,7 +153,9 @@ protected void activate() { } syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); - buildUsdPricesPerTickUnit(usdAveragePriceMapsPerTickUnit, tradeStatisticsManager.getObservableTradeStatisticsSet()); + + ChartCalculations.buildUsdPricesPerTickUnit(usdAveragePriceMapsPerTickUnit, tradeStatisticsManager.getObservableTradeStatisticsSet()); + updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); } @@ -248,33 +250,6 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - private static void buildUsdPricesPerTickUnit(Map> usdAveragePriceMapsPerTickUnit, - Set tradeStatisticsSet) { - if (usdAveragePriceMapsPerTickUnit.isEmpty()) { - Map>> dateMapsPerTickUnit = new HashMap<>(); - for (TickUnit tick : TickUnit.values()) { - dateMapsPerTickUnit.put(tick, new HashMap<>()); - } - - tradeStatisticsSet.stream() - .filter(e -> e.getCurrency().equals("USD")) - .forEach(tradeStatistics -> { - for (TickUnit tick : TickUnit.values()) { - long time = ChartCalculations.roundToTick(tradeStatistics.getLocalDateTime(), tick).getTime(); - Map> map = dateMapsPerTickUnit.get(tick); - map.putIfAbsent(time, new ArrayList<>()); - map.get(time).add(tradeStatistics); - } - }); - - dateMapsPerTickUnit.forEach((tick, map) -> { - HashMap priceMap = new HashMap<>(); - map.forEach((date, tradeStatisticsList) -> priceMap.put(date, ChartCalculations.getAveragePrice(tradeStatisticsList))); - usdAveragePriceMapsPerTickUnit.put(tick, priceMap); - }); - } - } - private void updateChartData() { // Generate date range and create sets for all ticks itemsPerInterval = new HashMap<>(); From ea66a510dc63761fa8dea698645b3928ef0c207d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 21:13:36 +0100 Subject: [PATCH 03/12] Make buildUsdPricesPerTickUnit async using CompletableFuture --- .../main/market/trades/ChartCalculations.java | 10 ++++++---- .../market/trades/TradesChartsViewModel.java | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index 60a45582176..e68561a9e1e 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -33,13 +33,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); - static void buildUsdPricesPerTickUnit(Map> usdAveragePriceMapsPerTickUnit, - Set tradeStatisticsSet) { - if (usdAveragePriceMapsPerTickUnit.isEmpty()) { + static CompletableFuture>> buildUsdPricesPerTickUnit(Set tradeStatisticsSet) { + return CompletableFuture.supplyAsync(() -> { + Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); Map>> dateMapsPerTickUnit = new HashMap<>(); for (TradesChartsViewModel.TickUnit tick : TradesChartsViewModel.TickUnit.values()) { dateMapsPerTickUnit.put(tick, new HashMap<>()); @@ -61,7 +62,8 @@ static void buildUsdPricesPerTickUnit(Map priceMap.put(date, getAveragePrice(tradeStatisticsList))); usdAveragePriceMapsPerTickUnit.put(tick, priceMap); }); - } + return usdAveragePriceMapsPerTickUnit; + }); } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 8c94072f9c0..59c6144bada 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -38,6 +38,7 @@ import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; +import bisq.common.UserThread; import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; @@ -154,10 +155,21 @@ protected void activate() { syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); - ChartCalculations.buildUsdPricesPerTickUnit(usdAveragePriceMapsPerTickUnit, tradeStatisticsManager.getObservableTradeStatisticsSet()); + ChartCalculations.buildUsdPricesPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) + .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { + if (throwable != null) { + log.error(throwable.toString()); + return; + } - updateSelectedTradeStatistics(getCurrencyCode()); - updateChartData(); + UserThread.execute(() -> { + this.usdAveragePriceMapsPerTickUnit.clear(); + this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); + + updateSelectedTradeStatistics(getCurrencyCode()); + updateChartData(); + }); + }); } @Override From 0271af995b5e925c2ccc747455afff0d7379748c Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 21:18:44 +0100 Subject: [PATCH 04/12] Add modelReady property and let view fill list once model is ready --- .../main/market/trades/TradesChartsView.java | 26 ++++++++++++++++--- .../market/trades/TradesChartsViewModel.java | 3 +++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 3c7d2501404..20af97e32c7 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -172,6 +172,7 @@ private CurrencyListItem specialShowAllItem() { private ChangeListener priceColumnLabelListener; private ListChangeListener> itemsChangeListener; private ListChangeListener tradeStatisticsByCurrencyListener; + private ChangeListener modelReadyListener; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding currencySelectionBinding; @@ -243,7 +244,9 @@ public void initialize() { }; tradeStatisticsByCurrencyListener = c -> { nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); - fillList(); + if (model.modelReady.get()) { + fillList(); + } }; parentHeightListener = (observable, oldValue, newValue) -> layout(); @@ -283,10 +286,23 @@ public void initialize() { return null; }); + modelReadyListener = (observable, oldValue, newValue) -> { + if (newValue) { + long ts = System.currentTimeMillis(); + + fillList(); + tableView.setItems(sortedList); + layout(); + + log.error("{}", System.currentTimeMillis() - ts); + UserThread.execute(() -> model.modelReady.removeListener(modelReadyListener)); + } + }; } @Override protected void activate() { + long ts = System.currentTimeMillis(); // root.getParent() is null at initialize tabPaneSelectionModel = GUIUtil.getParentOfType(root, JFXTabPane.class).getSelectionModel(); selectedTabIndexListener = (observable, oldValue, newValue) -> model.setSelectedTabIndex((int) newValue); @@ -358,9 +374,10 @@ else if (model.getSelectedCurrencyListItem().isPresent()) user.requestPersistence(); }); - fillList(); - tableView.setItems(sortedList); - layout(); + model.modelReady.addListener(modelReadyListener); + + // modelReadyListener will get triggered once async data preparation in model is completed + log.error("activate took {} ms", System.currentTimeMillis() - ts); } @Override @@ -388,6 +405,7 @@ protected void deactivate() { if (rootParent != null) { rootParent.heightProperty().removeListener(parentHeightListener); } + model.modelReady.removeListener(modelReadyListener); } private void showVolumeAsUsd(Boolean showUsd) { diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 59c6144bada..00333e89b8f 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -114,6 +114,8 @@ public enum TickUnit { private int selectedTabIndex; final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; + final BooleanProperty modelReady = new SimpleBooleanProperty(false); + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -168,6 +170,7 @@ protected void activate() { updateSelectedTradeStatistics(getCurrencyCode()); updateChartData(); + modelReady.set(true); }); }); } From ca145b0eb37700ac8d3d21a1b3f10a1be5f486c0 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 21:23:44 +0100 Subject: [PATCH 05/12] Add static method getTradeStatisticsForCurrency and use it instead of updateSelectedTradeStatistics (preparation for follow up commit) --- .../main/market/trades/TradesChartsViewModel.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 00333e89b8f..9f63faec5d8 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -168,7 +168,11 @@ protected void activate() { this.usdAveragePriceMapsPerTickUnit.clear(); this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); - updateSelectedTradeStatistics(getCurrencyCode()); + List list = getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsSet(), + getCurrencyCode(), + showAllTradeCurrenciesProperty.get()); + selectedTradeStatistics.setAll(list); + updateChartData(); modelReady.set(true); }); @@ -318,6 +322,14 @@ private void updateChartData() { .collect(Collectors.toList())); } + private static List getTradeStatisticsForCurrency(Set tradeStatisticsSet, + String currencyCode, + boolean showAllTradeCurrencies) { + return tradeStatisticsSet.stream() + .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) + .collect(Collectors.toList()); + } + private void updateSelectedTradeStatistics(String currencyCode) { selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) From 603e9fde17d612c2b2c2aa6535fdd0a4080dadaa Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 22:49:01 +0100 Subject: [PATCH 06/12] Add CompletableFutureUtils Convenience util for CompletableFuture.allOf method --- .../common/util/CompletableFutureUtils.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 common/src/main/java/bisq/common/util/CompletableFutureUtils.java diff --git a/common/src/main/java/bisq/common/util/CompletableFutureUtils.java b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java new file mode 100644 index 00000000000..f46cb760e04 --- /dev/null +++ b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java @@ -0,0 +1,37 @@ +package bisq.common.util;/* + * 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 . + */ + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class CompletableFutureUtils { + /** + * @param list List of futures + * @param The generic type of the future + * @return Returns a CompletableFuture with a list of the futures we got as parameter once all futures + * are completed (incl. exceptionally completed). + */ + public static CompletableFuture> allOf(List> list) { + CompletableFuture allFuturesResult = CompletableFuture.allOf(list.toArray(new CompletableFuture[list.size()])); + return allFuturesResult.thenApply(v -> + list.stream(). + map(CompletableFuture::join). + collect(Collectors.toList()) + ); + } +} From 65706e7c14f6e7a7443f569b529b9b75b79c439d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 22:52:39 +0100 Subject: [PATCH 07/12] Add async handling with CompletableFuture to fillList The creation of TradeStatistics3ListItem is rather fast but the applying to the list is due sorting pretty slow (300 ms) as its > 100k items. We do the applying on the callback thread. Seems JavaFx permits that. So we can keep the UI thread unblocked. Remove modelReadyListener Renamed model.selectedTradeStatistics to model.tradeStatisticsByCurrency --- .../main/market/trades/TradesChartsView.java | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 20af97e32c7..2c51f35f031 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -103,6 +103,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -172,7 +173,6 @@ private CurrencyListItem specialShowAllItem() { private ChangeListener priceColumnLabelListener; private ListChangeListener> itemsChangeListener; private ListChangeListener tradeStatisticsByCurrencyListener; - private ChangeListener modelReadyListener; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding currencySelectionBinding; @@ -243,10 +243,8 @@ public void initialize() { layoutChart(); }; tradeStatisticsByCurrencyListener = c -> { - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); - if (model.modelReady.get()) { - fillList(); - } + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); + fillList(); }; parentHeightListener = (observable, oldValue, newValue) -> layout(); @@ -285,19 +283,6 @@ public void initialize() { layout(); return null; }); - - modelReadyListener = (observable, oldValue, newValue) -> { - if (newValue) { - long ts = System.currentTimeMillis(); - - fillList(); - tableView.setItems(sortedList); - layout(); - - log.error("{}", System.currentTimeMillis() - ts); - UserThread.execute(() -> model.modelReady.removeListener(modelReadyListener)); - } - }; } @Override @@ -336,7 +321,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) toggleGroup.selectedToggleProperty().addListener(timeUnitChangeListener); priceAxisY.widthProperty().addListener(priceAxisYWidthListener); volumeAxisY.widthProperty().addListener(volumeAxisYWidthListener); - model.selectedTradeStatistics.addListener(tradeStatisticsByCurrencyListener); + model.tradeStatisticsByCurrency.addListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().bind(priceColumnLabel); priceColumnLabel.addListener(priceColumnLabelListener); @@ -353,7 +338,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); - nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.selectedTradeStatistics.size())); + nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); exportLink.setOnAction(e -> exportToCsv()); UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); @@ -374,9 +359,10 @@ else if (model.getSelectedCurrencyListItem().isPresent()) user.requestPersistence(); }); - model.modelReady.addListener(modelReadyListener); + tableView.setItems(sortedList); + fillList(); + layout(); - // modelReadyListener will get triggered once async data preparation in model is completed log.error("activate took {} ms", System.currentTimeMillis() - ts); } @@ -387,7 +373,7 @@ protected void deactivate() { toggleGroup.selectedToggleProperty().removeListener(timeUnitChangeListener); priceAxisY.widthProperty().removeListener(priceAxisYWidthListener); volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener); - model.selectedTradeStatistics.removeListener(tradeStatisticsByCurrencyListener); + model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); priceAxisY.labelProperty().unbind(); priceColumnLabel.removeListener(priceColumnLabelListener); @@ -405,7 +391,6 @@ protected void deactivate() { if (rootParent != null) { rootParent.heightProperty().removeListener(parentHeightListener); } - model.modelReady.removeListener(modelReadyListener); } private void showVolumeAsUsd(Boolean showUsd) { @@ -416,12 +401,22 @@ private void showVolumeAsUsd(Boolean showUsd) { } private void fillList() { - List tradeStatistics3ListItems = model.selectedTradeStatistics.stream() - .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, - coinFormatter, - model.showAllTradeCurrenciesProperty.get())) - .collect(Collectors.toList()); - listItems.setAll(tradeStatistics3ListItems); + long ts = System.currentTimeMillis(); + CompletableFuture.supplyAsync(() -> { + return model.tradeStatisticsByCurrency.stream() + .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, + coinFormatter, + model.showAllTradeCurrenciesProperty.get())) + .collect(Collectors.toList()); + }).whenComplete((items, throwable) -> { + log.error("Creating listItems took {} ms", System.currentTimeMillis() - ts); + long ts2 = System.currentTimeMillis(); + listItems.setAll(items); + // Is slow because of sorting of > 100k items. But at least it seems it works on the thread from the + // CompletableFuture callback, so we do not block the UI thread;. + log.error("Applying sorted list took {} ms", + System.currentTimeMillis() - ts2); + }); } private void exportToCsv() { From 7ad5993aad6224dad93fe675824dc954fd08d959 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 22:57:05 +0100 Subject: [PATCH 08/12] Run getUsdAveragePriceMapsPerTickUnit and getTradeStatisticsForCurrency in parallel and once both are done we call asyncUpdateChartData (not yet refactored). Clear all data at deactivate This cause a bit of costs when we activate again but as we delegate now all work to threads it should be OK. It decreases the memory usage if we do not keep those data in the fields. The View classes are cached in the view loader so all data in fields stays in memory once it was activated once and not manually cleared in deactivate. Move getTradeStatisticsForCurrency to ChartCalculations Rename buildUsdPricesPerTickUnit to getUsdAveragePriceMapsPerTickUnit Rename selectedTradeStatistics to tradeStatisticsByCurrency Make itemsPerInterval final Remove modelReady Add deactivateCalled flag --- .../main/market/trades/ChartCalculations.java | 12 +- .../market/trades/TradesChartsViewModel.java | 104 ++++++++++++++---- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index e68561a9e1e..4a1cc60e2e0 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -34,11 +34,12 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); - static CompletableFuture>> buildUsdPricesPerTickUnit(Set tradeStatisticsSet) { + static CompletableFuture>> getUsdAveragePriceMapsPerTickUnit(Set tradeStatisticsSet) { return CompletableFuture.supplyAsync(() -> { Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); Map>> dateMapsPerTickUnit = new HashMap<>(); @@ -66,6 +67,15 @@ static CompletableFuture>> b }); } + static CompletableFuture> getTradeStatisticsForCurrency(Set tradeStatisticsSet, + String currencyCode, + boolean showAllTradeCurrencies) { + return CompletableFuture.supplyAsync(() -> { + return tradeStatisticsSet.stream() + .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) + .collect(Collectors.toList()); + }); + } static long getAveragePrice(List tradeStatisticsList) { long accumulatedAmount = 0; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 9f63faec5d8..5a5c9529379 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -39,6 +39,7 @@ import bisq.core.user.Preferences; import bisq.common.UserThread; +import bisq.common.util.CompletableFutureUtils; import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; @@ -70,6 +71,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -103,18 +105,18 @@ public enum TickUnit { final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); private final CurrencyList currencyListItems; private final CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""), -1); - final ObservableList selectedTradeStatistics = FXCollections.observableArrayList(); + final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); final ObservableList> volumeItems = FXCollections.observableArrayList(); final ObservableList> volumeInUsdItems = FXCollections.observableArrayList(); - private Map>> itemsPerInterval; + private final Map>> itemsPerInterval = new HashMap<>(); TickUnit tickUnit; final int maxTicks = 90; private int selectedTabIndex; final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; - final BooleanProperty modelReady = new SimpleBooleanProperty(false); + private volatile boolean deactivateCalled; /////////////////////////////////////////////////////////////////////////////////////////// @@ -149,6 +151,10 @@ public enum TickUnit { @Override protected void activate() { + deactivateCalled = false; + long ts = System.currentTimeMillis(); + + tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); if (!fillTradeCurrenciesOnActivateCalled) { fillTradeCurrencies(); @@ -157,32 +163,92 @@ protected void activate() { syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); - ChartCalculations.buildUsdPricesPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) - .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { + long ts1 = System.currentTimeMillis(); + + List> allFutures = new ArrayList<>(); + CompletableFuture task1Done = new CompletableFuture<>(); + allFutures.add(task1Done); + CompletableFuture task2Done = new CompletableFuture<>(); + allFutures.add(task2Done); + CompletableFutureUtils.allOf(allFutures) + .whenComplete((res, throwable) -> { + if (deactivateCalled) { + return; + } if (throwable != null) { log.error(throwable.toString()); return; } + //Once getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit are both completed we + // call updateChartData2 + UserThread.execute(this::asyncUpdateChartData); + }); + // We start getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit in parallel threads for + // better performance + ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) + .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error(throwable.toString()); + task1Done.completeExceptionally(throwable); + return; + } UserThread.execute(() -> { this.usdAveragePriceMapsPerTickUnit.clear(); this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); + log.error("getUsdAveragePriceMapsPerTickUnit took {}", System.currentTimeMillis() - ts1); + task1Done.complete(true); + }); + }); - List list = getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsSet(), - getCurrencyCode(), - showAllTradeCurrenciesProperty.get()); - selectedTradeStatistics.setAll(list); + long ts2 = System.currentTimeMillis(); + ChartCalculations.getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsSet(), + getCurrencyCode(), + showAllTradeCurrenciesProperty.get()) + .whenComplete((list, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error(throwable.toString()); + task2Done.completeExceptionally(throwable); + return; + } - updateChartData(); - modelReady.set(true); + UserThread.execute(() -> { + tradeStatisticsByCurrency.setAll(list); + log.error("getTradeStatisticsForCurrency took {}", System.currentTimeMillis() - ts2); + task2Done.complete(true); }); }); + + log.error("activate took {}", System.currentTimeMillis() - ts); + } + + private void asyncUpdateChartData() { + long ts = System.currentTimeMillis(); + updateChartData(); + log.error("updateChartData took {}", System.currentTimeMillis() - ts); } @Override protected void deactivate() { + deactivateCalled = true; tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); - usdAveragePriceMapsPerTickUnit.clear(); + + // We want to avoid to trigger listeners in the view so we delay a bit. Deactivate on model is called before + // deactivate on view. + UserThread.execute(() -> { + usdAveragePriceMapsPerTickUnit.clear(); + tradeStatisticsByCurrency.clear(); + priceItems.clear(); + volumeItems.clear(); + volumeInUsdItems.clear(); + itemsPerInterval.clear(); + }); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -271,7 +337,7 @@ private void syncPriceFeedCurrency() { private void updateChartData() { // Generate date range and create sets for all ticks - itemsPerInterval = new HashMap<>(); + itemsPerInterval.clear(); Date time = new Date(); for (long i = maxTicks + 1; i >= 0; --i) { Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); @@ -282,7 +348,7 @@ private void updateChartData() { } // Get all entries for the defined time interval - selectedTradeStatistics.forEach(tradeStatistics -> { + tradeStatisticsByCurrency.forEach(tradeStatistics -> { for (long i = maxTicks; i > 0; --i) { Pair> pair = itemsPerInterval.get(i); if (tradeStatistics.getDate().after(pair.getKey())) { @@ -322,16 +388,8 @@ private void updateChartData() { .collect(Collectors.toList())); } - private static List getTradeStatisticsForCurrency(Set tradeStatisticsSet, - String currencyCode, - boolean showAllTradeCurrencies) { - return tradeStatisticsSet.stream() - .filter(e -> showAllTradeCurrencies || e.getCurrency().equals(currencyCode)) - .collect(Collectors.toList()); - } - private void updateSelectedTradeStatistics(String currencyCode) { - selectedTradeStatistics.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList())); } From 041b07e2e105a84a6e257be8c43e275e985e2bfc Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 23:30:48 +0100 Subject: [PATCH 09/12] Move getUpdateChartResult, getCandleData, getTimeFromTickIndex to ChartCalculations Make maxTicks static and rename to MAX_TICKS --- .../main/market/trades/ChartCalculations.java | 203 +++++++++++++++++- .../main/market/trades/TradesChartsView.java | 8 +- .../market/trades/TradesChartsViewModel.java | 151 ++----------- .../trades/TradesChartsViewModelTest.java | 14 +- 4 files changed, 225 insertions(+), 151 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index 4a1cc60e2e0..25165b4d8c1 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -17,28 +17,50 @@ package bisq.desktop.main.market.trades; +import bisq.desktop.main.market.trades.charts.CandleData; +import bisq.desktop.util.DisplayUtils; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.monetary.Altcoin; import bisq.core.trade.statistics.TradeStatistics3; import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; +import com.google.common.annotations.VisibleForTesting; + +import javafx.scene.chart.XYChart; + +import javafx.util.Pair; + import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import lombok.Getter; + public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Async + /////////////////////////////////////////////////////////////////////////////////////////// + static CompletableFuture>> getUsdAveragePriceMapsPerTickUnit(Set tradeStatisticsSet) { return CompletableFuture.supplyAsync(() -> { Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); @@ -77,18 +99,96 @@ static CompletableFuture> getTradeStatisticsForCurrency(S }); } - static long getAveragePrice(List tradeStatisticsList) { - long accumulatedAmount = 0; - long accumulatedVolume = 0; - for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { - accumulatedAmount += tradeStatistics.getAmount(); - accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + static UpdateChartResult getUpdateChartResult(List tradeStatisticsByCurrency, + TradesChartsViewModel.TickUnit tickUnit, + Map> usdAveragePriceMapsPerTickUnit, + String currencyCode) { + // Generate date range and create sets for all ticks + Map>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); + + Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); + AtomicLong averageUsdPrice = new AtomicLong(0); + + // create CandleData for defined time interval + List candleDataList = itemsPerInterval.entrySet().stream() + .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) + .map(entry -> { + long tickStartDate = entry.getValue().getKey().getTime(); + // If we don't have a price we take the previous one + if (usdAveragePriceMap.containsKey(tickStartDate)) { + averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); + } + return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); + }) + .sorted(Comparator.comparingLong(o -> o.tick)) + .collect(Collectors.toList()); + + List> priceItems = candleDataList.stream() + .map(e -> new XYChart.Data(e.tick, e.open, e)) + .collect(Collectors.toList()); + + List> volumeItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) + .collect(Collectors.toList()); + + List> volumeInUsdItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) + .collect(Collectors.toList()); + + return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems); + } + + @Getter + static class UpdateChartResult { + private final Map>> itemsPerInterval; + private final List> priceItems; + private final List> volumeItems; + private final List> volumeInUsdItems; + + public UpdateChartResult(Map>> itemsPerInterval, + List> priceItems, + List> volumeItems, + List> volumeInUsdItems) { + + this.itemsPerInterval = itemsPerInterval; + this.priceItems = priceItems; + this.volumeItems = volumeItems; + this.volumeInUsdItems = volumeInUsdItems; } + } - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); - return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + static Map>> getItemsPerInterval(List tradeStatisticsByCurrency, + TradesChartsViewModel.TickUnit tickUnit) { + // Generate date range and create sets for all ticks + Map>> itemsPerInterval = new HashMap<>(); + Date time = new Date(); + for (long i = TradesChartsViewModel.MAX_TICKS + 1; i >= 0; --i) { + Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); + itemsPerInterval.put(i, pair); + // We adjust the time for the next iteration + time.setTime(time.getTime() - 1); + time = roundToTick(time, tickUnit); + } + + // Get all entries for the defined time interval + tradeStatisticsByCurrency.forEach(tradeStatistics -> { + for (long i = TradesChartsViewModel.MAX_TICKS; i > 0; --i) { + Pair> pair = itemsPerInterval.get(i); + if (tradeStatistics.getDate().after(pair.getKey())) { + pair.getValue().add(tradeStatistics); + break; + } + } + }); + return itemsPerInterval; } + static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit tickUnit) { switch (tickUnit) { case YEAR: @@ -109,4 +209,91 @@ static Date roundToTick(LocalDateTime localDate, TradesChartsViewModel.TickUnit return Date.from(localDate.atZone(ZONE_ID).toInstant()); } } + + static Date roundToTick(Date time, TradesChartsViewModel.TickUnit tickUnit) { + return roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); + } + + private static long getAveragePrice(List tradeStatisticsList) { + long accumulatedAmount = 0; + long accumulatedVolume = 0; + for (TradeStatistics3 tradeStatistics : tradeStatisticsList) { + accumulatedAmount += tradeStatistics.getAmount(); + accumulatedVolume += tradeStatistics.getTradeVolume().getValue(); + } + + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + return MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + } + + @VisibleForTesting + static CandleData getCandleData(long tick, Set set, + long averageUsdPrice, + TradesChartsViewModel.TickUnit tickUnit, + String currencyCode, + Map>> itemsPerInterval) { + long open = 0; + long close = 0; + long high = 0; + long low = 0; + long accumulatedVolume = 0; + long accumulatedAmount = 0; + long numTrades = set.size(); + List tradePrices = new ArrayList<>(); + for (TradeStatistics3 item : set) { + long tradePriceAsLong = item.getTradePrice().getValue(); + // Previously a check was done which inverted the low and high for cryptocurrencies. + low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; + high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; + + accumulatedVolume += item.getTradeVolume().getValue(); + accumulatedAmount += item.getTradeAmount().getValue(); + tradePrices.add(tradePriceAsLong); + } + Collections.sort(tradePrices); + + List list = new ArrayList<>(set); + list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); + if (list.size() > 0) { + open = list.get(0).getTradePrice().getValue(); + close = list.get(list.size() - 1).getTradePrice().getValue(); + } + + long averagePrice; + Long[] prices = new Long[tradePrices.size()]; + tradePrices.toArray(prices); + long medianPrice = MathUtils.getMedian(prices); + boolean isBullish; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + isBullish = close < open; + double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); + averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume); + } else { + isBullish = close > open; + double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); + averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); + } + + Date dateFrom = new Date(getTimeFromTickIndex(tick, itemsPerInterval)); + Date dateTo = new Date(getTimeFromTickIndex(tick + 1, itemsPerInterval)); + String dateString = tickUnit.ordinal() > TradesChartsViewModel.TickUnit.DAY.ordinal() ? + DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : + DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); + + // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. + averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); + long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); + // We store USD value without decimals as its only total volume, no precision is needed. + volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); + return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, + numTrades, isBullish, dateString, volumeInUsd); + } + + static long getTimeFromTickIndex(long tick, Map>> itemsPerInterval) { + if (tick > TradesChartsViewModel.MAX_TICKS + 1 || + itemsPerInterval.get(tick) == null) { + return 0; + } + return itemsPerInterval.get(tick).getKey().getTime(); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 2c51f35f031..13ace457bd5 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -473,7 +473,7 @@ private void exportToCsv() { private void createCharts() { priceSeries = new XYChart.Series<>(); - priceAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + priceAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); priceAxisX.setTickUnit(4); priceAxisX.setMinorTickCount(4); priceAxisX.setMinorTickVisible(true); @@ -539,11 +539,11 @@ public Number fromString(String string) { priceChartPane.getChildren().add(priceChart); - volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + volumeAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); volumeAxisY = new NumberAxis(); volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC"); - volumeInUsdAxisX = new NumberAxis(0, model.maxTicks + 1, 1); + volumeInUsdAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); NumberAxis volumeInUsdAxisY = new NumberAxis(); volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); volumeInUsdChart.setVisible(false); @@ -650,7 +650,7 @@ public String toString(Number object) { long index = MathUtils.doubleToLong((double) object); // The last tick is on the chart edge, it is not well spaced with // the previous tick and interferes with its label. - if (model.maxTicks + 1 == index) return ""; + if (TradesChartsViewModel.MAX_TICKS + 1 == index) return ""; long time = model.getTimeFromTickIndex(index); String fmt = ""; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 5a5c9529379..b7488cb0642 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -20,19 +20,16 @@ import bisq.desktop.Navigation; import bisq.desktop.common.model.ActivatableViewModel; import bisq.desktop.main.MainView; -import bisq.desktop.main.market.trades.charts.CandleData; import bisq.desktop.main.settings.SettingsView; import bisq.desktop.main.settings.preferences.PreferencesView; import bisq.desktop.util.CurrencyList; import bisq.desktop.util.CurrencyListItem; -import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; import bisq.core.locale.CryptoCurrency; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.GlobalSettings; import bisq.core.locale.TradeCurrency; -import bisq.core.monetary.Altcoin; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -40,14 +37,9 @@ import bisq.common.UserThread; import bisq.common.util.CompletableFutureUtils; -import bisq.common.util.MathUtils; - -import org.bitcoinj.core.Coin; import com.google.inject.Inject; -import com.google.common.annotations.VisibleForTesting; - import javafx.scene.chart.XYChart; import javafx.beans.property.BooleanProperty; @@ -62,26 +54,21 @@ import javafx.util.Pair; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import javax.annotation.Nullable; class TradesChartsViewModel extends ActivatableViewModel { - + static final int MAX_TICKS = 90; private static final int TAB_INDEX = 2; - /////////////////////////////////////////////////////////////////////////////////////////// // Enum /////////////////////////////////////////////////////////////////////////////////////////// @@ -112,7 +99,6 @@ public enum TickUnit { private final Map>> itemsPerInterval = new HashMap<>(); TickUnit tickUnit; - final int maxTicks = 90; private int selectedTabIndex; final Map> usdAveragePriceMapsPerTickUnit = new HashMap<>(); private boolean fillTradeCurrenciesOnActivateCalled; @@ -132,7 +118,7 @@ public enum TickUnit { this.navigation = navigation; setChangeListener = change -> { - updateSelectedTradeStatistics(getCurrencyCode()); + updateSelectedTradeStatistics(getCurrencyCode()); //todo updateChartData(); fillTradeCurrencies(); }; @@ -275,7 +261,7 @@ void onSetTradeCurrency(TradeCurrency tradeCurrency) { } preferences.setTradeChartsScreenCurrencyCode(code); - updateSelectedTradeStatistics(getCurrencyCode()); + updateSelectedTradeStatistics(getCurrencyCode());//todo updateChartData(); } } @@ -335,136 +321,26 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - private void updateChartData() { - // Generate date range and create sets for all ticks + void updateChartData() { + ChartCalculations.UpdateChartResult updateChartResult = ChartCalculations.getUpdateChartResult(tradeStatisticsByCurrency, tickUnit, usdAveragePriceMapsPerTickUnit, getCurrencyCode()); itemsPerInterval.clear(); - Date time = new Date(); - for (long i = maxTicks + 1; i >= 0; --i) { - Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); - itemsPerInterval.put(i, pair); - // We adjust the time for the next iteration - time.setTime(time.getTime() - 1); - time = roundToTick(time, tickUnit); - } - - // Get all entries for the defined time interval - tradeStatisticsByCurrency.forEach(tradeStatistics -> { - for (long i = maxTicks; i > 0; --i) { - Pair> pair = itemsPerInterval.get(i); - if (tradeStatistics.getDate().after(pair.getKey())) { - pair.getValue().add(tradeStatistics); - break; - } - } - }); - - Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); - AtomicLong averageUsdPrice = new AtomicLong(0); - - // create CandleData for defined time interval - List candleDataList = itemsPerInterval.entrySet().stream() - .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) - .map(entry -> { - long tickStartDate = entry.getValue().getKey().getTime(); - // If we don't have a price we take the previous one - if (usdAveragePriceMap.containsKey(tickStartDate)) { - averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); - } - return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get()); - }) - .sorted(Comparator.comparingLong(o -> o.tick)) - .collect(Collectors.toList()); - - priceItems.setAll(candleDataList.stream() - .map(e -> new XYChart.Data(e.tick, e.open, e)) - .collect(Collectors.toList())); - - volumeItems.setAll(candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) - .collect(Collectors.toList())); + itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); - volumeInUsdItems.setAll(candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) - .collect(Collectors.toList())); + priceItems.setAll(updateChartResult.getPriceItems()); + volumeItems.setAll(updateChartResult.getVolumeItems()); + volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); + getCurrencyCode(); } + //todo private void updateSelectedTradeStatistics(String currencyCode) { tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) .collect(Collectors.toList())); } - @VisibleForTesting - CandleData getCandleData(long tick, Set set, long averageUsdPrice) { - long open = 0; - long close = 0; - long high = 0; - long low = 0; - long accumulatedVolume = 0; - long accumulatedAmount = 0; - long numTrades = set.size(); - List tradePrices = new ArrayList<>(); - for (TradeStatistics3 item : set) { - long tradePriceAsLong = item.getTradePrice().getValue(); - // Previously a check was done which inverted the low and high for cryptocurrencies. - low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong; - high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong; - - accumulatedVolume += item.getTradeVolume().getValue(); - accumulatedAmount += item.getTradeAmount().getValue(); - tradePrices.add(tradePriceAsLong); - } - Collections.sort(tradePrices); - - List list = new ArrayList<>(set); - list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong)); - if (list.size() > 0) { - open = list.get(0).getTradePrice().getValue(); - close = list.get(list.size() - 1).getTradePrice().getValue(); - } - - long averagePrice; - Long[] prices = new Long[tradePrices.size()]; - tradePrices.toArray(prices); - long medianPrice = MathUtils.getMedian(prices); - boolean isBullish; - if (CurrencyUtil.isCryptoCurrency(getCurrencyCode())) { - isBullish = close < open; - double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume); - } else { - isBullish = close > open; - double accumulatedVolumeAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedVolume, Coin.SMALLEST_UNIT_EXPONENT); - averagePrice = MathUtils.roundDoubleToLong(accumulatedVolumeAsDouble / (double) accumulatedAmount); - } - - Date dateFrom = new Date(getTimeFromTickIndex(tick)); - Date dateTo = new Date(getTimeFromTickIndex(tick + 1)); - String dateString = tickUnit.ordinal() > TickUnit.DAY.ordinal() ? - DisplayUtils.formatDateTimeSpan(dateFrom, dateTo) : - DisplayUtils.formatDate(dateFrom) + " - " + DisplayUtils.formatDate(dateTo); - - // We do not need precision, so we scale down before multiplication otherwise we could get an overflow. - averageUsdPrice = (long) MathUtils.scaleDownByPowerOf10((double) averageUsdPrice, 4); - long volumeInUsd = averageUsdPrice * (long) MathUtils.scaleDownByPowerOf10((double) accumulatedAmount, 4); - // We store USD value without decimals as its only total volume, no precision is needed. - volumeInUsd = (long) MathUtils.scaleDownByPowerOf10((double) volumeInUsd, 4); - return new CandleData(tick, open, close, high, low, averagePrice, medianPrice, accumulatedAmount, accumulatedVolume, - numTrades, isBullish, dateString, volumeInUsd); - } - - Date roundToTick(Date time, TickUnit tickUnit) { - return ChartCalculations.roundToTick(time.toInstant().atZone(ChartCalculations.ZONE_ID).toLocalDateTime(), tickUnit); - } - - private long getTimeFromTick(long tick) { - if (itemsPerInterval == null || itemsPerInterval.get(tick) == null) return 0; - return itemsPerInterval.get(tick).getKey().getTime(); - } - - long getTimeFromTickIndex(long index) { - if (index > maxTicks + 1) return 0; - return getTimeFromTick(index); + long getTimeFromTickIndex(long tick) { + return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); } private boolean isShowAllEntry(@Nullable String id) { @@ -474,4 +350,5 @@ private boolean isShowAllEntry(@Nullable String id) { private boolean isEditEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.EDIT_FLAG); } + } diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index 3fc291f2739..492121b6aca 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -35,6 +35,8 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableSet; +import javafx.util.Pair; + import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -45,6 +47,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.junit.Before; @@ -115,7 +118,8 @@ public void setup() throws IOException { @SuppressWarnings("ConstantConditions") @Test public void testGetCandleData() { - model.selectedTradeCurrencyProperty.setValue(new FiatCurrency("EUR")); + String currencyCode = "EUR"; + model.selectedTradeCurrencyProperty.setValue(new FiatCurrency(currencyCode)); long low = Fiat.parseFiat("EUR", "500").value; long open = Fiat.parseFiat("EUR", "520").value; @@ -167,7 +171,13 @@ public void testGetCandleData() { null, null)); - CandleData candleData = model.getCandleData(model.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(), set, 0); + Map>> itemsPerInterval = null; + long tick = ChartCalculations.roundToTick(now, TradesChartsViewModel.TickUnit.DAY).getTime(); + CandleData candleData = ChartCalculations.getCandleData(tick, + set, + 0, + TradesChartsViewModel.TickUnit.DAY, currencyCode, + itemsPerInterval); assertEquals(open, candleData.open); assertEquals(close, candleData.close); assertEquals(high, candleData.high); From ce8a91fdb3a99f504e331d478981b57ca2dddbcb Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Nov 2021 23:36:39 +0100 Subject: [PATCH 10/12] Make getUpdateChartResult ansync --- .../main/market/trades/ChartCalculations.java | 76 ++++++++++--------- .../market/trades/TradesChartsViewModel.java | 36 +++++---- 2 files changed, 60 insertions(+), 52 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index 25165b4d8c1..9612fdad05a 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -99,43 +99,45 @@ static CompletableFuture> getTradeStatisticsForCurrency(S }); } - static UpdateChartResult getUpdateChartResult(List tradeStatisticsByCurrency, - TradesChartsViewModel.TickUnit tickUnit, - Map> usdAveragePriceMapsPerTickUnit, - String currencyCode) { - // Generate date range and create sets for all ticks - Map>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); - - Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); - AtomicLong averageUsdPrice = new AtomicLong(0); - - // create CandleData for defined time interval - List candleDataList = itemsPerInterval.entrySet().stream() - .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) - .map(entry -> { - long tickStartDate = entry.getValue().getKey().getTime(); - // If we don't have a price we take the previous one - if (usdAveragePriceMap.containsKey(tickStartDate)) { - averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); - } - return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); - }) - .sorted(Comparator.comparingLong(o -> o.tick)) - .collect(Collectors.toList()); - - List> priceItems = candleDataList.stream() - .map(e -> new XYChart.Data(e.tick, e.open, e)) - .collect(Collectors.toList()); - - List> volumeItems = candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) - .collect(Collectors.toList()); - - List> volumeInUsdItems = candleDataList.stream() - .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) - .collect(Collectors.toList()); - - return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems); + static CompletableFuture getUpdateChartResult(List tradeStatisticsByCurrency, + TradesChartsViewModel.TickUnit tickUnit, + Map> usdAveragePriceMapsPerTickUnit, + String currencyCode) { + return CompletableFuture.supplyAsync(() -> { + // Generate date range and create sets for all ticks + Map>> itemsPerInterval = getItemsPerInterval(tradeStatisticsByCurrency, tickUnit); + + Map usdAveragePriceMap = usdAveragePriceMapsPerTickUnit.get(tickUnit); + AtomicLong averageUsdPrice = new AtomicLong(0); + + // create CandleData for defined time interval + List candleDataList = itemsPerInterval.entrySet().stream() + .filter(entry -> entry.getKey() >= 0 && !entry.getValue().getValue().isEmpty()) + .map(entry -> { + long tickStartDate = entry.getValue().getKey().getTime(); + // If we don't have a price we take the previous one + if (usdAveragePriceMap.containsKey(tickStartDate)) { + averageUsdPrice.set(usdAveragePriceMap.get(tickStartDate)); + } + return getCandleData(entry.getKey(), entry.getValue().getValue(), averageUsdPrice.get(), tickUnit, currencyCode, itemsPerInterval); + }) + .sorted(Comparator.comparingLong(o -> o.tick)) + .collect(Collectors.toList()); + + List> priceItems = candleDataList.stream() + .map(e -> new XYChart.Data(e.tick, e.open, e)) + .collect(Collectors.toList()); + + List> volumeItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.accumulatedAmount, candleData)) + .collect(Collectors.toList()); + + List> volumeInUsdItems = candleDataList.stream() + .map(candleData -> new XYChart.Data(candleData.tick, candleData.volumeInUsd, candleData)) + .collect(Collectors.toList()); + + return new UpdateChartResult(itemsPerInterval, priceItems, volumeItems, volumeInUsdItems); + }); } @Getter diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index b7488cb0642..2a18f3962de 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -167,7 +167,7 @@ protected void activate() { } //Once getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit are both completed we // call updateChartData2 - UserThread.execute(this::asyncUpdateChartData); + UserThread.execute(this::updateChartData); }); // We start getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit in parallel threads for @@ -214,10 +214,27 @@ protected void activate() { log.error("activate took {}", System.currentTimeMillis() - ts); } - private void asyncUpdateChartData() { + private void updateChartData() { long ts = System.currentTimeMillis(); - updateChartData(); - log.error("updateChartData took {}", System.currentTimeMillis() - ts); + ChartCalculations.getUpdateChartResult(tradeStatisticsByCurrency, tickUnit, usdAveragePriceMapsPerTickUnit, getCurrencyCode()) + .whenComplete((updateChartResult, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error(throwable.toString()); + return; + } + UserThread.execute(() -> { + itemsPerInterval.clear(); + itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); + + priceItems.setAll(updateChartResult.getPriceItems()); + volumeItems.setAll(updateChartResult.getVolumeItems()); + volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); + log.error("updateChartData took {}", System.currentTimeMillis() - ts); + }); + }); } @Override @@ -321,17 +338,6 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - void updateChartData() { - ChartCalculations.UpdateChartResult updateChartResult = ChartCalculations.getUpdateChartResult(tradeStatisticsByCurrency, tickUnit, usdAveragePriceMapsPerTickUnit, getCurrencyCode()); - itemsPerInterval.clear(); - itemsPerInterval.putAll(updateChartResult.getItemsPerInterval()); - - priceItems.setAll(updateChartResult.getPriceItems()); - volumeItems.setAll(updateChartResult.getVolumeItems()); - volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); - getCurrencyCode(); - } - //todo private void updateSelectedTradeStatistics(String currencyCode) { tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() From a659a78db6bac144edc432f5b87417ac0702dd88 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 2 Nov 2021 00:57:30 +0100 Subject: [PATCH 11/12] Do sorting on non UI thread as its slow and only attach it to table in UI thread afterwards. Chain updateSelectedTradeStatistics and updateChartData calls. --- .../main/market/trades/ChartCalculations.java | 8 +- .../main/market/trades/TradesChartsView.java | 43 +++-- .../market/trades/TradesChartsViewModel.java | 155 +++++++++++------- 3 files changed, 120 insertions(+), 86 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java index 9612fdad05a..3c720ee48d9 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/ChartCalculations.java @@ -53,6 +53,8 @@ import lombok.Getter; +import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; + public class ChartCalculations { static final ZoneId ZONE_ID = ZoneId.systemDefault(); @@ -169,7 +171,7 @@ static Map>> getItemsPerInterval(List>> itemsPerInterval = new HashMap<>(); Date time = new Date(); - for (long i = TradesChartsViewModel.MAX_TICKS + 1; i >= 0; --i) { + for (long i = MAX_TICKS + 1; i >= 0; --i) { Pair> pair = new Pair<>((Date) time.clone(), new HashSet<>()); itemsPerInterval.put(i, pair); // We adjust the time for the next iteration @@ -179,7 +181,7 @@ static Map>> getItemsPerInterval(List { - for (long i = TradesChartsViewModel.MAX_TICKS; i > 0; --i) { + for (long i = MAX_TICKS; i > 0; --i) { Pair> pair = itemsPerInterval.get(i); if (tradeStatistics.getDate().after(pair.getKey())) { pair.getValue().add(tradeStatistics); @@ -292,7 +294,7 @@ static CandleData getCandleData(long tick, Set set, } static long getTimeFromTickIndex(long tick, Map>> itemsPerInterval) { - if (tick > TradesChartsViewModel.MAX_TICKS + 1 || + if (tick > MAX_TICKS + 1 || itemsPerInterval.get(tick) == null) { return 0; } diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java index 13ace457bd5..46cb807607f 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsView.java @@ -104,11 +104,11 @@ import java.util.Date; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; +import static bisq.desktop.main.market.trades.TradesChartsViewModel.MAX_TICKS; import static bisq.desktop.util.FormBuilder.addTopLabelAutocompleteComboBox; import static bisq.desktop.util.FormBuilder.getTopLabelWithVBox; @@ -162,8 +162,7 @@ private CurrencyListItem specialShowAllItem() { private SingleSelectionModel tabPaneSelectionModel; private TableColumn priceColumn, volumeColumn, marketColumn; - private final ObservableList listItems = FXCollections.observableArrayList(); - private final SortedList sortedList = new SortedList<>(listItems); + private SortedList sortedList = new SortedList<>(FXCollections.observableArrayList()); private ChangeListener timeUnitChangeListener; private ChangeListener priceAxisYWidthListener; @@ -287,8 +286,6 @@ public void initialize() { @Override protected void activate() { - long ts = System.currentTimeMillis(); - // root.getParent() is null at initialize tabPaneSelectionModel = GUIUtil.getParentOfType(root, JFXTabPane.class).getSelectionModel(); selectedTabIndexListener = (observable, oldValue, newValue) -> model.setSelectedTabIndex((int) newValue); model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex()); @@ -328,8 +325,6 @@ else if (model.getSelectedCurrencyListItem().isPresent()) currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { }); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - boolean useAnimations = model.preferences.isUseAnimations(); priceChart.setAnimated(useAnimations); volumeChart.setAnimated(useAnimations); @@ -341,7 +336,6 @@ else if (model.getSelectedCurrencyListItem().isPresent()) nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); exportLink.setOnAction(e -> exportToCsv()); - UserThread.runAfter(this::updateChartData, 100, TimeUnit.MILLISECONDS); if (root.getParent() instanceof Pane) { rootParent = (Pane) root.getParent(); @@ -359,11 +353,7 @@ else if (model.getSelectedCurrencyListItem().isPresent()) user.requestPersistence(); }); - tableView.setItems(sortedList); - fillList(); layout(); - - log.error("activate took {} ms", System.currentTimeMillis() - ts); } @Override @@ -407,15 +397,20 @@ private void fillList() { .map(tradeStatistics -> new TradeStatistics3ListItem(tradeStatistics, coinFormatter, model.showAllTradeCurrenciesProperty.get())) - .collect(Collectors.toList()); - }).whenComplete((items, throwable) -> { - log.error("Creating listItems took {} ms", System.currentTimeMillis() - ts); + .collect(Collectors.toCollection(FXCollections::observableArrayList)); + }).whenComplete((listItems, throwable) -> { + log.debug("Creating listItems took {} ms", System.currentTimeMillis() - ts); + long ts2 = System.currentTimeMillis(); - listItems.setAll(items); - // Is slow because of sorting of > 100k items. But at least it seems it works on the thread from the - // CompletableFuture callback, so we do not block the UI thread;. - log.error("Applying sorted list took {} ms", - System.currentTimeMillis() - ts2); + sortedList.comparatorProperty().unbind(); + // Sorting is slow as we have > 100k items. So we prefer to do it on the non UI thread. + sortedList = new SortedList<>(listItems); + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + log.debug("Created sorted list took {} ms", System.currentTimeMillis() - ts2); + UserThread.execute(() -> { + // When we attach the list to the table we need to be on the UI thread. + tableView.setItems(sortedList); + }); }); } @@ -473,7 +468,7 @@ private void exportToCsv() { private void createCharts() { priceSeries = new XYChart.Series<>(); - priceAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); + priceAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); priceAxisX.setTickUnit(4); priceAxisX.setMinorTickCount(4); priceAxisX.setMinorTickVisible(true); @@ -539,11 +534,11 @@ public Number fromString(String string) { priceChartPane.getChildren().add(priceChart); - volumeAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); + volumeAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); volumeAxisY = new NumberAxis(); volumeChart = getVolumeChart(volumeAxisX, volumeAxisY, volumeSeries, "BTC"); - volumeInUsdAxisX = new NumberAxis(0, TradesChartsViewModel.MAX_TICKS + 1, 1); + volumeInUsdAxisX = new NumberAxis(0, MAX_TICKS + 1, 1); NumberAxis volumeInUsdAxisY = new NumberAxis(); volumeInUsdChart = getVolumeChart(volumeInUsdAxisX, volumeInUsdAxisY, volumeInUsdSeries, "USD"); volumeInUsdChart.setVisible(false); @@ -650,7 +645,7 @@ public String toString(Number object) { long index = MathUtils.doubleToLong((double) object); // The last tick is on the chart edge, it is not well spaced with // the previous tick and interferes with its label. - if (TradesChartsViewModel.MAX_TICKS + 1 == index) return ""; + if (MAX_TICKS + 1 == index) return ""; long time = model.getTimeFromTickIndex(index); String fmt = ""; diff --git a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java index 2a18f3962de..ff44daccb06 100644 --- a/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/market/trades/TradesChartsViewModel.java @@ -118,8 +118,17 @@ public enum TickUnit { this.navigation = navigation; setChangeListener = change -> { - updateSelectedTradeStatistics(getCurrencyCode()); //todo - updateChartData(); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) + .whenComplete((result, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at setChangeListener/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + return; + } + applyAsyncChartData(); + }); fillTradeCurrencies(); }; @@ -137,9 +146,8 @@ public enum TickUnit { @Override protected void activate() { - deactivateCalled = false; long ts = System.currentTimeMillis(); - + deactivateCalled = false; tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener); if (!fillTradeCurrenciesOnActivateCalled) { @@ -149,8 +157,6 @@ protected void activate() { syncPriceFeedCurrency(); setMarketPriceFeedCurrency(); - long ts1 = System.currentTimeMillis(); - List> allFutures = new ArrayList<>(); CompletableFuture task1Done = new CompletableFuture<>(); allFutures.add(task1Done); @@ -165,64 +171,109 @@ protected void activate() { log.error(throwable.toString()); return; } - //Once getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit are both completed we - // call updateChartData2 - UserThread.execute(this::updateChartData); + //Once applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency are + // both completed we call applyAsyncChartData + UserThread.execute(this::applyAsyncChartData); }); - // We start getUsdAveragePriceMapsPerTickUnit and getUsdAveragePriceMapsPerTickUnit in parallel threads for - // better performance + // We call applyAsyncUsdAveragePriceMapsPerTickUnit and applyAsyncTradeStatisticsForCurrency + // in parallel for better performance + applyAsyncUsdAveragePriceMapsPerTickUnit(task1Done); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode(), task2Done); + + log.debug("activate took {}", System.currentTimeMillis() - ts); + } + + @Override + protected void deactivate() { + deactivateCalled = true; + tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); + + // We want to avoid to trigger listeners in the view so we delay a bit. Deactivate on model is called before + // deactivate on view. + UserThread.execute(() -> { + usdAveragePriceMapsPerTickUnit.clear(); + tradeStatisticsByCurrency.clear(); + priceItems.clear(); + volumeItems.clear(); + volumeInUsdItems.clear(); + itemsPerInterval.clear(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Async calls + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyAsyncUsdAveragePriceMapsPerTickUnit(CompletableFuture completeFuture) { + long ts = System.currentTimeMillis(); ChartCalculations.getUsdAveragePriceMapsPerTickUnit(tradeStatisticsManager.getObservableTradeStatisticsSet()) .whenComplete((usdAveragePriceMapsPerTickUnit, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { - log.error(throwable.toString()); - task1Done.completeExceptionally(throwable); + log.error("Error at applyAsyncUsdAveragePriceMapsPerTickUnit. {}", throwable.toString()); + completeFuture.completeExceptionally(throwable); return; } UserThread.execute(() -> { this.usdAveragePriceMapsPerTickUnit.clear(); this.usdAveragePriceMapsPerTickUnit.putAll(usdAveragePriceMapsPerTickUnit); - log.error("getUsdAveragePriceMapsPerTickUnit took {}", System.currentTimeMillis() - ts1); - task1Done.complete(true); + log.debug("applyAsyncUsdAveragePriceMapsPerTickUnit took {}", System.currentTimeMillis() - ts); + completeFuture.complete(true); }); }); + } + + private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode) { + return applyAsyncTradeStatisticsForCurrency(currencyCode, null); + } - long ts2 = System.currentTimeMillis(); + private CompletableFuture applyAsyncTradeStatisticsForCurrency(String currencyCode, + @Nullable CompletableFuture completeFuture) { + CompletableFuture future = new CompletableFuture<>(); + long ts = System.currentTimeMillis(); ChartCalculations.getTradeStatisticsForCurrency(tradeStatisticsManager.getObservableTradeStatisticsSet(), - getCurrencyCode(), + currencyCode, showAllTradeCurrenciesProperty.get()) .whenComplete((list, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { - log.error(throwable.toString()); - task2Done.completeExceptionally(throwable); + log.error("Error at applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + if (completeFuture != null) { + completeFuture.completeExceptionally(throwable); + } return; } UserThread.execute(() -> { tradeStatisticsByCurrency.setAll(list); - log.error("getTradeStatisticsForCurrency took {}", System.currentTimeMillis() - ts2); - task2Done.complete(true); + log.debug("applyAsyncTradeStatisticsForCurrency took {}", System.currentTimeMillis() - ts); + if (completeFuture != null) { + completeFuture.complete(true); + } + future.complete(true); }); }); - - log.error("activate took {}", System.currentTimeMillis() - ts); + return future; } - private void updateChartData() { + private void applyAsyncChartData() { long ts = System.currentTimeMillis(); - ChartCalculations.getUpdateChartResult(tradeStatisticsByCurrency, tickUnit, usdAveragePriceMapsPerTickUnit, getCurrencyCode()) + ChartCalculations.getUpdateChartResult(new ArrayList<>(tradeStatisticsByCurrency), + tickUnit, + usdAveragePriceMapsPerTickUnit, + getCurrencyCode()) .whenComplete((updateChartResult, throwable) -> { if (deactivateCalled) { return; } if (throwable != null) { - log.error(throwable.toString()); + log.error("Error at applyAsyncChartData. {}", throwable.toString()); return; } UserThread.execute(() -> { @@ -232,27 +283,11 @@ private void updateChartData() { priceItems.setAll(updateChartResult.getPriceItems()); volumeItems.setAll(updateChartResult.getVolumeItems()); volumeInUsdItems.setAll(updateChartResult.getVolumeInUsdItems()); - log.error("updateChartData took {}", System.currentTimeMillis() - ts); + log.debug("applyAsyncChartData took {}", System.currentTimeMillis() - ts); }); }); } - @Override - protected void deactivate() { - deactivateCalled = true; - tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); - - // We want to avoid to trigger listeners in the view so we delay a bit. Deactivate on model is called before - // deactivate on view. - UserThread.execute(() -> { - usdAveragePriceMapsPerTickUnit.clear(); - tradeStatisticsByCurrency.clear(); - priceItems.clear(); - volumeItems.clear(); - volumeInUsdItems.clear(); - itemsPerInterval.clear(); - }); - } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions @@ -278,15 +313,24 @@ void onSetTradeCurrency(TradeCurrency tradeCurrency) { } preferences.setTradeChartsScreenCurrencyCode(code); - updateSelectedTradeStatistics(getCurrencyCode());//todo - updateChartData(); + applyAsyncTradeStatisticsForCurrency(getCurrencyCode()) + .whenComplete((result, throwable) -> { + if (deactivateCalled) { + return; + } + if (throwable != null) { + log.error("Error at onSetTradeCurrency/applyAsyncTradeStatisticsForCurrency. {}", throwable.toString()); + return; + } + applyAsyncChartData(); + }); } } void setTickUnit(TickUnit tickUnit) { this.tickUnit = tickUnit; preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal()); - updateChartData(); + applyAsyncChartData(); } void setSelectedTabIndex(int selectedTabIndex) { @@ -295,6 +339,7 @@ void setSelectedTabIndex(int selectedTabIndex) { setMarketPriceFeedCurrency(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -311,6 +356,11 @@ public Optional getSelectedCurrencyListItem() { return currencyListItems.getObservableList().stream().filter(e -> e.tradeCurrency.equals(selectedTradeCurrencyProperty.get())).findAny(); } + long getTimeFromTickIndex(long tick) { + return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -320,7 +370,6 @@ private void fillTradeCurrencies() { List tradeCurrencyList = tradeStatisticsManager.getObservableTradeStatisticsSet().stream() .flatMap(e -> CurrencyUtil.getTradeCurrency(e.getCurrency()).stream()) .collect(Collectors.toList()); - currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); } @@ -338,17 +387,6 @@ private void syncPriceFeedCurrency() { priceFeedService.setCurrencyCode(selectedTradeCurrencyProperty.get().getCode()); } - //todo - private void updateSelectedTradeStatistics(String currencyCode) { - tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream() - .filter(e -> showAllTradeCurrenciesProperty.get() || e.getCurrency().equals(currencyCode)) - .collect(Collectors.toList())); - } - - long getTimeFromTickIndex(long tick) { - return ChartCalculations.getTimeFromTickIndex(tick, itemsPerInterval); - } - private boolean isShowAllEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.SHOW_ALL_FLAG); } @@ -356,5 +394,4 @@ private boolean isShowAllEntry(@Nullable String id) { private boolean isEditEntry(@Nullable String id) { return id != null && id.equals(GUIUtil.EDIT_FLAG); } - } From c209da7c66b81ae975e1039969f47ff95a604671 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Tue, 9 Nov 2021 19:32:44 +0100 Subject: [PATCH 12/12] Fix package statement --- .../main/java/bisq/common/util/CompletableFutureUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/bisq/common/util/CompletableFutureUtils.java b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java index f46cb760e04..6f26ed3a455 100644 --- a/common/src/main/java/bisq/common/util/CompletableFutureUtils.java +++ b/common/src/main/java/bisq/common/util/CompletableFutureUtils.java @@ -1,4 +1,4 @@ -package bisq.common.util;/* +/* * This file is part of Bisq. * * Bisq is free software: you can redistribute it and/or modify it @@ -15,6 +15,8 @@ * along with Bisq. If not, see . */ +package bisq.common.util; + import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors;