Skip to content

Commit e7868e9

Browse files
authored
Restore date aggregation performance in UTC case (#38221) (#38700)
The benchmarks showed a sharp decrease in aggregation performance for the UTC case. This commit uses the same calculation as joda time, which requires no conversion into any java time object, also, the check for an fixedoffset has been put into the ctor to reduce the need for runtime calculations. The same goes for the amount of the used unit in milliseconds. Closes #37826
1 parent bd4ca4c commit e7868e9

File tree

10 files changed

+610
-31
lines changed

10 files changed

+610
-31
lines changed

NOTICE.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ Copyright 2009-2018 Elasticsearch
33

44
This product includes software developed by The Apache Software
55
Foundation (http://www.apache.org/).
6+
7+
This product includes software developed by
8+
Joda.org (http://www.joda.org/).

benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@
3434
import org.openjdk.jmh.annotations.Warmup;
3535

3636
import java.time.ZoneId;
37+
import java.time.ZoneOffset;
3738
import java.util.concurrent.TimeUnit;
3839

40+
import static org.elasticsearch.common.Rounding.DateTimeUnit.DAY_OF_MONTH;
41+
import static org.elasticsearch.common.Rounding.DateTimeUnit.MONTH_OF_YEAR;
42+
import static org.elasticsearch.common.Rounding.DateTimeUnit.QUARTER_OF_YEAR;
43+
import static org.elasticsearch.common.Rounding.DateTimeUnit.YEAR_OF_CENTURY;
44+
3945
@Fork(3)
4046
@Warmup(iterations = 10)
4147
@Measurement(iterations = 10)
@@ -48,23 +54,13 @@ public class RoundingBenchmark {
4854
private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam");
4955
private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId);
5056

57+
private long timestamp = 1548879021354L;
58+
5159
private final org.elasticsearch.common.rounding.Rounding jodaRounding =
52-
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
60+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
5361
private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY)
5462
.timeZone(zoneId).build();
5563

56-
private final org.elasticsearch.common.rounding.Rounding jodaDayOfMonthRounding =
57-
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
58-
private final Rounding javaDayOfMonthRounding = Rounding.builder(TimeValue.timeValueMinutes(60))
59-
.timeZone(zoneId).build();
60-
61-
private final org.elasticsearch.common.rounding.Rounding timeIntervalRoundingJoda =
62-
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
63-
private final Rounding timeIntervalRoundingJava = Rounding.builder(TimeValue.timeValueMinutes(60))
64-
.timeZone(zoneId).build();
65-
66-
private final long timestamp = 1548879021354L;
67-
6864
@Benchmark
6965
public long timeRoundingDateTimeUnitJoda() {
7066
return jodaRounding.round(timestamp);
@@ -75,6 +71,11 @@ public long timeRoundingDateTimeUnitJava() {
7571
return javaRounding.round(timestamp);
7672
}
7773

74+
private final org.elasticsearch.common.rounding.Rounding jodaDayOfMonthRounding =
75+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
76+
private final Rounding javaDayOfMonthRounding = Rounding.builder(DAY_OF_MONTH)
77+
.timeZone(zoneId).build();
78+
7879
@Benchmark
7980
public long timeRoundingDateTimeUnitDayOfMonthJoda() {
8081
return jodaDayOfMonthRounding.round(timestamp);
@@ -85,6 +86,11 @@ public long timeRoundingDateTimeUnitDayOfMonthJava() {
8586
return javaDayOfMonthRounding.round(timestamp);
8687
}
8788

89+
private final org.elasticsearch.common.rounding.Rounding timeIntervalRoundingJoda =
90+
org.elasticsearch.common.rounding.Rounding.builder(TimeValue.timeValueMinutes(60)).timeZone(timeZone).build();
91+
private final Rounding timeIntervalRoundingJava = Rounding.builder(TimeValue.timeValueMinutes(60))
92+
.timeZone(zoneId).build();
93+
8894
@Benchmark
8995
public long timeIntervalRoundingJava() {
9096
return timeIntervalRoundingJava.round(timestamp);
@@ -94,4 +100,65 @@ public long timeIntervalRoundingJava() {
94100
public long timeIntervalRoundingJoda() {
95101
return timeIntervalRoundingJoda.round(timestamp);
96102
}
103+
104+
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcDayOfMonthJoda =
105+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build();
106+
private final Rounding timeUnitRoundingUtcDayOfMonthJava = Rounding.builder(DAY_OF_MONTH)
107+
.timeZone(ZoneOffset.UTC).build();
108+
109+
@Benchmark
110+
public long timeUnitRoundingUtcDayOfMonthJava() {
111+
return timeUnitRoundingUtcDayOfMonthJava.round(timestamp);
112+
}
113+
114+
@Benchmark
115+
public long timeUnitRoundingUtcDayOfMonthJoda() {
116+
return timeUnitRoundingUtcDayOfMonthJoda.round(timestamp);
117+
}
118+
119+
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcQuarterOfYearJoda =
120+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.QUARTER).timeZone(DateTimeZone.UTC).build();
121+
private final Rounding timeUnitRoundingUtcQuarterOfYearJava = Rounding.builder(QUARTER_OF_YEAR)
122+
.timeZone(ZoneOffset.UTC).build();
123+
124+
@Benchmark
125+
public long timeUnitRoundingUtcQuarterOfYearJava() {
126+
return timeUnitRoundingUtcQuarterOfYearJava.round(timestamp);
127+
}
128+
129+
@Benchmark
130+
public long timeUnitRoundingUtcQuarterOfYearJoda() {
131+
return timeUnitRoundingUtcQuarterOfYearJoda.round(timestamp);
132+
}
133+
134+
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda =
135+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.MONTH_OF_YEAR).timeZone(DateTimeZone.UTC).build();
136+
private final Rounding timeUnitRoundingUtcMonthOfYearJava = Rounding.builder(MONTH_OF_YEAR)
137+
.timeZone(ZoneOffset.UTC).build();
138+
139+
@Benchmark
140+
public long timeUnitRoundingUtcMonthOfYearJava() {
141+
return timeUnitRoundingUtcMonthOfYearJava.round(timestamp);
142+
}
143+
144+
@Benchmark
145+
public long timeUnitRoundingUtcMonthOfYearJoda() {
146+
return timeUnitRoundingUtcMonthOfYearJoda.round(timestamp);
147+
}
148+
149+
150+
private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcYearOfCenturyJoda =
151+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.YEAR_OF_CENTURY).timeZone(DateTimeZone.UTC).build();
152+
private final Rounding timeUnitRoundingUtcYearOfCenturyJava = Rounding.builder(YEAR_OF_CENTURY)
153+
.timeZone(ZoneOffset.UTC).build();
154+
155+
@Benchmark
156+
public long timeUnitRoundingUtcYearOfCenturyJava() {
157+
return timeUnitRoundingUtcYearOfCenturyJava.round(timestamp);
158+
}
159+
160+
@Benchmark
161+
public long timeUnitRoundingUtcYearOfCenturyJoda() {
162+
return timeUnitRoundingUtcYearOfCenturyJoda.round(timestamp);
163+
}
97164
}

server/src/main/java/org/elasticsearch/common/Rounding.java

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,51 @@
5454
*/
5555
public abstract class Rounding implements Writeable {
5656

57-
public static String format(long epochMillis) {
58-
return Instant.ofEpochMilli(epochMillis) + "/" + epochMillis;
59-
}
60-
6157
public enum DateTimeUnit {
62-
WEEK_OF_WEEKYEAR( (byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR),
63-
YEAR_OF_CENTURY( (byte) 2, ChronoField.YEAR_OF_ERA),
64-
QUARTER_OF_YEAR( (byte) 3, IsoFields.QUARTER_OF_YEAR),
65-
MONTH_OF_YEAR( (byte) 4, ChronoField.MONTH_OF_YEAR),
66-
DAY_OF_MONTH( (byte) 5, ChronoField.DAY_OF_MONTH),
67-
HOUR_OF_DAY( (byte) 6, ChronoField.HOUR_OF_DAY),
68-
MINUTES_OF_HOUR( (byte) 7, ChronoField.MINUTE_OF_HOUR),
69-
SECOND_OF_MINUTE( (byte) 8, ChronoField.SECOND_OF_MINUTE);
58+
WEEK_OF_WEEKYEAR((byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR) {
59+
long roundFloor(long utcMillis) {
60+
return DateUtils.roundWeekOfWeekYear(utcMillis);
61+
}
62+
},
63+
YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) {
64+
long roundFloor(long utcMillis) {
65+
return DateUtils.roundYear(utcMillis);
66+
}
67+
},
68+
QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) {
69+
long roundFloor(long utcMillis) {
70+
return DateUtils.roundQuarterOfYear(utcMillis);
71+
}
72+
},
73+
MONTH_OF_YEAR((byte) 4, ChronoField.MONTH_OF_YEAR) {
74+
long roundFloor(long utcMillis) {
75+
return DateUtils.roundMonthOfYear(utcMillis);
76+
}
77+
},
78+
DAY_OF_MONTH((byte) 5, ChronoField.DAY_OF_MONTH) {
79+
final long unitMillis = ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis();
80+
long roundFloor(long utcMillis) {
81+
return DateUtils.roundFloor(utcMillis, unitMillis);
82+
}
83+
},
84+
HOUR_OF_DAY((byte) 6, ChronoField.HOUR_OF_DAY) {
85+
final long unitMillis = ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis();
86+
long roundFloor(long utcMillis) {
87+
return DateUtils.roundFloor(utcMillis, unitMillis);
88+
}
89+
},
90+
MINUTES_OF_HOUR((byte) 7, ChronoField.MINUTE_OF_HOUR) {
91+
final long unitMillis = ChronoField.MINUTE_OF_HOUR.getBaseUnit().getDuration().toMillis();
92+
long roundFloor(long utcMillis) {
93+
return DateUtils.roundFloor(utcMillis, unitMillis);
94+
}
95+
},
96+
SECOND_OF_MINUTE((byte) 8, ChronoField.SECOND_OF_MINUTE) {
97+
final long unitMillis = ChronoField.SECOND_OF_MINUTE.getBaseUnit().getDuration().toMillis();
98+
long roundFloor(long utcMillis) {
99+
return DateUtils.roundFloor(utcMillis, unitMillis);
100+
}
101+
};
70102

71103
private final byte id;
72104
private final TemporalField field;
@@ -76,6 +108,15 @@ public enum DateTimeUnit {
76108
this.field = field;
77109
}
78110

111+
/**
112+
* This rounds down the supplied milliseconds since the epoch down to the next unit. In order to retain performance this method
113+
* should be as fast as possible and not try to convert dates to java-time objects if possible
114+
*
115+
* @param utcMillis the milliseconds since the epoch
116+
* @return the rounded down milliseconds since the epoch
117+
*/
118+
abstract long roundFloor(long utcMillis);
119+
79120
public byte getId() {
80121
return id;
81122
}
@@ -182,12 +223,13 @@ static class TimeUnitRounding extends Rounding {
182223
private final DateTimeUnit unit;
183224
private final ZoneId timeZone;
184225
private final boolean unitRoundsToMidnight;
185-
226+
private final boolean isUtcTimeZone;
186227

187228
TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
188229
this.unit = unit;
189230
this.timeZone = timeZone;
190231
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
232+
this.isUtcTimeZone = timeZone.normalized().equals(ZoneOffset.UTC);
191233
}
192234

193235
TimeUnitRounding(StreamInput in) throws IOException {
@@ -223,9 +265,7 @@ private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) {
223265
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0);
224266

225267
case QUARTER_OF_YEAR:
226-
int quarter = (int) IsoFields.QUARTER_OF_YEAR.getFrom(localDateTime);
227-
int month = ((quarter - 1) * 3) + 1;
228-
return LocalDateTime.of(localDateTime.getYear(), month, 1, 0, 0);
268+
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth().firstMonthOfQuarter(), 1, 0, 0);
229269

230270
case YEAR_OF_CENTURY:
231271
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);
@@ -236,7 +276,14 @@ private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) {
236276
}
237277

238278
@Override
239-
public long round(final long utcMillis) {
279+
public long round(long utcMillis) {
280+
// this works as long as the offset doesn't change. It is worth getting this case out of the way first, as
281+
// the calculations for fixing things near to offset changes are a little expensive and are unnecessary in the common case
282+
// of working in UTC.
283+
if (isUtcTimeZone) {
284+
return unit.roundFloor(utcMillis);
285+
}
286+
240287
Instant instant = Instant.ofEpochMilli(utcMillis);
241288
if (unitRoundsToMidnight) {
242289
final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);

server/src/main/java/org/elasticsearch/common/time/DateUtils.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
import java.util.Map;
3232
import java.util.Set;
3333

34+
import static org.elasticsearch.common.time.DateUtilsRounding.getMonthOfYear;
35+
import static org.elasticsearch.common.time.DateUtilsRounding.getTotalMillisByYearMonth;
36+
import static org.elasticsearch.common.time.DateUtilsRounding.getYear;
37+
import static org.elasticsearch.common.time.DateUtilsRounding.utcMillisAtStartOfYear;
38+
3439
public class DateUtils {
3540
public static DateTimeZone zoneIdToDateTimeZone(ZoneId zoneId) {
3641
if (zoneId == null) {
@@ -139,4 +144,77 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) {
139144

140145
return nanoSecondsSinceEpoch / 1_000_000;
141146
}
147+
148+
/**
149+
* Rounds the given utc milliseconds sicne the epoch down to the next unit millis
150+
*
151+
* Note: This does not check for correctness of the result, as this only works with units smaller or equal than a day
152+
* In order to ensure the performane of this methods, there are no guards or checks in it
153+
*
154+
* @param utcMillis the milliseconds since the epoch
155+
* @param unitMillis the unit to round to
156+
* @return the rounded milliseconds since the epoch
157+
*/
158+
public static long roundFloor(long utcMillis, final long unitMillis) {
159+
if (utcMillis >= 0) {
160+
return utcMillis - utcMillis % unitMillis;
161+
} else {
162+
utcMillis += 1;
163+
return utcMillis - utcMillis % unitMillis - unitMillis;
164+
}
165+
}
166+
167+
/**
168+
* Round down to the beginning of the quarter of the year of the specified time
169+
* @param utcMillis the milliseconds since the epoch
170+
* @return The milliseconds since the epoch rounded down to the quarter of the year
171+
*/
172+
public static long roundQuarterOfYear(final long utcMillis) {
173+
int year = getYear(utcMillis);
174+
int month = getMonthOfYear(utcMillis, year);
175+
int firstMonthOfQuarter = (((month-1) / 3) * 3) + 1;
176+
return DateUtils.of(year, firstMonthOfQuarter);
177+
}
178+
179+
/**
180+
* Round down to the beginning of the month of the year of the specified time
181+
* @param utcMillis the milliseconds since the epoch
182+
* @return The milliseconds since the epoch rounded down to the month of the year
183+
*/
184+
public static long roundMonthOfYear(final long utcMillis) {
185+
int year = getYear(utcMillis);
186+
int month = getMonthOfYear(utcMillis, year);
187+
return DateUtils.of(year, month);
188+
}
189+
190+
/**
191+
* Round down to the beginning of the year of the specified time
192+
* @param utcMillis the milliseconds since the epoch
193+
* @return The milliseconds since the epoch rounded down to the beginning of the year
194+
*/
195+
public static long roundYear(final long utcMillis) {
196+
int year = getYear(utcMillis);
197+
return utcMillisAtStartOfYear(year);
198+
}
199+
200+
/**
201+
* Round down to the beginning of the week based on week year of the specified time
202+
* @param utcMillis the milliseconds since the epoch
203+
* @return The milliseconds since the epoch rounded down to the beginning of the week based on week year
204+
*/
205+
public static long roundWeekOfWeekYear(final long utcMillis) {
206+
return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L;
207+
}
208+
209+
/**
210+
* Return the first day of the month
211+
* @param year the year to return
212+
* @param month the month to return, ranging from 1-12
213+
* @return the milliseconds since the epoch of the first day of the month in the year
214+
*/
215+
private static long of(final int year, final int month) {
216+
long millis = utcMillisAtStartOfYear(year);
217+
millis += getTotalMillisByYearMonth(year, month);
218+
return millis;
219+
}
142220
}

0 commit comments

Comments
 (0)