diff --git a/BENCHMARK.md b/BENCHMARK.md new file mode 100644 index 00000000..49129c96 --- /dev/null +++ b/BENCHMARK.md @@ -0,0 +1,28 @@ +# Benchmarks + +Benchmarks are set up for GeoJSON serialization and deserialization. + +## Running Benchmarks + +```shell +./gradlew :geojson:benchmark +``` + +This will run benchmarks on the JVM, NodeJS, and Kotlin/Native. + +These benchmarks measure the time taken to serialize and deserialize a `FeatureCollection` containing 15,000 randomly +generated features. +See [GeoJsonBenchmark.kt](geojson/src/commonBench/kotlin/io/github/dellisd/spatialk/geojson/GeoJsonBenchmark.kt) for +details. + +## Results + +All measurements are in ms/op (milliseconds per operation). Lower score is better. + +| Target | Serialization | Deserialization | +|---------------------|-----------------------|---------------------| +| JVM | `834.176 ± 55.719` | `77.700 ± 1.123` | +| JS | `3,287.40 ± 8.763` | `423.472 ± 17.805` | +| Native (`linuxX64`) | `5624.209 ± 1242.046` | `993.008 ± 243.719` | + +_Run on Ubuntu 20.04 (WSL2). 32GB RAM, 3.6 GHz 8-core Intel Core i7_ diff --git a/build.gradle.kts b/build.gradle.kts index f2f90483..45456ba6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,15 +21,20 @@ allprojects { detekt { buildUponDefaultConfig = true - reports { - html.enabled = true - } - input = files(rootProject.projectDir) + source = files( + project(":geojson").projectDir.resolve("src"), + project(":turf").projectDir.resolve("src") + ) } + tasks.withType { + buildUponDefaultConfig = true jvmTarget = "11" + reports { + html.required.set(true) + } } tasks.dokkaGfmMultiModule.configure { diff --git a/geojson/build.gradle.kts b/geojson/build.gradle.kts index 34383fa0..6b847bfd 100644 --- a/geojson/build.gradle.kts +++ b/geojson/build.gradle.kts @@ -3,49 +3,32 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.dokka) alias(libs.plugins.publish) + alias(libs.plugins.kotlinx.benchmark) } kotlin { - jvm() + jvm { + compilations.create("bench") + } js { browser { } nodejs { } + + compilations.create("bench") } // For ARM, should be changed to iosArm32 or iosArm64 // For Linux, should be changed to e.g. linuxX64 // For MacOS, should be changed to e.g. macosX64 // For Windows, should be changed to e.g. mingwX64 - linuxX64("native") + linuxX64("native") { + compilations.create("bench") + } mingwX64("mingw") macosX64("macos") ios("ios") - sourceSets["commonMain"].dependencies { - api(libs.kotlinx.serialization) - } - - sourceSets["commonTest"].dependencies { - implementation(kotlin("test")) - implementation(kotlin("test-annotations-common")) - } - - sourceSets["jvmMain"].dependencies { - } - - sourceSets["jvmTest"].dependencies { - } - - sourceSets["jsMain"].dependencies { - } - - sourceSets["jsTest"].dependencies { - } - - sourceSets["nativeMain"].dependencies {} - sourceSets["nativeTest"].dependencies {} - sourceSets { all { with(languageSettings) { @@ -56,15 +39,71 @@ kotlin { } } - val nativeMain by getting {} - getByName("macosMain").dependsOn(nativeMain) - getByName("iosMain").dependsOn(nativeMain) - getByName("mingwMain").dependsOn(nativeMain) + val commonMain by getting { + dependencies { + api(libs.kotlinx.serialization) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-annotations-common")) + } + } + + val jsMain by getting {} + + val jvmMain by getting {} + + val nativeMain by getting { + getByName("macosMain").dependsOn(this) + getByName("iosMain").dependsOn(this) + getByName("mingwMain").dependsOn(this) + } + + val nativeTest by getting { + getByName("macosTest").dependsOn(this) + getByName("iosTest").dependsOn(this) + getByName("mingwTest").dependsOn(this) + } + + val commonBench by creating { + dependsOn(commonMain) + dependencies { + implementation(libs.kotlinx.benchmark) + } + } + + val jsBench by getting { + dependsOn(commonBench) + dependsOn(jsMain) + } + + val jvmBench by getting { + dependsOn(commonBench) + dependsOn(jvmMain) + } + + val nativeBench by getting { + dependsOn(commonBench) + dependsOn(nativeMain) + } + } +} + +benchmark { + this.configurations { + getByName("main") { + iterations = 5 + } + } - val nativeTest by getting {} - getByName("macosTest").dependsOn(nativeTest) - getByName("iosTest").dependsOn(nativeTest) - getByName("mingwTest").dependsOn(nativeTest) + targets { + register("jvmBench") + // Broken on 1.6.20??? + // register("jsBench") + register("nativeBench") } } diff --git a/geojson/src/commonBench/kotlin/io/github/dellisd/spatialk/geojson/GeoJsonBenchmark.kt b/geojson/src/commonBench/kotlin/io/github/dellisd/spatialk/geojson/GeoJsonBenchmark.kt new file mode 100644 index 00000000..dc68fe1c --- /dev/null +++ b/geojson/src/commonBench/kotlin/io/github/dellisd/spatialk/geojson/GeoJsonBenchmark.kt @@ -0,0 +1,76 @@ +@file:Suppress("MagicNumber") + +package io.github.dellisd.spatialk.geojson + +import io.github.dellisd.spatialk.geojson.FeatureCollection.Companion.toFeatureCollection +import io.github.dellisd.spatialk.geojson.dsl.feature +import io.github.dellisd.spatialk.geojson.dsl.featureCollection +import io.github.dellisd.spatialk.geojson.dsl.lineString +import io.github.dellisd.spatialk.geojson.dsl.point +import io.github.dellisd.spatialk.geojson.dsl.polygon +import kotlinx.benchmark.Benchmark +import kotlinx.benchmark.BenchmarkMode +import kotlinx.benchmark.BenchmarkTimeUnit +import kotlinx.benchmark.Mode +import kotlinx.benchmark.OutputTimeUnit +import kotlinx.benchmark.Scope +import kotlinx.benchmark.Setup +import kotlinx.benchmark.State +import kotlin.random.Random + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(BenchmarkTimeUnit.MILLISECONDS) +open class GeoJsonBenchmark { + private lateinit var dataset: FeatureCollection + private lateinit var geojson: String + + private fun generateDataset(): FeatureCollection { + val random = Random(0) + return featureCollection { + repeat(5000) { + +feature { + geometry = point(random.nextDouble(360.0) - 180, random.nextDouble(360.0) - 180) + } + } + + repeat(5000) { + +feature { + geometry = lineString { + repeat(10) { + +Position(random.nextDouble(360.0) - 180, random.nextDouble(360.0) - 180) + } + } + } + } + + repeat(5000) { + +feature { + geometry = polygon { + ring { + repeat(10) { + +Position(random.nextDouble(360.0) - 180, random.nextDouble(360.0) - 180) + } + } + } + } + } + } + } + + @Setup + fun setup() { + dataset = generateDataset() + geojson = dataset.json + } + + @Benchmark + fun serialization() { + dataset.json + } + + @Benchmark + fun deserialization() { + geojson.toFeatureCollection() + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a69c8c0f..d2bbfda6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,19 @@ [versions] kotlin = "1.6.20" resources = "0.2.1" +benchmark = "0.4.2" [libraries] resources = { module = "com.goncalossilva:resources", version.ref = "resources" } kotlinx-serialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" +kotlinx-benchmark = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "benchmark" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "benchmark" } publish = { id = "com.vanniktech.maven.publish", version = "0.19.0" } -detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.17.0" } +detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.20.0" } dokka = { id = "org.jetbrains.dokka", version = "1.4.32" } resources = { id = "com.goncalossilva.resources", version.ref = "resources" } \ No newline at end of file diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index e41ad303..d0cfc79e 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -294,6 +294,14 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== +benchmark@*: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -582,6 +590,14 @@ dom-serialize@^2.2.1: extend "^3.0.0" void-elements "^2.0.0" +dukat@0.5.8-rc.4: + version "0.5.8-rc.4" + resolved "https://registry.yarnpkg.com/dukat/-/dukat-0.5.8-rc.4.tgz#90384dcb50b14c26f0e99dae92b2dea44f5fce21" + integrity sha512-ZnMt6DGBjlVgK2uQamXfd7uP/AxH7RqI0BL9GLrrJb2gKdDxvJChWy+M9AQEaL+7/6TmxzJxFOsRiInY9oGWTA== + dependencies: + google-protobuf "3.12.2" + typescript "3.9.5" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -865,6 +881,11 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +google-protobuf@3.12.2: + version "3.12.2" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.12.2.tgz#50ce9f9b6281235724eb243d6a83e969a2176e53" + integrity sha512-4CZhpuRr1d6HjlyrxoXoocoGFnRYgKULgMtikMddA9ztRyYR59Aondv2FioyxWVamRo0rF2XpYawkTCBEQOSkA== + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -1153,7 +1174,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1418,6 +1439,11 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -1764,6 +1790,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typescript@3.9.5: + version "3.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36" + integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ== + ua-parser-js@^0.7.28: version "0.7.31" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"