From 242df6f171b1d6330e68e5cb3f8c3a8ae72235fc Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 1 Feb 2019 20:50:20 +0100 Subject: [PATCH 01/18] Restore date aggregation performance in UTC case 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 --- .../benchmark/time/RoundingBenchmark.java | 44 +++++++++++++------ .../org/elasticsearch/common/Rounding.java | 23 +++++++++- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 6da6d5290bfee..8b563c9038b1f 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -34,8 +34,11 @@ 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; + @Fork(3) @Warmup(iterations = 10) @Measurement(iterations = 10) @@ -48,23 +51,13 @@ public class RoundingBenchmark { private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam"); private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId); + private final 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); @@ -75,6 +68,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); @@ -85,6 +83,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); @@ -94,4 +97,19 @@ public long timeIntervalRoundingJava() { public long timeIntervalRoundingJoda() { return timeIntervalRoundingJoda.round(timestamp); } + + private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda = + org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build(); + private final Rounding timeUnitRoundingUtcMonthOfYearJava = Rounding.builder(DAY_OF_MONTH) + .timeZone(ZoneOffset.UTC).build(); + + @Benchmark + public long timeUnitRoundingUtcMonthOfYearJava() { + return timeUnitRoundingUtcMonthOfYearJava.round(timestamp); + } + + @Benchmark + public long timeUnitRoundingUtcMonthOfYearJoda() { + return timeUnitRoundingUtcMonthOfYearJoda.round(timestamp); + } } diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 6d11739133dda..38478a47e22a5 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -70,10 +70,16 @@ public enum DateTimeUnit { private final byte id; private final TemporalField field; + private final long unitMillis; DateTimeUnit(byte id, TemporalField field) { this.id = id; this.field = field; + this.unitMillis = field.getBaseUnit().getDuration().toMillis(); + } + + public long getUnitMillis() { + return unitMillis; } public byte getId() { @@ -182,12 +188,14 @@ static class TimeUnitRounding extends Rounding { private final DateTimeUnit unit; private final ZoneId timeZone; private final boolean unitRoundsToMidnight; + private final boolean isFixedOffset; TimeUnitRounding(DateTimeUnit unit, ZoneId timeZone) { this.unit = unit; this.timeZone = timeZone; this.unitRoundsToMidnight = this.unit.field.getBaseUnit().getDuration().toMillis() > 3600000L; + this.isFixedOffset = timeZone.getRules().isFixedOffset(); } TimeUnitRounding(StreamInput in) throws IOException { @@ -236,7 +244,20 @@ 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 (isFixedOffset) { + long unitMillis = unit.getUnitMillis(); + if (utcMillis >= 0) { + return utcMillis - utcMillis % unitMillis; + } else { + utcMillis += 1; + return utcMillis - utcMillis % unitMillis - unitMillis; + } + } + Instant instant = Instant.ofEpochMilli(utcMillis); if (unitRoundsToMidnight) { final LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, timeZone); From 1326aa1aaaa1046bed07cfb69cbbf8132e5377fc Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sat, 2 Feb 2019 01:59:44 +0100 Subject: [PATCH 02/18] fix tests, fast path is only with UTC and small date time units though --- .../benchmark/time/RoundingBenchmark.java | 102 ++++++++++-------- .../org/elasticsearch/common/Rounding.java | 52 ++++++--- .../elasticsearch/common/RoundingTests.java | 24 +++++ .../common/rounding/RoundingDuelTests.java | 21 ++++ .../rounding/TimeZoneRoundingTests.java | 4 + 5 files changed, 144 insertions(+), 59 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 8b563c9038b1f..09f22ec46a6e8 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -21,7 +21,6 @@ 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; @@ -39,9 +38,9 @@ import static org.elasticsearch.common.Rounding.DateTimeUnit.DAY_OF_MONTH; -@Fork(3) -@Warmup(iterations = 10) -@Measurement(iterations = 10) +@Fork(2) +@Warmup(iterations = 5) +@Measurement(iterations = 5) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @@ -58,45 +57,45 @@ public class RoundingBenchmark { private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY) .timeZone(zoneId).build(); - @Benchmark - public long timeRoundingDateTimeUnitJoda() { - return jodaRounding.round(timestamp); - } - - @Benchmark - 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); - } - - @Benchmark - 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); - } - - @Benchmark - public long timeIntervalRoundingJoda() { - return timeIntervalRoundingJoda.round(timestamp); - } +// @Benchmark +// public long timeRoundingDateTimeUnitJoda() { +// return jodaRounding.round(timestamp); +// } +// +// @Benchmark +// 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); +// } +// +// @Benchmark +// 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); +// } +// +// @Benchmark +// public long timeIntervalRoundingJoda() { +// return timeIntervalRoundingJoda.round(timestamp); +// } private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda = org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build(); @@ -112,4 +111,21 @@ public long timeUnitRoundingUtcMonthOfYearJava() { public long timeUnitRoundingUtcMonthOfYearJoda() { return timeUnitRoundingUtcMonthOfYearJoda.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); +// } + + // TODO add benchmarks with timezones! java time might be far worse here } diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 38478a47e22a5..45727e003ed70 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -32,6 +32,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.Year; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -78,8 +79,36 @@ public enum DateTimeUnit { this.unitMillis = field.getBaseUnit().getDuration().toMillis(); } - public long getUnitMillis() { - return unitMillis; + public long roundFloor(long utcMillis) { + switch (this) { + case MONTH_OF_YEAR: + // TODO check if this can be done with static milliseconds, compare to joda impl + final LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); + return dt.toLocalDate().withDayOfMonth(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + case QUARTER_OF_YEAR: + // TODO check if this can be done with static milliseconds, compare to joda impl... should work?! + final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); + LocalDate localDate = localDateTime.toLocalDate(); + return LocalDateTime.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1, 0, 0) + .toInstant(ZoneOffset.UTC).toEpochMilli(); + case YEAR_OF_CENTURY: + // TODO check if this can be done with static milliseconds, compare to joda impl + final LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); + return Year.of(dateTime.getYear()).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + case WEEK_OF_WEEKYEAR: + return staticRoundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; + default: + return staticRoundFloor(utcMillis, unitMillis); + } + } + + private long staticRoundFloor(long utcMillis, long unitMillis) { + if (utcMillis >= 0) { + return utcMillis - utcMillis % unitMillis; + } else { + utcMillis += 1; + return utcMillis - utcMillis % unitMillis - unitMillis; + } } public byte getId() { @@ -188,14 +217,13 @@ static class TimeUnitRounding extends Rounding { private final DateTimeUnit unit; private final ZoneId timeZone; private final boolean unitRoundsToMidnight; - private final boolean isFixedOffset; - + 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.isFixedOffset = timeZone.getRules().isFixedOffset(); + this.isUtcTimeZone = timeZone.normalized().equals(ZoneOffset.UTC); } TimeUnitRounding(StreamInput in) throws IOException { @@ -231,9 +259,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); @@ -248,14 +274,8 @@ 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 (isFixedOffset) { - long unitMillis = unit.getUnitMillis(); - if (utcMillis >= 0) { - return utcMillis - utcMillis % unitMillis; - } else { - utcMillis += 1; - return utcMillis - utcMillis % unitMillis - unitMillis; - } + if (isUtcTimeZone) { + return unit.roundFloor(utcMillis); } Instant instant = Instant.ofEpochMilli(utcMillis); diff --git a/server/src/test/java/org/elasticsearch/common/RoundingTests.java b/server/src/test/java/org/elasticsearch/common/RoundingTests.java index a809131b932e2..aa7aeef1b0f28 100644 --- a/server/src/test/java/org/elasticsearch/common/RoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/RoundingTests.java @@ -57,6 +57,30 @@ public void testUTCTimeUnitRounding() { tzRounding = Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR).build(); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-09T00:00:00.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-16T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.QUARTER_OF_YEAR).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-04-01T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T01:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-10T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2013-01-01T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.MINUTES_OF_HOUR).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:01:00.000Z"), tz)); + + tzRounding = Rounding.builder(Rounding.DateTimeUnit.SECOND_OF_MINUTE).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:01.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz)); } public void testUTCIntervalRounding() { diff --git a/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java b/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java index 3ee4ce0e7d7bf..b791d0e3ce9dd 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/RoundingDuelTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.VersionUtils; +import org.joda.time.DateTimeZone; import java.time.ZoneOffset; @@ -56,6 +57,26 @@ public void testSerialization() throws Exception { assertThat(roundingJoda.nextRoundingValue(randomInt), is(roundingJavaTime.nextRoundingValue(randomInt))); } + public void testDuellingImplementations() { + org.elasticsearch.common.Rounding.DateTimeUnit randomDateTimeUnit = + randomFrom(org.elasticsearch.common.Rounding.DateTimeUnit.values()); + org.elasticsearch.common.Rounding rounding; + Rounding roundingJoda; + + if (randomBoolean()) { + rounding = org.elasticsearch.common.Rounding.builder(randomDateTimeUnit).timeZone(ZoneOffset.UTC).build(); + DateTimeUnit dateTimeUnit = DateTimeUnit.resolve(randomDateTimeUnit.getId()); + roundingJoda = Rounding.builder(dateTimeUnit).timeZone(DateTimeZone.UTC).build(); + } else { + TimeValue interval = timeValue(); + rounding = org.elasticsearch.common.Rounding.builder(interval).timeZone(ZoneOffset.UTC).build(); + roundingJoda = Rounding.builder(interval).timeZone(DateTimeZone.UTC).build(); + } + + long roundValue = randomLong(); + assertThat(roundingJoda.round(roundValue), is(rounding.round(roundValue))); + } + static TimeValue timeValue() { return TimeValue.parseTimeValue(randomIntBetween(1, 1000) + randomFrom(ALLOWED_TIME_SUFFIXES), "settingName"); } diff --git a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java index e49f25772a726..8189d2aa57a5d 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java @@ -56,6 +56,10 @@ public void testUTCTimeUnitRounding() { tzRounding = Rounding.builder(DateTimeUnit.WEEK_OF_WEEKYEAR).build(); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-09T00:00:00.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-16T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.QUARTER).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-04-01T00:00:00.000Z"), tz)); } public void testUTCIntervalRounding() { From 1c5fb71f5b521b7f723c3a7a2761e7791bbaa482 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sat, 2 Feb 2019 02:01:53 +0100 Subject: [PATCH 03/18] add back benchmark tests --- .../benchmark/time/RoundingBenchmark.java | 111 +++++++++--------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 09f22ec46a6e8..dbd1b529a7dfe 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -21,6 +21,7 @@ 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; @@ -37,6 +38,7 @@ import java.util.concurrent.TimeUnit; import static org.elasticsearch.common.Rounding.DateTimeUnit.DAY_OF_MONTH; +import static org.elasticsearch.common.Rounding.DateTimeUnit.QUARTER_OF_YEAR; @Fork(2) @Warmup(iterations = 5) @@ -57,45 +59,45 @@ public class RoundingBenchmark { private final Rounding javaRounding = Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY) .timeZone(zoneId).build(); -// @Benchmark -// public long timeRoundingDateTimeUnitJoda() { -// return jodaRounding.round(timestamp); -// } -// -// @Benchmark -// 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); -// } -// -// @Benchmark -// 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); -// } -// -// @Benchmark -// public long timeIntervalRoundingJoda() { -// return timeIntervalRoundingJoda.round(timestamp); -// } + @Benchmark + public long timeRoundingDateTimeUnitJoda() { + return jodaRounding.round(timestamp); + } + + @Benchmark + 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); + } + + @Benchmark + 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); + } + + @Benchmark + public long timeIntervalRoundingJoda() { + return timeIntervalRoundingJoda.round(timestamp); + } private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda = org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.DAY_OF_MONTH).timeZone(DateTimeZone.UTC).build(); @@ -112,20 +114,19 @@ public long timeUnitRoundingUtcMonthOfYearJoda() { return timeUnitRoundingUtcMonthOfYearJoda.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); -// } - - // TODO add benchmarks with timezones! java time might be far worse here + 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); + } + } From 6eeeb55b78f78952ee3046ed2e77f0de3e6ac363 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sat, 2 Feb 2019 21:00:37 +0100 Subject: [PATCH 04/18] Add more tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit current results show slowdowns in quarter of year and year of century parsing, the rest is faster/similar in this case. RoundingBenchmark.timeIntervalRoundingJava avgt 10 227,639 ± 12,084 ns/op RoundingBenchmark.timeIntervalRoundingJoda avgt 10 329,288 ± 15,914 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJava avgt 10 87,215 ± 5,923 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJoda avgt 10 370,219 ± 73,419 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJava avgt 10 135,884 ± 3,506 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJoda avgt 10 327,793 ± 27,550 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJava avgt 10 11,724 ± 0,266 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJoda avgt 10 11,785 ± 0,069 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJava avgt 10 57,007 ± 1,023 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJoda avgt 10 56,605 ± 0,725 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJava avgt 10 57,992 ± 1,289 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJoda avgt 10 29,979 ± 0,574 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJava avgt 10 56,557 ± 0,684 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJoda avgt 10 6,344 ± 0,035 ns/op --- .../benchmark/time/RoundingBenchmark.java | 43 ++++++++++++++++--- .../org/elasticsearch/common/Rounding.java | 9 ++-- .../elasticsearch/common/RoundingTests.java | 2 +- .../rounding/TimeZoneRoundingTests.java | 20 +++++++++ 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index dbd1b529a7dfe..9d1881d4d07e5 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -38,7 +38,9 @@ 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(2) @Warmup(iterations = 5) @@ -99,19 +101,19 @@ public long timeIntervalRoundingJoda() { return timeIntervalRoundingJoda.round(timestamp); } - private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcMonthOfYearJoda = + 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 timeUnitRoundingUtcMonthOfYearJava = Rounding.builder(DAY_OF_MONTH) + private final Rounding timeUnitRoundingUtcDayOfMonthJava = Rounding.builder(DAY_OF_MONTH) .timeZone(ZoneOffset.UTC).build(); @Benchmark - public long timeUnitRoundingUtcMonthOfYearJava() { - return timeUnitRoundingUtcMonthOfYearJava.round(timestamp); + public long timeUnitRoundingUtcDayOfMonthJava() { + return timeUnitRoundingUtcDayOfMonthJava.round(timestamp); } @Benchmark - public long timeUnitRoundingUtcMonthOfYearJoda() { - return timeUnitRoundingUtcMonthOfYearJoda.round(timestamp); + public long timeUnitRoundingUtcDayOfMonthJoda() { + return timeUnitRoundingUtcDayOfMonthJoda.round(timestamp); } private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcQuarterOfYearJoda = @@ -129,4 +131,33 @@ 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 timeUnitRoundingUtcMonthOfYearJava.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); + } } diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 45727e003ed70..f09e78d3aaf83 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -79,19 +79,20 @@ public enum DateTimeUnit { this.unitMillis = field.getBaseUnit().getDuration().toMillis(); } - public long roundFloor(long utcMillis) { + public long roundFloorUtc(long utcMillis) { switch (this) { case MONTH_OF_YEAR: - // TODO check if this can be done with static milliseconds, compare to joda impl final LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); return dt.toLocalDate().withDayOfMonth(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); case QUARTER_OF_YEAR: - // TODO check if this can be done with static milliseconds, compare to joda impl... should work?! + // TODO 2x slower than joda + // TODO check if this can be done with static milliseconds final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); LocalDate localDate = localDateTime.toLocalDate(); return LocalDateTime.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1, 0, 0) .toInstant(ZoneOffset.UTC).toEpochMilli(); case YEAR_OF_CENTURY: + // TODO 10x slower than joda // TODO check if this can be done with static milliseconds, compare to joda impl final LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); return Year.of(dateTime.getYear()).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); @@ -275,7 +276,7 @@ public long round(long utcMillis) { // 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); + return unit.roundFloorUtc(utcMillis); } Instant instant = Instant.ofEpochMilli(utcMillis); diff --git a/server/src/test/java/org/elasticsearch/common/RoundingTests.java b/server/src/test/java/org/elasticsearch/common/RoundingTests.java index aa7aeef1b0f28..bee3f57764f32 100644 --- a/server/src/test/java/org/elasticsearch/common/RoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/RoundingTests.java @@ -691,7 +691,7 @@ private void assertInterval(long rounded, long nextRoundingValue, Rounding round } /** - * perform a number on assertions and checks on {@link org.elasticsearch.common.rounding.Rounding.TimeUnitRounding} intervals + * perform a number on assertions and checks on {@link org.elasticsearch.common.Rounding.TimeUnitRounding} intervals * @param rounded the expected low end of the rounding interval * @param unrounded a date in the interval to be checked for rounding * @param nextRoundingValue the expected upper end of the rounding interval diff --git a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java index 8189d2aa57a5d..029eb3b041d3d 100644 --- a/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java +++ b/server/src/test/java/org/elasticsearch/common/rounding/TimeZoneRoundingTests.java @@ -60,6 +60,26 @@ public void testUTCTimeUnitRounding() { tzRounding = Rounding.builder(DateTimeUnit.QUARTER).build(); assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-04-01T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.HOUR_OF_DAY).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T01:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.DAY_OF_MONTH).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-10T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.YEAR_OF_CENTURY).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-01T00:00:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2013-01-01T00:00:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.MINUTES_OF_HOUR).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:00.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:01:00.000Z"), tz)); + + tzRounding = Rounding.builder(DateTimeUnit.SECOND_OF_MINUTE).build(); + assertThat(tzRounding.round(time("2012-01-10T01:01:01")), isDate(time("2012-01-10T01:01:01.000Z"), tz)); + assertThat(tzRounding.nextRoundingValue(time("2012-01-09T00:00:00.000Z")), isDate(time("2012-01-09T00:00:01.000Z"), tz)); } public void testUTCIntervalRounding() { From 439966140622e5fb49abda0768d11f7414af5592 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sat, 2 Feb 2019 23:29:44 +0100 Subject: [PATCH 05/18] Speed up year of century parsing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use joda time code which solely uses milliseconds and never converts to any other joda time field, thus being super fast. Benchmark Mode Cnt Score Error Units RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJava avgt 10 3,378 ± 0,036 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJoda avgt 10 6,460 ± 0,027 ns/op --- .../benchmark/time/RoundingBenchmark.java | 7 +- .../org/elasticsearch/common/Rounding.java | 10 +-- .../elasticsearch/common/time/DateUtils.java | 86 +++++++++++++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 9d1881d4d07e5..0e763185e67cb 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -21,7 +21,6 @@ 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; @@ -37,9 +36,6 @@ 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(2) @@ -56,6 +52,7 @@ public class RoundingBenchmark { private final long timestamp = 1548879021354L; + /* 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) @@ -146,6 +143,8 @@ public long timeUnitRoundingUtcMonthOfYearJoda() { return timeUnitRoundingUtcMonthOfYearJava.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) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index f09e78d3aaf83..66c42b2e46b17 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -32,7 +32,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.Year; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -55,10 +54,6 @@ */ 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), @@ -92,10 +87,7 @@ public long roundFloorUtc(long utcMillis) { return LocalDateTime.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1, 0, 0) .toInstant(ZoneOffset.UTC).toEpochMilli(); case YEAR_OF_CENTURY: - // TODO 10x slower than joda - // TODO check if this can be done with static milliseconds, compare to joda impl - final LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); - return Year.of(dateTime.getYear()).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + return DateUtils.getFirstDayOfYearMillis(utcMillis); case WEEK_OF_WEEKYEAR: return staticRoundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; default: diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index e913a69dca776..a859db8a876d9 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -77,4 +77,90 @@ public static ZoneId of(String zoneId) { } return ZoneId.of(zoneId).normalized(); } + + private static final int DAYS_0000_TO_1970 = 719527; + private static final int MILLIS_PER_DAY = 86_400_000; + private static final long MILLIS_PER_YEAR = 31556952000L; + + private static boolean isLeapYear(int year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + /** + * calculates the first day of a year in milliseconds since the epoch (assuming UTC) + * + * @param year the year + * @return the milliseconds since the epoch of the first of january at midnight of the specified year + */ + private static long calculateFirstDayOfYearMillis(int year) { + // Initial value is just temporary. + int leapYears = year / 100; + if (year < 0) { + // Add 3 before shifting right since /4 and >>2 behave differently + // on negative numbers. When the expression is written as + // (year / 4) - (year / 100) + (year / 400), + // it works for both positive and negative values, except this optimization + // eliminates two divisions. + leapYears = ((year + 3) >> 2) - leapYears + ((leapYears + 3) >> 2) - 1; + } else { + leapYears = (year >> 2) - leapYears + (leapYears >> 2); + if (isLeapYear(year)) { + leapYears--; + } + } + + return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day + } + + private static int getYear(long instant) { + // Get an initial estimate of the year, and the millis value that + // represents the start of that year. Then verify estimate and fix if + // necessary. + + // Initial estimate uses values divided by two to avoid overflow. + long unitMillis = getAverageMillisPerYearDividedByTwo(); + long i2 = (instant >> 1) + getApproxMillisAtEpochDividedByTwo(); + if (i2 < 0) { + i2 = i2 - unitMillis + 1; + } + int year = (int) (i2 / unitMillis); + + long yearStart = calculateFirstDayOfYearMillis(year); + long diff = instant - yearStart; + + if (diff < 0) { + year--; + } else if (diff >= MILLIS_PER_DAY * 365L) { + // One year may need to be added to fix estimate. + long oneYear; + if (isLeapYear(year)) { + oneYear = MILLIS_PER_DAY * 366L; + } else { + oneYear = MILLIS_PER_DAY * 365L; + } + + yearStart += oneYear; + + if (yearStart <= instant) { + // Didn't go too far, so actually add one year. + year++; + } + } + + return year; + } + + private static long getApproxMillisAtEpochDividedByTwo() { + return (1970L * MILLIS_PER_YEAR) / 2; + } + + private static long getAverageMillisPerYearDividedByTwo() { + return MILLIS_PER_YEAR / 2; + } + + + public static long getFirstDayOfYearMillis(long utcMillis) { + int year = getYear(utcMillis); + return calculateFirstDayOfYearMillis(year); + } } From ed263c97de801b23cb89dc480d8de0b2557d625c Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sun, 3 Feb 2019 00:14:24 +0100 Subject: [PATCH 06/18] Bring quarter of year for UTC case up to speed again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units RoundingBenchmark.timeIntervalRoundingJava avgt 30 223,906 ± 1,248 ns/op RoundingBenchmark.timeIntervalRoundingJoda avgt 30 337,049 ± 14,682 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJava avgt 30 85,784 ± 1,719 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJoda avgt 30 364,764 ± 19,697 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJava avgt 30 133,247 ± 1,022 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJoda avgt 30 356,372 ± 17,340 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJava avgt 30 11,778 ± 0,044 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJoda avgt 30 11,922 ± 0,060 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJava avgt 30 52,529 ± 3,865 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJoda avgt 30 61,306 ± 1,581 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJava avgt 30 22,152 ± 0,164 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJoda avgt 30 30,734 ± 0,099 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJava avgt 30 3,352 ± 0,021 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJoda avgt 30 6,453 ± 0,072 ns/op --- .../benchmark/time/RoundingBenchmark.java | 12 +++--- .../org/elasticsearch/common/Rounding.java | 9 ++--- .../elasticsearch/common/time/DateUtils.java | 37 ++++++++++++++++++- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 0e763185e67cb..775c6e0f66813 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -21,6 +21,7 @@ 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; @@ -36,11 +37,14 @@ 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(2) -@Warmup(iterations = 5) -@Measurement(iterations = 5) +@Fork(3) +@Warmup(iterations = 10) +@Measurement(iterations = 10) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @@ -52,7 +56,6 @@ public class RoundingBenchmark { private final long timestamp = 1548879021354L; - /* 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) @@ -143,7 +146,6 @@ public long timeUnitRoundingUtcMonthOfYearJoda() { return timeUnitRoundingUtcMonthOfYearJava.round(timestamp); } - */ private final org.elasticsearch.common.rounding.Rounding timeUnitRoundingUtcYearOfCenturyJoda = org.elasticsearch.common.rounding.Rounding.builder(DateTimeUnit.YEAR_OF_CENTURY).timeZone(DateTimeZone.UTC).build(); diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 66c42b2e46b17..ae612c3191ee0 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -31,6 +31,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.Month; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; @@ -80,11 +81,9 @@ public long roundFloorUtc(long utcMillis) { final LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); return dt.toLocalDate().withDayOfMonth(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); case QUARTER_OF_YEAR: - // TODO 2x slower than joda - // TODO check if this can be done with static milliseconds - final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); - LocalDate localDate = localDateTime.toLocalDate(); - return LocalDateTime.of(localDate.getYear(), localDate.getMonth().firstMonthOfQuarter(), 1, 0, 0) + int year = DateUtils.getYear(utcMillis); + int month = DateUtils.getMonthOfYear(utcMillis, year); + return LocalDateTime.of(year, Month.of(month).firstMonthOfQuarter(), 1, 0, 0) .toInstant(ZoneOffset.UTC).toEpochMilli(); case YEAR_OF_CENTURY: return DateUtils.getFirstDayOfYearMillis(utcMillis); diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index a859db8a876d9..bdb781242330c 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -112,7 +112,7 @@ private static long calculateFirstDayOfYearMillis(int year) { return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day } - private static int getYear(long instant) { + public static int getYear(long instant) { // Get an initial estimate of the year, and the millis value that // represents the start of that year. Then verify estimate and fix if // necessary. @@ -158,6 +158,41 @@ private static long getAverageMillisPerYearDividedByTwo() { return MILLIS_PER_YEAR / 2; } + public static int getMonthOfYear(long millis, int year) { + // Perform a binary search to get the month. To make it go even faster, + // compare using ints instead of longs. The number of milliseconds per + // year exceeds the limit of a 32-bit int's capacity, so divide by + // 1024. No precision is lost (except time of day) since the number of + // milliseconds per day contains 1024 as a factor. After the division, + // the instant isn't measured in milliseconds, but in units of + // (128/125)seconds. + + int i = (int)((millis - getYearMillis(year)) >> 10); + + // There are 86400000 milliseconds per day, but divided by 1024 is + // 84375. There are 84375 (128/125)seconds per day. + + return + (isLeapYear(year)) + ? ((i < 182 * 84375) + ? ((i < 91 * 84375) + ? ((i < 31 * 84375) ? 1 : (i < 60 * 84375) ? 2 : 3) + : ((i < 121 * 84375) ? 4 : (i < 152 * 84375) ? 5 : 6)) + : ((i < 274 * 84375) + ? ((i < 213 * 84375) ? 7 : (i < 244 * 84375) ? 8 : 9) + : ((i < 305 * 84375) ? 10 : (i < 335 * 84375) ? 11 : 12))) + : ((i < 181 * 84375) + ? ((i < 90 * 84375) + ? ((i < 31 * 84375) ? 1 : (i < 59 * 84375) ? 2 : 3) + : ((i < 120 * 84375) ? 4 : (i < 151 * 84375) ? 5 : 6)) + : ((i < 273 * 84375) + ? ((i < 212 * 84375) ? 7 : (i < 243 * 84375) ? 8 : 9) + : ((i < 304 * 84375) ? 10 : (i < 334 * 84375) ? 11 : 12))); + } + + private static long getYearMillis(int year) { + return calculateFirstDayOfYearMillis(year); + } public static long getFirstDayOfYearMillis(long utcMillis) { int year = getYear(utcMillis); From 6dd0d6e33801aaec4a875922b8b7939a3fe7f4b1 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sun, 3 Feb 2019 00:43:00 +0100 Subject: [PATCH 07/18] Fix month of year benchmark, where joda is still much faster due to object creation in java time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJava avgt 30 18,864 ± 0,636 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJoda avgt 30 5,597 ± 0,117 ns/op --- .../org/elasticsearch/benchmark/time/RoundingBenchmark.java | 2 +- server/src/main/java/org/elasticsearch/common/Rounding.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 775c6e0f66813..912d5265c44c6 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -143,7 +143,7 @@ public long timeUnitRoundingUtcMonthOfYearJava() { @Benchmark public long timeUnitRoundingUtcMonthOfYearJoda() { - return timeUnitRoundingUtcMonthOfYearJava.round(timestamp); + return timeUnitRoundingUtcMonthOfYearJoda.round(timestamp); } diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index ae612c3191ee0..772a93792dee6 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -78,8 +78,10 @@ public enum DateTimeUnit { public long roundFloorUtc(long utcMillis) { switch (this) { case MONTH_OF_YEAR: - final LocalDateTime dt = LocalDateTime.ofInstant(Instant.ofEpochMilli(utcMillis), ZoneOffset.UTC); - return dt.toLocalDate().withDayOfMonth(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + int currentYear = DateUtils.getYear(utcMillis); + int currentMonth = DateUtils.getMonthOfYear(utcMillis, currentYear); + LocalDate localDate = LocalDate.of(currentYear, currentMonth, 1); + return LocalDateTime.of(localDate, LocalTime.MIDNIGHT).toInstant(ZoneOffset.UTC).toEpochMilli(); case QUARTER_OF_YEAR: int year = DateUtils.getYear(utcMillis); int month = DateUtils.getMonthOfYear(utcMillis, year); From 59db0aef6b5af6185762f5b7b1af09b7b4c4c094 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sun, 3 Feb 2019 00:56:50 +0100 Subject: [PATCH 08/18] speed up month of year MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit now java is same or faster everywhere Benchmark Mode Cnt Score Error Units RoundingBenchmark.timeIntervalRoundingJava avgt 10 228,960 ± 4,507 ns/op RoundingBenchmark.timeIntervalRoundingJoda avgt 10 324,240 ± 1,546 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJava avgt 10 87,284 ± 0,857 ns/op RoundingBenchmark.timeRoundingDateTimeUnitDayOfMonthJoda avgt 10 358,485 ± 4,671 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJava avgt 10 137,124 ± 3,098 ns/op RoundingBenchmark.timeRoundingDateTimeUnitJoda avgt 10 327,330 ± 2,473 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJava avgt 10 11,772 ± 0,088 ns/op RoundingBenchmark.timeUnitRoundingUtcDayOfMonthJoda avgt 10 11,907 ± 0,132 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJava avgt 10 3,538 ± 0,037 ns/op RoundingBenchmark.timeUnitRoundingUtcMonthOfYearJoda avgt 10 5,567 ± 0,145 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJava avgt 10 4,872 ± 0,126 ns/op RoundingBenchmark.timeUnitRoundingUtcQuarterOfYearJoda avgt 10 30,843 ± 2,215 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJava avgt 10 3,382 ± 0,071 ns/op RoundingBenchmark.timeUnitRoundingUtcYearOfCenturyJoda avgt 10 6,466 ± 0,065 ns/op --- .../org/elasticsearch/common/Rounding.java | 6 +-- .../elasticsearch/common/time/DateUtils.java | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 772a93792dee6..072d8b10ea2a9 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -80,13 +80,11 @@ public long roundFloorUtc(long utcMillis) { case MONTH_OF_YEAR: int currentYear = DateUtils.getYear(utcMillis); int currentMonth = DateUtils.getMonthOfYear(utcMillis, currentYear); - LocalDate localDate = LocalDate.of(currentYear, currentMonth, 1); - return LocalDateTime.of(localDate, LocalTime.MIDNIGHT).toInstant(ZoneOffset.UTC).toEpochMilli(); + return DateUtils.of(currentYear, currentMonth, 1); case QUARTER_OF_YEAR: int year = DateUtils.getYear(utcMillis); int month = DateUtils.getMonthOfYear(utcMillis, year); - return LocalDateTime.of(year, Month.of(month).firstMonthOfQuarter(), 1, 0, 0) - .toInstant(ZoneOffset.UTC).toEpochMilli(); + return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue(), 1); case YEAR_OF_CENTURY: return DateUtils.getFirstDayOfYearMillis(utcMillis); case WEEK_OF_WEEKYEAR: diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index bdb781242330c..e260b06c822c0 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -198,4 +198,48 @@ public static long getFirstDayOfYearMillis(long utcMillis) { int year = getYear(utcMillis); return calculateFirstDayOfYearMillis(year); } + + public static long of(int year, int month, int dayOfMonth) { + long millis = getYearMillis(year); + millis += getTotalMillisByYearMonth(year, month); + return millis + (dayOfMonth - 1) * (long) MILLIS_PER_DAY; + } + + private static long getTotalMillisByYearMonth(int year, int month) { + if (isLeapYear(year)) { + return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; + } else { + return MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; + } + } + + private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final int[] MIN_DAYS_PER_MONTH_ARRAY = { + 31,28,31,30,31,30,31,31,30,31,30,31 + }; + private static final int[] MAX_DAYS_PER_MONTH_ARRAY = { + 31,29,31,30,31,30,31,31,30,31,30,31 + }; + + static { + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + + long minSum = 0; + long maxSum = 0; + for (int i = 0; i < 11; i++) { + long millis = MIN_DAYS_PER_MONTH_ARRAY[i] + * (long)MILLIS_PER_DAY; + minSum += millis; + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum; + + millis = MAX_DAYS_PER_MONTH_ARRAY[i] + * (long)MILLIS_PER_DAY; + maxSum += millis; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; + } + } + + } From e6f72551076b67e23ae0a732e7044816ae1813af Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sun, 3 Feb 2019 12:24:49 +0100 Subject: [PATCH 09/18] minor refactoring --- .../org/elasticsearch/common/Rounding.java | 88 +++++++++++-------- .../elasticsearch/common/time/DateUtils.java | 27 +++++- 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 072d8b10ea2a9..896c8bbcf3a58 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -56,52 +56,64 @@ public abstract class Rounding implements Writeable { 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.roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; + } + }, + YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) { + long roundFloor(long utcMillis) { + return DateUtils.getFirstDayOfYearMillis(utcMillis); + } + }, + QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) { + long roundFloor(long utcMillis) { + int year = DateUtils.getYear(utcMillis); + int month = DateUtils.getMonthOfYear(utcMillis, year); + return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue(), 1); + } + }, + MONTH_OF_YEAR((byte) 4, ChronoField.MONTH_OF_YEAR) { + long roundFloor(long utcMillis) { + int year = DateUtils.getYear(utcMillis); + int month = DateUtils.getMonthOfYear(utcMillis, year); + return DateUtils.of(year, month, 1); + } + }, + 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; - private final long unitMillis; DateTimeUnit(byte id, TemporalField field) { this.id = id; this.field = field; - this.unitMillis = field.getBaseUnit().getDuration().toMillis(); } - public long roundFloorUtc(long utcMillis) { - switch (this) { - case MONTH_OF_YEAR: - int currentYear = DateUtils.getYear(utcMillis); - int currentMonth = DateUtils.getMonthOfYear(utcMillis, currentYear); - return DateUtils.of(currentYear, currentMonth, 1); - case QUARTER_OF_YEAR: - int year = DateUtils.getYear(utcMillis); - int month = DateUtils.getMonthOfYear(utcMillis, year); - return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue(), 1); - case YEAR_OF_CENTURY: - return DateUtils.getFirstDayOfYearMillis(utcMillis); - case WEEK_OF_WEEKYEAR: - return staticRoundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; - default: - return staticRoundFloor(utcMillis, unitMillis); - } - } - - private long staticRoundFloor(long utcMillis, long unitMillis) { - if (utcMillis >= 0) { - return utcMillis - utcMillis % unitMillis; - } else { - utcMillis += 1; - return utcMillis - utcMillis % unitMillis - unitMillis; - } - } + abstract long roundFloor(long utcMillis); public byte getId() { return id; @@ -267,7 +279,7 @@ public long round(long utcMillis) { // 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.roundFloorUtc(utcMillis); + return unit.roundFloor(utcMillis); } Instant instant = Instant.ofEpochMilli(utcMillis); diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index e260b06c822c0..1742574e3222f 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -78,10 +78,35 @@ public static ZoneId of(String zoneId) { return ZoneId.of(zoneId).normalized(); } + /* + * begin of code that is partially copied from the joda time implementation in order to make calculations about utc rounding much + * faster than using java-time and assigning all those objects + * + */ + private static final int DAYS_0000_TO_1970 = 719527; private static final int MILLIS_PER_DAY = 86_400_000; private static final long MILLIS_PER_YEAR = 31556952000L; + /** + * 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, long unitMillis) { + if (utcMillis >= 0) { + return utcMillis - utcMillis % unitMillis; + } else { + utcMillis += 1; + return utcMillis - utcMillis % unitMillis - unitMillis; + } + } + private static boolean isLeapYear(int year) { return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); } @@ -240,6 +265,4 @@ private static long getTotalMillisByYearMonth(int year, int month) { MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; } } - - } From 4fca03623f77f7bf33e7ecc1caddda7ddb271948 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 09:24:12 +0100 Subject: [PATCH 10/18] refactorings, more docs --- .../org/elasticsearch/common/Rounding.java | 15 ++-- .../elasticsearch/common/time/DateUtils.java | 83 +++++++++++++------ 2 files changed, 67 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 896c8bbcf3a58..75f070067e99a 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -68,16 +68,12 @@ long roundFloor(long utcMillis) { }, QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) { long roundFloor(long utcMillis) { - int year = DateUtils.getYear(utcMillis); - int month = DateUtils.getMonthOfYear(utcMillis, year); - return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue(), 1); + return DateUtils.roundQuarterOfYear(utcMillis); } }, MONTH_OF_YEAR((byte) 4, ChronoField.MONTH_OF_YEAR) { long roundFloor(long utcMillis) { - int year = DateUtils.getYear(utcMillis); - int month = DateUtils.getMonthOfYear(utcMillis, year); - return DateUtils.of(year, month, 1); + return DateUtils.roundMonthOfYear(utcMillis); } }, DAY_OF_MONTH((byte) 5, ChronoField.DAY_OF_MONTH) { @@ -113,6 +109,13 @@ long roundFloor(long utcMillis) { 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 possiblee 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() { diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index 1742574e3222f..cdffe14c4a058 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.logging.DeprecationLogger; import org.joda.time.DateTimeZone; +import java.time.Month; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Collections; @@ -107,8 +108,48 @@ public static long roundFloor(long utcMillis, long unitMillis) { } } - private static boolean isLeapYear(int year) { - return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + /** + * 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(long utcMillis) { + int year = DateUtils.getYear(utcMillis); + int month = DateUtils.getMonthOfYear(utcMillis, year); + return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue()); + } + + /** + * 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(long utcMillis) { + int year = DateUtils.getYear(utcMillis); + int month = DateUtils.getMonthOfYear(utcMillis, year); + return DateUtils.of(year, month); + } + + /** + * Round down to the beginning of 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 getFirstDayOfYearMillis(long utcMillis) { + int year = getYear(utcMillis); + return calculateFirstDayOfYearMillis(year); + } + + /** + * 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(int year, int month) { + long millis = getYearMillis(year); + millis += getTotalMillisByYearMonth(year, month); + return millis; } /** @@ -117,6 +158,7 @@ private static boolean isLeapYear(int year) { * @param year the year * @return the milliseconds since the epoch of the first of january at midnight of the specified year */ + // see org.joda.time.chrono.GregorianChronology private static long calculateFirstDayOfYearMillis(int year) { // Initial value is just temporary. int leapYears = year / 100; @@ -137,14 +179,22 @@ private static long calculateFirstDayOfYearMillis(int year) { return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day } - public static int getYear(long instant) { + private static boolean isLeapYear(int year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + private static final long AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO = MILLIS_PER_YEAR / 2; + private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2; + + // see org.joda.time.chrono.BasicChronology + private static int getYear(long instant) { // Get an initial estimate of the year, and the millis value that // represents the start of that year. Then verify estimate and fix if // necessary. // Initial estimate uses values divided by two to avoid overflow. - long unitMillis = getAverageMillisPerYearDividedByTwo(); - long i2 = (instant >> 1) + getApproxMillisAtEpochDividedByTwo(); + long unitMillis = AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO; + long i2 = (instant >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO; if (i2 < 0) { i2 = i2 - unitMillis + 1; } @@ -175,15 +225,8 @@ public static int getYear(long instant) { return year; } - private static long getApproxMillisAtEpochDividedByTwo() { - return (1970L * MILLIS_PER_YEAR) / 2; - } - - private static long getAverageMillisPerYearDividedByTwo() { - return MILLIS_PER_YEAR / 2; - } - - public static int getMonthOfYear(long millis, int year) { + // see org.joda.time.chrono.BasicGJChronology + private static int getMonthOfYear(long millis, int year) { // Perform a binary search to get the month. To make it go even faster, // compare using ints instead of longs. The number of milliseconds per // year exceeds the limit of a 32-bit int's capacity, so divide by @@ -219,17 +262,7 @@ private static long getYearMillis(int year) { return calculateFirstDayOfYearMillis(year); } - public static long getFirstDayOfYearMillis(long utcMillis) { - int year = getYear(utcMillis); - return calculateFirstDayOfYearMillis(year); - } - - public static long of(int year, int month, int dayOfMonth) { - long millis = getYearMillis(year); - millis += getTotalMillisByYearMonth(year, month); - return millis + (dayOfMonth - 1) * (long) MILLIS_PER_DAY; - } - + // see org.joda.time.chrono.BasicGJChronology private static long getTotalMillisByYearMonth(int year, int month) { if (isLeapYear(year)) { return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; From 35bd6401b4e63a3378ba3bd2b961800dc6b22bba Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 09:48:29 +0100 Subject: [PATCH 11/18] remove unused import --- server/src/main/java/org/elasticsearch/common/Rounding.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 75f070067e99a..e0b3316854b93 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -31,7 +31,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.Month; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; From 72e315d3649f3d4f062acea1e3fe5c4303b91018 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 10:43:28 +0100 Subject: [PATCH 12/18] fix typo --- server/src/main/java/org/elasticsearch/common/Rounding.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index e0b3316854b93..5a8c6bc4a23cf 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -110,7 +110,7 @@ long roundFloor(long utcMillis) { /** * 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 possiblee and not try to convert dates to java-time objects if possible + * 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 From 368dd698a01afb8a8b88f16886d8a89d675c4ab8 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 11:57:06 +0100 Subject: [PATCH 13/18] review comment, calculate quarter of year without using month --- .../benchmark/time/RoundingBenchmark.java | 2 +- .../elasticsearch/common/time/DateUtils.java | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java index 912d5265c44c6..0928a7565607b 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/time/RoundingBenchmark.java @@ -54,7 +54,7 @@ public class RoundingBenchmark { private final ZoneId zoneId = ZoneId.of("Europe/Amsterdam"); private final DateTimeZone timeZone = DateUtils.zoneIdToDateTimeZone(zoneId); - private final long timestamp = 1548879021354L; + 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(); diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index 2c238cdaf4d3f..bcd31ff707908 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -24,7 +24,6 @@ import org.joda.time.DateTimeZone; import java.time.Instant; -import java.time.Month; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Collections; @@ -80,7 +79,6 @@ public static ZoneId of(String zoneId) { return ZoneId.of(zoneId).normalized(); } - ======= private static final Instant MAX_NANOSECOND_INSTANT = Instant.parse("2262-04-11T23:47:16.854775807Z"); /** @@ -142,16 +140,6 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) { return nanoSecondsSinceEpoch / 1_000_000; } - /* - * begin of code that is partially copied from the joda time implementation in order to make calculations about utc rounding much - * faster than using java-time and assigning all those objects - * - */ - - private static final int DAYS_0000_TO_1970 = 719527; - private static final int MILLIS_PER_DAY = 86_400_000; - private static final long MILLIS_PER_YEAR = 31556952000L; - /** * Rounds the given utc milliseconds sicne the epoch down to the next unit millis * @@ -179,7 +167,8 @@ public static long roundFloor(long utcMillis, long unitMillis) { public static long roundQuarterOfYear(long utcMillis) { int year = DateUtils.getYear(utcMillis); int month = DateUtils.getMonthOfYear(utcMillis, year); - return DateUtils.of(year, Month.of(month).firstMonthOfQuarter().getValue()); + int firstMonthOfQuarter = ((month + 1) / 3) * 3; + return DateUtils.of(year, firstMonthOfQuarter); } /** @@ -215,6 +204,15 @@ private static long of(int year, int month) { return millis; } + /* + * begin of code that is partially copied from the joda time implementation in order to make calculations about utc rounding much + * faster than using java-time and assigning all those objects + * + */ + private static final int DAYS_0000_TO_1970 = 719527; + private static final int MILLIS_PER_DAY = 86_400_000; + private static final long MILLIS_PER_YEAR = 31556952000L; + /** * calculates the first day of a year in milliseconds since the epoch (assuming UTC) * From b8c8b57723a252a09d166fcdd482e986d1cda1ab Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 12:00:09 +0100 Subject: [PATCH 14/18] work on review comments --- .../org/elasticsearch/common/Rounding.java | 2 +- .../elasticsearch/common/time/DateUtils.java | 26 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 5a8c6bc4a23cf..241339419e0c6 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -62,7 +62,7 @@ long roundFloor(long utcMillis) { }, YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) { long roundFloor(long utcMillis) { - return DateUtils.getFirstDayOfYearMillis(utcMillis); + return DateUtils.roundYear(utcMillis); } }, QUARTER_OF_YEAR((byte) 3, IsoFields.QUARTER_OF_YEAR) { diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index bcd31ff707908..d7bcbf0faaec4 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -187,9 +187,9 @@ public static long roundMonthOfYear(long utcMillis) { * @param utcMillis the milliseconds since the epoch * @return The milliseconds since the epoch rounded down to the beginning of the year */ - public static long getFirstDayOfYearMillis(long utcMillis) { + public static long roundYear(long utcMillis) { int year = getYear(utcMillis); - return calculateFirstDayOfYearMillis(year); + return utcMillisAtStartOfYear(year); } /** @@ -219,8 +219,8 @@ private static long of(int year, int month) { * @param year the year * @return the milliseconds since the epoch of the first of january at midnight of the specified year */ - // see org.joda.time.chrono.GregorianChronology - private static long calculateFirstDayOfYearMillis(int year) { + // see org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis + private static long utcMillisAtStartOfYear(int year) { // Initial value is just temporary. int leapYears = year / 100; if (year < 0) { @@ -248,21 +248,21 @@ private static boolean isLeapYear(int year) { private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2; // see org.joda.time.chrono.BasicChronology - private static int getYear(long instant) { + private static int getYear(long utcMillis) { // Get an initial estimate of the year, and the millis value that // represents the start of that year. Then verify estimate and fix if // necessary. // Initial estimate uses values divided by two to avoid overflow. long unitMillis = AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO; - long i2 = (instant >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO; + long i2 = (utcMillis >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO; if (i2 < 0) { i2 = i2 - unitMillis + 1; } int year = (int) (i2 / unitMillis); - long yearStart = calculateFirstDayOfYearMillis(year); - long diff = instant - yearStart; + long yearStart = utcMillisAtStartOfYear(year); + long diff = utcMillis - yearStart; if (diff < 0) { year--; @@ -277,7 +277,7 @@ private static int getYear(long instant) { yearStart += oneYear; - if (yearStart <= instant) { + if (yearStart <= utcMillis) { // Didn't go too far, so actually add one year. year++; } @@ -287,7 +287,7 @@ private static int getYear(long instant) { } // see org.joda.time.chrono.BasicGJChronology - private static int getMonthOfYear(long millis, int year) { + private static int getMonthOfYear(long utcMillis, int year) { // Perform a binary search to get the month. To make it go even faster, // compare using ints instead of longs. The number of milliseconds per // year exceeds the limit of a 32-bit int's capacity, so divide by @@ -296,7 +296,7 @@ private static int getMonthOfYear(long millis, int year) { // the instant isn't measured in milliseconds, but in units of // (128/125)seconds. - int i = (int)((millis - getYearMillis(year)) >> 10); + int i = (int)((utcMillis - utcMillisAtStartOfYear(year)) >> 10); // There are 86400000 milliseconds per day, but divided by 1024 is // 84375. There are 84375 (128/125)seconds per day. @@ -319,10 +319,6 @@ private static int getMonthOfYear(long millis, int year) { : ((i < 304 * 84375) ? 10 : (i < 334 * 84375) ? 11 : 12))); } - private static long getYearMillis(int year) { - return calculateFirstDayOfYearMillis(year); - } - // see org.joda.time.chrono.BasicGJChronology private static long getTotalMillisByYearMonth(int year, int month) { if (isLeapYear(year)) { From 00308c8b2537e4332ce815f2d358d4ed2c69b00f Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 14:57:43 +0100 Subject: [PATCH 15/18] incorporate review comments, add tests --- .../org/elasticsearch/common/Rounding.java | 2 +- .../elasticsearch/common/time/DateUtils.java | 35 +++++--- .../common/time/DateUtilsTests.java | 85 +++++++++++++++++++ 3 files changed, 108 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/Rounding.java b/server/src/main/java/org/elasticsearch/common/Rounding.java index 241339419e0c6..3558b16aac1c8 100644 --- a/server/src/main/java/org/elasticsearch/common/Rounding.java +++ b/server/src/main/java/org/elasticsearch/common/Rounding.java @@ -57,7 +57,7 @@ public abstract class Rounding implements Writeable { public enum DateTimeUnit { WEEK_OF_WEEKYEAR((byte) 1, IsoFields.WEEK_OF_WEEK_BASED_YEAR) { long roundFloor(long utcMillis) { - return DateUtils.roundFloor(utcMillis + 3 * 86400 * 1000L, 604800000) - 3 * 86400 * 1000L; + return DateUtils.roundWeekOfWeekYear(utcMillis); } }, YEAR_OF_CENTURY((byte) 2, ChronoField.YEAR_OF_ERA) { diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index d7bcbf0faaec4..1f266eb450d7d 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -150,7 +150,7 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) { * @param unitMillis the unit to round to * @return the rounded milliseconds since the epoch */ - public static long roundFloor(long utcMillis, long unitMillis) { + public static long roundFloor(long utcMillis, final long unitMillis) { if (utcMillis >= 0) { return utcMillis - utcMillis % unitMillis; } else { @@ -164,10 +164,10 @@ public static long roundFloor(long utcMillis, long unitMillis) { * @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(long utcMillis) { + public static long roundQuarterOfYear(final long utcMillis) { int year = DateUtils.getYear(utcMillis); int month = DateUtils.getMonthOfYear(utcMillis, year); - int firstMonthOfQuarter = ((month + 1) / 3) * 3; + int firstMonthOfQuarter = (((month-1) / 3) * 3) + 1; return DateUtils.of(year, firstMonthOfQuarter); } @@ -176,30 +176,39 @@ public static long roundQuarterOfYear(long utcMillis) { * @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(long utcMillis) { + public static long roundMonthOfYear(final long utcMillis) { int year = DateUtils.getYear(utcMillis); int month = DateUtils.getMonthOfYear(utcMillis, year); return DateUtils.of(year, month); } /** - * Round down to the beginning of the beginning of the year of the specified time + * 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(long utcMillis) { + 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(int year, int month) { - long millis = getYearMillis(year); + private static long of(final int year, final int month) { + long millis = utcMillisAtStartOfYear(year); millis += getTotalMillisByYearMonth(year, month); return millis; } @@ -220,7 +229,7 @@ private static long of(int year, int month) { * @return the milliseconds since the epoch of the first of january at midnight of the specified year */ // see org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis - private static long utcMillisAtStartOfYear(int year) { + private static long utcMillisAtStartOfYear(final int year) { // Initial value is just temporary. int leapYears = year / 100; if (year < 0) { @@ -240,7 +249,7 @@ private static long utcMillisAtStartOfYear(int year) { return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day } - private static boolean isLeapYear(int year) { + private static boolean isLeapYear(final int year) { return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); } @@ -248,7 +257,7 @@ private static boolean isLeapYear(int year) { private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2; // see org.joda.time.chrono.BasicChronology - private static int getYear(long utcMillis) { + private static int getYear(final long utcMillis) { // Get an initial estimate of the year, and the millis value that // represents the start of that year. Then verify estimate and fix if // necessary. @@ -287,7 +296,7 @@ private static int getYear(long utcMillis) { } // see org.joda.time.chrono.BasicGJChronology - private static int getMonthOfYear(long utcMillis, int year) { + private static int getMonthOfYear(final long utcMillis, final int year) { // Perform a binary search to get the month. To make it go even faster, // compare using ints instead of longs. The number of milliseconds per // year exceeds the limit of a 32-bit int's capacity, so divide by @@ -320,7 +329,7 @@ private static int getMonthOfYear(long utcMillis, int year) { } // see org.joda.time.chrono.BasicGJChronology - private static long getTotalMillisByYearMonth(int year, int month) { + private static long getTotalMillisByYearMonth(final int year, final int month) { if (isLeapYear(year)) { return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; } else { diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java index e35d8df1b9c06..faec1ffce649c 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java @@ -23,8 +23,14 @@ import org.joda.time.DateTimeZone; import java.time.Instant; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.time.YearMonth; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -114,4 +120,83 @@ private Instant createRandomInstant() { long nanos = randomLongBetween(0, 999_999_999L); return Instant.ofEpochSecond(seconds, nanos); } + + public void testRoundFloor() { + assertThat(DateUtils.roundFloor(0, randomLongBetween(0, Long.MAX_VALUE)), is(0L)); + + ChronoField randomChronoField = + randomFrom(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE); + long unitMillis = randomChronoField.getBaseUnit().getDuration().toMillis(); + + int year = randomIntBetween(-3000, 3000); + int month = randomIntBetween(1, 12); + int day = randomIntBetween(1, YearMonth.of(year, month).lengthOfMonth()); + int hour = randomIntBetween(1, 23); + int minute = randomIntBetween(1, 59); + int second = randomIntBetween(1, 59); + int nanos = randomIntBetween(1, 999_999_999); + + ZonedDateTime randomDate = ZonedDateTime.of(year, month, day, hour, minute, second, nanos, ZoneOffset.UTC); + + ZonedDateTime result = randomDate; + switch (randomChronoField) { + case SECOND_OF_MINUTE: + result = result.withNano(0); + break; + case MINUTE_OF_HOUR: + result = result.withNano(0).withSecond(0); + break; + case HOUR_OF_DAY: + result = result.withNano(0).withSecond(0).withMinute(0); + break; + case DAY_OF_MONTH: + result = result.withNano(0).withSecond(0).withMinute(0).withHour(0); + break; + } + + long rounded = DateUtils.roundFloor(randomDate.toInstant().toEpochMilli(), unitMillis); + assertThat(rounded, is(result.toInstant().toEpochMilli())); + } + + public void testRoundQuarterOfYear() { + assertThat(DateUtils.roundQuarterOfYear(0), is(0L)); + long lastQuarter1969 = ZonedDateTime.of(1969, 10, 1, 0, 0, 0, 0, ZoneOffset.UTC) + .toInstant().toEpochMilli(); + assertThat(DateUtils.roundQuarterOfYear(-1), is(lastQuarter1969)); + + int year = randomIntBetween(1970, 2040); + int month = randomIntBetween(1, 12); + int day = randomIntBetween(1, YearMonth.of(year, month).lengthOfMonth()); + + ZonedDateTime randomZonedDateTime = ZonedDateTime.of(year, month, day, + randomIntBetween(0, 23), randomIntBetween(0, 59), randomIntBetween(0, 59), 999_999_999, ZoneOffset.UTC); + long quarterInMillis = Year.of(randomZonedDateTime.getYear()).atMonth(Month.of(month).firstMonthOfQuarter()).atDay(1) + .atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); + long result = DateUtils.roundQuarterOfYear(randomZonedDateTime.toInstant().toEpochMilli()); + assertThat(result, is(quarterInMillis)); + } + + public void testRoundMonthOfYear() { + assertThat(DateUtils.roundMonthOfYear(0), is(0L)); + assertThat(DateUtils.roundMonthOfYear(1), is(0L)); + long dec1969 = LocalDate.of(1969, 12, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + assertThat(DateUtils.roundMonthOfYear(-1), is(dec1969)); + } + + public void testRoundYear() { + assertThat(DateUtils.roundYear(0), is(0L)); + assertThat(DateUtils.roundYear(1), is(0L)); + long startOf1969 = ZonedDateTime.of(1969, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC) + .toInstant().toEpochMilli(); + assertThat(DateUtils.roundYear(-1), is(startOf1969)); + long endOf1970 = ZonedDateTime.of(1970, 12, 31, 23, 59, 59, 999_999_999, ZoneOffset.UTC) + .toInstant().toEpochMilli(); + assertThat(DateUtils.roundYear(endOf1970), is(0L)); + // test with some leapyear + long endOf1996 = ZonedDateTime.of(1996, 12, 31, 23, 59, 59, 999_999_999, ZoneOffset.UTC) + .toInstant().toEpochMilli(); + long startOf1996 = Year.of(1996).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); + assertThat(DateUtils.roundYear(endOf1996), is(startOf1996)); + } + } From f6acf7ca5a9773ed61a9d18e9a360695f7a757e8 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Mon, 4 Feb 2019 15:12:34 +0100 Subject: [PATCH 16/18] split joda time code into separate class --- .../elasticsearch/common/time/DateUtils.java | 165 +--------------- .../common/time/DateUtilsRounding.java | 181 ++++++++++++++++++ 2 files changed, 190 insertions(+), 156 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index 1f266eb450d7d..46a45f80aedff 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -31,6 +31,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) { @@ -165,8 +170,8 @@ public static long roundFloor(long utcMillis, final long unitMillis) { * @return The milliseconds since the epoch rounded down to the quarter of the year */ public static long roundQuarterOfYear(final long utcMillis) { - int year = DateUtils.getYear(utcMillis); - int month = DateUtils.getMonthOfYear(utcMillis, year); + int year = getYear(utcMillis); + int month = getMonthOfYear(utcMillis, year); int firstMonthOfQuarter = (((month-1) / 3) * 3) + 1; return DateUtils.of(year, firstMonthOfQuarter); } @@ -177,8 +182,8 @@ public static long roundQuarterOfYear(final long utcMillis) { * @return The milliseconds since the epoch rounded down to the month of the year */ public static long roundMonthOfYear(final long utcMillis) { - int year = DateUtils.getYear(utcMillis); - int month = DateUtils.getMonthOfYear(utcMillis, year); + int year = getYear(utcMillis); + int month = getMonthOfYear(utcMillis, year); return DateUtils.of(year, month); } @@ -212,156 +217,4 @@ private static long of(final int year, final int month) { millis += getTotalMillisByYearMonth(year, month); return millis; } - - /* - * begin of code that is partially copied from the joda time implementation in order to make calculations about utc rounding much - * faster than using java-time and assigning all those objects - * - */ - private static final int DAYS_0000_TO_1970 = 719527; - private static final int MILLIS_PER_DAY = 86_400_000; - private static final long MILLIS_PER_YEAR = 31556952000L; - - /** - * calculates the first day of a year in milliseconds since the epoch (assuming UTC) - * - * @param year the year - * @return the milliseconds since the epoch of the first of january at midnight of the specified year - */ - // see org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis - private static long utcMillisAtStartOfYear(final int year) { - // Initial value is just temporary. - int leapYears = year / 100; - if (year < 0) { - // Add 3 before shifting right since /4 and >>2 behave differently - // on negative numbers. When the expression is written as - // (year / 4) - (year / 100) + (year / 400), - // it works for both positive and negative values, except this optimization - // eliminates two divisions. - leapYears = ((year + 3) >> 2) - leapYears + ((leapYears + 3) >> 2) - 1; - } else { - leapYears = (year >> 2) - leapYears + (leapYears >> 2); - if (isLeapYear(year)) { - leapYears--; - } - } - - return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day - } - - private static boolean isLeapYear(final int year) { - return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); - } - - private static final long AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO = MILLIS_PER_YEAR / 2; - private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2; - - // see org.joda.time.chrono.BasicChronology - private static int getYear(final long utcMillis) { - // Get an initial estimate of the year, and the millis value that - // represents the start of that year. Then verify estimate and fix if - // necessary. - - // Initial estimate uses values divided by two to avoid overflow. - long unitMillis = AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO; - long i2 = (utcMillis >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO; - if (i2 < 0) { - i2 = i2 - unitMillis + 1; - } - int year = (int) (i2 / unitMillis); - - long yearStart = utcMillisAtStartOfYear(year); - long diff = utcMillis - yearStart; - - if (diff < 0) { - year--; - } else if (diff >= MILLIS_PER_DAY * 365L) { - // One year may need to be added to fix estimate. - long oneYear; - if (isLeapYear(year)) { - oneYear = MILLIS_PER_DAY * 366L; - } else { - oneYear = MILLIS_PER_DAY * 365L; - } - - yearStart += oneYear; - - if (yearStart <= utcMillis) { - // Didn't go too far, so actually add one year. - year++; - } - } - - return year; - } - - // see org.joda.time.chrono.BasicGJChronology - private static int getMonthOfYear(final long utcMillis, final int year) { - // Perform a binary search to get the month. To make it go even faster, - // compare using ints instead of longs. The number of milliseconds per - // year exceeds the limit of a 32-bit int's capacity, so divide by - // 1024. No precision is lost (except time of day) since the number of - // milliseconds per day contains 1024 as a factor. After the division, - // the instant isn't measured in milliseconds, but in units of - // (128/125)seconds. - - int i = (int)((utcMillis - utcMillisAtStartOfYear(year)) >> 10); - - // There are 86400000 milliseconds per day, but divided by 1024 is - // 84375. There are 84375 (128/125)seconds per day. - - return - (isLeapYear(year)) - ? ((i < 182 * 84375) - ? ((i < 91 * 84375) - ? ((i < 31 * 84375) ? 1 : (i < 60 * 84375) ? 2 : 3) - : ((i < 121 * 84375) ? 4 : (i < 152 * 84375) ? 5 : 6)) - : ((i < 274 * 84375) - ? ((i < 213 * 84375) ? 7 : (i < 244 * 84375) ? 8 : 9) - : ((i < 305 * 84375) ? 10 : (i < 335 * 84375) ? 11 : 12))) - : ((i < 181 * 84375) - ? ((i < 90 * 84375) - ? ((i < 31 * 84375) ? 1 : (i < 59 * 84375) ? 2 : 3) - : ((i < 120 * 84375) ? 4 : (i < 151 * 84375) ? 5 : 6)) - : ((i < 273 * 84375) - ? ((i < 212 * 84375) ? 7 : (i < 243 * 84375) ? 8 : 9) - : ((i < 304 * 84375) ? 10 : (i < 334 * 84375) ? 11 : 12))); - } - - // see org.joda.time.chrono.BasicGJChronology - private static long getTotalMillisByYearMonth(final int year, final int month) { - if (isLeapYear(year)) { - return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; - } else { - return MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; - } - } - - private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY; - private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY; - private static final int[] MIN_DAYS_PER_MONTH_ARRAY = { - 31,28,31,30,31,30,31,31,30,31,30,31 - }; - private static final int[] MAX_DAYS_PER_MONTH_ARRAY = { - 31,29,31,30,31,30,31,31,30,31,30,31 - }; - - static { - MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; - MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; - - long minSum = 0; - long maxSum = 0; - for (int i = 0; i < 11; i++) { - long millis = MIN_DAYS_PER_MONTH_ARRAY[i] - * (long) MILLIS_PER_DAY; - minSum += millis; - MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum; - - millis = MAX_DAYS_PER_MONTH_ARRAY[i] - * (long) MILLIS_PER_DAY; - maxSum += millis; - MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; - } - } } diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java new file mode 100644 index 0000000000000..eed91f7343aef --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java @@ -0,0 +1,181 @@ +/* + * Copyright 2001-2014 Stephen Colebourne + * + * Licensed 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.common.time; + +/** + * This class has been copied from different locations within the joda time package, as + * these methods fast when used for rounding, as they do not require conversion to java + * time objects + * + * This code has been copied from jodatime 2.10.1 + * The source can be found at https://github.com/JodaOrg/joda-time/tree/v2.10.1 + * + * See following methods have been copied (along with required helper variables) + * + * - org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis(int year) + * - org.joda.time.chrono.BasicChronology.getYear(int year) + * - org.joda.time.chrono.BasicGJChronology.getMonthOfYear(long utcMillis, int year) + * - org.joda.time.chrono.BasicGJChronology.getTotalMillisByYearMonth(int year, int month) + */ +class DateUtilsRounding { + + private static final int DAYS_0000_TO_1970 = 719527; + private static final int MILLIS_PER_DAY = 86_400_000; + private static final long MILLIS_PER_YEAR = 31556952000L; + + /** + * calculates the first day of a year in milliseconds since the epoch (assuming UTC) + * + * @param year the year + * @return the milliseconds since the epoch of the first of january at midnight of the specified year + */ + // see org.joda.time.chrono.GregorianChronology.calculateFirstDayOfYearMillis + static long utcMillisAtStartOfYear(final int year) { + // Initial value is just temporary. + int leapYears = year / 100; + if (year < 0) { + // Add 3 before shifting right since /4 and >>2 behave differently + // on negative numbers. When the expression is written as + // (year / 4) - (year / 100) + (year / 400), + // it works for both positive and negative values, except this optimization + // eliminates two divisions. + leapYears = ((year + 3) >> 2) - leapYears + ((leapYears + 3) >> 2) - 1; + } else { + leapYears = (year >> 2) - leapYears + (leapYears >> 2); + if (isLeapYear(year)) { + leapYears--; + } + } + + return (year * 365L + (leapYears - DAYS_0000_TO_1970)) * MILLIS_PER_DAY; // millis per day + } + + private static boolean isLeapYear(final int year) { + return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); + } + + private static final long AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO = MILLIS_PER_YEAR / 2; + private static final long APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO = (1970L * MILLIS_PER_YEAR) / 2; + + // see org.joda.time.chrono.BasicChronology + static int getYear(final long utcMillis) { + // Get an initial estimate of the year, and the millis value that + // represents the start of that year. Then verify estimate and fix if + // necessary. + + // Initial estimate uses values divided by two to avoid overflow. + long unitMillis = AVERAGE_MILLIS_PER_YEAR_DIVIDED_BY_TWO; + long i2 = (utcMillis >> 1) + APPROX_MILLIS_AT_EPOCH_DIVIDED_BY_TWO; + if (i2 < 0) { + i2 = i2 - unitMillis + 1; + } + int year = (int) (i2 / unitMillis); + + long yearStart = utcMillisAtStartOfYear(year); + long diff = utcMillis - yearStart; + + if (diff < 0) { + year--; + } else if (diff >= MILLIS_PER_DAY * 365L) { + // One year may need to be added to fix estimate. + long oneYear; + if (isLeapYear(year)) { + oneYear = MILLIS_PER_DAY * 366L; + } else { + oneYear = MILLIS_PER_DAY * 365L; + } + + yearStart += oneYear; + + if (yearStart <= utcMillis) { + // Didn't go too far, so actually add one year. + year++; + } + } + + return year; + } + + // see org.joda.time.chrono.BasicGJChronology + static int getMonthOfYear(final long utcMillis, final int year) { + // Perform a binary search to get the month. To make it go even faster, + // compare using ints instead of longs. The number of milliseconds per + // year exceeds the limit of a 32-bit int's capacity, so divide by + // 1024. No precision is lost (except time of day) since the number of + // milliseconds per day contains 1024 as a factor. After the division, + // the instant isn't measured in milliseconds, but in units of + // (128/125)seconds. + + int i = (int)((utcMillis - utcMillisAtStartOfYear(year)) >> 10); + + // There are 86400000 milliseconds per day, but divided by 1024 is + // 84375. There are 84375 (128/125)seconds per day. + + return + (isLeapYear(year)) + ? ((i < 182 * 84375) + ? ((i < 91 * 84375) + ? ((i < 31 * 84375) ? 1 : (i < 60 * 84375) ? 2 : 3) + : ((i < 121 * 84375) ? 4 : (i < 152 * 84375) ? 5 : 6)) + : ((i < 274 * 84375) + ? ((i < 213 * 84375) ? 7 : (i < 244 * 84375) ? 8 : 9) + : ((i < 305 * 84375) ? 10 : (i < 335 * 84375) ? 11 : 12))) + : ((i < 181 * 84375) + ? ((i < 90 * 84375) + ? ((i < 31 * 84375) ? 1 : (i < 59 * 84375) ? 2 : 3) + : ((i < 120 * 84375) ? 4 : (i < 151 * 84375) ? 5 : 6)) + : ((i < 273 * 84375) + ? ((i < 212 * 84375) ? 7 : (i < 243 * 84375) ? 8 : 9) + : ((i < 304 * 84375) ? 10 : (i < 334 * 84375) ? 11 : 12))); + } + + // see org.joda.time.chrono.BasicGJChronology + static long getTotalMillisByYearMonth(final int year, final int month) { + if (isLeapYear(year)) { + return MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; + } else { + return MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; + } + } + + private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final int[] MIN_DAYS_PER_MONTH_ARRAY = { + 31,28,31,30,31,30,31,31,30,31,30,31 + }; + private static final int[] MAX_DAYS_PER_MONTH_ARRAY = { + 31,29,31,30,31,30,31,31,30,31,30,31 + }; + + static { + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + + long minSum = 0; + long maxSum = 0; + for (int i = 0; i < 11; i++) { + long millis = MIN_DAYS_PER_MONTH_ARRAY[i] + * (long) MILLIS_PER_DAY; + minSum += millis; + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum; + + millis = MAX_DAYS_PER_MONTH_ARRAY[i] + * (long) MILLIS_PER_DAY; + maxSum += millis; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; + } + } +} From 3d9cb8b178dd348ea6c81a1fee483039445dd87e Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Tue, 5 Feb 2019 09:08:47 +0100 Subject: [PATCH 17/18] incorporate review comments, add joda time to NOTICE.txt according to Tom --- NOTICE.txt | 3 + .../common/time/DateUtilsRounding.java | 57 ++++++++++--------- .../common/time/DateUtilsRoundingTests.java | 49 ++++++++++++++++ .../common/time/DateUtilsTests.java | 1 - 4 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java diff --git a/NOTICE.txt b/NOTICE.txt index f1e3198ab4a9a..9dd784d5a09ec 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -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/). diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java index eed91f7343aef..d9c0a9597b8a1 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtilsRounding.java @@ -36,6 +36,35 @@ class DateUtilsRounding { private static final int MILLIS_PER_DAY = 86_400_000; private static final long MILLIS_PER_YEAR = 31556952000L; + // see org.joda.time.chrono.BasicGJChronology + private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY; + private static final int[] MIN_DAYS_PER_MONTH_ARRAY = { + 31,28,31,30,31,30,31,31,30,31,30,31 + }; + private static final int[] MAX_DAYS_PER_MONTH_ARRAY = { + 31,29,31,30,31,30,31,31,30,31,30,31 + }; + + static { + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; + + long minSum = 0; + long maxSum = 0; + for (int i = 0; i < 11; i++) { + long millis = MIN_DAYS_PER_MONTH_ARRAY[i] + * (long) MILLIS_PER_DAY; + minSum += millis; + MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum; + + millis = MAX_DAYS_PER_MONTH_ARRAY[i] + * (long) MILLIS_PER_DAY; + maxSum += millis; + MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; + } + } + /** * calculates the first day of a year in milliseconds since the epoch (assuming UTC) * @@ -150,32 +179,4 @@ static long getTotalMillisByYearMonth(final int year, final int month) { return MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[month - 1]; } } - - private static final long[] MIN_TOTAL_MILLIS_BY_MONTH_ARRAY; - private static final long[] MAX_TOTAL_MILLIS_BY_MONTH_ARRAY; - private static final int[] MIN_DAYS_PER_MONTH_ARRAY = { - 31,28,31,30,31,30,31,31,30,31,30,31 - }; - private static final int[] MAX_DAYS_PER_MONTH_ARRAY = { - 31,29,31,30,31,30,31,31,30,31,30,31 - }; - - static { - MIN_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; - MAX_TOTAL_MILLIS_BY_MONTH_ARRAY = new long[12]; - - long minSum = 0; - long maxSum = 0; - for (int i = 0; i < 11; i++) { - long millis = MIN_DAYS_PER_MONTH_ARRAY[i] - * (long) MILLIS_PER_DAY; - minSum += millis; - MIN_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = minSum; - - millis = MAX_DAYS_PER_MONTH_ARRAY[i] - * (long) MILLIS_PER_DAY; - maxSum += millis; - MAX_TOTAL_MILLIS_BY_MONTH_ARRAY[i + 1] = maxSum; - } - } } diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java new file mode 100644 index 0000000000000..4ec1c261a2ace --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsRoundingTests.java @@ -0,0 +1,49 @@ +/* + * 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.common.time; + +import org.elasticsearch.test.ESTestCase; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import static org.hamcrest.Matchers.equalTo; + +public class DateUtilsRoundingTests extends ESTestCase { + + public void testDateUtilsRounding() { + for (int year = -1000; year < 3000; year++) { + final long startOfYear = DateUtilsRounding.utcMillisAtStartOfYear(year); + assertThat(startOfYear, equalTo(ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli())); + assertThat(DateUtilsRounding.getYear(startOfYear), equalTo(year)); + assertThat(DateUtilsRounding.getYear(startOfYear - 1), equalTo(year - 1)); + assertThat(DateUtilsRounding.getMonthOfYear(startOfYear, year), equalTo(1)); + assertThat(DateUtilsRounding.getMonthOfYear(startOfYear - 1, year - 1), equalTo(12)); + for (int month = 1; month <= 12; month++) { + final long startOfMonth = ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, ZoneOffset.UTC).toInstant().toEpochMilli(); + assertThat(DateUtilsRounding.getMonthOfYear(startOfMonth, year), equalTo(month)); + if (month > 1) { + assertThat(DateUtilsRounding.getYear(startOfMonth - 1), equalTo(year)); + assertThat(DateUtilsRounding.getMonthOfYear(startOfMonth - 1, year), equalTo(month - 1)); + } + } + } + } +} diff --git a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java index faec1ffce649c..e9d0a5a5b9a33 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateUtilsTests.java @@ -198,5 +198,4 @@ public void testRoundYear() { long startOf1996 = Year.of(1996).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli(); assertThat(DateUtils.roundYear(endOf1996), is(startOf1996)); } - } From 82b8f76a740729bbc918c7436df4ddcbc550b52d Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Sat, 9 Feb 2019 14:13:41 +0100 Subject: [PATCH 18/18] remove merge header --- .../src/main/java/org/elasticsearch/common/time/DateUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java index f7edecc825165..48edae06c20e3 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateUtils.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateUtils.java @@ -149,7 +149,6 @@ public static long toMilliSeconds(long nanoSecondsSinceEpoch) { } /** -<<<<<<< HEAD * 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