You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2025/07/29 21:38:04 UTC

(logging-log4j2) branch feature/3.x/port-3338 created (now 3a9166525e)

This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a change to branch feature/3.x/port-3338
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


      at 3a9166525e Use garbage-free formatter for `s` and `S` patterns (#3338)

This branch includes the following new commits:

     new 3a9166525e Use garbage-free formatter for `s` and `S` patterns (#3338)

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(logging-log4j2) 01/01: Use garbage-free formatter for `s` and `S` patterns (#3338)

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch feature/3.x/port-3338
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 3a9166525ea9b00a3cfa043955e9139a97ac3ae1
Author: Piotr P. Karwasz <pi...@karwasz.org>
AuthorDate: Tue Dec 31 10:53:59 2024 +0100

    Use garbage-free formatter for `s` and `S` patterns (#3338)
    
    This PR improves #3139, by introducing a new `InstantPatternFormatter` for patterns of the form "ss\.S{n}". Unlike the previous formatter based on `DateTimeFormatter`, the formatter is garbage-free.
    
    We also simplify the merging algorithm for pattern formatter factories, by moving the merging logic to the pattern formatter factories themselves.
    
    This PR does not contain a separate change log entry, since #3139 has not been published yet.
    
    Fixes #3337.
    
    Co-authored-by: Volkan Yazıcı <vo...@yazi.ci>
---
 .../InstantPatternDynamicFormatterTest.java        | 175 ++---
 ...stantPatternThreadLocalCachedFormatterTest.java |   2 +-
 .../instant/InstantPatternDynamicFormatter.java    | 713 ++++++++++-----------
 .../ROOT/pages/manual/json-template-layout.adoc    |   7 +
 .../modules/ROOT/pages/manual/pattern-layout.adoc  |   4 +-
 5 files changed, 459 insertions(+), 442 deletions(-)

diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
index ddb8c31103..d3f80a3607 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -32,9 +32,9 @@ import java.util.TimeZone;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.CompositePatternSequence;
 import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.DynamicPatternSequence;
 import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.SecondPatternSequence;
 import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
 import org.apache.logging.log4j.util.Constants;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -55,26 +55,23 @@ public class InstantPatternDynamicFormatterTest {
     static List<Arguments> sequencingTestCases() {
         final List<Arguments> testCases = new ArrayList<>();
 
+        // Merged constants
+        testCases.add(Arguments.of(":'foo',", ChronoUnit.DAYS, singletonList(new StaticPatternSequence(":foo,"))));
+
         // `SSSX` should be treated constant for daily updates
-        testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, singletonList(pCom(pDyn("SSS"), pDyn("X")))));
+        testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, asList(pMilliSec(), pDyn("X"))));
 
         // `yyyyMMddHHmmssSSSX` instant cache updated hourly
         testCases.add(Arguments.of(
                 "yyyyMMddHHmmssSSSX",
                 ChronoUnit.HOURS,
-                asList(
-                        pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH")),
-                        pCom(pDyn("mm"), pDyn("ss"), pDyn("SSS")),
-                        pDyn("X"))));
+                asList(pDyn("yyyyMMddHH", ChronoUnit.HOURS), pDyn("mm"), pSec("", 3), pDyn("X"))));
 
         // `yyyyMMddHHmmssSSSX` instant cache updated per minute
         testCases.add(Arguments.of(
                 "yyyyMMddHHmmssSSSX",
                 ChronoUnit.MINUTES,
-                asList(
-                        pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH"), pDyn("mm")),
-                        pCom(pDyn("ss"), pDyn("SSS")),
-                        pDyn("X"))));
+                asList(pDyn("yyyyMMddHHmm", ChronoUnit.MINUTES), pSec("", 3), pDyn("X"))));
 
         // ISO9601 instant cache updated daily
         final String iso8601InstantPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
