Add caches to TemporalAdjusterModel to speed up BSQ dashboard view

Now that the trade statistics are retrieved in chronological order,
optimise the per-interval BSQ & USD price and volume calculations in
PriceChartDataModel & VolumeChartDataModel, by adding caches to avoid
relatively expensive timezone calculations in TemporalAdjusterModel,
similarly to the cache added for 'ChartCalculations.roundToTick' (as
profiling shows 'TemporalAdjusterModel.toTimeInteval' is a hotspot).

Add a cache to speed up Instant -> LocalDate mappings by storing the
unix time (Instant) range of the last seen day (LocalDate) in a tuple,
then just returning that day if the next Instant falls in range. Also
add a cache of the last temporal adjustment (start of month, week, etc.)
of that day. In this way, successive calls to 'toTimeInteval(Instant)'
with input times on the same day are sped up.

Since TemporalAdjusterModel is used by multiple threads simultaneously,
store the caches in instance fields and add a 'withCache' method which
clones the model and enables the caching, since otherwise the separate
threads keep invalidating one another's caches, making it slower than it
would be without them. (We could use ThreadLocals, but profiling
suggests they are too heavyweight to be very useful here, so instead use
unsynchronised caching with nonfinal fields and benign data races.)

Provide the method 'ChartDataModel.toCachedTimeIntervalFn' which returns
a method reference to a cloned & cache-enabled TemporalAdjustedModel, to
use in place of the delegate method 'ChartDataModel.toTimeInterval' when
the caching is beneficial.
This commit is contained in:
Steven Barclay 2023-05-09 18:43:28 +01:00
parent 964321a1e1
commit f3fd555ced
No known key found for this signature in database
GPG Key ID: 9FED6BF1176D500B
4 changed files with 56 additions and 13 deletions

View File

@ -22,10 +22,10 @@ import bisq.desktop.common.model.ActivatableDataModel;
import java.time.Instant;
import java.time.temporal.TemporalAdjuster;
import java.util.Comparator;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -64,6 +64,11 @@ public abstract class ChartDataModel extends ActivatableDataModel {
return temporalAdjusterModel.toTimeInterval(instant);
}
// optimized for use when the input times are sequential and not too spread out
public ToLongFunction<Instant> toCachedTimeIntervalFn() {
return temporalAdjusterModel.withCache()::toTimeInterval;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Date filter predicate
@ -88,7 +93,7 @@ public abstract class ChartDataModel extends ActivatableDataModel {
Map<Long, Long> map2,
BinaryOperator<Long> mergeFunction) {
return Stream.concat(map1.entrySet().stream(),
map2.entrySet().stream())
map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
mergeFunction));

View File

@ -18,6 +18,7 @@
package bisq.desktop.components.chart;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple3;
import java.time.DayOfWeek;
import java.time.Instant;
@ -37,6 +38,19 @@ import static java.time.temporal.ChronoField.DAY_OF_YEAR;
public class TemporalAdjusterModel {
private static final ZoneId ZONE_ID = ZoneId.systemDefault();
protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
private boolean enableCache;
private Tuple3<LocalDate, Instant, Instant> cachedDateStartEndTuple;
private Tuple3<LocalDate, TemporalAdjuster, Long> cachedTimeIntervalMapping;
public TemporalAdjusterModel withCache() {
var model = new TemporalAdjusterModel();
model.temporalAdjuster = this.temporalAdjuster;
model.enableCache = true;
return model;
}
public enum Interval {
YEAR(TemporalAdjusters.firstDayOfYear()),
HALF_YEAR(temporal -> {
@ -81,8 +95,6 @@ public class TemporalAdjusterModel {
}
}
protected TemporalAdjuster temporalAdjuster = Interval.MONTH.getAdjuster();
public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
this.temporalAdjuster = temporalAdjuster;
}
@ -96,12 +108,36 @@ public class TemporalAdjusterModel {
}
public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) {
return instant
.atZone(ZONE_ID)
.toLocalDate()
.with(temporalAdjuster)
.atStartOfDay(ZONE_ID)
.toInstant()
.getEpochSecond();
LocalDate date = toLocalDate(instant);
Tuple3<LocalDate, TemporalAdjuster, Long> tuple = cachedTimeIntervalMapping;
long timeInterval;
if (tuple != null && date.equals(tuple.first) && temporalAdjuster.equals(tuple.second)) {
timeInterval = tuple.third;
} else {
timeInterval = date
.with(temporalAdjuster)
.atStartOfDay(ZONE_ID)
.toEpochSecond();
if (enableCache) {
cachedTimeIntervalMapping = new Tuple3<>(date, temporalAdjuster, timeInterval);
}
}
return timeInterval;
}
private LocalDate toLocalDate(Instant instant) {
LocalDate date;
Tuple3<LocalDate, Instant, Instant> tuple = cachedDateStartEndTuple;
if (tuple != null && !instant.isBefore(tuple.second) && instant.isBefore(tuple.third)) {
date = tuple.first;
} else {
date = instant.atZone(ZONE_ID).toLocalDate();
if (enableCache) {
cachedDateStartEndTuple = new Tuple3<>(date,
date.atStartOfDay(ZONE_ID).toInstant(),
date.plusDays(1).atStartOfDay(ZONE_ID).toInstant());
}
}
return date;
}
}

View File

@ -188,9 +188,10 @@ public class PriceChartDataModel extends ChartDataModel {
private Map<Long, Double> getPriceByInterval(Predicate<TradeStatistics3> collectionFilter,
Function<List<TradeStatistics3>, Double> getAveragePriceFunction) {
var toTimeIntervalFn = toCachedTimeIntervalFn();
return getPriceByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
collectionFilter,
tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter,
getAveragePriceFunction);
}

View File

@ -134,8 +134,9 @@ public class VolumeChartDataModel extends ChartDataModel {
///////////////////////////////////////////////////////////////////////////////////////////
private Map<Long, Long> getVolumeByInterval(Function<List<TradeStatistics3>, Long> getVolumeFunction) {
var toTimeIntervalFn = toCachedTimeIntervalFn();
return getVolumeByInterval(tradeStatisticsManager.getObservableTradeStatisticsSet(),
tradeStatistics -> toTimeInterval(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
tradeStatistics -> toTimeIntervalFn.applyAsLong(Instant.ofEpochMilli(tradeStatistics.getDateAsLong())),
dateFilter,
getVolumeFunction);
}