Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce object creation in Rounding class #38061

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.benchmark.time;

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

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

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

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

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

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

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

private final long timestamp = 1548879021354L;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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