From 7bac0a878d918745ed036954cbbee590ce810c71 Mon Sep 17 00:00:00 2001 From: Claes Redestad Date: Fri, 25 Mar 2022 22:37:55 +0000 Subject: [PATCH] 8283681: Improve ZonedDateTime offset handling Reviewed-by: scolebourne, naoto, rriggs --- .../share/classes/java/time/ZoneId.java | 5 + .../share/classes/java/time/ZoneOffset.java | 23 +++- .../share/classes/java/time/ZoneRegion.java | 5 + .../classes/java/time/ZonedDateTime.java | 6 +- .../openjdk/bench/java/time/GetYearBench.java | 121 ++++++++++++++++++ 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 test/micro/org/openjdk/bench/java/time/GetYearBench.java diff --git a/src/java.base/share/classes/java/time/ZoneId.java b/src/java.base/share/classes/java/time/ZoneId.java index 829ed5f2fdf..14196c160fb 100644 --- a/src/java.base/share/classes/java/time/ZoneId.java +++ b/src/java.base/share/classes/java/time/ZoneId.java @@ -584,6 +584,11 @@ public abstract sealed class ZoneId implements Serializable permits ZoneOffset, return this; } + /** + * Get the effective offset for an instant at the given epochSecond. + */ + /* package-private */ abstract ZoneOffset getOffset(long epochSecond); + //----------------------------------------------------------------------- /** * Checks if this time-zone ID is equal to another time-zone ID. diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index f061040b9e2..9dd64789409 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -86,6 +86,8 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import jdk.internal.vm.annotation.Stable; + /** * A time-zone offset from Greenwich/UTC, such as {@code +02:00}. *

@@ -168,6 +170,11 @@ public final class ZoneOffset * The string form of the time-zone offset. */ private final transient String id; + /** + * The zone rules for an offset will always return this offset. Cache it for efficiency. + */ + @Stable + private transient ZoneRules rules; //----------------------------------------------------------------------- /** @@ -504,7 +511,21 @@ public final class ZoneOffset */ @Override public ZoneRules getRules() { - return ZoneRules.of(this); + ZoneRules rules = this.rules; + if (rules == null) { + rules = this.rules = ZoneRules.of(this); + } + return rules; + } + + @Override + public ZoneId normalized() { + return this; + } + + @Override + /* package-private */ ZoneOffset getOffset(long epochSecond) { + return this; } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/ZoneRegion.java b/src/java.base/share/classes/java/time/ZoneRegion.java index aec8a2b108b..9a7e66f0943 100644 --- a/src/java.base/share/classes/java/time/ZoneRegion.java +++ b/src/java.base/share/classes/java/time/ZoneRegion.java @@ -178,6 +178,11 @@ final class ZoneRegion extends ZoneId implements Serializable { return (rules != null ? rules : ZoneRulesProvider.getRules(id, false)); } + @Override + /* package-private */ ZoneOffset getOffset(long epochSecond) { + return getRules().getOffset(Instant.ofEpochSecond(epochSecond)); + } + //----------------------------------------------------------------------- /** * Writes the object using a diff --git a/src/java.base/share/classes/java/time/ZonedDateTime.java b/src/java.base/share/classes/java/time/ZonedDateTime.java index acd68706b1c..555392cf513 100644 --- a/src/java.base/share/classes/java/time/ZonedDateTime.java +++ b/src/java.base/share/classes/java/time/ZonedDateTime.java @@ -452,9 +452,9 @@ public final class ZonedDateTime * @throws DateTimeException if the result exceeds the supported range */ private static ZonedDateTime create(long epochSecond, int nanoOfSecond, ZoneId zone) { - ZoneRules rules = zone.getRules(); - Instant instant = Instant.ofEpochSecond(epochSecond, nanoOfSecond); // TODO: rules should be queryable by epochSeconds - ZoneOffset offset = rules.getOffset(instant); + // nanoOfSecond is in a range that'll not affect epochSecond, validated + // by LocalDateTime.ofEpochSecond + ZoneOffset offset = zone.getOffset(epochSecond); LocalDateTime ldt = LocalDateTime.ofEpochSecond(epochSecond, nanoOfSecond, offset); return new ZonedDateTime(ldt, offset, zone); } diff --git a/test/micro/org/openjdk/bench/java/time/GetYearBench.java b/test/micro/org/openjdk/bench/java/time/GetYearBench.java new file mode 100644 index 00000000000..59d3b84a297 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/time/GetYearBench.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.time; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +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.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Examine ability to perform escape analysis on expressions + * such as {@code Instant.ofEpochMilli(value).atZone(ZoneOffset.UTC).getYear()} + */ +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@State(Scope.Benchmark) +public class GetYearBench { + + private TimeZone UTC = TimeZone.getTimeZone("UTC"); + + private TimeZone LONDON = TimeZone.getTimeZone("Europe/London"); + + private long[] INSTANT_MILLIS; + + private int[] YEARS; + + @Setup + public void createInstants() { + // Various instants during the same day + final Instant loInstant = Instant.EPOCH.plus(Duration.ofDays(365*50)); // 2020-01-01 + final Instant hiInstant = loInstant.plus(Duration.ofDays(1)); + final long maxOffsetNanos = Duration.between(loInstant, hiInstant).toNanos(); + final Random random = new Random(0); + INSTANT_MILLIS = IntStream + .range(0, 1_000) + .mapToObj(ignored -> { + final long offsetNanos = (long) Math.floor(random.nextDouble() * maxOffsetNanos); + return loInstant.plus(offsetNanos, ChronoUnit.NANOS); + }) + .mapToLong(instant -> instant.toEpochMilli()) + .toArray(); + YEARS = new int[INSTANT_MILLIS.length]; + } + + @Benchmark + public int[] getYearFromMillisZoneOffset() { + for (int i = 0; i < YEARS.length; i++) { + YEARS[i] = Instant.ofEpochMilli(INSTANT_MILLIS[i]).atZone(ZoneOffset.UTC).getYear(); + } + return YEARS; + } + + @Benchmark + public int[] getYearFromMillisZoneRegionUTC() { + for (int i = 0; i < YEARS.length; i++) { + YEARS[i] = Instant.ofEpochMilli(INSTANT_MILLIS[i]).atZone(UTC.toZoneId()).getYear(); + } + return YEARS; + } + + @Benchmark + public int[] getYearFromMillisZoneRegion() { + for (int i = 0; i < YEARS.length; i++) { + YEARS[i] = Instant.ofEpochMilli(INSTANT_MILLIS[i]).atZone(LONDON.toZoneId()).getYear(); + } + return YEARS; + } + + @Benchmark + public int[] getYearFromMillisZoneRegionNormalized() { + for (int i = 0; i < YEARS.length; i++) { + YEARS[i] = Instant.ofEpochMilli(INSTANT_MILLIS[i]).atZone(UTC.toZoneId().normalized()).getYear(); + } + return YEARS; + } +}