Skip to content

Commit 9f026bb

Browse files
authored
Reduce object creation in Rounding class (elastic#38061)
This reduces objects creations in the rounding class (used by aggs) by properly creating the objects only once. Furthermore a few unneeded ZonedDateTime objects were created in order to create other objects out of them. This was changed as well. Running the benchmarks shows a much faster performance for all of the java time based Rounding classes.
1 parent a536fa7 commit 9f026bb

File tree

2 files changed

+150
-73
lines changed

2 files changed

+150
-73
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.elasticsearch.benchmark.time;
20+
21+
import org.elasticsearch.common.Rounding;
22+
import org.elasticsearch.common.rounding.DateTimeUnit;
23+
import org.elasticsearch.common.time.DateUtils;
24+
import org.elasticsearch.common.unit.TimeValue;
25+
import org.joda.time.DateTimeZone;
26+
import org.openjdk.jmh.annotations.Benchmark;
27+
import org.openjdk.jmh.annotations.BenchmarkMode;
28+
import org.openjdk.jmh.annotations.Fork;
29+
import org.openjdk.jmh.annotations.Measurement;
30+
import org.openjdk.jmh.annotations.Mode;
31+
import org.openjdk.jmh.annotations.OutputTimeUnit;
32+
import org.openjdk.jmh.annotations.Scope;
33+
import org.openjdk.jmh.annotations.State;
34+
import org.openjdk.jmh.annotations.Warmup;
35+
36+
import java.time.ZoneId;
37+
import java.util.concurrent.TimeUnit;
38+
39+
@Fork(3)
40+
@Warmup(iterations = 10)
41+
@Measurement(iterations = 10)
42+
@BenchmarkMode(Mode.AverageTime)
43+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
44+
@State(Scope.Benchmark)
45+
@SuppressWarnings("unused") //invoked by benchmarking framework
46+
public class RoundingBenchmark {
47+
48+
private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam");
49+
private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId);
50+
51+
private final org.elasticsearch.common.rounding.Rounding jodaRounding =
52+
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
53+
private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY)
54+
.timeZone(zoneId).build();
55+
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+
68+
@Benchmark
69+
public long timeRoundingDateTimeUnitJoda() {
70+
return jodaRounding.round(timestamp);
71+
}
72+
73+
@Benchmark
74+
public long timeRoundingDateTimeUnitJava() {
75+
return javaRounding.round(timestamp);
76+
}
77+
78+
@Benchmark
79+
public long timeRoundingDateTimeUnitDayOfMonthJoda() {
80+
return jodaDayOfMonthRounding.round(timestamp);
81+
}
82+
83+
@Benchmark
84+
public long timeRoundingDateTimeUnitDayOfMonthJava() {
85+
return javaDayOfMonthRounding.round(timestamp);
86+
}
87+
88+
@Benchmark
89+
public long timeIntervalRoundingJava() {
90+
return timeIntervalRoundingJava.round(timestamp);
91+
}
92+
93+
@Benchmark
94+
public long timeIntervalRoundingJoda() {
95+
return timeIntervalRoundingJoda.round(timestamp);
96+
}
97+
}

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

Lines changed: 53 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
import org.elasticsearch.common.unit.TimeValue;
2828

2929
import java.io.IOException;
30-
import java.time.DayOfWeek;
3130
import java.time.Instant;
31+
import java.time.LocalDate;
3232
import java.time.LocalDateTime;
3333
import java.time.LocalTime;
3434
import java.time.OffsetDateTime;
@@ -39,7 +39,9 @@
3939
import java.time.temporal.ChronoUnit;
4040
import java.time.temporal.IsoFields;
4141
import java.time.temporal.TemporalField;
42+
import java.time.temporal.TemporalQueries;
4243
import java.time.zone.ZoneOffsetTransition;
44+
import java.time.zone.ZoneRules;
4345
import java.util.List;
4446
import java.util.Objects;
4547

@@ -185,13 +187,11 @@ static class TimeUnitRounding extends Rounding {
185187
TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
186188
this.unit = unit;
187189
this.timeZone = timeZone;
188-
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 60L * 60L * 1000L;
190+
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
189191
}
190192

191193
TimeUnitRounding(StreamInput in) throws IOException {
192-
unit = DateTimeUnit.resolve(in.readByte());
193-
timeZone = DateUtils.of(in.readString());
194-
unitRoundsToMidnight = unit.getField().getBaseUnit().getDuration().toMillis() > 60L * 60L * 1000L;
194+
this(DateTimeUnit.resolve(in.readByte()), DateUtils.of(in.readString()));
195195
}
196196

197197
@Override
@@ -200,85 +200,67 @@ public byte id() {
200200
}
201201

