From b8bbed8f91cb15c2fb2d2b60d38e16c6cf13b3ae Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:23:18 +0400 Subject: [PATCH 01/32] Add JMH benchmarks for datetime format creation and performance evaluation - Implement SerialFormatBenchmark to test repeated datetime format sequences. - Implement PythonDateTimeFormatBenchmark to evaluate Python-compatible datetime formats. - Implement ParallelFormatBenchmark to test creation of formats using nested and alternative parsing logic. --- .../src/jmh/kotlin/ParallelFormatBenchmark.kt | 88 +++++++++++++++++++ .../kotlin/PythonDateTimeFormatBenchmark.kt | 47 ++++++++++ .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 ++++++++++ 3 files changed, 180 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt create mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt new file mode 100644 index 000000000..07bc7eed7 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.alternativeParsing +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class ParallelFormatBenchmark { + + @Param("2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12") + var n = 0 + + @Benchmark + fun formatCreationWithAlternativeParsing(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + alternativeParsing( + { monthNumber() }, + { day() }, + primaryFormat = { hour() } + ) + char('@') + minute() + char('#') + second() + } + } + blackhole.consume(format) + } + + @Benchmark + fun formatCreationWithNestedAlternativeParsing(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { index -> + alternativeParsing( + { monthNumber(); char('-'); day() }, + { day(); char('/'); monthNumber() }, + primaryFormat = { year(); char('-'); monthNumber(); char('-'); day() } + ) + + if (index and 1 == 0) { + alternativeParsing( + { + alternativeParsing( + { hour(); char(':'); minute() }, + { minute(); char(':'); second() }, + primaryFormat = { hour(); char(':'); minute(); char(':'); second() } + ) + }, + primaryFormat = { + year(); char('-'); monthNumber(); char('-'); day() + char('T') + hour(); char(':'); minute(); char(':'); second() + } + ) + } + + char('|') + if (index % 3 == 0) { + char('|') + } + + if (index and 2 == 0) { + alternativeParsing( + { char('Z') }, + { char('+'); hour(); char(':'); minute() }, + primaryFormat = { char('-'); hour(); char(':'); minute() } + ) + } + } + } + blackhole.consume(format) + } +} diff --git a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt new file mode 100644 index 000000000..24e3bbfcb --- /dev/null +++ b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import kotlinx.datetime.format.optional +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.* + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class PythonDateTimeFormatBenchmark { + + @Benchmark + fun buildPythonDateTimeFormat(blackhole: Blackhole) { + val v = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + day() + char(' ') + hour() + char(':') + minute() + optional { + char(':') + second() + optional { + char('.') + secondFraction() + } + } + } + blackhole.consume(v) + } +} diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt new file mode 100644 index 000000000..fb63f577e --- /dev/null +++ b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class SerialFormatBenchmark { + + @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") + var n = 0 + + @Benchmark + fun largeSerialFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + char('^') + monthNumber() + char('&') + day() + char('!') + hour() + char('$') + minute() + char('#') + second() + char('@') + } + } + blackhole.consume(format) + } +} From 7fcb69947f650f63cf15af65445d90494abe93db Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:23:35 +0400 Subject: [PATCH 02/32] Introduce `ConcatenatedListView` for efficient list concatenation in internal datetime parsing logic. --- .../format/parser/ConcatenatedListView.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 core/common/src/internal/format/parser/ConcatenatedListView.kt diff --git a/core/common/src/internal/format/parser/ConcatenatedListView.kt b/core/common/src/internal/format/parser/ConcatenatedListView.kt new file mode 100644 index 000000000..3f11c3826 --- /dev/null +++ b/core/common/src/internal/format/parser/ConcatenatedListView.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.internal.format.parser + +internal class ConcatenatedListView(val list1: List, val list2: List) : AbstractList() { + override val size: Int + get() = list1.size + list2.size + + override fun get(index: Int): T = if (index < list1.size) list1[index] else list2[index - list1.size] + + override fun iterator(): Iterator = ConcatenatedListViewIterator() + + private inner class ConcatenatedListViewIterator : Iterator { + private val iterators: List> = buildList { + collectIterators(list1) + collectIterators(list2) + } + private var index = 0 + + private fun MutableList>.collectIterators(list: List) { + if (list is ConcatenatedListView) { + collectIterators(list.list1) + collectIterators(list.list2) + } else { + add(list.iterator()) + } + } + + override fun hasNext(): Boolean { + while (index < iterators.size && !iterators[index].hasNext()) { + index++ + } + return index < iterators.size + } + + override fun next(): T = iterators[index].next() + } +} From 941e426add20a80a50ad0f08ef36c1c23abc3188 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 12 Nov 2025 18:32:34 +0400 Subject: [PATCH 03/32] Remove redundant type parameter `` in `ParserStructure.simplify` method definition --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 9958e3fb9..f7b93c4ba 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,7 +49,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { + fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModificationsForTails = unconditionalModifications.toMutableList() From 2e096a680982eed07273fbce92e7ca57be3f1514 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:25:49 +0400 Subject: [PATCH 04/32] Add unconditionalModifications after each step of simplification --- .../src/internal/format/parser/Parser.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f7b93c4ba..30e75960d 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,10 +49,10 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(unconditionalModifications: List>): ParserStructure { + fun ParserStructure.simplify(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null - val unconditionalModificationsForTails = unconditionalModifications.toMutableList() + val unconditionalModificationsForTails = mutableListOf>() // joining together the number consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { @@ -73,7 +73,7 @@ internal fun List>.concat(): ParserStructure { } } val mergedTails = followedBy.flatMap { - val simplified = it.simplify(unconditionalModificationsForTails) + val simplified = it.simplify() // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, // unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d). // As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty @@ -82,25 +82,24 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - }.ifEmpty { - // preserving the invariant that `mergedTails` contains all unconditional modifications - listOf(ParserStructure(unconditionalModificationsForTails, emptyList())) } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt + newOperations.addAll(unconditionalModificationsForTails) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + newOperations.addAll(unconditionalModificationsForTails) ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { is NumberSpanParserOperation -> { ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + it.operations.drop( + listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModificationsForTails + it.operations.drop( 1 ), it.followedBy @@ -108,12 +107,12 @@ internal fun List>.concat(): ParserStructure { } null -> ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan)), + unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)), it.followedBy ) else -> ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, it.followedBy ) } @@ -122,7 +121,7 @@ internal fun List>.concat(): ParserStructure { } } val naiveParser = foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.append(acc) } - return naiveParser.simplify(emptyList()) + return naiveParser.simplify() } internal interface Copyable { From 4ad707b0f98478c3c38fed56035b9977d4161e67 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:37:26 +0400 Subject: [PATCH 05/32] Rename `unconditionalModificationsForTails` to `unconditionalModifications` for consistency and clarity in `simplify` method logic --- core/common/src/internal/format/parser/Parser.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 30e75960d..4f0d1138a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -52,7 +52,7 @@ internal fun List>.concat(): ParserStructure { fun ParserStructure.simplify(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null - val unconditionalModificationsForTails = mutableListOf>() + val unconditionalModifications = mutableListOf>() // joining together the number consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { @@ -63,7 +63,7 @@ internal fun List>.concat(): ParserStructure { currentNumberSpan = op.consumers.toMutableList() } } else if (op is UnconditionalModification) { - unconditionalModificationsForTails.add(op) + unconditionalModifications.add(op) } else { if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) @@ -85,21 +85,21 @@ internal fun List>.concat(): ParserStructure { } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt - newOperations.addAll(unconditionalModificationsForTails) + newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else if (mergedTails.none { it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) - newOperations.addAll(unconditionalModificationsForTails) + newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { is NumberSpanParserOperation -> { ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModificationsForTails + it.operations.drop( + listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModifications + it.operations.drop( 1 ), it.followedBy @@ -107,12 +107,12 @@ internal fun List>.concat(): ParserStructure { } null -> ParserStructure( - unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)), + unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)), it.followedBy ) else -> ParserStructure( - unconditionalModificationsForTails + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, it.followedBy ) } From d4a6b029734570a90a8219656bbe48bdf3277432 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:50:19 +0400 Subject: [PATCH 06/32] Refactor `ParserStructure` logic to use `buildList` for streamlined list construction and improved readability. --- .../src/internal/format/parser/Parser.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4f0d1138a..f83643059 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -97,22 +97,26 @@ internal fun List>.concat(): ParserStructure { } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> { - ParserStructure( - listOf(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + unconditionalModifications + it.operations.drop( - 1 - ), - it.followedBy - ) - } + is NumberSpanParserOperation -> ParserStructure(buildList(unconditionalModifications.size + it.operations.size) { + add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + addAll(unconditionalModifications) + addAll(it.operations.drop(1)) + }, it.followedBy) null -> ParserStructure( - unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)), + buildList(unconditionalModifications.size + 1) { + addAll(unconditionalModifications) + add(NumberSpanParserOperation(currentNumberSpan)) + }, it.followedBy ) else -> ParserStructure( - unconditionalModifications + listOf(NumberSpanParserOperation(currentNumberSpan)) + it.operations, + buildList(unconditionalModifications.size + 1 + it.operations.size) { + addAll(unconditionalModifications) + add(NumberSpanParserOperation(currentNumberSpan)) + addAll(it.operations) + }, it.followedBy ) } From 970068ced63ebdeec42ccdfac4d77cedad4ea826 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 13:55:29 +0400 Subject: [PATCH 07/32] Refactor `ParserStructure` instantiation to improve formatting and readability. --- core/common/src/internal/format/parser/Parser.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f83643059..e1fb7ab4e 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -97,11 +97,14 @@ internal fun List>.concat(): ParserStructure { } else { val newTails = mergedTails.map { when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> ParserStructure(buildList(unconditionalModifications.size + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - addAll(unconditionalModifications) - addAll(it.operations.drop(1)) - }, it.followedBy) + is NumberSpanParserOperation -> ParserStructure( + buildList(unconditionalModifications.size + it.operations.size) { + add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + addAll(unconditionalModifications) + addAll(it.operations.drop(1)) + }, + it.followedBy + ) null -> ParserStructure( buildList(unconditionalModifications.size + 1) { From 8f2bdebee63c854158cd952f781866225b2e278c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 19:22:37 +0400 Subject: [PATCH 08/32] Reorder `unconditionalModifications` placement in `ParserStructure`. --- core/common/src/internal/format/parser/Parser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e1fb7ab4e..66be1f065 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -108,16 +108,16 @@ internal fun List>.concat(): ParserStructure { null -> ParserStructure( buildList(unconditionalModifications.size + 1) { - addAll(unconditionalModifications) add(NumberSpanParserOperation(currentNumberSpan)) + addAll(unconditionalModifications) }, it.followedBy ) else -> ParserStructure( buildList(unconditionalModifications.size + 1 + it.operations.size) { - addAll(unconditionalModifications) add(NumberSpanParserOperation(currentNumberSpan)) + addAll(unconditionalModifications) addAll(it.operations) }, it.followedBy From c35a73d9096c489df0d64e6fec8198e5af8ac29e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 19:51:24 +0400 Subject: [PATCH 09/32] Passes all tests and it has benchmarc score increase on Python datetime format! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units PythonDateTimeFormatBenchmark.buildPythonDateTimeFormat avgt 5 4142.002 ± 374.247 ns/op --- .../src/internal/format/parser/Parser.kt | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 66be1f065..dbaa565ca 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,7 +49,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } - fun ParserStructure.simplify(): ParserStructure { + fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() @@ -72,8 +72,37 @@ internal fun List>.concat(): ParserStructure { newOperations.add(op) } } + + if (followedBy.isEmpty()) { + if (other.operations.isNotEmpty()) { + if (currentNumberSpan == null) { + val firstOperation = other.operations.first() + if (firstOperation is NumberSpanParserOperation) { + newOperations.add(other.operations.first()) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations.drop(1)) + } else { + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations) + } + } else { + val firstOperation = other.operations.first() + if (firstOperation is NumberSpanParserOperation) { + newOperations.add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations.drop(1)) + } else { + newOperations.add(NumberSpanParserOperation(currentNumberSpan)) + newOperations.addAll(unconditionalModifications) + newOperations.addAll(other.operations) + } + } + return ParserStructure(newOperations, other.followedBy) + } + } + val mergedTails = followedBy.flatMap { - val simplified = it.simplify() + val simplified = it.simplifyAndAppend(other) // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, // unless `p` is empty. For example, ((a|b)|(c|d)) is equivalent to (a|b|c|d). // As a special case, `ParserStructure(emptyList(), emptyList())` represents a parser that recognizes an empty @@ -82,7 +111,7 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - } + }.ifEmpty { other.followedBy } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -127,8 +156,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, newTails) } } - val naiveParser = foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.append(acc) } - return naiveParser.simplify() + return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } } internal interface Copyable { From 27bd65c428c9d8e69f503f3e8bb877d2d4efde59 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:02:18 +0400 Subject: [PATCH 10/32] Refactor `ParserStructure` logic to extract `mergeOperations` for cleaner handling of operation merging and reduce duplication. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark Mode Cnt Score Error Units PythonDateTimeFormatBenchmark.buildPythonDateTimeFormat avgt 5 3708.643 ± 29.908 ns/op --- .../src/internal/format/parser/Parser.kt | 100 ++++++++---------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index dbaa565ca..f65df6001 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -49,6 +49,43 @@ internal fun List>.concat(): ParserStructure { ParserStructure(operations, followedBy.map { it.append(other) }) } + fun mergeOperations( + baseOperations: List>, + numberSpan: List>?, + unconditionalModifications: List>, + operationsToMerge: List>, + followedBy: List> + ): ParserStructure { + val operations = buildList { + addAll(baseOperations) + when (val firstOperation = operationsToMerge.firstOrNull()) { + is NumberSpanParserOperation -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + } else { + add(firstOperation) + } + addAll(unconditionalModifications) + addAll(operationsToMerge.drop(1)) + } + null -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan)) + } + addAll(unconditionalModifications) + } + else -> { + if (numberSpan != null) { + add(NumberSpanParserOperation(numberSpan)) + } + addAll(unconditionalModifications) + addAll(operationsToMerge) + } + } + } + return ParserStructure(operations, followedBy) + } + fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null @@ -73,34 +110,6 @@ internal fun List>.concat(): ParserStructure { } } - if (followedBy.isEmpty()) { - if (other.operations.isNotEmpty()) { - if (currentNumberSpan == null) { - val firstOperation = other.operations.first() - if (firstOperation is NumberSpanParserOperation) { - newOperations.add(other.operations.first()) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations.drop(1)) - } else { - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations) - } - } else { - val firstOperation = other.operations.first() - if (firstOperation is NumberSpanParserOperation) { - newOperations.add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations.drop(1)) - } else { - newOperations.add(NumberSpanParserOperation(currentNumberSpan)) - newOperations.addAll(unconditionalModifications) - newOperations.addAll(other.operations) - } - } - return ParserStructure(newOperations, other.followedBy) - } - } - val mergedTails = followedBy.flatMap { val simplified = it.simplifyAndAppend(other) // parser `ParserStructure(emptyList(), p)` is equivalent to `p`, @@ -111,7 +120,12 @@ internal fun List>.concat(): ParserStructure { simplified.followedBy.ifEmpty { listOf(simplified) } else listOf(simplified) - }.ifEmpty { other.followedBy } + }.ifEmpty { + if (other.operations.isNotEmpty()) { + return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other.operations, other.followedBy) + } + other.followedBy + } return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -125,33 +139,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { - when (val firstOperation = it.operations.firstOrNull()) { - is NumberSpanParserOperation -> ParserStructure( - buildList(unconditionalModifications.size + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan + firstOperation.consumers)) - addAll(unconditionalModifications) - addAll(it.operations.drop(1)) - }, - it.followedBy - ) - - null -> ParserStructure( - buildList(unconditionalModifications.size + 1) { - add(NumberSpanParserOperation(currentNumberSpan)) - addAll(unconditionalModifications) - }, - it.followedBy - ) - - else -> ParserStructure( - buildList(unconditionalModifications.size + 1 + it.operations.size) { - add(NumberSpanParserOperation(currentNumberSpan)) - addAll(unconditionalModifications) - addAll(it.operations) - }, - it.followedBy - ) - } + mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it.operations, it.followedBy) } ParserStructure(newOperations, newTails) } From b6e80f877a285a3ce3906d69e61e2676c323c69e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:09:56 +0400 Subject: [PATCH 11/32] Refactor `ParserStructure.concat` logic to simplify `mergeOperations` usage by passing `ParserStructure` directly instead of separating operations and followedBy. --- .../src/internal/format/parser/Parser.kt | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index f65df6001..c75b92ab3 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -41,24 +41,16 @@ internal class ParserStructure( "${operations.joinToString(", ")}(${followedBy.joinToString(";")})" } -// TODO: O(size of the resulting parser ^ 2), but can be O(size of the resulting parser) internal fun List>.concat(): ParserStructure { - fun ParserStructure.append(other: ParserStructure): ParserStructure = if (followedBy.isEmpty()) { - ParserStructure(operations + other.operations, other.followedBy) - } else { - ParserStructure(operations, followedBy.map { it.append(other) }) - } - fun mergeOperations( baseOperations: List>, numberSpan: List>?, unconditionalModifications: List>, - operationsToMerge: List>, - followedBy: List> + simplifiedParserStructure: ParserStructure, ): ParserStructure { val operations = buildList { addAll(baseOperations) - when (val firstOperation = operationsToMerge.firstOrNull()) { + when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { is NumberSpanParserOperation -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) @@ -66,7 +58,7 @@ internal fun List>.concat(): ParserStructure { add(firstOperation) } addAll(unconditionalModifications) - addAll(operationsToMerge.drop(1)) + addAll(simplifiedParserStructure.operations.drop(1)) } null -> { if (numberSpan != null) { @@ -79,11 +71,11 @@ internal fun List>.concat(): ParserStructure { add(NumberSpanParserOperation(numberSpan)) } addAll(unconditionalModifications) - addAll(operationsToMerge) + addAll(simplifiedParserStructure.operations) } } } - return ParserStructure(operations, followedBy) + return ParserStructure(operations, simplifiedParserStructure.followedBy) } fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { @@ -122,7 +114,7 @@ internal fun List>.concat(): ParserStructure { listOf(simplified) }.ifEmpty { if (other.operations.isNotEmpty()) { - return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other.operations, other.followedBy) + return mergeOperations(newOperations, currentNumberSpan, unconditionalModifications, other) } other.followedBy } @@ -139,7 +131,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, mergedTails) } else { val newTails = mergedTails.map { - mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it.operations, it.followedBy) + mergeOperations(emptyList(), currentNumberSpan, unconditionalModifications, it) } ParserStructure(newOperations, newTails) } From 82ffb49e5ccfa9c76f8bc39502ae3b76f67c1ed5 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:12:13 +0400 Subject: [PATCH 12/32] Add missing newline --- core/common/src/internal/format/parser/Parser.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index c75b92ab3..2360ac0d2 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -118,6 +118,7 @@ internal fun List>.concat(): ParserStructure { } other.followedBy } + return if (currentNumberSpan == null) { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) @@ -136,6 +137,7 @@ internal fun List>.concat(): ParserStructure { ParserStructure(newOperations, newTails) } } + return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } } From 93accde5642f53e86e7bf9966f0c53f0392861fe Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Tue, 18 Nov 2025 20:14:54 +0400 Subject: [PATCH 13/32] Rename `operations` to `mergedOperations`. --- core/common/src/internal/format/parser/Parser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 2360ac0d2..4102c6f12 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -48,7 +48,7 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { - val operations = buildList { + val mergedOperations = buildList { addAll(baseOperations) when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { is NumberSpanParserOperation -> { @@ -75,7 +75,7 @@ internal fun List>.concat(): ParserStructure { } } } - return ParserStructure(operations, simplifiedParserStructure.followedBy) + return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } fun ParserStructure.simplifyAndAppend(other: ParserStructure): ParserStructure { From 17d0bb919faf5d3367323897dfaeefad15e8a1fb Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 12:52:25 +0400 Subject: [PATCH 14/32] Simplify the ` mergedTails ` condition. --- core/common/src/internal/format/parser/Parser.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4102c6f12..8d37c0dda 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -123,9 +123,7 @@ internal fun List>.concat(): ParserStructure { // the last operation was not a number span, or it was a number span that we are allowed to interrupt newOperations.addAll(unconditionalModifications) ParserStructure(newOperations, mergedTails) - } else if (mergedTails.none { - it.operations.firstOrNull()?.let { it is NumberSpanParserOperation } == true - }) { + } else if (mergedTails.none { it.operations.firstOrNull() is NumberSpanParserOperation }) { // the last operation was a number span, but there are no alternatives that start with a number span. newOperations.add(NumberSpanParserOperation(currentNumberSpan)) newOperations.addAll(unconditionalModifications) From 6e7d0531a0f4fb21e13536ed3a2b567f41e27e94 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:06:23 +0400 Subject: [PATCH 15/32] Reorder and simplify `unconditionalModifications` handling in `ParserStructure` logic. --- core/common/src/internal/format/parser/Parser.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 8d37c0dda..e4b62dbce 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -54,26 +54,24 @@ internal fun List>.concat(): ParserStructure { is NumberSpanParserOperation -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + addAll(simplifiedParserStructure.operations.drop(1)) } else { - add(firstOperation) + addAll(simplifiedParserStructure.operations) } - addAll(unconditionalModifications) - addAll(simplifiedParserStructure.operations.drop(1)) } null -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan)) } - addAll(unconditionalModifications) } else -> { if (numberSpan != null) { add(NumberSpanParserOperation(numberSpan)) } - addAll(unconditionalModifications) addAll(simplifiedParserStructure.operations) } } + addAll(unconditionalModifications) } return ParserStructure(mergedOperations, simplifiedParserStructure.followedBy) } From 9ce4e7750633e78fd8251a5eb5a71a8f587e9844 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:12:59 +0400 Subject: [PATCH 16/32] Refactor `ParserStructure` logic to streamline `mergedOperations` construction and simplify `NumberSpanParserOperation` handling. --- .../src/internal/format/parser/Parser.kt | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e4b62dbce..e0d709899 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -50,24 +50,17 @@ internal fun List>.concat(): ParserStructure { ): ParserStructure { val mergedOperations = buildList { addAll(baseOperations) - when (val firstOperation = simplifiedParserStructure.operations.firstOrNull()) { - is NumberSpanParserOperation -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(simplifiedParserStructure.operations.drop(1)) - } else { - addAll(simplifiedParserStructure.operations) - } + val firstOperation = simplifiedParserStructure.operations.firstOrNull() + when { + numberSpan == null -> { + addAll(simplifiedParserStructure.operations) } - null -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan)) - } + firstOperation is NumberSpanParserOperation -> { + add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) + addAll(simplifiedParserStructure.operations.drop(1)) } else -> { - if (numberSpan != null) { - add(NumberSpanParserOperation(numberSpan)) - } + add(NumberSpanParserOperation(numberSpan)) addAll(simplifiedParserStructure.operations) } } From 82e6929e3e12c05446dad874086c5743601681a5 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:37:22 +0400 Subject: [PATCH 17/32] =?UTF-8?q?Fix=20typo=20in=20comment:=20"number=20co?= =?UTF-8?q?nsumers"=20=E2=86=92=20"number=20of=20consumers".?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e0d709899..4f1d3be98 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -73,7 +73,7 @@ internal fun List>.concat(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null val unconditionalModifications = mutableListOf>() - // joining together the number consumers in this parser before the first alternative; + // joining together the number of consumers in this parser before the first alternative; // collecting the unconditional modifications to push them to the end of all the parser's branches. for (op in operations) { if (op is NumberSpanParserOperation) { From 64ea6efddcab6280949b8e6723f9886f3dda035c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 13:49:02 +0400 Subject: [PATCH 18/32] Refactor `mergedOperations` construction to reuse `operationsToMerge` and avoid duplication. --- core/common/src/internal/format/parser/Parser.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 4f1d3be98..0eabba3d1 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -48,20 +48,21 @@ internal fun List>.concat(): ParserStructure { unconditionalModifications: List>, simplifiedParserStructure: ParserStructure, ): ParserStructure { + val operationsToMerge = simplifiedParserStructure.operations + val firstOperation = operationsToMerge.firstOrNull() val mergedOperations = buildList { addAll(baseOperations) - val firstOperation = simplifiedParserStructure.operations.firstOrNull() when { numberSpan == null -> { - addAll(simplifiedParserStructure.operations) + addAll(operationsToMerge) } firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(simplifiedParserStructure.operations.drop(1)) + addAll(operationsToMerge.drop(1)) } else -> { add(NumberSpanParserOperation(numberSpan)) - addAll(simplifiedParserStructure.operations) + addAll(operationsToMerge) } } addAll(unconditionalModifications) From d97ef895aa91a6aa235ee5bd72c18a8eae5f652e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:37:37 +0400 Subject: [PATCH 19/32] Refactor `PythonDateTimeFormatBenchmark` into `CommonFormats`, update benchmarking parameters, and add ISO DateTime format benchmark. --- ...imeFormatBenchmark.kt => CommonFormats.kt} | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) rename benchmarks/src/jmh/kotlin/{PythonDateTimeFormatBenchmark.kt => CommonFormats.kt} (52%) diff --git a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt similarity index 52% rename from benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt rename to benchmarks/src/jmh/kotlin/CommonFormats.kt index 24e3bbfcb..5229a6d2e 100644 --- a/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,19 +7,20 @@ package kotlinx.datetime +import kotlinx.datetime.format.alternativeParsing import kotlinx.datetime.format.char import kotlinx.datetime.format.optional import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.* -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 20, time = 2) +@Measurement(iterations = 30, time = 2) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) -@Fork(1) -open class PythonDateTimeFormatBenchmark { +@Fork(2) +open class CommonFormats { @Benchmark fun buildPythonDateTimeFormat(blackhole: Blackhole) { @@ -44,4 +45,32 @@ open class PythonDateTimeFormatBenchmark { } blackhole.consume(v) } + + @Benchmark + fun buildIsoDateTimeFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + date(LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + day() + }) + alternativeParsing({ char('t') }) { char('T') } + time(LocalTime.Format { + hour() + char(':') + minute() + alternativeParsing({}) { + char(':') + second() + optional { + char('.') + secondFraction(1, 9) + } + } + }) + } + blackhole.consume(format) + } } From 423b3b312b20a53d494f0ae0aa5f6ff837777a6e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:45:48 +0400 Subject: [PATCH 20/32] Add benchmark for building four-digit UTC offset format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index 5229a6d2e..d7728cfeb 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -73,4 +73,13 @@ open class CommonFormats { } blackhole.consume(format) } + + @Benchmark + fun buildFourDigitsUtcOffsetFormat(blackhole: Blackhole) { + val format = UtcOffset.Format { + offsetHours() + offsetMinutesOfHour() + } + blackhole.consume(format) + } } From e14c8fb4a60c8f0613a4f167c8b21685e11dcf5d Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:54:48 +0400 Subject: [PATCH 21/32] Add benchmark for building RFC 1123 DateTime format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index d7728cfeb..e76065e30 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,6 +7,10 @@ package kotlinx.datetime +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.MonthNames +import kotlinx.datetime.format.Padding import kotlinx.datetime.format.alternativeParsing import kotlinx.datetime.format.char import kotlinx.datetime.format.optional @@ -82,4 +86,10 @@ open class CommonFormats { } blackhole.consume(format) } + + @Benchmark + fun buildRfc1123DateTimeFormat(blackhole: Blackhole) { + val format = DateTimeComponents.Formats.RFC_1123 + blackhole.consume(format) + } } From 6aa9898450c4aef384f00c5a7104660ec4da9ddf Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 19:58:31 +0400 Subject: [PATCH 22/32] Add benchmark for building ISO DateTime with offset format --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index e76065e30..12e831fb7 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -9,6 +9,7 @@ package kotlinx.datetime import kotlinx.datetime.format.DateTimeComponents import kotlinx.datetime.format.DayOfWeekNames +import kotlinx.datetime.format.ISO_DATE import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.Padding import kotlinx.datetime.format.alternativeParsing @@ -92,4 +93,37 @@ open class CommonFormats { val format = DateTimeComponents.Formats.RFC_1123 blackhole.consume(format) } + + @Benchmark + fun buildIsoDateTimeOffsetFormat(blackhole: Blackhole) { + val format = DateTimeComponents.Format { + date(LocalDate.Format { + year() + char('-') + monthNumber() + char('-') + day() + }) + alternativeParsing({ + char('t') + }) { + char('T') + } + hour() + char(':') + minute() + char(':') + second() + optional { + char('.') + secondFraction(1, 9) + } + alternativeParsing({ + offsetHours() + }) { + offset(UtcOffset.Formats.ISO) + } + } + blackhole.consume(format) + } } From 0c5a7a660640f93e2e309c855cf253bf0465d6d1 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:01:23 +0400 Subject: [PATCH 23/32] Refactor RFC 1123 and UTC offset format benchmarks to use inline format construction. --- benchmarks/src/jmh/kotlin/CommonFormats.kt | 58 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/CommonFormats.kt b/benchmarks/src/jmh/kotlin/CommonFormats.kt index 12e831fb7..1c383c608 100644 --- a/benchmarks/src/jmh/kotlin/CommonFormats.kt +++ b/benchmarks/src/jmh/kotlin/CommonFormats.kt @@ -7,14 +7,7 @@ package kotlinx.datetime -import kotlinx.datetime.format.DateTimeComponents -import kotlinx.datetime.format.DayOfWeekNames -import kotlinx.datetime.format.ISO_DATE -import kotlinx.datetime.format.MonthNames -import kotlinx.datetime.format.Padding -import kotlinx.datetime.format.alternativeParsing -import kotlinx.datetime.format.char -import kotlinx.datetime.format.optional +import kotlinx.datetime.format.* import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.* @@ -90,7 +83,40 @@ open class CommonFormats { @Benchmark fun buildRfc1123DateTimeFormat(blackhole: Blackhole) { - val format = DateTimeComponents.Formats.RFC_1123 + val format = DateTimeComponents.Format { + alternativeParsing({ + // the day of week may be missing + }) { + dayOfWeek(DayOfWeekNames.ENGLISH_ABBREVIATED) + chars(", ") + } + day(Padding.NONE) + char(' ') + monthName(MonthNames.ENGLISH_ABBREVIATED) + char(' ') + year() + char(' ') + hour() + char(':') + minute() + optional { + char(':') + second() + } + chars(" ") + alternativeParsing({ + chars("UT") + }, { + chars("Z") + }) { + optional("GMT") { + offset(UtcOffset.Format { + offsetHours() + offsetMinutesOfHour() + }) + } + } + } blackhole.consume(format) } @@ -121,7 +147,19 @@ open class CommonFormats { alternativeParsing({ offsetHours() }) { - offset(UtcOffset.Formats.ISO) + offset(UtcOffset.Format { + alternativeParsing({ chars("z") }) { + optional("Z") { + offsetHours() + char(':') + offsetMinutesOfHour() + optional { + char(':') + offsetSecondsOfMinute() + } + } + } + }) } } blackhole.consume(format) From 81266f43d8f2409c5b68d813f351df9c2ec48a4b Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:46:34 +0400 Subject: [PATCH 24/32] Remove `ConcatenatedListView` as it is no longer in use. --- .../format/parser/ConcatenatedListView.kt | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 core/common/src/internal/format/parser/ConcatenatedListView.kt diff --git a/core/common/src/internal/format/parser/ConcatenatedListView.kt b/core/common/src/internal/format/parser/ConcatenatedListView.kt deleted file mode 100644 index 3f11c3826..000000000 --- a/core/common/src/internal/format/parser/ConcatenatedListView.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019-2025 JetBrains s.r.o. and contributors. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -package kotlinx.datetime.internal.format.parser - -internal class ConcatenatedListView(val list1: List, val list2: List) : AbstractList() { - override val size: Int - get() = list1.size + list2.size - - override fun get(index: Int): T = if (index < list1.size) list1[index] else list2[index - list1.size] - - override fun iterator(): Iterator = ConcatenatedListViewIterator() - - private inner class ConcatenatedListViewIterator : Iterator { - private val iterators: List> = buildList { - collectIterators(list1) - collectIterators(list2) - } - private var index = 0 - - private fun MutableList>.collectIterators(list: List) { - if (list is ConcatenatedListView) { - collectIterators(list.list1) - collectIterators(list.list2) - } else { - add(list.iterator()) - } - } - - override fun hasNext(): Boolean { - while (index < iterators.size && !iterators[index].hasNext()) { - index++ - } - return index < iterators.size - } - - override fun next(): T = iterators[index].next() - } -} From 6095101e146aacf6eb7cd13a6c7aa656e37cba13 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:48:10 +0400 Subject: [PATCH 25/32] Remove `SerialFormatBenchmark` as it is no longer in use. --- .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt deleted file mode 100644 index fb63f577e..000000000 --- a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019-2025 JetBrains s.r.o. and contributors. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -@file:Suppress("unused") - -package kotlinx.datetime - -import kotlinx.datetime.format.char -import org.openjdk.jmh.annotations.* -import org.openjdk.jmh.infra.Blackhole -import java.util.concurrent.TimeUnit - -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -@Fork(1) -open class SerialFormatBenchmark { - - @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") - var n = 0 - - @Benchmark - fun largeSerialFormat(blackhole: Blackhole) { - val format = LocalDateTime.Format { - repeat(n) { - char('^') - monthNumber() - char('&') - day() - char('!') - hour() - char('$') - minute() - char('#') - second() - char('@') - } - } - blackhole.consume(format) - } -} From 28c36e677557f91ae99daa1d585715c835ba2547 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:49:16 +0400 Subject: [PATCH 26/32] Update `ParallelFormatBenchmark` warmup and measurement parameters. --- benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt index 07bc7eed7..c74dfaa02 100644 --- a/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/ParallelFormatBenchmark.kt @@ -13,8 +13,8 @@ import org.openjdk.jmh.annotations.* import org.openjdk.jmh.infra.Blackhole import java.util.concurrent.TimeUnit -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 1) +@Warmup(iterations = 10, time = 2) +@Measurement(iterations = 20, time = 2) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) From edd5d29bbf63b9739ef6e79c7c4d2f90ade928f0 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Wed, 19 Nov 2025 20:54:47 +0400 Subject: [PATCH 27/32] Update copyright year range in `Parser.kt` header. --- core/common/src/internal/format/parser/Parser.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 0eabba3d1..86bd19ee6 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 JetBrains s.r.o. and contributors. + * Copyright 2023-2025 JetBrains s.r.o. and contributors. * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. */ From cc93dc012b2cad8f76b2845c264527ab03c6402a Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:39:52 +0400 Subject: [PATCH 28/32] Optimize concatenation of flat parsers. --- .../src/internal/format/parser/Parser.kt | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 86bd19ee6..cebf3c76a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -128,7 +128,36 @@ internal fun List>.concat(): ParserStructure { } } - return foldRight(ParserStructure(emptyList(), emptyList())) { parser, acc -> parser.simplifyAndAppend(acc) } + var result = ParserStructure(emptyList(), emptyList()) + val flatParsers = mutableListOf>>() + + for (parser in this.asReversed()) { + if (parser.followedBy.isEmpty()) { + flatParsers.add(parser.operations) + } else { + if (flatParsers.isNotEmpty()) { + val operations = buildList() { + for (i in flatParsers.lastIndex downTo 0) { + addAll(flatParsers[i]) + } + } + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + flatParsers.clear() + } + result = parser.simplifyAndAppend(result) + } + } + + if (flatParsers.isNotEmpty()) { + val operations = buildList { + for (i in flatParsers.lastIndex downTo 0) { + addAll(flatParsers[i]) + } + } + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + } + + return result } internal interface Copyable { From 096f97072dd8540990b3d2b03323824b78ceee9c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:48:41 +0400 Subject: [PATCH 29/32] Refactor `Parser` to streamline handling of accumulated operations. --- .../src/internal/format/parser/Parser.kt | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index cebf3c76a..d27920290 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -129,34 +129,30 @@ internal fun List>.concat(): ParserStructure { } var result = ParserStructure(emptyList(), emptyList()) - val flatParsers = mutableListOf>>() + val accumulatedOperations = mutableListOf>>() - for (parser in this.asReversed()) { - if (parser.followedBy.isEmpty()) { - flatParsers.add(parser.operations) - } else { - if (flatParsers.isNotEmpty()) { - val operations = buildList() { - for (i in flatParsers.lastIndex downTo 0) { - addAll(flatParsers[i]) - } + fun flushAccumulatedOperations() { + if (accumulatedOperations.isNotEmpty()) { + val operations = buildList { + for (parserOperations in accumulatedOperations.asReversed()) { + addAll(parserOperations) } - result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) - flatParsers.clear() } - result = parser.simplifyAndAppend(result) + result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) + accumulatedOperations.clear() } } - if (flatParsers.isNotEmpty()) { - val operations = buildList { - for (i in flatParsers.lastIndex downTo 0) { - addAll(flatParsers[i]) - } + for (parser in this.asReversed()) { + if (parser.followedBy.isEmpty()) { + accumulatedOperations.add(parser.operations) + } else { + flushAccumulatedOperations() + result = parser.simplifyAndAppend(result) } - result = ParserStructure(operations, emptyList()).simplifyAndAppend(result) } + flushAccumulatedOperations() return result } From 61a5b066446c03ab5aca4c7c23a2faa465265f38 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 20 Nov 2025 16:56:57 +0400 Subject: [PATCH 30/32] Add `SerialFormatBenchmark` for evaluating large format serialization performance --- .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt new file mode 100644 index 000000000..d6eeaad03 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2025 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:Suppress("unused") + +package kotlinx.datetime + +import kotlinx.datetime.format.char +import org.openjdk.jmh.annotations.* +import org.openjdk.jmh.infra.Blackhole +import java.util.concurrent.TimeUnit + +@Warmup(iterations = 10, time = 2) +@Measurement(iterations = 20, time = 2) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Fork(1) +open class SerialFormatBenchmark { + + @Param("1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024") + var n = 0 + + @Benchmark + fun largeSerialFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + char('^') + monthNumber() + char('&') + day() + char('!') + hour() + char('$') + minute() + char('#') + second() + char('@') + } + } + blackhole.consume(format) + } +} From d33437311f3848d5c830d83e6c17ca0f8980ab16 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Fri, 21 Nov 2025 11:28:55 +0400 Subject: [PATCH 31/32] Refactor `Parser` to replace `drop(1)` with an explicit loop for merging operations. --- core/common/src/internal/format/parser/Parser.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index d27920290..e5d3f7382 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -58,7 +58,9 @@ internal fun List>.concat(): ParserStructure { } firstOperation is NumberSpanParserOperation -> { add(NumberSpanParserOperation(numberSpan + firstOperation.consumers)) - addAll(operationsToMerge.drop(1)) + for (i in 1..operationsToMerge.lastIndex) { + add(operationsToMerge[i]) + } } else -> { add(NumberSpanParserOperation(numberSpan)) From 31dfbe5854fd6fe32f1cfd7fccd4e47a865e535c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Fri, 21 Nov 2025 11:44:02 +0400 Subject: [PATCH 32/32] Refactor `Parser` to apply `unconditionalModifications` after processing `currentNumberSpan`. --- core/common/src/internal/format/parser/Parser.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index e5d3f7382..22901f38a 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -91,6 +91,8 @@ internal fun List>.concat(): ParserStructure { if (currentNumberSpan != null) { newOperations.add(NumberSpanParserOperation(currentNumberSpan)) currentNumberSpan = null + newOperations.addAll(unconditionalModifications) + unconditionalModifications.clear() } newOperations.add(op) }