From 4f8a1e1cf275586c3ce783a67a4cc7d559616ac7 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 3 Nov 2025 18:31:18 +0400 Subject: [PATCH 1/7] Optimize parser performance by introducing `ConcatenatedListView` - Replace direct list concatenation in `ParserStructure.append()` with `ConcatenatedListView` for improved efficiency. - Add `ConcatenatedListView` implementation to lazily combine two lists without creating a new collection. --- .../internal/format/parser/ConcatenatedListView.kt | 13 +++++++++++++ core/common/src/internal/format/parser/Parser.kt | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) 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..b7f44b2a1 --- /dev/null +++ b/core/common/src/internal/format/parser/ConcatenatedListView.kt @@ -0,0 +1,13 @@ +/* + * 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] +} diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index 9958e3fb9..27fc9d277 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -44,7 +44,7 @@ internal class ParserStructure( // 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) + ParserStructure(ConcatenatedListView(operations, other.operations), other.followedBy) } else { ParserStructure(operations, followedBy.map { it.append(other) }) } From d8fae29ef4acdfcebfd9db9e8ee2b1e3a64864d9 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Thu, 6 Nov 2025 17:29:33 +0400 Subject: [PATCH 2/7] Add JMH benchmark for parser structure formatting --- .../kotlin/ParserStructureConcatBenchmark.kt | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt new file mode 100644 index 000000000..b70def048 --- /dev/null +++ b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.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 ParserStructureConcatBenchmark { + + @Benchmark + fun buildFormat(blackhole: Blackhole) { + val v = LocalDateTime.Format { + year() + char('-') + monthNumber() + char('-') + day() + char(' ') + hour() + char(':') + minute() + optional { + char(':') + second() + optional { + char('.') + secondFraction() + } + } + } + blackhole.consume(v) + } +} From 71d545de92fe05b55e2aa5c72028c696a7cab9a2 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 10 Nov 2025 13:30:34 +0400 Subject: [PATCH 3/7] Rename `buildFormat` to `buildPythonDateTimeFormat` for improved readability and specificity. --- benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt index b70def048..2f7a914a1 100644 --- a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt @@ -22,7 +22,7 @@ import java.util.concurrent.* open class ParserStructureConcatBenchmark { @Benchmark - fun buildFormat(blackhole: Blackhole) { + fun buildPythonDateTimeFormat(blackhole: Blackhole) { val v = LocalDateTime.Format { year() char('-') From 4e59e09f61d6f869b30366fee4fd34c5ebb39e3e Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 10 Nov 2025 14:12:00 +0400 Subject: [PATCH 4/7] Add parameterized benchmark `largeSerialFormat` to test parser structure formatting scalability. --- .../kotlin/ParserStructureConcatBenchmark.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt index 2f7a914a1..8fbd6bb9c 100644 --- a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt @@ -21,6 +21,30 @@ import java.util.concurrent.* @Fork(1) open class ParserStructureConcatBenchmark { + @Param("1", "2", "4", "8", "16") + var n = 0 + + @Benchmark + fun largeSerialFormat(blackhole: Blackhole) { + val format = LocalDateTime.Format { + repeat(n) { + year() + char('-') + monthNumber() + char('-') + day() + char(' ') + hour() + char(':') + minute() + char(':') + second() + char('_') + } + } + blackhole.consume(format) + } + @Benchmark fun buildPythonDateTimeFormat(blackhole: Blackhole) { val v = LocalDateTime.Format { From 5630daf07d0ae22167c46e73e1b639a5cddd5bf5 Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 10 Nov 2025 16:28:24 +0400 Subject: [PATCH 5/7] Add iterator implementation for `ConcatenatedListView` - Introduce `ConcatenatedListViewIterator` to enable iteration without materializing combined lists. - Optimize nested list handling by directly traversing inner `ConcatenatedListView` instances. --- .../format/parser/ConcatenatedListView.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/common/src/internal/format/parser/ConcatenatedListView.kt b/core/common/src/internal/format/parser/ConcatenatedListView.kt index b7f44b2a1..3f11c3826 100644 --- a/core/common/src/internal/format/parser/ConcatenatedListView.kt +++ b/core/common/src/internal/format/parser/ConcatenatedListView.kt @@ -10,4 +10,32 @@ internal class ConcatenatedListView(val list1: List, val list2: List) : 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 39dd4570e7a2b0502b356f4c023d68722e7ccc3c Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 10 Nov 2025 16:29:28 +0400 Subject: [PATCH 6/7] Expand `largeSerialFormat` benchmark parameters and update character tokens in format definition --- .../jmh/kotlin/ParserStructureConcatBenchmark.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt index 8fbd6bb9c..80a622cfd 100644 --- a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt @@ -21,25 +21,24 @@ import java.util.concurrent.* @Fork(1) open class ParserStructureConcatBenchmark { - @Param("1", "2", "4", "8", "16") + @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) { - year() - char('-') + char('^') monthNumber() - char('-') + char('&') day() - char(' ') + char('!') hour() - char(':') + char('$') minute() - char(':') + char('#') second() - char('_') + char('@') } } blackhole.consume(format) From bc2a745c6dbbde653a5cfd9d09132eed886aa44a Mon Sep 17 00:00:00 2001 From: Dmitry Nekrasov Date: Mon, 10 Nov 2025 17:52:45 +0400 Subject: [PATCH 7/7] Move largeSerialFormat benchmark to the separate file --- ...rk.kt => PythonDateTimeFormatBenchmark.kt} | 25 +---------- .../src/jmh/kotlin/SerialFormatBenchmark.kt | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 24 deletions(-) rename benchmarks/src/jmh/kotlin/{ParserStructureConcatBenchmark.kt => PythonDateTimeFormatBenchmark.kt} (64%) create mode 100644 benchmarks/src/jmh/kotlin/SerialFormatBenchmark.kt diff --git a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt similarity index 64% rename from benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt rename to benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt index 80a622cfd..24e3bbfcb 100644 --- a/benchmarks/src/jmh/kotlin/ParserStructureConcatBenchmark.kt +++ b/benchmarks/src/jmh/kotlin/PythonDateTimeFormatBenchmark.kt @@ -19,30 +19,7 @@ import java.util.concurrent.* @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @Fork(1) -open class ParserStructureConcatBenchmark { - - @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) - } +open class PythonDateTimeFormatBenchmark { @Benchmark fun buildPythonDateTimeFormat(blackhole: Blackhole) { 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) + } +}