diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java b/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java index 070dc8a15112..e56a8eb0d722 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/DebugListener.java @@ -185,7 +185,7 @@ protected void log(String format, Object... arg) long now = System.currentTimeMillis(); long ms = now % 1000; if (_out != null) - _out.printf("%s.%03d:%s%n", __date.formatNow(now), ms, s); + _out.printf("%s.%03d:%s%n", __date.format(now), ms, s); if (LOG.isDebugEnabled()) LOG.debug(s); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java index fd8360ff43f9..942c00691670 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java @@ -104,7 +104,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques private void print(String name, String message) { long now = System.currentTimeMillis(); - final String d = _date.formatNow(now); + final String d = _date.format(now); final int ms = (int)(now % 1000); _print.println(d + (ms > 99 ? "." : (ms > 9 ? ".0" : ".00")) + ms + ":" + name + " " + message); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java b/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java index 65c4f476b780..fc05de1fc4f2 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/DateCache.java @@ -15,55 +15,83 @@ import java.time.Instant; import java.time.ZoneId; -import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Locale; import java.util.TimeZone; /** - * Date Format Cache. - * Computes String representations of Dates and caches - * the results so that subsequent requests within the same second - * will be fast. - * - * Only format strings that contain either "ss". Sub second formatting is - * not handled. - * - * The timezone of the date may be included as an ID with the "zzz" - * format string or as an offset with the "ZZZ" format string. - * + * Computes String representations of Dates then caches the results so + * that subsequent requests within the same second will be fast. + *

* If consecutive calls are frequently very different, then this * may be a little slower than a normal DateFormat. + *

+ * @see DateTimeFormatter for date formatting patterns. */ public class DateCache { public static final String DEFAULT_FORMAT = "EEE MMM dd HH:mm:ss zzz yyyy"; private final String _formatString; - private final String _tzFormatString; - private final DateTimeFormatter _tzFormat; - private final Locale _locale; + private final DateTimeFormatter _tzFormat1; + private final DateTimeFormatter _tzFormat2; private final ZoneId _zoneId; - private volatile Tick _tick; + private volatile TickHolder _tickHolder; + + private static class TickHolder + { + public TickHolder(Tick t1, Tick t2) + { + tick1 = t1; + tick2 = t2; + } + + final Tick tick1; + final Tick tick2; + } public static class Tick { - final long _seconds; - final String _string; + private final long _seconds; + private final String _prefix; + private final String _suffix; - public Tick(long seconds, String string) + public Tick(long seconds, String prefix, String suffix) { _seconds = seconds; - _string = string; + _prefix = prefix; + _suffix = suffix; + } + + public long getSeconds() + { + return _seconds; + } + + public String format(long inDate) + { + if (_suffix == null) + return _prefix; + + long ms = inDate % 1000; + StringBuilder sb = new StringBuilder(); + sb.append(_prefix); + if (ms < 10) + sb.append("00").append(ms); + else if (ms < 100) + sb.append('0').append(ms); + else + sb.append(ms); + sb.append(_suffix); + return sb.toString(); } } /** - * Constructor. - * Make a DateCache that will use a default format. The default format - * generates the same results as Date.toString(). + * Make a DateCache that will use a default format. + * The default format generates the same results as Date.toString(). */ public DateCache() { @@ -71,8 +99,7 @@ public DateCache() } /** - * Constructor. - * Make a DateCache that will use the given format + * Make a DateCache that will use the given format. * * @param format the format to use */ @@ -93,56 +120,44 @@ public DateCache(String format, Locale l, String tz) public DateCache(String format, Locale l, TimeZone tz) { + this(format, l, tz, true); + } + + public DateCache(String format, Locale l, TimeZone tz, boolean subSecondPrecision) + { + format = format.replaceFirst("S+", "SSS"); _formatString = format; - _locale = l; + _zoneId = tz.toZoneId(); - int zIndex = _formatString.indexOf("ZZZ"); - if (zIndex >= 0) + String format1 = format; + String format2 = null; + boolean subSecond; + if (subSecondPrecision) { - final String ss1 = _formatString.substring(0, zIndex); - final String ss2 = _formatString.substring(zIndex + 3); - int tzOffset = tz.getRawOffset(); - - StringBuilder sb = new StringBuilder(_formatString.length() + 10); - sb.append(ss1); - sb.append("'"); - if (tzOffset >= 0) - sb.append('+'); - else + int msIndex = format.indexOf("SSS"); + subSecond = (msIndex >= 0); + if (subSecond) { - tzOffset = -tzOffset; - sb.append('-'); + format1 = format.substring(0, msIndex); + format2 = format.substring(msIndex + 3); } - - int raw = tzOffset / (1000 * 60); // Convert to seconds - int hr = raw / 60; - int min = raw % 60; - - if (hr < 10) - sb.append('0'); - sb.append(hr); - if (min < 10) - sb.append('0'); - sb.append(min); - sb.append('\''); - - sb.append(ss2); - _tzFormatString = sb.toString(); } else - _tzFormatString = _formatString; - - if (_locale != null) { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString, _locale); + subSecond = false; + format1 = format.replace("SSS", "000"); } + + _tzFormat1 = createFormatter(format1, l, _zoneId); + _tzFormat2 = subSecond ? createFormatter(format2, l, _zoneId) : null; + } + + private DateTimeFormatter createFormatter(String format, Locale locale, ZoneId zoneId) + { + if (locale == null) + return DateTimeFormatter.ofPattern(format).withZone(zoneId); else - { - _tzFormat = DateTimeFormatter.ofPattern(_tzFormatString); - } - _zoneId = tz.toZoneId(); - _tzFormat.withZone(_zoneId); - _tick = null; + return DateTimeFormatter.ofPattern(format, locale).withZone(zoneId); } public TimeZone getTimeZone() @@ -152,92 +167,90 @@ public TimeZone getTimeZone() /** * Format a date according to our stored formatter. + * If it happens to be in the same second as the last + * formatNow call, then the format is reused. * - * @param inDate the Date - * @return Formatted date + * @param inDate the Date. + * @return Formatted date. */ public String format(Date inDate) { - long seconds = inDate.getTime() / 1000; - - Tick tick = _tick; - - // Is this the cached time - if (tick == null || seconds != tick._seconds) - { - return ZonedDateTime.ofInstant(inDate.toInstant(), _zoneId).format(_tzFormat); - } - - return tick._string; + return format(inDate.getTime()); } /** * Format a date according to our stored formatter. - * If it happens to be in the same second as the last formatNow - * call, then the format is reused. + * If it happens to be in the same second as the last + * formatNow call, then the format is reused. * - * @param inDate the date in milliseconds since unix epoch - * @return Formatted date + * @param inDate the date in milliseconds since unix epoch. + * @return Formatted date. */ public String format(long inDate) { - long seconds = inDate / 1000; - - Tick tick = _tick; - - // Is this the cached time - if (tick == null || seconds != tick._seconds) - { - // It's a cache miss - return ZonedDateTime.ofInstant(Instant.ofEpochMilli(inDate), _zoneId).format(_tzFormat); - } + return formatTick(inDate).format(inDate); + } - return tick._string; + /** + * Format a date according to supplied formatter. + * + * @param inDate the date in milliseconds since unix epoch. + * @return Formatted date. + */ + protected String doFormat(long inDate, DateTimeFormatter formatter) + { + if (formatter == null) + return null; + return formatter.format(Instant.ofEpochMilli(inDate)); } /** * Format a date according to our stored formatter. * The passed time is expected to be close to the current time, so it is * compared to the last value passed and if it is within the same second, - * the format is reused. Otherwise a new cached format is created. + * the format is reused. Otherwise, a new cached format is created. * * @param now the milliseconds since unix epoch * @return Formatted date + * @deprecated use {@link #format(long)} */ + @Deprecated public String formatNow(long now) { - long seconds = now / 1000; - - Tick tick = _tick; - - // Is this the cached time - if (tick != null && tick._seconds == seconds) - return tick._string; - return formatTick(now)._string; + return format(now); } + @Deprecated public String now() { return formatNow(System.currentTimeMillis()); } + @Deprecated public Tick tick() { return formatTick(System.currentTimeMillis()); } - protected Tick formatTick(long now) + protected Tick formatTick(long inDate) { - long seconds = now / 1000; + long seconds = inDate / 1000; - Tick tick = _tick; - // recheck the tick, to save multiple formats - if (tick == null || tick._seconds != seconds) + // Two Ticks are cached so that for monotonically increasing times to not see any jitter from multiple cores. + // The ticks are kept in a volatile field, so there a small risk of inconsequential multiple recalculations + TickHolder holder = _tickHolder; + if (holder != null) { - String s = ZonedDateTime.ofInstant(Instant.ofEpochMilli(now), _zoneId).format(_tzFormat); - _tick = new Tick(seconds, s); - tick = _tick; + if (holder.tick1 != null && holder.tick1.getSeconds() == seconds) + return holder.tick1; + if (holder.tick2 != null && holder.tick2.getSeconds() == seconds) + return holder.tick2; } + + String prefix = doFormat(inDate, _tzFormat1); + String suffix = doFormat(inDate, _tzFormat2); + Tick tick = new Tick(seconds, prefix, suffix); + _tickHolder = new TickHolder(tick, (holder == null) ? null : holder.tick1); return tick; } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java index 36b8da4fd3de..e9d27721b006 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/DateCacheTest.java @@ -14,15 +14,22 @@ package org.eclipse.jetty.util; import java.time.Instant; +import java.time.format.DateTimeFormatter; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertNotNull; public class DateCacheTest @@ -39,7 +46,7 @@ public void testDateCache() throws Exception Instant now = Instant.now(); Instant end = now.plusSeconds(3); - String f = dc.formatNow(now.toEpochMilli()); + String f = dc.format(now.toEpochMilli()); int hits = 0; int misses = 0; @@ -47,7 +54,7 @@ public void testDateCache() throws Exception while (now.isBefore(end)) { String last = f; - f = dc.formatNow(now.toEpochMilli()); + f = dc.format(now.toEpochMilli()); // System.err.printf("%s %s%n",f,last==f); if (last == f) hits++; @@ -65,16 +72,11 @@ public void testAllMethods() { // we simply check we do not have any exception DateCache dateCache = new DateCache(); - assertNotNull(dateCache.formatNow(System.currentTimeMillis())); - assertNotNull(dateCache.formatNow(new Date().getTime())); - assertNotNull(dateCache.formatNow(Instant.now().toEpochMilli())); - - assertNotNull(dateCache.format(new Date())); - assertNotNull(dateCache.format(new Date(System.currentTimeMillis()))); - assertNotNull(dateCache.format(System.currentTimeMillis())); assertNotNull(dateCache.format(new Date().getTime())); assertNotNull(dateCache.format(Instant.now().toEpochMilli())); + assertNotNull(dateCache.format(new Date())); + assertNotNull(dateCache.format(new Date(System.currentTimeMillis()))); assertNotNull(dateCache.formatTick(System.currentTimeMillis())); assertNotNull(dateCache.formatTick(new Date().getTime())); @@ -88,4 +90,61 @@ public void testAllMethods() assertNotNull(dateCache.tick()); } + + @Test + public void testChangeOfSecond() + { + AtomicInteger counter = new AtomicInteger(); + DateCache dateCache = new DateCache(DateCache.DEFAULT_FORMAT + " | SSS", null, TimeZone.getTimeZone("UTC")) + { + @Override + protected String doFormat(long inDate, DateTimeFormatter formatter) + { + counter.incrementAndGet(); + return super.doFormat(inDate, formatter); + } + }; + + + for (int i = 0; i < 10; i++) + { + assertThat(format(dateCache, "2012-12-21T10:15:30.55Z"), equalTo("Fri Dec 21 10:15:30 UTC 2012 | 550")); + assertThat(format(dateCache, "2012-12-21T10:15:31.33Z"), equalTo("Fri Dec 21 10:15:31 UTC 2012 | 330")); + } + + // We have 4 formats, two for each second, suffix and prefix. + assertThat(counter.get(), equalTo(4)); + } + + static Stream msFormatArgs() + { + // Given a time of "2012-12-21T10:15:31.123Z" what will the format string result in. + return Stream.of( + Arguments.of("S", "123", "SSS", true), + Arguments.of("SS", "123", "SSS", true), + Arguments.of("SSS", "123", "SSS", true), + Arguments.of("SSSS", "123", "SSS", true), + Arguments.of("SSSSSS", "123", "SSS", true), + Arguments.of("S", "000", "SSS", false), + Arguments.of("SS", "000", "SSS", false), + Arguments.of("SSS", "000", "SSS", false), + Arguments.of("SSSS", "000", "SSS", false), + Arguments.of("SSSSSS", "000", "SSS", false) + ); + } + + @ParameterizedTest + @MethodSource("msFormatArgs") + public void testMsFormat(String format, String expected, String correctedFormatString, boolean msPrecision) throws Exception + { + String timeString = "2012-12-21T10:15:31.123Z"; + DateCache dateCache = new DateCache(format, null, TimeZone.getDefault(), msPrecision); + assertThat(dateCache.getFormatString(), equalTo(correctedFormatString)); + assertThat(format(dateCache, timeString), equalTo(expected)); + } + + private static String format(DateCache dateCache, String instant) + { + return dateCache.format(Date.from(Instant.parse(instant))); + } } diff --git a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheBenchmark.java b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheBenchmark.java index 2ba8f67b3ff7..0cf6d205a3cd 100644 --- a/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheBenchmark.java +++ b/tests/jetty-jmh/src/main/java/org/eclipse/jetty/util/jmh/DateCacheBenchmark.java @@ -14,7 +14,8 @@ package org.eclipse.jetty.util.jmh; import java.time.Instant; -import java.util.Date; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.util.DateCache; @@ -38,29 +39,22 @@ @Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS) public class DateCacheBenchmark { - - DateCache dateCache = new DateCache(); - long timestamp = Instant.now().toEpochMilli(); - - @Benchmark - @BenchmarkMode(Mode.Throughput) - public void testDateCacheTimestamp() - { - dateCache.format(timestamp); - } + TimeZone timeZone = TimeZone.getDefault(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateCache.DEFAULT_FORMAT + " SSS").withZone(timeZone.toZoneId()); + DateCache dateCache = new DateCache(DateCache.DEFAULT_FORMAT + " SSS", null, timeZone, true); @Benchmark @BenchmarkMode(Mode.Throughput) - public void testDateCacheNow() + public void testDateTimeFormatter() { - dateCache.format(new Date()); + formatter.format(Instant.now()); } @Benchmark @BenchmarkMode(Mode.Throughput) - public void testDateCacheFormatNow() + public void testDateCache() { - dateCache.formatNow(System.currentTimeMillis()); + dateCache.format(System.currentTimeMillis()); } public static void main(String[] args) throws RunnerException