cleanups of DateCache (#10176)
* improve the formatting for precise ms in DateCache * return original format string with DateCache.getFormatString * calculate index in tick constructor because format strings can be different size * use two ticks so that switching between seconds is less likely going to have cache miss * use boolean instead of index to denote if sub second is needed * remove formatWithoutCache and replace with doFormat as it doesn't work with sub second time * allow the option of not having sub second precision * use two separate formatters for the prefix/suffix around the SSS format code * use a simple class to store both ticks in DateCache * rename DateCache.Tick.getString(long) to format() Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
3a85b66ee4
commit
c55363d43f
|
@ -185,7 +185,7 @@ public class DebugListener extends AbstractLifeCycle implements ServletContextLi
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ public class DebugHandler extends HandlerWrapper implements Connection.Listener
|
|||
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);
|
||||
|
|
|
@ -15,55 +15,83 @@ package org.eclipse.jetty.util;
|
|||
|
||||
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.
|
||||
* <p>
|
||||
* If consecutive calls are frequently very different, then this
|
||||
* may be a little slower than a normal DateFormat.
|
||||
* <p>
|
||||
* @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 class 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 class DateCache
|
|||
|
||||
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;
|
||||
|
||||
int zIndex = _formatString.indexOf("ZZZ");
|
||||
if (zIndex >= 0)
|
||||
{
|
||||
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
|
||||
{
|
||||
tzOffset = -tzOffset;
|
||||
sb.append('-');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tzFormat = DateTimeFormatter.ofPattern(_tzFormatString);
|
||||
}
|
||||
_zoneId = tz.toZoneId();
|
||||
_tzFormat.withZone(_zoneId);
|
||||
_tick = null;
|
||||
|
||||
String format1 = format;
|
||||
String format2 = null;
|
||||
boolean subSecond;
|
||||
if (subSecondPrecision)
|
||||
{
|
||||
int msIndex = format.indexOf("SSS");
|
||||
subSecond = (msIndex >= 0);
|
||||
if (subSecond)
|
||||
{
|
||||
format1 = format.substring(0, msIndex);
|
||||
format2 = format.substring(msIndex + 3);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
return DateTimeFormatter.ofPattern(format, locale).withZone(zoneId);
|
||||
}
|
||||
|
||||
public TimeZone getTimeZone()
|
||||
|
@ -152,92 +167,90 @@ public class DateCache
|
|||
|
||||
/**
|
||||
* 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;
|
||||
return formatTick(inDate).format(inDate);
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 class DateCacheTest
|
|||
|
||||
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 class DateCacheTest
|
|||
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 class DateCacheTest
|
|||
{
|
||||
// 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 class DateCacheTest
|
|||
|
||||
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<Arguments> 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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @@ import org.openjdk.jmh.runner.options.TimeValue;
|
|||
@Measurement(iterations = 7, time = 500, timeUnit = TimeUnit.MILLISECONDS)
|
||||
public class DateCacheBenchmark
|
||||
{
|
||||
|
||||
DateCache dateCache = new DateCache();
|
||||
long timestamp = Instant.now().toEpochMilli();
|
||||
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 testDateCacheTimestamp()
|
||||
public void testDateTimeFormatter()
|
||||
{
|
||||
dateCache.format(timestamp);
|
||||
formatter.format(Instant.now());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheNow()
|
||||
public void testDateCache()
|
||||
{
|
||||
dateCache.format(new Date());
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public void testDateCacheFormatNow()
|
||||
{
|
||||
dateCache.formatNow(System.currentTimeMillis());
|
||||
dateCache.format(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
|
|
Loading…
Reference in New Issue