title: Extremely slow parsing of time zone with the new java.time API
body: <p>I was just migrating a module from the old java dates to the new java.time API, and noticed a huge drop in performance. It boiled down to parsing of dates with timezone (I parse millions of them at a time).</p>
  
  <p>Parsing of date string without a time zone (<code>yyyy/MM/dd HH:mm:ss</code>) is fast - about 2 times faster than with the old java date, about 1.5M operations per second on my PC. </p>
  
  <p>However, when the pattern contains a time zone (<code>yyyy/MM/dd HH:mm:ss z</code>), the performance drops about 15 times with the new <code>java.time</code> API, while with the old API it is about as fast as without a time zone. See the performance benchmark below.</p>
  
  <p>Does anyone have an idea if I can somehow parse these strings quickly using the new <code>java.time</code> API? At the moment, as a workaround, I am using the old API for parsing and then convert the <code>Date</code> to Instant, which is not particularly nice.</p>
  
  <pre><code>import java.text.ParseException;
  import java.text.SimpleDateFormat;
  import java.time.format.DateTimeFormatter;
  import java.time.format.DateTimeFormatterBuilder;
  import java.util.concurrent.TimeUnit;
  
  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.OperationsPerInvocation;
  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 org.openjdk.jmh.infra.Blackhole;
  import org.openjdk.jmh.runner.Runner;
  import org.openjdk.jmh.runner.RunnerException;
  import org.openjdk.jmh.runner.options.Options;
  import org.openjdk.jmh.runner.options.OptionsBuilder;
  
  @OutputTimeUnit(TimeUnit.MILLISECONDS)
  @BenchmarkMode(Mode.AverageTime)
  @OperationsPerInvocation(1)
  @Fork(1)
  @Warmup(iterations = 3)
  @Measurement(iterations = 5)
  @State(Scope.Thread)
  public class DateParsingBenchmark {
  
      private final int iterations = 100000;
  
      @Benchmark
      public void oldFormat_noZone(Blackhole bh, DateParsingBenchmark st) throws ParseException {
  
          SimpleDateFormat simpleDateFormat = 
                  new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
  
          for(int i=0; i&lt;iterations; i++) {
              bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12"));
          }
      }
  
      @Benchmark
      public void oldFormat_withZone(Blackhole bh, DateParsingBenchmark st) throws ParseException {
  
          SimpleDateFormat simpleDateFormat = 
                  new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z");
  
          for(int i=0; i&lt;iterations; i++) {
              bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12 CET"));
          }
      }
  
      @Benchmark
      public void newFormat_noZone(Blackhole bh, DateParsingBenchmark st) {
  
          DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
                  .appendPattern("yyyy/MM/dd HH:mm:ss").toFormatter();
  
          for(int i=0; i&lt;iterations; i++) {
              bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12"));
          }
      }
  
      @Benchmark
      public void newFormat_withZone(Blackhole bh, DateParsingBenchmark st) {
  
          DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
                  .appendPattern("yyyy/MM/dd HH:mm:ss z").toFormatter();
  
          for(int i=0; i&lt;iterations; i++) {
              bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12 CET"));
          }
      }
  
      public static void main(String[] args) throws RunnerException {
          Options opt = new OptionsBuilder().include(DateParsingBenchmark.class.getSimpleName()).build();
          new Runner(opt).run();    
      }
  }
  </code></pre>
  
  <p>And the results for 100K operations:</p>
  
  <pre><code>Benchmark                                Mode  Cnt     Score     Error  Units
  DateParsingBenchmark.newFormat_noZone    avgt    5    61.165 ±  11.173  ms/op
  DateParsingBenchmark.newFormat_withZone  avgt    5  1662.370 ± 191.013  ms/op
  DateParsingBenchmark.oldFormat_noZone    avgt    5    93.317 ±  29.307  ms/op
  DateParsingBenchmark.oldFormat_withZone  avgt    5   107.247 ±  24.322  ms/op
  </code></pre>
  
  <p><strong>UPDATE:</strong></p>
  
  <p>I just did some profiling of the java.time classes, and indeed, the time zone parser seems to be implemented quite inefficiently. Just parsing a standalone timezone is responsible for all the slowness.</p>
  
  <pre><code>@Benchmark
  public void newFormat_zoneOnly(Blackhole bh, DateParsingBenchmark st) {
  
      DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
              .appendPattern("z").toFormatter();
  
      for(int i=0; i&lt;iterations; i++) {
          bh.consume(dateTimeFormatter.parse("CET"));
      }
  }
  </code></pre>
  
  <p>There is a class called <code>ZoneTextPrinterParser</code> in the <code>java.time</code> bundle, which is internally making a copy of the set of all available time zones in every <code>parse()</code> call (via <code>ZoneRulesProvider.getAvailableZoneIds()</code>), and this is  accountable for 99% of the time spent in the zone parsing.</p>
  
  <p>Well, an answer then might be to write my own zone parser, which would not be too nice either, because then I could not build the <code>DateTimeFormatter</code> via <code>appendPattern()</code>.</p>