@@ -82,77 +79,50 @@ public class InstantPatternDynamicFormatterTest {
                 iso8601InstantPattern,
                 ChronoUnit.DAYS,
                 asList(
-                        pCom(pDyn("yyyy"), pSta("-"), pDyn("MM"), pSta("-"), pDyn("dd"), pSta("T")),
-                        pCom(
-                                pDyn("HH"),
-                                pSta(":"),
-                                pDyn("mm"),
-                                pSta(":"),
-                                pDyn("ss"),
-                                pSta("."),
-                                pDyn("SSS"),
-                                pDyn("X")))));
+                        pDyn("yyyy'-'MM'-'dd'T'", ChronoUnit.DAYS),
+                        pDyn("HH':'mm':'", ChronoUnit.MINUTES),
+                        pSec(".", 3),
+                        pDyn("X"))));
 
         // ISO9601 instant cache updated per minute
         testCases.add(Arguments.of(
                 iso8601InstantPattern,
                 ChronoUnit.MINUTES,
-                asList(
-                        pCom(
-                                pDyn("yyyy"),
-                                pSta("-"),
-                                pDyn("MM"),
-                                pSta("-"),
-                                pDyn("dd"),
-                                pSta("T"),
-                                pDyn("HH"),
-                                pSta(":"),
-                                pDyn("mm"),
-                                pSta(":")),
-                        pCom(pDyn("ss"), pSta("."), pDyn("SSS")),
-                        pDyn("X"))));
+                asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X"))));
 
         // ISO9601 instant cache updated per second
         testCases.add(Arguments.of(
                 iso8601InstantPattern,
                 ChronoUnit.SECONDS,
-                asList(
-                        pCom(
-                                pDyn("yyyy"),
-                                pSta("-"),
-                                pDyn("MM"),
-                                pSta("-"),
-                                pDyn("dd"),
-                                pSta("T"),
-                                pDyn("HH"),
-                                pSta(":"),
-                                pDyn("mm"),
-                                pSta(":"),
-                                pDyn("ss"),
-                                pSta(".")),
-                        pDyn("SSS"),
-                        pDyn("X"))));
+                asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X"))));
+
+        // Seconds and micros
+        testCases.add(Arguments.of(
+                "HH:mm:ss.SSSSSS", ChronoUnit.MINUTES, asList(pDyn("HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 6))));
 
         return testCases;
     }
 
-    private static CompositePatternSequence pCom(final PatternSequence... sequences) {
-        return new CompositePatternSequence(asList(sequences));
+    private static DynamicPatternSequence pDyn(final String singlePattern) {
+        return new DynamicPatternSequence(singlePattern);
+    }
+
+    private static DynamicPatternSequence pDyn(final String pattern, final ChronoUnit precision) {
+        return new DynamicPatternSequence(pattern, precision);
     }
 
-    private static DynamicPatternSequence pDyn(final String pattern) {
-        return new DynamicPatternSequence(pattern);
+    private static SecondPatternSequence pSec(String separator, int fractionalDigits) {
+        return new SecondPatternSequence(true, separator, fractionalDigits);
     }
 
-    private static StaticPatternSequence pSta(final String literal) {
-        return new StaticPatternSequence(literal);
+    private static SecondPatternSequence pMilliSec() {
+        return new SecondPatternSequence(false, "", 3);
     }
 
     @ParameterizedTest
     @ValueSource(
             strings = {
                 // Basics
-                "S",
                 "SSSSSSS",
                 "SSSSSSSSS",
                 "n",
@@ -163,8 +133,7 @@ public class InstantPatternDynamicFormatterTest {
                 "yyyy-MM-dd HH:mm:ss,SSSSSSS",
                 "yyyy-MM-dd HH:mm:ss,SSSSSSSS",
                 "yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
-                "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS",
-                "yyyy-MM-dd'T'HH:mm:ss.SXXX"
+                "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"
             })
     void should_recognize_patterns_of_nano_precision(final String pattern) {
         assertPatternPrecision(pattern, ChronoUnit.NANOS);
@@ -233,20 +202,31 @@ public class InstantPatternDynamicFormatterTest {
         assertPatternPrecision(pattern, ChronoUnit.SECONDS);
     }
 
-    @ParameterizedTest
-    @ValueSource(
-            strings = {
+    static Stream<String> should_recognize_patterns_of_minute_precision() {
+        Stream<String> stream = Stream.of(
                 // Basics
                 "m",
                 "mm",
+                "Z",
+                "x",
+                "X",
+                "O",
+                "z",
+                "VV",
                 // Mixed with other stuff
                 "yyyy-MM-dd HH:mm",
                 "yyyy-MM-dd'T'HH:mm",
                 "HH:mm",
+                "yyyy-MM-dd HH x",
+                "yyyy-MM-dd'T'HH XX",
                 // Single-quoted text containing nanosecond and millisecond directives
                 "yyyy-MM-dd'S'HH:mm",
-                "yyyy-MM-dd'n'HH:mm"
-            })
+                "yyyy-MM-dd'n'HH:mm");
+        return Constants.JAVA_MAJOR_VERSION > 8 ? Stream.concat(stream, Stream.of("v")) : stream;
+    }
+
+    @ParameterizedTest
+    @MethodSource
     void should_recognize_patterns_of_minute_precision(final String pattern) {
         assertPatternPrecision(pattern, ChronoUnit.MINUTES);
     }
@@ -267,28 +247,71 @@ public class InstantPatternDynamicFormatterTest {
                 "K",
                 "k",
                 "H",
-                "Z",
-                "x",
-                "X",
-                "O",
-                "z",
-                "VV",
                 // Mixed with other stuff
                 "yyyy-MM-dd HH",
                 "yyyy-MM-dd'T'HH",
-                "yyyy-MM-dd HH x",
-                "yyyy-MM-dd'T'HH XX",
                 "ddHH",
                 // Single-quoted text containing nanosecond and millisecond directives
                 "yyyy-MM-dd'S'HH",
                 "yyyy-MM-dd'n'HH"));
         if (Constants.JAVA_MAJOR_VERSION > 8) {
             java8Patterns.add("B");
-            java8Patterns.add("v");
         }
         return java8Patterns;
     }
 
+    static Stream<Arguments> dynamic_pattern_should_correctly_determine_precision() {
+        // When no a precise unit is not available, uses the closest smaller unit.
+        return Stream.of(
+                Arguments.of("G", ChronoUnit.ERAS),
+                Arguments.of("u", ChronoUnit.YEARS),
+                Arguments.of("D", ChronoUnit.DAYS),
+                Arguments.of("M", ChronoUnit.MONTHS),
+                Arguments.of("L", ChronoUnit.MONTHS),
+                Arguments.of("d", ChronoUnit.DAYS),
+                Arguments.of("Q", ChronoUnit.MONTHS),
+                Arguments.of("q", ChronoUnit.MONTHS),
+                Arguments.of("Y", ChronoUnit.YEARS),
+                Arguments.of("w", ChronoUnit.WEEKS),
+                Arguments.of("W", ChronoUnit.DAYS), // The month can change in the middle of the week
+                Arguments.of("F", ChronoUnit.DAYS), // The month can change in the middle of the week
+                Arguments.of("E", ChronoUnit.DAYS),
+                Arguments.of("e", ChronoUnit.DAYS),
+                Arguments.of("c", ChronoUnit.DAYS),
+                Arguments.of("a", ChronoUnit.HOURS), // Let us round it down
+                Arguments.of("h", ChronoUnit.HOURS),
+                Arguments.of("K", ChronoUnit.HOURS),
+                Arguments.of("k", ChronoUnit.HOURS),
+                Arguments.of("H", ChronoUnit.HOURS),
+                Arguments.of("m", ChronoUnit.MINUTES),
+                Arguments.of("s", ChronoUnit.SECONDS),
+                Arguments.of("S", ChronoUnit.MILLIS),
+                Arguments.of("SS", ChronoUnit.MILLIS),
+                Arguments.of("SSS", ChronoUnit.MILLIS),
+                Arguments.of("SSSS", ChronoUnit.MICROS),
+                Arguments.of("SSSSS", ChronoUnit.MICROS),
+                Arguments.of("SSSSSS", ChronoUnit.MICROS),
+                Arguments.of("SSSSSSS", ChronoUnit.NANOS),
+                Arguments.of("SSSSSSSS", ChronoUnit.NANOS),
+                Arguments.of("SSSSSSSSS", ChronoUnit.NANOS),
+                Arguments.of("A", ChronoUnit.MILLIS),
+                Arguments.of("n", ChronoUnit.NANOS),
+                Arguments.of("N", ChronoUnit.NANOS),
+                // Time zones can change in the middle of a UTC hour (e.g. India)
+                Arguments.of("VV", ChronoUnit.MINUTES),
+                Arguments.of("z", ChronoUnit.MINUTES),
+                Arguments.of("O", ChronoUnit.MINUTES),
+                Arguments.of("X", ChronoUnit.MINUTES),
+                Arguments.of("x", ChronoUnit.MINUTES),
+                Arguments.of("Z", ChronoUnit.MINUTES));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void dynamic_pattern_should_correctly_determine_precision(String singlePattern, ChronoUnit expectedPrecision) {
+        assertThat(pDyn(singlePattern).precision).isEqualTo(expectedPrecision);
+    }
+
     private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
         final InstantPatternFormatter formatter =
                 new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), TimeZone.getDefault());
@@ -351,7 +374,11 @@ public class InstantPatternDynamicFormatterTest {
 
     private static MutableInstant randomInstant() {
         final MutableInstant instant = new MutableInstant();
-        final long epochSecond = RANDOM.nextInt(1_621_280_470); // 2021-05-17 21:41:10
+        // In the 1970's some time zones had sub-minute offsets to UTC, e.g., Africa/Monrovia.
+        // We will exclude them for tests:
+        final int minEpochSecond = 315_532_800; // 1980-01-01 01:00:00
+        final int maxEpochSecond = 1_621_280_470; // 2021-05-17 21:41:10
+        final long epochSecond = minEpochSecond + RANDOM.nextInt(maxEpochSecond - minEpochSecond);
         final int epochSecondNano = randomNanos();
         instant.initFromEpochSecond(epochSecond, epochSecondNano);
         return instant;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
index b25fb85d74..1f94b0b9ae 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
@@ -107,7 +107,7 @@ class InstantPatternThreadLocalCachedFormatterTest {
     }
 
     @ParameterizedTest
-    @ValueSource(strings = {"S", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"})
+    @ValueSource(strings = {"SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"})
     void ofMilliPrecision_should_fail_on_inconsistent_precision(final String subMilliPattern) {
         final InstantPatternDynamicFormatter dynamicFormatter =
                 new InstantPatternDynamicFormatter(subMilliPattern, LOCALE, TIME_ZONE);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index a5486f7a50..225ee8dce0 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -29,10 +29,11 @@ import java.util.Locale;
 import java.util.Objects;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.logging.log4j.core.time.Instant;
 import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.Strings;
 import org.jspecify.annotations.Nullable;
 
 /**
@@ -47,30 +48,6 @@ import org.jspecify.annotations.Nullable;
  * <li>Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:} and {@code X}) and cache it</li>
  * <li>Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})</li>
  * </ol>
- * <h2>Implementation note</h2>
- * <p>
- * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows:
- * </p>
- * <pre>{@code
- * int offsetMillis = timeZone.getOffset(mutableInstant.getEpochMillisecond());
- * long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
- * int local_s = (int) (adjustedEpochSeconds % 60);
- * int local_S = instant.getNanoOfSecond() / 100000000;
- * int local_SS = instant.getNanoOfSecond() / 10000000;
- * int local_SSS = instant.getNanoOfSecond() / 1000000;
- * int local_SSSS = instant.getNanoOfSecond() / 100000;
- * int local_SSSSS = instant.getNanoOfSecond() / 10000;
- * int local_SSSSSS = instant.getNanoOfSecond() / 1000;
- * int local_SSSSSSS = instant.getNanoOfSecond() / 100;
- * int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
- * int local_SSSSSSSSS = instant.getNanoOfSecond();
- * int local_n = instant.getNanoOfSecond();
- * }</pre>
- * <p>
- * Though this will require more hardcoded formatting and a change in the sequence merging strategies.
- * Hence, this optimization is intentionally shelved off due to involved complexity.
- * See {@code verify_manually_computed_sub_minute_precision_values()} in {@code InstantPatternDynamicFormatterTest} for a demonstration of this optimization.
- * </p>
  *
  * @since 2.25.0
  */
@@ -165,7 +142,7 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
 
         // Sequence the pattern and create associated formatters
         final List<PatternSequence> sequences = sequencePattern(pattern, precisionThreshold);
-        final List<InstantPatternFormatter> formatters = sequences.stream()
+        final InstantPatternFormatter[] formatters = sequences.stream()
                 .map(sequence -> {
                     final InstantPatternFormatter formatter = sequence.createFormatter(locale, timeZone);
                     final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
@@ -185,9 +162,9 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
                         }
                     };
                 })
-                .collect(Collectors.toList());
+                .toArray(InstantPatternFormatter[]::new);
 
-        switch (formatters.size()) {
+        switch (formatters.length) {
 
             // If found an empty pattern, return an empty formatter
             case 0:
@@ -200,17 +177,33 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
 
             // If extracted a single formatter, return it as is
             case 1:
-                return formatters.get(0);
+                return formatters[0];
+
+                // Profiling shows that unrolling the generic loop boosts performance
+            case 2:
+                final InstantPatternFormatter first = formatters[0];
+                final InstantPatternFormatter second = formatters[1];
+                return new AbstractFormatter(
+                        pattern, locale, timeZone, min(first.getPrecision(), second.getPrecision())) {
+                    @Override
+                    public void formatTo(StringBuilder buffer, Instant instant) {
+                        first.formatTo(buffer, instant);
+                        second.formatTo(buffer, instant);
+                    }
+                };
 
             // Combine all extracted formatters into one
             default:
-                final ChronoUnit precision = new CompositePatternSequence(sequences).precision;
+                final ChronoUnit precision = Stream.of(formatters)
+                        .map(InstantFormatter::getPrecision)
+                        .min(Comparator.comparing(ChronoUnit::getDuration))
+                        .get();
                 return new AbstractFormatter(pattern, locale, timeZone, precision) {
                     @Override
                     public void formatTo(final StringBuilder buffer, final Instant instant) {
                         // noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
-                        for (int formatterIndex = 0; formatterIndex < formatters.size(); formatterIndex++) {
-                            final InstantPatternFormatter formatter = formatters.get(formatterIndex);
+                        for (int formatterIndex = 0; formatterIndex < formatters.length; formatterIndex++) {
+                            final InstantPatternFormatter formatter = formatters[formatterIndex];
                             formatter.formatTo(buffer, instant);
                         }
                     }
@@ -218,10 +211,13 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
         }
     }
 
+    private static ChronoUnit min(ChronoUnit left, ChronoUnit right) {
+        return left.getDuration().compareTo(right.getDuration()) < 0 ? left : right;
+    }
+
     static List<PatternSequence> sequencePattern(final String pattern, final ChronoUnit precisionThreshold) {
         List<PatternSequence> sequences = sequencePattern(pattern);
-        final List<PatternSequence> mergedSequences = mergeDynamicSequences(sequences, precisionThreshold);
-        return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold);
+        return mergeFactories(sequences, precisionThreshold);
     }
 
     private static List<PatternSequence> sequencePattern(final String pattern) {
@@ -240,7 +236,17 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
                     endIndex++;
                 }
                 final String sequenceContent = pattern.substring(startIndex, endIndex);
-                final PatternSequence sequence = new DynamicPatternSequence(sequenceContent);
+                final PatternSequence sequence;
+                switch (c) {
+                    case 's':
+                        sequence = new SecondPatternSequence(true, "", 0);
+                        break;
+                    case 'S':
+                        sequence = new SecondPatternSequence(false, "", sequenceContent.length());
+                        break;
+                    default:
+                        sequence = new DynamicPatternSequence(sequenceContent);
+                }
                 sequences.add(sequence);
                 startIndex = endIndex;
             }
@@ -248,15 +254,7 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
             // Handle single-quotes
             else if (c == '\'') {
                 final int endIndex = pattern.indexOf('\'', startIndex + 1);
-                if (endIndex < 0) {
-                    final String message = String.format(
-                            "pattern ends with an incomplete string literal that started at index %d: `%s`",
-                            startIndex, pattern);
-                    throw new IllegalArgumentException(message);
-                }
-                final String sequenceLiteral =
-                        (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
-                final PatternSequence sequence = new StaticPatternSequence(sequenceLiteral);
+                final PatternSequence sequence = getStaticPatternSequence(pattern, startIndex, endIndex);
                 sequences.add(sequence);
                 startIndex = endIndex + 1;
             }
@@ -268,243 +266,53 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
                 startIndex++;
             }
         }
-        return mergeConsequentStaticPatternSequences(sequences);
-    }
-
-    private static boolean isDynamicPatternLetter(final char c) {
-        return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
+        return sequences;
     }
 
-    /**
-     * Merges consequent static sequences.
-     *
-     * <p>
-     * For example, the sequencing of the {@code [MM-dd] HH:mm} pattern will create two static sequences for {@code ]} (right brace) and {@code } (whitespace) characters.
-     * This method will combine such consequent static sequences into one.
-     * </p>
-     *
-     * <h2>Example</h2>
-     *
-     * <p>
-     * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
-     * </p>
-     *
-     * <pre>{@code
-     * [
-     *     static(literal="["),
-     *     dynamic(pattern="MM", precision=MONTHS),
-     *     static(literal="-"),
-     *     dynamic(pattern="dd", precision=DAYS),
-     *     static(literal="]"),
-     *     static(literal=" "),
-     *     dynamic(pattern="HH", precision=HOURS),
-     *     static(literal=":"),
-     *     dynamic(pattern="mm", precision=MINUTES)
-     * ]
-     * }</pre>
-     *
-     * <p>
-     * The above sequencing implies creation of 9 {@link AbstractFormatter}s.
-     * This method transforms it to the following:
-     * </p>
-     *
-     * <pre>{@code
-     * [
-     *     static(literal="["),
-     *     dynamic(pattern="MM", precision=MONTHS),
-     *     static(literal="-"),
-     *     dynamic(pattern="dd", precision=DAYS),
-     *     static(literal="] "),
-     *     dynamic(pattern="HH", precision=HOURS),
-     *     static(literal=":"),
-     *     dynamic(pattern="mm", precision=MINUTES)
-     * ]
-     * }</pre>
-     *
-     * <p>
-     * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
-     * </p>
-     *
-     * @param sequences sequences to be transformed
-     * @return transformed sequencing where consequent static sequences are merged
-     */
-    private static List<PatternSequence> mergeConsequentStaticPatternSequences(final List<PatternSequence> sequences) {
-
-        // Short-circuit if there is nothing to merge
-        if (sequences.size() < 2) {
-            return sequences;
-        }
-
-        final List<PatternSequence> mergedSequences = new ArrayList<>();
-        final List<StaticPatternSequence> accumulatedSequences = new ArrayList<>();
-        for (final PatternSequence sequence : sequences) {
-
-            // Spotted a static sequence? Stage it for merging.
-            if (sequence instanceof StaticPatternSequence) {
-                accumulatedSequences.add((StaticPatternSequence) sequence);
-            }
-
-            // Spotted a dynamic sequence.
-            // Merge the accumulated static sequences, and then append the dynamic sequence.
-            else {
-                mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
-                mergedSequences.add(sequence);
-            }
+    private static PatternSequence getStaticPatternSequence(String pattern, int startIndex, int endIndex) {
+        if (endIndex < 0) {
+            final String message = String.format(
+                    "pattern ends with an incomplete string literal that started at index %d: `%s`",
+                    startIndex, pattern);
+            throw new IllegalArgumentException(message);
         }
-
-        // Merge leftover static sequences
-        mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
-        return mergedSequences;
+        final String sequenceLiteral = (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
+        return new StaticPatternSequence(sequenceLiteral);
     }
 
-    private static void mergeConsequentStaticPatternSequences(
-            final List<PatternSequence> mergedSequences, final List<StaticPatternSequence> accumulatedSequences) {
-        mergeAccumulatedSequences(mergedSequences, accumulatedSequences, () -> {
-            final String literal = accumulatedSequences.stream()
-                    .map(sequence -> sequence.literal)
-                    .collect(Collectors.joining());
-            return new StaticPatternSequence(literal);
-        });
+    private static boolean isDynamicPatternLetter(final char c) {
+        return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
     }
 
     /**
-     * Merges the sequences in between the first and the last found dynamic (i.e., non-constant) sequences.
-     *
-     * <p>
-     * For example, given the {@code ss.SSS} pattern – where {@code ss} and {@code SSS} is effectively not constant, yet {@code .} is – this method will combine it into a single dynamic sequence.
-     * Because, as demonstrated in {@code DateTimeFormatterSequencingBenchmark}, formatting {@code ss.SSS} is approximately 20% faster than formatting first {@code ss}, then manually appending a {@code .}, and then formatting {@code SSS}.
-     * </p>
+     * Merges pattern sequences using {@link PatternSequence#tryMerge}.
      *
      * <h2>Example</h2>
      *
      * <p>
-     * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following:
+     * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern, a precision threshold of {@link ChronoUnit#MINUTES}
+     * and the three implementations ({@link DynamicPatternSequence}, {@link StaticPatternSequence} and
+     * {@link SecondPatternSequence}) from this class,
+     * this method will combine pattern sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence,
+     * since these are consecutive and effectively constant sequences.
      * </p>
      *
      * <pre>{@code
      * [
-     *     dynamic(pattern="yyyy", precision=YEARS),
+     *     dateTimeFormatter(pattern="yyyy", precision=YEARS),
      *     static(literal="-"),
-     *     dynamic(pattern="MM", precision=MONTHS),
+     *     dateTimeFormatter(pattern="MM", precision=MONTHS),
      *     static(literal="-"),
-     *     dynamic(pattern="dd", precision=DAYS),
+     *     dateTimeFormatter(pattern="dd", precision=DAYS),
      *     static(literal="T"),
-     *     dynamic(pattern="HH", precision=HOURS),
+     *     dateTimeFormatter(pattern="HH", precision=HOURS),
      *     static(literal=":"),
-     *     dynamic(pattern="mm", precision=MINUTES),
+     *     dateTimeFormatter(pattern="mm", precision=MINUTES),
      *     static(literal=":"),
-     *     dynamic(pattern="ss", precision=SECONDS),
+     *     second(pattern="ss", precision=SECONDS),
      *     static(literal="."),
-     *     dynamic(pattern="SSS", precision=MILLISECONDS),
-     *     dynamic(pattern="X", precision=HOURS),
-     * ]
-     * }</pre>
-     *
-     * <p>
-     * For a threshold precision of {@link ChronoUnit#MINUTES}, this sequencing effectively translates to two {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocations for each {@link #formatTo(StringBuilder, Instant)} call: one for {@code ss}, and another one for {@code SSS}.
-     * This method transforms the above sequencing into the following:
-     * </p>
-     *
-     * <pre>{@code
-     * [
-     *     dynamic(pattern="yyyy", precision=YEARS),
-     *     static(literal="-"),
-     *     dynamic(pattern="MM", precision=MONTHS),
-     *     static(literal="-"),
-     *     dynamic(pattern="dd", precision=DAYS),
-     *     static(literal="T"),
-     *     dynamic(pattern="HH", precision=HOURS),
-     *     static(literal=":"),
-     *     dynamic(pattern="mm", precision=MINUTES),
-     *     static(literal=":"),
-     *     composite(
-     *         sequences=[
-     *             dynamic(pattern="ss", precision=SECONDS),
-     *             static(literal="."),
-     *             dynamic(pattern="SSS", precision=MILLISECONDS)
-     *         ],
-     *         precision=MILLISECONDS),
-     *     dynamic(pattern="X", precision=HOURS),
-     * ]
-     * }</pre>
-     *
-     * <p>
-     * The resultant sequencing effectively translates to a single {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocation for each {@link #formatTo(StringBuilder, Instant)} call: only one fore {@code ss.SSS}.
-     * </p>
-     *
-     * @param sequences sequences, preferable produced by {@link #mergeConsequentStaticPatternSequences(List)}, to be transformed
-     * @param precisionThreshold  a precision threshold to determine dynamic (i.e., non-constant) sequences
-     * @return transformed sequencing where sequences in between the first and the last found dynamic (i.e., non-constant) sequences are merged
-     */
-    private static List<PatternSequence> mergeDynamicSequences(
-            final List<PatternSequence> sequences, final ChronoUnit precisionThreshold) {
-
-        // Locate the first and the last dynamic (i.e., non-constant) sequence indices
-        int firstDynamicSequenceIndex = -1;
-        int lastDynamicSequenceIndex = -1;
-        for (int sequenceIndex = 0; sequenceIndex < sequences.size(); sequenceIndex++) {
-            final PatternSequence sequence = sequences.get(sequenceIndex);
-            final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
-            if (!constant) {
-                if (firstDynamicSequenceIndex < 0) {
-                    firstDynamicSequenceIndex = sequenceIndex;
-                }
-                lastDynamicSequenceIndex = sequenceIndex;
-            }
-        }
-
-        // Short-circuit if there are less than 2 dynamic sequences
-        if (firstDynamicSequenceIndex < 0 || firstDynamicSequenceIndex == lastDynamicSequenceIndex) {
-            return sequences;
-        }
-
-        // Merge dynamic sequences
-        final List<PatternSequence> mergedSequences = new ArrayList<>();
-        if (firstDynamicSequenceIndex > 0) {
-            mergedSequences.addAll(sequences.subList(0, firstDynamicSequenceIndex));
-        }
-        final PatternSequence mergedDynamicSequence = new CompositePatternSequence(
-                sequences.subList(firstDynamicSequenceIndex, lastDynamicSequenceIndex + 1));
-        mergedSequences.add(mergedDynamicSequence);
-        if ((lastDynamicSequenceIndex + 1) < sequences.size()) {
-            mergedSequences.addAll(sequences.subList(lastDynamicSequenceIndex + 1, sequences.size()));
-        }
-        return mergedSequences;
-    }
-
-    /**
-     * Merges sequences that are consequent and effectively constant for the provided precision threshold.
-     *
-     * <p>
-     * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern and a precision threshold of {@link ChronoUnit#MINUTES}, this method will combine sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence, since these are consequent and effectively constant sequences.
-     * </p>
-     *
-     * <h2>Example</h2>
-     *
-     * <p>
-     * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following:
-     * </p>
-     *
-     * <pre>{@code
-     * [
-     *     dynamic(pattern="yyyy", precision=YEARS),
-     *     static(literal="-"),
-     *     dynamic(pattern="MM", precision=MONTHS),
-     *     static(literal="-"),
-     *     dynamic(pattern="dd", precision=DAYS),
-     *     static(literal="T"),
-     *     dynamic(pattern="HH", precision=HOURS),
-     *     static(literal=":"),
-     *     dynamic(pattern="mm", precision=MINUTES),
-     *     static(literal=":"),
-     *     composite(
-     *         sequences=[
-     *             dynamic(pattern="ss", precision=SECONDS),
-     *             static(literal="."),
-     *             dynamic(pattern="SSS", precision=MILLISECONDS)
-     *         ],
-     *         precision=MILLISECONDS),
-     *     dynamic(pattern="X", precision=HOURS),
+     *     second(pattern="SSS", precision=MILLISECONDS)
+     *     dateTimeFormatter(pattern="X", precision=HOURS),
      * ]
      * }</pre>
      *
@@ -515,28 +323,9 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
      *
      * <pre>{@code
      * [
-     *     composite(
-     *         sequences=[
-     *             dynamic(pattern="yyyy", precision=YEARS),
-     *             static(literal="-"),
-     *             dynamic(pattern="MM", precision=MONTHS),
-     *             static(literal="-"),
-     *             dynamic(pattern="dd", precision=DAYS),
-     *             static(literal="T"),
-     *             dynamic(pattern="HH", precision=HOURS),
-     *             static(literal=":"),
-     *             dynamic(pattern="mm", precision=MINUTES),
-     *             static(literal=":")
-     *         ],
-     *         precision=MINUTES),
-     *     composite(
-     *         sequences=[
-     *             dynamic(pattern="ss", precision=SECONDS),
-     *             static(literal="."),
-     *             dynamic(pattern="SSS", precision=MILLISECONDS)
-     *         ],
-     *         precision=MILLISECONDS),
-     *     dynamic(pattern="X", precision=HOURS),
+     *     dateTimeFormatter(pattern="yyyy-MM-dd'T'HH:mm", precision=MINUTES),
+     *     second(pattern="ss.SSS", precision=MILLISECONDS),
+     *     dateTimeFormatter(pattern="X", precision=MINUTES)
      * ]
      * }</pre>
      *
@@ -544,54 +333,32 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
      * The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s.
      * </p>
      *
-     * @param sequences sequences, preferable produced by {@link #mergeDynamicSequences(List, ChronoUnit)}, to be transformed
+     * @param sequences a list of pattern formatter factories
      * @param precisionThreshold  a precision threshold to determine effectively constant sequences
-     * @return transformed sequencing where sequences that are consequent and effectively constant for the provided precision threshold are merged
+     * @return transformed sequencing, where sequences that are effectively constant or effectively dynamic are merged.
      */
-    private static List<PatternSequence> mergeConsequentEffectivelyConstantSequences(
+    private static List<PatternSequence> mergeFactories(
             final List<PatternSequence> sequences, final ChronoUnit precisionThreshold) {
-
-        // Short-circuit if there is nothing to merge
         if (sequences.size() < 2) {
             return sequences;
         }
-
         final List<PatternSequence> mergedSequences = new ArrayList<>();
-        boolean accumulatorConstant = true;
-        final List<PatternSequence> accumulatedSequences = new ArrayList<>();
-        for (final PatternSequence sequence : sequences) {
-            final boolean sequenceConstant = sequence.isConstantForDurationOf(precisionThreshold);
-            if (sequenceConstant != accumulatorConstant) {
-                mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
-                accumulatorConstant = sequenceConstant;
+        PatternSequence currentFactory = sequences.get(0);
+        for (int i = 1; i < sequences.size(); i++) {
+            PatternSequence nextFactory = sequences.get(i);
+            PatternSequence mergedFactory = currentFactory.tryMerge(nextFactory, precisionThreshold);
+            // The current factory cannot be merged with the next one.
+            if (mergedFactory == null) {
+                mergedSequences.add(currentFactory);
+                currentFactory = nextFactory;
+            } else {
+                currentFactory = mergedFactory;
             }
-            accumulatedSequences.add(sequence);
         }
-
-        // Merge the accumulator leftover
-        mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+        mergedSequences.add(currentFactory);
         return mergedSequences;
     }
 
-    private static void mergeConsequentEffectivelyConstantSequences(
-            final List<PatternSequence> mergedSequences, final List<PatternSequence> accumulatedSequences) {
-        mergeAccumulatedSequences(
-                mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences));
-    }
-
-    private static <S extends PatternSequence> void mergeAccumulatedSequences(
-            final List<PatternSequence> mergedSequences,
-            final List<S> accumulatedSequences,
-            final Supplier<PatternSequence> mergedSequenceSupplier) {
-        if (accumulatedSequences.isEmpty()) {
-            return;
-        }
-        final PatternSequence mergedSequence =
-                accumulatedSequences.size() == 1 ? accumulatedSequences.get(0) : mergedSequenceSupplier.get();
-        mergedSequences.add(mergedSequence);
-        accumulatedSequences.clear();
-    }
-
     private static long toEpochMinutes(final Instant instant) {
         return instant.getEpochSecond() / 60;
     }
@@ -612,7 +379,7 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
 
         private final ChronoUnit precision;
 
-        private AbstractFormatter(
+        AbstractFormatter(
                 final String pattern, final Locale locale, final TimeZone timeZone, final ChronoUnit precision) {
             this.pattern = pattern;
             this.locale = locale;
@@ -654,22 +421,74 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
             this.precision = precision;
         }
 
-        InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
-            final DateTimeFormatter dateTimeFormatter =
-                    DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
-            return new AbstractFormatter(pattern, locale, timeZone, precision) {
-                @Override
-                public void formatTo(final StringBuilder buffer, final Instant instant) {
-                    final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
-                    dateTimeFormatter.formatTo(instantAccessor, buffer);
-                }
-            };
+        abstract InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone);
+
+        /**
+         * Tries to merge two pattern sequences.
+         *
+         * <p>
+         *     If not {@link null}, the pattern sequence returned by this method must:
+         * </p>
+         * <ol>
+         *     <li>Have a {@link #precision}, which is the minimum of the precisions of the two merged sequences.</li>
+         *     <li>
+         *         Create formatters that are equivalent to the concatenation of the formatters produced by the
+         *         two merged sequences.
+         *     </li>
+         * </ol>
+         * <p>
+         *     The returned pattern sequence should try to achieve these two goals:
+         * </p>
+         * <ol>
+         *     <li>
+         *         Create formatters which are faster than the concatenation of the formatters produced by the
+         *         two merged sequences.
+         *     </li>
+         *     <li>
+         *         It should be {@link null} if one of the pattern sequences is effectively constant over
+         *         {@code thresholdPrecision}, but the other one is not.
+         *     </li>
+         * </ol>
+         *
+         * @param other A pattern sequence.
+         * @param thresholdPrecision A precision threshold to determine effectively constant sequences.
+         *                           This prevents merging effectively constant and dynamic pattern sequences.
+         * @return A merged formatter factory or {@code null} if merging is not possible.
+         */
+        @Nullable
+        PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+            return null;
         }
 
-        private boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
+        boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
             return precision.compareTo(thresholdPrecision) >= 0;
         }
 
+        static String escapeLiteral(String literal) {
+            StringBuilder sb = new StringBuilder(literal.length() + 2);
+            boolean inSingleQuotes = false;
+            for (int i = 0; i < literal.length(); i++) {
+                char c = literal.charAt(i);
+                if (c == '\'') {
+                    if (inSingleQuotes) {
+                        sb.append("'");
+                    }
+                    inSingleQuotes = false;
+                    sb.append("''");
+                } else {
+                    if (!inSingleQuotes) {
+                        sb.append("'");
+                    }
+                    inSingleQuotes = true;
+                    sb.append(c);
+                }
+            }
+            if (inSingleQuotes) {
+                sb.append("'");
+            }
+            return sb.toString();
+        }
+
         @Override
         public boolean equals(final Object object) {
             if (this == object) {
@@ -689,7 +508,7 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
 
         @Override
         public String toString() {
-            return String.format("<%s>%s", pattern, precision);
+            return getClass().getSimpleName() + "[" + "pattern='" + pattern + '\'' + ", precision=" + precision + ']';
         }
     }
 
@@ -698,7 +517,7 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
         private final String literal;
 
         StaticPatternSequence(final String literal) {
-            super(literal.equals("'") ? "''" : ("'" + literal + "'"), ChronoUnit.FOREVER);
+            super(escapeLiteral(literal), ChronoUnit.FOREVER);
             this.literal = literal;
         }
 
@@ -711,44 +530,109 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
                 }
             };
         }
+
+        @Override
+        @Nullable
+        PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+            // We always merge consecutive static pattern factories
+            if (other instanceof StaticPatternSequence) {
+                final StaticPatternSequence otherStatic = (StaticPatternSequence) other;
+                return new StaticPatternSequence(this.literal + otherStatic.literal);
+            }
+            // We also merge a static pattern factory with a DTF factory
+            if (other instanceof DynamicPatternSequence) {
+                final DynamicPatternSequence otherDtf = (DynamicPatternSequence) other;
+                return new DynamicPatternSequence(this.pattern + otherDtf.pattern, otherDtf.precision);
+            }
+            return null;
+        }
     }
 
+    /**
+     * Creates formatters that use {@link DateTimeFormatter}.
+     */
     static final class DynamicPatternSequence extends PatternSequence {
 
-        DynamicPatternSequence(final String content) {
-            super(content, contentPrecision(content));
+        /**
+         * @param singlePattern A {@link DateTimeFormatter} pattern containing a single letter.
+         */
+        DynamicPatternSequence(final String singlePattern) {
+            this(singlePattern, patternPrecision(singlePattern));
         }
 
         /**
-         * @param content a single-letter directive content complying (e.g., {@code H}, {@code HH}, or {@code pHH})
-         * @return the time precision of the directive
+         * @param pattern Any {@link DateTimeFormatter} pattern.
+         * @param precision The maximum interval of time over which this pattern is constant.
          */
+        DynamicPatternSequence(final String pattern, final ChronoUnit precision) {
+            super(pattern, precision);
+        }
+
+        InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+            final DateTimeFormatter dateTimeFormatter =
+                    DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
+            return new AbstractFormatter(pattern, locale, timeZone, precision) {
+                @Override
+                public void formatTo(final StringBuilder buffer, final Instant instant) {
+                    final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
+                    dateTimeFormatter.formatTo(instantAccessor, buffer);
+                }
+            };
+        }
+
+        @Override
         @Nullable
-        private static ChronoUnit contentPrecision(final String content) {
+        PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+            // We merge two DTF factories if they are both above or below the threshold
+            if (other instanceof DynamicPatternSequence) {
+                final DynamicPatternSequence otherDtf = (DynamicPatternSequence) other;
+                if (isConstantForDurationOf(thresholdPrecision)
+                        == otherDtf.isConstantForDurationOf(thresholdPrecision)) {
+                    ChronoUnit precision = this.precision.getDuration().compareTo(otherDtf.precision.getDuration()) < 0
+                            ? this.precision
+                            : otherDtf.precision;
+                    return new DynamicPatternSequence(this.pattern + otherDtf.pattern, precision);
+                }
+            }
+            // We merge a static pattern factory
+            if (other instanceof StaticPatternSequence) {
+                final StaticPatternSequence otherStatic = (StaticPatternSequence) other;
+                return new DynamicPatternSequence(this.pattern + otherStatic.pattern, this.precision);
+            }
+            return null;
+        }
 
-            validateContent(content);
-            final String paddingRemovedContent = removePadding(content);
+        /**
+         * @param singlePattern a single-letter directive singlePattern complying (e.g., {@code H}, {@code HH}, or {@code pHH})
+         * @return the time precision of the directive
+         */
+        private static ChronoUnit patternPrecision(final String singlePattern) {
 
-            if (paddingRemovedContent.matches("[GuyY]+")) {
+            validateContent(singlePattern);
+            final String paddingRemovedContent = removePadding(singlePattern);
+
+            if (paddingRemovedContent.matches("G+")) {
+                return ChronoUnit.ERAS;
+            } else if (paddingRemovedContent.matches("[uyY]+")) {
                 return ChronoUnit.YEARS;
             } else if (paddingRemovedContent.matches("[MLQq]+")) {
                 return ChronoUnit.MONTHS;
-            } else if (paddingRemovedContent.matches("[wW]+")) {
+            } else if (paddingRemovedContent.matches("w+")) {
                 return ChronoUnit.WEEKS;
-            } else if (paddingRemovedContent.matches("[DdgEecF]+")) {
+            } else if (paddingRemovedContent.matches("[DdgEecFW]+")) {
                 return ChronoUnit.DAYS;
-            } else if (paddingRemovedContent.matches("[aBhKkH]+")
-                    // Time-zone directives
-                    || paddingRemovedContent.matches("[ZxXOzvV]+")) {
+            } else if (paddingRemovedContent.matches("[aBhKkH]+")) {
                 return ChronoUnit.HOURS;
-            } else if (paddingRemovedContent.contains("m")) {
+            } else if (paddingRemovedContent.contains("m")
+                    // Time-zone directives
+                    || paddingRemovedContent.matches("[ZxXOzVv]+")) {
                 return ChronoUnit.MINUTES;
             } else if (paddingRemovedContent.contains("s")) {
                 return ChronoUnit.SECONDS;
             }
 
             // 2 to 3 consequent `S` characters output millisecond precision
-            else if (paddingRemovedContent.matches("S{2,3}")
+            else if (paddingRemovedContent.matches("S{1,3}")
                     // `A` (milli-of-day) outputs millisecond precision.
                     || paddingRemovedContent.contains("A")) {
                 return ChronoUnit.MILLIS;
@@ -759,17 +643,15 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
                 return ChronoUnit.MICROS;
             }
 
-            // A single `S` (fraction-of-second) outputs nanosecond precision
-            else if (paddingRemovedContent.equals("S")
-                    // 7 to 9 consequent `S` characters output nanosecond precision
-                    || paddingRemovedContent.matches("S{7,9}")
+            // 7 to 9 consequent `S` characters output nanosecond precision
+            else if (paddingRemovedContent.matches("S{7,9}")
                     // `n` (nano-of-second) and `N` (nano-of-day) always output nanosecond precision.
                     // This is independent of how many times they occur sequentially.
                     || paddingRemovedContent.matches("[nN]+")) {
                 return ChronoUnit.NANOS;
             }
 
-            final String message = String.format("unrecognized pattern: `%s`", content);
+            final String message = String.format("unrecognized pattern: `%s`", singlePattern);
             throw new IllegalArgumentException(message);
         }
 
@@ -806,26 +688,125 @@ final class InstantPatternDynamicFormatter implements InstantPatternFormatter {
         }
     }
 
-    static final class CompositePatternSequence extends PatternSequence {
+    static class SecondPatternSequence extends PatternSequence {
+
+        private static final int[] POWERS_OF_TEN = {
+            100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1
+        };
+
+        private final boolean printSeconds;
+        private final String separator;
+        private final int fractionalDigits;
+
+        SecondPatternSequence(boolean printSeconds, String separator, int fractionalDigits) {
+            super(
+                    createPattern(printSeconds, separator, fractionalDigits),
+                    determinePrecision(printSeconds, fractionalDigits));
+            this.printSeconds = printSeconds;
+            this.separator = separator;
+            this.fractionalDigits = fractionalDigits;
+        }
+
+        private static String createPattern(boolean printSeconds, String separator, int fractionalDigits) {
+            StringBuilder builder = new StringBuilder();
+            if (printSeconds) {
+                builder.append("ss");
+            }
+            builder.append(StaticPatternSequence.escapeLiteral(separator));
+            if (fractionalDigits > 0) {
+                builder.append(Strings.repeat("S", fractionalDigits));
+            }
+            return builder.toString();
+        }
+
+        private static ChronoUnit determinePrecision(boolean printSeconds, int digits) {
+            if (digits > 6) return ChronoUnit.NANOS;
+            if (digits > 3) return ChronoUnit.MICROS;
+            if (digits > 0) return ChronoUnit.MILLIS;
+            return printSeconds ? ChronoUnit.SECONDS : ChronoUnit.FOREVER;
+        }
+
+        private static void formatSeconds(StringBuilder buffer, Instant instant) {
+            long secondsInMinute = instant.getEpochSecond() % 60L;
+            buffer.append((char) ((secondsInMinute / 10L) + '0'));
+            buffer.append((char) ((secondsInMinute % 10L) + '0'));
+        }
 
-        CompositePatternSequence(final List<PatternSequence> sequences) {
-            super(concatSequencePatterns(sequences), findSequenceMaxPrecision(sequences));
-            // Only allow two or more sequences
-            if (sequences.size() < 2) {
-                throw new IllegalArgumentException("was expecting two or more sequences: " + sequences);
+        private void formatFractionalDigits(StringBuilder buffer, Instant instant) {
+            int nanos = instant.getNanoOfSecond();
+            // digits contain the first idx digits.
+            int digits;
+            // moreDigits contains the first (idx + 1) digits
+            int moreDigits = 0;
+            // Print the digits
+            for (int idx = 0; idx < fractionalDigits; idx++) {
+                digits = moreDigits;
+                moreDigits = nanos / POWERS_OF_TEN[idx];
+                buffer.append((char) ('0' + moreDigits - 10 * digits));
             }
         }
 
-        @SuppressWarnings("OptionalGetWithoutIsPresent")
-        private static ChronoUnit findSequenceMaxPrecision(List<PatternSequence> sequences) {
-            return sequences.stream()
-                    .map(sequence -> sequence.precision)
-                    .min(Comparator.comparing(ChronoUnit::getDuration))
-                    .get();
+        private static void formatMillis(StringBuilder buffer, Instant instant) {
+            int ms = instant.getNanoOfSecond() / 1_000_000;
+            int cs = ms / 10;
+            int ds = cs / 10;
+            buffer.append((char) ('0' + ds));
+            buffer.append((char) ('0' + cs - 10 * ds));
+            buffer.append((char) ('0' + ms - 10 * cs));
         }
 
-        private static String concatSequencePatterns(List<PatternSequence> sequences) {
-            return sequences.stream().map(sequence -> sequence.pattern).collect(Collectors.joining());
+        @Override
+        InstantPatternFormatter createFormatter(Locale locale, TimeZone timeZone) {
+            final BiConsumer<StringBuilder, Instant> fractionDigitsFormatter =
+                    fractionalDigits == 3 ? SecondPatternSequence::formatMillis : this::formatFractionalDigits;
+            if (!printSeconds) {
+                return new AbstractFormatter(pattern, locale, timeZone, precision) {
+                    @Override
+                    public void formatTo(StringBuilder buffer, Instant instant) {
+                        buffer.append(separator);
+                        fractionDigitsFormatter.accept(buffer, instant);
+                    }
+                };
+            }
+            if (fractionalDigits == 0) {
+                return new AbstractFormatter(pattern, locale, timeZone, precision) {
+                    @Override
+                    public void formatTo(StringBuilder buffer, Instant instant) {
+                        formatSeconds(buffer, instant);
+                        buffer.append(separator);
+                    }
+                };
+            }
+            return new AbstractFormatter(pattern, locale, timeZone, precision) {
+                @Override
+                public void formatTo(StringBuilder buffer, Instant instant) {
+                    formatSeconds(buffer, instant);
+                    buffer.append(separator);
+                    fractionDigitsFormatter.accept(buffer, instant);
+                }
+            };
+        }
+
+        @Override
+        @Nullable
+        PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+            // If we don't have a fractional part, we can merge a literal separator
+            if (other instanceof StaticPatternSequence) {
+                StaticPatternSequence staticOther = (StaticPatternSequence) other;
+                if (fractionalDigits == 0) {
+                    return new SecondPatternSequence(
+                            printSeconds, this.separator + staticOther.literal, fractionalDigits);
+                }
+            }
+            // We can always append more fractional digits
+            if (other instanceof SecondPatternSequence) {
+                SecondPatternSequence secondOther = (SecondPatternSequence) other;
+                if (!secondOther.printSeconds && secondOther.separator.isEmpty()) {
+                    return new SecondPatternSequence(
+                            printSeconds, this.separator, this.fractionalDigits + secondOther.fractionalDigits);
+                }
+            }
+            return null;
         }
     }
 }
diff --git a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
index 6f20199718..ff79bfad9c 100644
--- a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
@@ -1263,6 +1263,13 @@ unit          = "unit" -> (
 rounded       = "rounded" -> boolean
 ----
 
+[NOTE]
+====
+The resolvers based on the `epochConfig` expression are garbage-free.
+
+The resolvers based on the `patternConfig` expression are low-garbage and generate temporary objects only once a minute.
+====
+
 .See examples
 [%collapsible]
 ====
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index a1e01e809d..ee9115a2ed 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -1571,7 +1571,9 @@ Format modifiers to control such things as field width, padding, left, and right
 
 |xref:#converter-date[%d +
 %date]
-|Only the predefined date formats (`DEFAULT`, `ISO8601`, `UNIX`, `UNIX_MILLIS`, etc.) are garbage-free
+|
+The numeric formats (`UNIX` and `UNIX_MILLIS`) are garbage-free.
+The remaining formats are low-garbage and only generate temporary objects once per minute.
 
 |xref:#converter-encode[%enc\{pattern} +
 %encode\{pattern}]