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:
Lachlan 2023-08-25 02:43:46 +10:00 committed by GitHub
parent 3a85b66ee4
commit c55363d43f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 144 deletions

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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)));
}
}

View File

@ -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