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) + } +} 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() + } +} 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) }) }