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

Speed up converting of temporal accessor to zoned date time #37915

Merged
Merged
Show file tree
Hide file tree
Changes from 7 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,53 @@
/*
* 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.time.DateFormatter;
import org.elasticsearch.common.time.DateFormatters;
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.temporal.TemporalAccessor;
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 DateFormatterToZonedTimeBenchmark {

private final TemporalAccessor accessor = DateFormatter.forPattern("epoch_millis").parse("1234567890");

@Benchmark
public TemporalAccessor benchmarkToZonedDateTime() {
// benchmark an accessor that does not contain a timezone
// this used to throw an exception earlier and thus was very very slow
return DateFormatters.from(accessor);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static Date parseTimeField(XContentParser parser, String fieldName) throw
if (parser.currentToken() == XContentParser.Token.VALUE_NUMBER) {
return new Date(parser.longValue());
} else if (parser.currentToken() == XContentParser.Token.VALUE_STRING) {
return new Date(DateFormatters.toZonedDateTime(DateTimeFormatter.ISO_INSTANT.parse(parser.text())).toInstant().toEpochMilli());
return new Date(DateFormatters.from(DateTimeFormatter.ISO_INSTANT.parse(parser.text())).toInstant().toEpochMilli());
}
throw new IllegalArgumentException(
"unexpected token [" + parser.currentToken() + "] for [" + fieldName + "]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,22 @@

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;

import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_DAY;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.NANO_OF_SECOND;

enum DateFormat {
Iso8601 {
@Override
Expand Down Expand Up @@ -71,6 +81,9 @@ private long parseMillis(String date) {
}
},
Java {
private final List<ChronoField> FIELDS =
Arrays.asList(NANO_OF_SECOND, MINUTE_OF_DAY, HOUR_OF_DAY, DAY_OF_MONTH, MONTH_OF_YEAR);

@Override
Function<String, DateTime> getFunction(String format, DateTimeZone timezone, Locale locale) {
// in case you are wondering why we do not call 'DateFormatter.forPattern(format)' for all cases here, but only for the
Expand All @@ -80,20 +93,33 @@ Function<String, DateTime> getFunction(String format, DateTimeZone timezone, Loc
// This means that we leave the existing parsing logic in place, but will fall back to the new java date parsing logic, if an
// "8" is prepended to the date format string
int year = LocalDate.now(ZoneOffset.UTC).getYear();
ZoneId zoneId = DateUtils.dateTimeZoneToZoneId(timezone);
if (format.startsWith("8")) {
DateFormatter formatter = DateFormatter.forPattern(format)
.withLocale(locale)
.withZone(DateUtils.dateTimeZoneToZoneId(timezone));
.withZone(zoneId);
return text -> {
ZonedDateTime defaultZonedDateTime = Instant.EPOCH.atZone(ZoneOffset.UTC).withYear(year);
TemporalAccessor accessor = formatter.parse(text);
long millis = DateFormatters.toZonedDateTime(accessor, defaultZonedDateTime).toInstant().toEpochMilli();
// if there is no year, we fall back to the current one and
// fill the rest of the date up with the parsed date
if (accessor.isSupported(ChronoField.YEAR) == false) {
ZonedDateTime newTime = Instant.EPOCH.atZone(ZoneOffset.UTC).withYear(year);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the "current" year in the specified time zone instead of UTC? Wouldn't we otherwise either use the next or the previous year around the turn of the year?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I resembled the joda time behaviour here. I also created a test that compares joda time with java time to make sure the output is the same

for (ChronoField field : FIELDS) {
if (accessor.isSupported(field)) {
newTime = newTime.with(field, accessor.get(field));
}
}

accessor = newTime.withZoneSameLocal(zoneId);
}

long millis = DateFormatters.from(accessor).toInstant().toEpochMilli();
return new DateTime(millis, timezone);
};
} else {
DateFormatter formatter = Joda.forPattern(format)
.withYear(year)
.withZone(DateUtils.dateTimeZoneToZoneId(timezone))
.withZone(zoneId)
.withLocale(locale);
return text -> new DateTime(formatter.parseMillis(text), timezone);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,68 @@

package org.elasticsearch.ingest.common;

import org.elasticsearch.common.time.DateUtils;
import org.elasticsearch.test.ESTestCase;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.function.Function;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.IsEqual.equalTo;

public class DateFormatTests extends ESTestCase {

public void testParseJoda() {
Function<String, DateTime> jodaFunction = DateFormat.Java.getFunction("MMM dd HH:mm:ss Z",
public void testParseJava() {
Function<String, DateTime> javaFunction = DateFormat.Java.getFunction("MMM dd HH:mm:ss Z",
DateTimeZone.forOffsetHours(-8), Locale.ENGLISH);
assertThat(Instant.ofEpochMilli(jodaFunction.apply("Nov 24 01:29:01 -0800").getMillis())
assertThat(Instant.ofEpochMilli(javaFunction.apply("Nov 24 01:29:01 -0800").getMillis())
.atZone(ZoneId.of("GMT-8"))
.format(DateTimeFormatter.ofPattern("MM dd HH:mm:ss", Locale.ENGLISH)),
equalTo("11 24 01:29:01"));
}

public void testParseJodaDefaultYear() {
String format = randomFrom("dd/MM");
DateTimeZone timezone = DateUtils.zoneIdToDateTimeZone(ZoneId.of("Europe/Amsterdam"));
Function<String, DateTime> javaFunction = DateFormat.Java.getFunction(format, timezone, Locale.ENGLISH);
int year = ZonedDateTime.now(ZoneOffset.UTC).getYear();
DateTime dateTime = javaFunction.apply("12/06");
assertThat(dateTime.getYear(), is(year));
assertThat(dateTime.toString(), is(year + "-06-12T00:00:00.000+02:00"));
}

public void testParseJavaDefaultYear() {
String format = randomFrom("8dd/MM");
DateTimeZone timezone = DateUtils.zoneIdToDateTimeZone(ZoneId.of("Europe/Amsterdam"));
Function<String, DateTime> javaFunction = DateFormat.Java.getFunction(format, timezone, Locale.ENGLISH);
int year = ZonedDateTime.now(ZoneOffset.UTC).getYear();
DateTime dateTime = javaFunction.apply("12/06");
assertThat(dateTime.getYear(), is(year));
assertThat(dateTime.toString(), is(year + "-06-12T00:00:00.000+02:00"));
}

// if there is a time around end of year, which is different in UTC make sure the result is the same
public void testParseDefaultYearBackwardsCompatible() {
ZoneId zoneId = ZoneId.of("America/New_York");
DateTimeZone timezone = DateUtils.zoneIdToDateTimeZone(zoneId);
int year = ZonedDateTime.now(zoneId).getYear();
int nextYear = year + 1;

DateTime javaDateTime = DateFormat.Java.getFunction("8dd/MM HH:mm", timezone, Locale.ENGLISH).apply("31/12 23:59");
DateTime jodaDateTime = DateFormat.Java.getFunction("dd/MM HH:mm", timezone, Locale.ENGLISH).apply("31/12 23:59");
assertThat(javaDateTime.getYear(), is(jodaDateTime.getYear()));
assertThat(year, is(jodaDateTime.getYear()));
assertThat(javaDateTime.withZone(DateTimeZone.UTC), is(jodaDateTime.withZone(DateTimeZone.UTC)));
assertThat(nextYear, is(jodaDateTime.withZone(DateTimeZone.UTC).getYear()));
}

public void testParseUnixMs() {
assertThat(DateFormat.UnixMs.getFunction(null, DateTimeZone.UTC, null).apply("1000500").getMillis(), equalTo(1000500L));
}
Expand Down
Loading