Skip to content

Commit

Permalink
Reduce object creation in Rounding class (elastic#38061)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
spinscale committed Jan 31, 2019
1 parent a536fa7 commit 9f026bb
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.benchmark.time;

import org.elasticsearch.common.Rounding;
import org.elasticsearch.common.rounding.DateTimeUnit;
import org.elasticsearch.common.time.DateUtils;
import org.elasticsearch.common.unit.TimeValue;
import org.joda.time.DateTimeZone;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.time.ZoneId;
import java.util.concurrent.TimeUnit;

@Fork(3)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@SuppressWarnings("unused") //invoked by benchmarking framework
public class RoundingBenchmark {

private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam");
private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId);

private final org.elasticsearch.common.rounding.Rounding jodaRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY)
.timeZone(zoneId).build();

private final org.elasticsearch.common.rounding.Rounding jodaDayOfMonthRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
private final Rounding javaDayOfMonthRounding = Rounding.builder(TimeValue.timeValueMinutes(60))
.timeZone(zoneId).build();

private final org.elasticsearch.common.rounding.Rounding timeIntervalRoundingJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(timeZone).build();
private final Rounding timeIntervalRoundingJava = Rounding.builder(TimeValue.timeValueMinutes(60))
.timeZone(zoneId).build();

private final long timestamp = 1548879021354L;

@Benchmark
public long timeRoundingDateTimeUnitJoda() {
return jodaRounding.round(timestamp);
}

@Benchmark
public long timeRoundingDateTimeUnitJava() {
return javaRounding.round(timestamp);
}

@Benchmark
public long timeRoundingDateTimeUnitDayOfMonthJoda() {
return jodaDayOfMonthRounding.round(timestamp);
}

@Benchmark
public long timeRoundingDateTimeUnitDayOfMonthJava() {
return javaDayOfMonthRounding.round(timestamp);
}

@Benchmark
public long timeIntervalRoundingJava() {
return timeIntervalRoundingJava.round(timestamp);
}

@Benchmark
public long timeIntervalRoundingJoda() {
return timeIntervalRoundingJoda.round(timestamp);
}
}
126 changes: 53 additions & 73 deletions server/src/main/java/org/elasticsearch/common/Rounding.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
import org.elasticsearch.common.unit.TimeValue;

import java.io.IOException;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
Expand All @@ -39,7 +39,9 @@
import java.time.temporal.ChronoUnit;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalQueries;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneRules;
import java.util.List;
import java.util.Objects;

Expand Down Expand Up @@ -185,13 +187,11 @@ static class TimeUnitRounding extends Rounding {
TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
this.unit = unit;
this.timeZone = timeZone;
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 60L * 60L * 1000L;
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
}

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

@Override
Expand All @@ -200,85 +200,67 @@ public byte id() {
}

private LocalDateTime truncateLocalDateTime(LocalDateTime localDateTime) {
localDateTime = localDateTime.withNano(0);
assert localDateTime.getNano() == 0;
if (unit.equals(DateTimeUnit.SECOND_OF_MINUTE)) {
return localDateTime;
}
switch (unit) {
case SECOND_OF_MINUTE:
return localDateTime.withNano(0);

localDateTime = localDateTime.withSecond(0);
assert localDateTime.getSecond() == 0;
if (unit.equals(DateTimeUnit.MINUTES_OF_HOUR)) {
return localDateTime;
}
case MINUTES_OF_HOUR:
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), localDateTime.getDayOfMonth(),
localDateTime.getHour(), localDateTime.getMinute(), 0, 0);

localDateTime = localDateTime.withMinute(0);
assert localDateTime.getMinute() == 0;
if (unit.equals(DateTimeUnit.HOUR_OF_DAY)) {
return localDateTime;
}
case HOUR_OF_DAY:
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth(), localDateTime.getDayOfMonth(),
localDateTime.getHour(), 0, 0);

localDateTime = localDateTime.withHour(0);
assert localDateTime.getHour() == 0;
if (unit.equals(DateTimeUnit.DAY_OF_MONTH)) {
return localDateTime;
}
case DAY_OF_MONTH:
LocalDate localDate = localDateTime.query(TemporalQueries.localDate());
return localDate.atStartOfDay();

if (unit.equals(DateTimeUnit.WEEK_OF_WEEKYEAR)) {
localDateTime = localDateTime.with(ChronoField.DAY_OF_WEEK, 1);
assert localDateTime.getDayOfWeek() == DayOfWeek.MONDAY;
return localDateTime;
}
case WEEK_OF_WEEKYEAR:
return LocalDateTime.of(localDateTime.toLocalDate(), LocalTime.MIDNIGHT).with(ChronoField.DAY_OF_WEEK, 1);

localDateTime = localDateTime.withDayOfMonth(1);
assert localDateTime.getDayOfMonth() == 1;
if (unit.equals(DateTimeUnit.MONTH_OF_YEAR)) {
return localDateTime;
}
case MONTH_OF_YEAR:
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonthValue(), 1, 0, 0);

