Skip to content

Commit

Permalink
Add benchmarks (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
05nelsonm authored Dec 16, 2024
1 parent de95c9d commit cbd9fb1
Show file tree
Hide file tree
Showing 22 changed files with 2,062 additions and 74 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,33 @@ jobs:
name: test-report-${{ matrix.os }}-java${{ matrix.java-version }}
path: '**/build/reports/tests/**'
retention-days: 1

benchmark:
strategy:
fail-fast: false
matrix:
os: [ macos-latest, ubuntu-latest, windows-latest ]
target: [ jvm, js, wasmJs, nativeHost ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v3

- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 11

- name: Run Benchmark
run: >
./gradlew ${{ matrix.target }}Benchmark
- name: Upload Benchmark Reports
uses: actions/upload-artifact@v4
with:
name: benchmark-report-${{ matrix.os }}-${{ matrix.target }}
path: '**/build/reports/benchmarks/**'
1,566 changes: 1,557 additions & 9 deletions .kotlin-js-store/yarn.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions benchmarks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
28 changes: 28 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# benchmarks

Benchmarks for tracking performance of `encoding` implementation.

- Run All platforms:
```shell
./gradlew benchmark
```

- Run Jvm:
```shell
./gradlew benchmark -PKMP_TARGETS="JVM"
```

- Run Js:
```shell
./gradlew benchmark -PKMP_TARGETS="JS"
```

- Run WasmJs:
```shell
./gradlew benchmark -PKMP_TARGETS="WASM_JS"
```

- Run Native:
```shell
./gradlew benchmark -PKMP_TARGETS="LINUX_ARM64,LINUX_X64,MACOS_ARM64,MACOS_X64,MINGW_X64"
```
65 changes: 65 additions & 0 deletions benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
import io.matthewnelson.kmp.configuration.extension.container.target.KmpTarget
import kotlinx.benchmark.gradle.BenchmarksExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget

plugins {
id("configuration")
}

kmpConfiguration {
val benchmarks by lazy { extensions.getByType<BenchmarksExtension>() }

@OptIn(ExperimentalWasmDsl::class)
configure {
fun <T: KotlinTarget> KmpTarget<T>.register() {
target { benchmarks.targets.register(name) }
}

jvm { register() }

js { target { browser(); nodejs() }; register() }
wasmJs { target { browser(); nodejs() }; register() }

val nativeHost = "nativeHost"
when (HostManager.host) {
is KonanTarget.LINUX_X64 -> linuxX64(nativeHost) { register() }
is KonanTarget.LINUX_ARM64 -> linuxArm64(nativeHost) { register() }
is KonanTarget.MACOS_X64 -> macosX64(nativeHost) { register() }
is KonanTarget.MACOS_ARM64 -> macosArm64(nativeHost) { register() }
is KonanTarget.MINGW_X64 -> mingwX64(nativeHost) { register() }
else -> {}
}

common {
pluginIds(libs.plugins.benchmark.get().pluginId)

sourceSetMain {
dependencies {
implementation(libs.benchmark.runtime)
implementation(libs.immutable.collections)
implementation(project(":library:base16"))
implementation(project(":library:base32"))
implementation(project(":library:base64"))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.encoding.benchmarks

import io.matthewnelson.encoding.core.util.CTCase
import kotlinx.benchmark.*

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 3)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
open class CTCaseBenchmark {

private val case = CTCase("ABCDEFGH")

@Benchmark
fun lowercaseFirst(): Char = case.lowercase('A')!!
@Benchmark
fun lowercaseLast(): Char = case.lowercase('H')!!

@Benchmark
fun uppercaseFirst(): Char = case.uppercase('a')!!
@Benchmark
fun uppercaseLast(): Char = case.uppercase('h')!!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.encoding.benchmarks

const val ENC_ITERATIONS_WARMUP = 5
const val ENC_ITERATIONS_MEASURE = 5
const val ENC_TIME_WARMUP = 1
const val ENC_TIME_MEASURE = 3

const val TIME_QUICK = "quick-time"
const val TIME_CONST = "const-time"
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.encoding.benchmarks

import io.matthewnelson.encoding.core.util.DecoderAction
import kotlinx.benchmark.*

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 3)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
open class DecoderActionBenchmark {

@Param(TIME_QUICK, TIME_CONST)
var params: String = "-"

private var isConstantTime = false
private val parser = DecoderAction.Parser(
'0'..'9' to DecoderAction { 0 },
'A'..'F' to DecoderAction { 0 },
)

@Setup
fun setup() {
isConstantTime = params == TIME_CONST
}

@Benchmark
fun actionFirst() = parser.parse('0', isConstantTime)

@Benchmark
fun actionLast() = parser.parse('F', isConstantTime)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2024 Matthew Nelson
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
package io.matthewnelson.encoding.benchmarks

import io.matthewnelson.encoding.base16.Base16
import io.matthewnelson.encoding.base32.Base32
import io.matthewnelson.encoding.base32.Base32Crockford
import io.matthewnelson.encoding.base32.Base32Default
import io.matthewnelson.encoding.base32.Base32Hex
import io.matthewnelson.encoding.base64.Base64
import io.matthewnelson.encoding.core.Decoder
import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray
import io.matthewnelson.encoding.core.Encoder
import io.matthewnelson.encoding.core.EncoderDecoder
import kotlinx.benchmark.*

abstract class EncoderDecoderBenchmarkBase {

// "<Characters>:<isConstantTime>"
abstract var params: String
protected abstract fun encoder(isConstantTime: Boolean): EncoderDecoder<*>

private var bytes = ByteArray(0)
private var chars = "_"
private var feedDecoder: Decoder<*>.Feed = Base16.newDecoderFeed {}.apply { close() }
private var feedEncoder: Encoder<*>.Feed = Base16.newEncoderFeed {}.apply { close() }

@Setup
fun setup() {
val (chars, isConstantTime) = params.split(':').let {
it[0] to (it[1] == TIME_CONST)
}
val encoder = encoder(isConstantTime)

val (cLength, bSize) = when (encoder) {
is Base16 -> 2 to 1
is Base32<*> -> 8 to 5
is Base64 -> 4 to 3
else -> error("Unknown encoder >> $encoder")
}

this.bytes = chars.decodeToByteArray(encoder)
this.chars = chars
require(this.bytes.size == bSize) {
"bytes.size[${this.bytes.size}] did not match expected size[$bSize] for $encoder"
}
require(this.chars.length == cLength) {
"chars.length[${this.chars.length}] did not match expected length[$cLength] for $encoder"
}
feedDecoder = encoder.newDecoderFeed {}
feedEncoder = encoder.newEncoderFeed {}
}

@Benchmark
fun decode() {
chars.forEach { c -> feedDecoder.consume(c) }
}

@Benchmark
fun encode() {
bytes.forEach { b -> feedEncoder.consume(b) }
}
}

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
@Warmup(iterations = ENC_ITERATIONS_WARMUP, time = ENC_TIME_WARMUP)
@Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE)
open class Base16Benchmark: EncoderDecoderBenchmarkBase() {
// CHARS: 0123456789ABCDEF
@Param("0A:$TIME_QUICK", "E8:$TIME_QUICK", "0A:$TIME_CONST", "E8:$TIME_CONST")
override var params: String = "<Characters>:<isConstantTime>"
override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> {
return Base16 { this.isConstantTime = isConstantTime }
}
}

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
@Warmup(iterations = ENC_ITERATIONS_WARMUP, time = ENC_TIME_WARMUP)
@Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE)
open class Base32CrockfordBenchmark: EncoderDecoderBenchmarkBase() {
// CHARS: 0123456789ABCDEFGHJKMNPQRSTVWXYZ
@Param("0AC3DFJ7:$TIME_QUICK", "T9WYR8SZ:$TIME_QUICK", "0AC3DFJ7:$TIME_CONST", "T9WYR8SZ:$TIME_CONST")
override var params: String = "<Characters>:<isConstantTime>"
override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> {
return Base32Crockford { this.isConstantTime = isConstantTime }
}
}

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
@Warmup(iterations = ENC_ITERATIONS_WARMUP, time = ENC_TIME_WARMUP)
@Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE)
open class Base32DefaultBenchmark: EncoderDecoderBenchmarkBase() {
// CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
@Param("CA2EF3CB:$TIME_QUICK", "WSY2V4ZZ:$TIME_QUICK", "CA2EF3CB:$TIME_CONST", "WSY2V4ZZ:$TIME_CONST")
override var params: String = "<Characters>:<isConstantTime>"
override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> {
return Base32Default { this.isConstantTime = isConstantTime }
}
}

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
@Warmup(iterations = ENC_ITERATIONS_WARMUP, time = ENC_TIME_WARMUP)
@Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE)
open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() {
// CHARS: 0123456789ABCDEFGHIJKLMNOPQRSTUV
@Param("A3B4CC2A:$TIME_QUICK", "V7RS4JM6:$TIME_QUICK", "A3B4CC2A:$TIME_CONST", "V7RS4JM6:$TIME_CONST")
override var params: String = "<Characters>:<isConstantTime>"
override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> {
return Base32Hex { this.isConstantTime = isConstantTime }
}
}

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS)
@Warmup(iterations = ENC_ITERATIONS_WARMUP, time = ENC_TIME_WARMUP)
@Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE)
open class Base64Benchmark: EncoderDecoderBenchmarkBase() {
// CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
@Param("0CaI:$TIME_QUICK", "9tvw:$TIME_QUICK", "0CaI:$TIME_CONST", "9tvw:$TIME_CONST")
override var params: String = "<Characters>:<isConstantTime>"
override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> {
return Base64 { this.isConstantTime = isConstantTime }
}
}
Loading

0 comments on commit cbd9fb1

Please sign in to comment.