Skip to content

Commit

Permalink
Restore date aggregation performance in UTC case (#38221)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
spinscale authored Feb 11, 2019
1 parent 067e648 commit b955220
Show file tree
Hide file tree
Showing 10 changed files with 610 additions and 31 deletions.
3 changes: 3 additions & 0 deletions NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ Copyright 2009-2018 Elasticsearch

This product includes software developed by The Apache Software
Foundation (http://www.apache.org/).

This product includes software developed by
Joda.org (http://www.joda.org/).
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,14 @@
import org.openjdk.jmh.annotations.Warmup;

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

import static org.elasticsearch.common.Rounding.DateTimeUnit.DAY_OF_MONTH;
import static org.elasticsearch.common.Rounding.DateTimeUnit.MONTH_OF_YEAR;
import static org.elasticsearch.common.Rounding.DateTimeUnit.QUARTER_OF_YEAR;
import static org.elasticsearch.common.Rounding.DateTimeUnit.YEAR_OF_CENTURY;

@Fork(3)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
Expand All @@ -48,23 +54,13 @@ public class RoundingBenchmark {
private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam");
private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId);

private long timestamp = 1548879021354L;

private final org.elasticsearch.common.rounding.Rounding jodaRounding =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.HOUR_OF_DAY).timeZone(timeZone).build();
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);
Expand All @@ -75,6 +71,11 @@ public long timeRoundingDateTimeUnitJava() {
return javaRounding.round(timestamp);
}

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(DAY_OF_MONTH)
.timeZone(zoneId).build();

@Benchmark
public long timeRoundingDateTimeUnitDayOfMonthJoda() {
return jodaDayOfMonthRounding.round(timestamp);
Expand All @@ -85,6 +86,11 @@ public long timeRoundingDateTimeUnitDayOfMonthJava() {
return javaDayOfMonthRounding.round(timestamp);
}

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

@Benchmark
public long timeIntervalRoundingJava() {
return timeIntervalRoundingJava.round(timestamp);
Expand All @@ -94,4 +100,65 @@ public long timeIntervalRoundingJava() {
public long timeIntervalRoundingJoda() {
return timeIntervalRoundingJoda.round(timestamp);
}

private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcDayOfMonthJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcDayOfMonthJava = Rounding.builder(DAY_OF_MONTH)
.timeZone(ZoneOffset.UTC).build();

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

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

private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcQuarterOfYearJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.QUARTER).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcQuarterOfYearJava = Rounding.builder(QUARTER_OF_YEAR)
.timeZone(ZoneOffset.UTC).build();

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

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

private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.MONTH_OF_YEAR).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcMonthOfYearJava = Rounding.builder(MONTH_OF_YEAR)
.timeZone(ZoneOffset.UTC).build();

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

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


private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcYearOfCenturyJoda =
org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.YEAR_OF_CENTURY).timeZone(DateTimeZone.UTC).build();
private final Rounding timeUnitRoundingUtcYearOfCenturyJava = Rounding.builder(YEAR_OF_CENTURY)
.timeZone(ZoneOffset.UTC).build();

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

@Benchmark
public long timeUnitRoundingUtcYearOfCenturyJoda() {
return timeUnitRoundingUtcYearOfCenturyJoda.round(timestamp);
}
}
81 changes: 64 additions & 17 deletions server/src/main/java/org/elasticsearch/common/Rounding.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,51 @@
*/
public abstract class Rounding implements Writeable {

public static String format(long epochMillis) {
return Instant.ofEpochMilli(epochMillis) + "/" + epochMillis;
}

public enum DateTimeUnit {
WEEK_OF_WEEKYEAR( (byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR),
YEAR_OF_CENTURY( (byte) 2, ChronoField.YEAR_OF_ERA),
QUARTER_OF_YEAR( (byte) 3, IsoFields.QUARTER_OF_YEAR),
MONTH_OF_YEAR( (byte) 4, ChronoField.MONTH_OF_YEAR),
DAY_OF_MONTH( (byte) 5, ChronoField.DAY_OF_MONTH),
HOUR_OF_DAY( (byte) 6, ChronoField.HOUR_OF_DAY),
MINUTES_OF_HOUR( (byte) 7, ChronoField.MINUTE_OF_HOUR),
SECOND_OF_MINUTE( (byte) 8, ChronoField.SECOND_OF_MINUTE);
WEEK_OF_WEEKYEAR((byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundWeekOfWeekYear(utcMillis);
}
},
YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) {
long roundFloor(long utcMillis) {
return DateUtils.roundYear(utcMillis);
}
},
QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundQuarterOfYear(utcMillis);
}
},
MONTH_OF_YEAR((byte) 4, ChronoField.MONTH_OF_YEAR) {
long roundFloor(long utcMillis) {
return DateUtils.roundMonthOfYear(utcMillis);
}
},
DAY_OF_MONTH((byte) 5, ChronoField.DAY_OF_MONTH) {
final long unitMillis = ChronoField.DAY_OF_MONTH.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
HOUR_OF_DAY((byte) 6, ChronoField.HOUR_OF_DAY) {
final long unitMillis = ChronoField.HOUR_OF_DAY.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
MINUTES_OF_HOUR((byte) 7, ChronoField.MINUTE_OF_HOUR) {
final long unitMillis = ChronoField.MINUTE_OF_HOUR.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
},
SECOND_OF_MINUTE((byte) 8, ChronoField.SECOND_OF_MINUTE) {
final long unitMillis = ChronoField.SECOND_OF_MINUTE.getBaseUnit().getDuration().toMillis();
long roundFloor(long utcMillis) {
return DateUtils.roundFloor(utcMillis, unitMillis);
}
};

private final byte id;
private final TemporalField field;
Expand All @@ -76,6 +108,15 @@ public enum DateTimeUnit {
this.field = field;
}

/**
* This rounds down the supplied milliseconds since the epoch down to the next unit. In order to retain performance this method
* should be as fast as possible and not try to convert dates to java-time objects if possible
*
* @param utcMillis the milliseconds since the epoch
* @return the rounded down milliseconds since the epoch
*/
abstract long roundFloor(long utcMillis);

public byte getId() {
return id;
}
Expand Down Expand Up @@ -182,12 +223,13 @@ static class TimeUnitRounding extends Rounding {
private final DateTimeUnit unit;
private final ZoneId timeZone;
private final boolean unitRoundsToMidnight;

private final boolean isUtcTimeZone;

TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) {
this.unit = unit;
this.timeZone = timeZone;
this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L;
this.isUtcTimeZone = timeZone.normalized().equals(ZoneOffset.UTC);
}

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

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);
return LocalDateTime.of(localDateTime.getYear(), localDateTime.getMonth().firstMonthOfQuarter(), 1, 0, 0);

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

@Override
public long round(final long utcMillis) {
public long round(long utcMillis) {
// this works as long as the offset doesn't change. It is worth getting this case out of the way first, as
// the calculations for fixing things near to offset changes are a little expensive and are unnecessary in the common case
// of working in UTC.
if (isUtcTimeZone) {
return unit.roundFloor(utcMillis);
}

Instant instant = Instant.ofEpochMilli(utcMillis);
if (unitRoundsToMidnight) {
final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone);
Expand Down
78 changes: 78 additions & 0 deletions server/src/main/java/org/elasticsearch/common/time/DateUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.common.time.DateUtilsRounding.getMonthOfYear;
import static org.elasticsearch.common.time.DateUtilsRounding.getTotalMillisByYearMonth;
import static org.elasticsearch.common.time.DateUtilsRounding.getYear;
import static org.elasticsearch.common.time.DateUtilsRounding.utcMillisAtStartOfYear;

public class DateUtils {
public static DateTimeZone zoneIdToDateTimeZone(ZoneId zoneId) {
if (zoneId == null) {
Expand Down Expand Up @@ -143,6 +148,79 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) {
return nanoSecondsSinceEpoch / 1_000_000;
}

/**
* Rounds the given utc milliseconds sicne the epoch down to the next unit millis
*
* Note: This does not check for correctness of the result, as this only works with units smaller or equal than a day
* In order to ensure the performane of this methods, there are no guards or checks in it
*
* @param utcMillis the milliseconds since the epoch
* @param unitMillis the unit to round to
* @return the rounded milliseconds since the epoch
*/
public static long roundFloor(long utcMillis, final long unitMillis) {
if (utcMillis >= 0) {
return utcMillis - utcMillis % unitMillis;
} else {
utcMillis += 1;
return utcMillis - utcMillis % unitMillis - unitMillis;
}
}

/**
* Round down to the beginning of the quarter of the year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the quarter of the year
*/
public static long roundQuarterOfYear(final long utcMillis) {
int year = getYear(utcMillis);
int month = getMonthOfYear(utcMillis, year);
int firstMonthOfQuarter = (((month-1) / 3) * 3) + 1;
return DateUtils.of(year, firstMonthOfQuarter);
}

/**
* Round down to the beginning of the month of the year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the month of the year
*/
public static long roundMonthOfYear(final long utcMillis) {
int year = getYear(utcMillis);
int month = getMonthOfYear(utcMillis, year);
return DateUtils.of(year, month);
}

/**
* Round down to the beginning of the year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the beginning of the year
*/
public static long roundYear(final long utcMillis) {
int year = getYear(utcMillis);
return utcMillisAtStartOfYear(year);
}

/**
* Round down to the beginning of the week based on week year of the specified time
* @param utcMillis the milliseconds since the epoch
* @return The milliseconds since the epoch rounded down to the beginning of the week based on week year
*/
public static long roundWeekOfWeekYear(final long utcMillis) {
return roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L;
}

/**
* Return the first day of the month
* @param year the year to return
* @param month the month to return, ranging from 1-12
* @return the milliseconds since the epoch of the first day of the month in the year
*/
private static long of(final int year, final int month) {
long millis = utcMillisAtStartOfYear(year);
millis += getTotalMillisByYearMonth(year, month);
return millis;
}

/**
* Returns the current UTC date-time with milliseconds precision.
* In Java 9+ (as opposed to Java 8) the {@code Clock} implementation uses system's best clock implementation (which could mean
Expand Down
Loading

0 comments on commit b955220

Please sign in to comment.