if (unit.equals(DateTimeUnit.QUARTER_OF_YEAR)) {
int quarter = (int) IsoFields.QUARTER_OF_YEAR.getFrom(localDateTime);
int month = ((quarter - 1) * 3) + 1;
localDateTime = localDateTime.withMonth(month);
assert localDateTime.getMonthValue() % 3 == 1;
return localDateTime;
}
case QUARTER_OF_YEAR:
int quarter = (int) IsoFields.QUARTER_OF_YEAR.getFrom(localDateTime);
int month = ((quarter - 1) * 3) + 1;
return LocalDateTime.of(localDateTime.getYear(), month, 1, 0, 0);

if (unit.equals(DateTimeUnit.YEAR_OF_CENTURY)) {
localDateTime = localDateTime.withMonth(1);
assert localDateTime.getMonthValue() == 1;
return localDateTime;
}
case YEAR_OF_CENTURY:
return LocalDateTime.of(LocalDate.of(localDateTime.getYear(), 1, 1), LocalTime.MIDNIGHT);

throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit);
default:
throw new IllegalArgumentException("NOT YET IMPLEMENTED for unit " + unit);
}
}

@Override
public long round(long utcMillis) {
public long round(final long utcMillis) {
Instant instant = Instant.ofEpochMilli(utcMillis);
if (unitRoundsToMidnight) {
final ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone);
final LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);
final LocalDateTime localMidnight = truncateLocalDateTime(localDateTime);
return firstTimeOnDay(localMidnight);
} else {
final ZoneRules rules = timeZone.getRules();
while (true) {
final Instant truncatedTime = truncateAsLocalTime(utcMillis);
final ZoneOffsetTransition previousTransition = timeZone.getRules().previousTransition(Instant.ofEpochMilli(utcMillis));
final Instant truncatedTime = truncateAsLocalTime(instant, rules);
final ZoneOffsetTransition previousTransition = rules.previousTransition(instant);

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

final long previousTransitionMillis = previousTransition.getInstant().toEpochMilli();

if (truncatedTime != null && previousTransitionMillis <= truncatedTime.toEpochMilli()) {
Instant previousTransitionInstant = previousTransition.getInstant();
if (truncatedTime != null && previousTransitionInstant.compareTo(truncatedTime) < 1) {
return truncatedTime.toEpochMilli();
}

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

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

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

final LocalDateTime truncatedLocalDateTime
= truncateLocalDateTime(Instant.ofEpochMilli(utcMillis).atZone(timeZone).toLocalDateTime());
final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(truncatedLocalDateTime);
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);
final LocalDateTime truncatedLocalDateTime = truncateLocalDateTime(localDateTime);
final List<ZoneOffset> currentOffsets = rules.getValidOffsets(truncatedLocalDateTime);

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

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

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

switch (unit) {
Expand All @@ -350,8 +332,7 @@ private LocalDateTime nextRelevantMidnight(LocalDateTime localMidnight) {
@Override
public long nextRoundingValue(long utcMillis) {
if (unitRoundsToMidnight) {
final ZonedDateTime zonedDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone);
final LocalDateTime localDateTime = zonedDateTime.toLocalDateTime();
final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), timeZone);
final LocalDateTime earlierLocalMidnight = truncateLocalDateTime(localDateTime);
final LocalDateTime localMidnight = nextRelevantMidnight(earlierLocalMidnight);
return firstTimeOnDay(localMidnight);
Expand Down Expand Up @@ -433,14 +414,14 @@ public byte id() {
@Override
public long round(final long utcMillis) {
final Instant utcInstant = Instant.ofEpochMilli(utcMillis);
final LocalDateTime rawLocalDateTime = Instant.ofEpochMilli(utcMillis).atZone(timeZone).toLocalDateTime();
final LocalDateTime rawLocalDateTime = LocalDateTime.ofInstant(utcInstant, timeZone);

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

final long roundedMillis = roundKey(localMillis, interval) * interval;
final LocalDateTime roundedLocalDateTime = Instant.ofEpochMilli(roundedMillis).atZone(ZoneOffset.UTC).toLocalDateTime();
final LocalDateTime roundedLocalDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(roundedMillis), ZoneOffset.UTC);

// Now work out what roundedLocalDateTime actually means
final List<ZoneOffset> currentOffsets = timeZone.getRules().getValidOffsets(roundedLocalDateTime);
Expand Down Expand Up @@ -485,9 +466,8 @@ private static long roundKey(long value, long interval) {
@Override
public long nextRoundingValue(long time) {
int offsetSeconds = timeZone.getRules().getOffset(Instant.ofEpochMilli(time)).getTotalSeconds();
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC)
.plusSeconds(offsetSeconds)
.plusNanos(interval * 1_000_000)
long millis = time + interval + offsetSeconds * 1000;
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC)
.withZoneSameLocal(timeZone)
.toInstant().toEpochMilli();
}
Expand Down

0 comments on commit 9f026bb

Please sign in to comment.