202202
private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) {
203-
localDateTime = localDateTime.withNano(0);
204-
assert localDateTime.getNano() == 0;
205-
if (unit.equals(DateTimeUnit.SECOND_OF_MINUTE)) {
206-
return localDateTime;
207-
}
203+
switch (unit) {
204+
case SECOND_OF_MINUTE:
205+
return localDateTime.withNano(0);
208206

209-
localDateTime = localDateTime.withSecond(0);
210-
assert localDateTime.getSecond() == 0;
211-
if (unit.equals(DateTimeUnit.MINUTES_OF_HOUR)) {
212-
return localDateTime;
213-
}
207+
case MINUTES_OF_HOUR:
208+
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), localDateTime.getDayOfMonth(),
209+
localDateTime.getHour(), localDateTime.getMinute(), 0, 0);
214210

215-
localDateTime = localDateTime.withMinute(0);
216-
assert localDateTime.getMinute() == 0;
217-
if (unit.equals(DateTimeUnit.HOUR_OF_DAY)) {
218-
return localDateTime;
219-
}
211+
case HOUR_OF_DAY:
212+
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth(), localDateTime.getDayOfMonth(),
213+
localDateTime.getHour(), 0, 0);
220214

221-
localDateTime = localDateTime.withHour(0);
222-
assert localDateTime.getHour() == 0;
223-
if (unit.equals(DateTimeUnit.DAY_OF_MONTH)) {
224-
return localDateTime;
225-
}
215+
case DAY_OF_MONTH:
216+
LocalDate localDate = localDateTime.query(TemporalQueries.localDate());
217+
return localDate.atStartOfDay();
226218

227-
if (unit.equals(DateTimeUnit.WEEK_OF_WEEKYEAR)) {
228-
localDateTime = localDateTime.with(ChronoField.DAY_OF_WEEK, 1);
229-
assert localDateTime.getDayOfWeek() == DayOfWeek.MONDAY;
230-
return localDateTime;
231-
}
219+
case WEEK_OF_WEEKYEAR:
220+
return LocalDateTime.of(localDateTime.toLocalDate(), LocalTime.MIDNIGHT).with(ChronoField.DAY_OF_WEEK, 1);
232221

233-
localDateTime = localDateTime.withDayOfMonth(1);
234-
assert localDateTime.getDayOfMonth() == 1;
235-
if (unit.equals(DateTimeUnit.MONTH_OF_YEAR)) {
236-
return localDateTime;
237-
}
222+
case MONTH_OF_YEAR:
223+
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0);
238224

239-
if (unit.equals(DateTimeUnit.QUARTER_OF_YEAR)) {
240-
int quarter = (int) IsoFields.QUARTER_OF_YEAR.getFrom(localDateTime);
241-
int month = ((quarter - 1) * 3) + 1;
242-
localDateTime = localDateTime.withMonth(month);
243-
assert localDateTime.getMonthValue() % 3 == 1;
244-
return localDateTime;
245-
}
225+
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);
246229

247-
if (unit.equals(DateTimeUnit.YEAR_OF_CENTURY)) {
248-
localDateTime = localDateTime.withMonth(1);
249-
assert localDateTime.getMonthValue() == 1;
250-
return localDateTime;
251-
}
230+
case YEAR_OF_CENTURY:
231+
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);
252232

253-
throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit);
233+
default:
234+
throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit);
235+
}
254236
}
255237

256238
@Override
257-
public long round(long utcMillis) {
239+
public long round(final long utcMillis) {
240+
Instant instant = Instant.ofEpochMilli(utcMillis);
258241
if (unitRoundsToMidnight) {
259-
final ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone);
260-
final LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
242+
final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);
261243
final LocalDateTime localMidnight = truncateLocalDateTime(localDateTime);
262244
return firstTimeOnDay(localMidnight);
263245
} else {
246+
final ZoneRules rules = timeZone.getRules();
264247
while (true) {
265-
final Instant truncatedTime = truncateAsLocalTime(utcMillis);
266-
final ZoneOffsetTransition previousTransition = timeZone.getRules().previousTransition(Instant.ofEpochMilli(utcMillis));
248+
final Instant truncatedTime = truncateAsLocalTime(instant, rules);
249+
final ZoneOffsetTransition previousTransition = rules.previousTransition(instant);
267250

268251
if (previousTransition == null) {
269252
// truncateAsLocalTime cannot have failed if there were no previous transitions
270253
return truncatedTime.toEpochMilli();
271254
}
272255

273-
final long previousTransitionMillis = previousTransition.getInstant().toEpochMilli();
274-
275-
if (truncatedTime != null && previousTransitionMillis <= truncatedTime.toEpochMilli()) {
256+
Instant previousTransitionInstant = previousTransition.getInstant();
257+
if (truncatedTime != null && previousTransitionInstant.compareTo(truncatedTime) < 1) {
276258
return truncatedTime.toEpochMilli();
277259
}
278260

279261
// There was a transition in between the input time and the truncated time. Return to the transition time and
280262
// round that down instead.
281-
utcMillis = previousTransitionMillis - 1;
263+
instant = previousTransitionInstant.minusNanos(1_000_000);
282264
}
283265
}
284266
}
@@ -289,7 +271,7 @@ private long firstTimeOnDay(LocalDateTime localMidnight) {
289271

290272
// Now work out what localMidnight actually means
291273
final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(localMidnight);
292-
if (currentOffsets.size() >= 1) {
274+
if (currentOffsets.isEmpty() == false) {
293275
// There is at least one midnight on this day, so choose the first
294276
final ZoneOffset firstOffset = currentOffsets.get(0);
295277
final OffsetDateTime offsetMidnight = localMidnight.atOffset(firstOffset);
@@ -302,23 +284,23 @@ private long firstTimeOnDay(LocalDateTime localMidnight) {
302284
}
303285
}
304286

305-
private Instant truncateAsLocalTime(long utcMillis) {
287+
private Instant truncateAsLocalTime(Instant instant, final ZoneRules rules) {
306288
assert unitRoundsToMidnight == false : "truncateAsLocalTime should not be called if unitRoundsToMidnight";
307289

308-
final LocalDateTime truncatedLocalDateTime
309-
= truncateLocalDateTime(Instant.ofEpochMilli(utcMillis).atZone(timeZone).toLocalDateTime());
310-
final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(truncatedLocalDateTime);
290+
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);
291+
final LocalDateTime truncatedLocalDateTime = truncateLocalDateTime(localDateTime);
292+
final List<ZoneOffset> currentOffsets = rules.getValidOffsets(truncatedLocalDateTime);
311293

312-
if (currentOffsets.size() >= 1) {
294+
if (currentOffsets.isEmpty() == false) {
313295
// at least one possibilities - choose the latest one that's still no later than the input time
314296
for (int offsetIndex = currentOffsets.size() - 1; offsetIndex >= 0; offsetIndex--) {
315297
final Instant result = truncatedLocalDateTime.atOffset(currentOffsets.get(offsetIndex)).toInstant();
316-
if (result.toEpochMilli() <= utcMillis) {
298+
if (result.isAfter(instant) == false) {
317299
return result;
318300
}
319301
}
320302

321-
assert false : "rounded time not found for " + utcMillis + " with " + this;
303+
assert false : "rounded time not found for " + instant + " with " + this;
322304
return null;
323305
} else {
324306
// The chosen local time didn't happen. This means we were given a time in an hour (or a minute) whose start
@@ -328,7 +310,7 @@ private Instant truncateAsLocalTime(long utcMillis) {
328310
}
329311

330312
private LocalDateTime nextRelevantMidnight(LocalDateTime localMidnight) {
331-
assert localMidnight.toLocalTime().equals(LocalTime.of(0, 0, 0)) : "nextRelevantMidnight should only be called at midnight";
313+
assert localMidnight.toLocalTime().equals(LocalTime.MIDNIGHT) : "nextRelevantMidnight should only be called at midnight";
332314
assert unitRoundsToMidnight : "firstTimeOnDay should only be called if unitRoundsToMidnight";
333315

334316
switch (unit) {
@@ -350,8 +332,7 @@ private LocalDateTime nextRelevantMidnight(LocalDateTime localMidnight) {
350332
@Override
351333
public long nextRoundingValue(long utcMillis) {
352334
if (unitRoundsToMidnight) {
353-
final ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone);
354-
final LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
335+
final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), timeZone);
355336
final LocalDateTime earlierLocalMidnight = truncateLocalDateTime(localDateTime);
356337
final LocalDateTime localMidnight = nextRelevantMidnight(earlierLocalMidnight);
357338
return firstTimeOnDay(localMidnight);
@@ -433,14 +414,14 @@ public byte id() {
433414
@Override
434415
public long round(final long utcMillis) {
435416
final Instant utcInstant = Instant.ofEpochMilli(utcMillis);
436-
final LocalDateTime rawLocalDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone).toLocalDateTime();
417+
final LocalDateTime rawLocalDateTime = LocalDateTime.ofInstant(utcInstant, timeZone);
437418

438419
// a millisecond value with the same local time, in UTC, as `utcMillis` has in `timeZone`
439420
final long localMillis = utcMillis + timeZone.getRules().getOffset(utcInstant).getTotalSeconds() * 1000;
440421
assert localMillis == rawLocalDateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
441422

442423
final long roundedMillis = roundKey(localMillis, interval) * interval;
443-
final LocalDateTime roundedLocalDateTime = Instant.ofEpochMilli(roundedMillis).atZone(ZoneOffset.UTC).toLocalDateTime();
424+
final LocalDateTime roundedLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(roundedMillis), ZoneOffset.UTC);
444425

445426
// Now work out what roundedLocalDateTime actually means
446427
final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(roundedLocalDateTime);
@@ -485,9 +466,8 @@ private static long roundKey(long value, long interval) {
485466
@Override
486467
public long nextRoundingValue(long time) {
487468
int offsetSeconds = timeZone.getRules().getOffset(Instant.ofEpochMilli(time)).getTotalSeconds();
488-
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC)
489-
.plusSeconds(offsetSeconds)
490-
.plusNanos(interval * 1_000_000)
469+
long millis = time + interval + offsetSeconds * 1000;
470+
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC)
491471
.withZoneSameLocal(timeZone)
492472
.toInstant().toEpochMilli();
493473
}

0 commit comments

Comments
 (0)