From 43a1fc7a0aed9b8ada049de440ebc0d5b93bc50f Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Fri, 15 Jul 2022 22:21:30 +0800 Subject: [PATCH] build: benchmark tooling --- .gitignore | 1 + komga/build.gradle.kts | 34 +++++- .../komga/benchmark/AbstractBenchmark.kt | 55 +++++++++ .../komga/benchmark/BenchmarkProperties.kt | 26 +++++ .../benchmark/rest/AbstractRestBenchmark.kt | 46 ++++++++ .../benchmark/rest/DashboardBenchmark.kt | 107 ++++++++++++++++++ .../benchmark/rest/PaginationBenchmark.kt | 47 ++++++++ .../resources/application-benchmark.yml | 24 ++++ .../resources/junit-platform.properties | 1 + 9 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 komga/src/benchmark/kotlin/org/gotson/komga/benchmark/AbstractBenchmark.kt create mode 100644 komga/src/benchmark/kotlin/org/gotson/komga/benchmark/BenchmarkProperties.kt create mode 100644 komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/AbstractRestBenchmark.kt create mode 100644 komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/DashboardBenchmark.kt create mode 100644 komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/PaginationBenchmark.kt create mode 100644 komga/src/benchmark/resources/application-benchmark.yml create mode 100644 komga/src/benchmark/resources/junit-platform.properties diff --git a/.gitignore b/.gitignore index 06d5383b0..193572082 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ nbdist/ /komga/src/main/resources/public/ /config-dir/ application-oauth2.yml +/benchmark diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index 1563bfcca..1e8fbaabe 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -21,6 +21,20 @@ plugins { group = "org.gotson" +val benchmarkSourceSet = sourceSets.create("benchmark") { + java { + compileClasspath += sourceSets.main.get().output + runtimeClasspath += sourceSets.main.get().runtimeClasspath + } +} + +val benchmarkImplementation by configurations.getting { + extendsFrom(configurations.testImplementation.get()) +} +val kaptBenchmark by configurations.getting { + extendsFrom(configurations.kaptTest.get()) +} + dependencies { implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) @@ -111,6 +125,11 @@ dependencies { testImplementation("com.tngtech.archunit:archunit-junit5:0.23.1") + benchmarkImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + benchmarkImplementation("org.openjdk.jmh:jmh-core:1.35") + kaptBenchmark("org.openjdk.jmh:jmh-generator-annprocess:1.35") + kaptBenchmark("org.springframework.boot:spring-boot-configuration-processor:2.7.1") + developmentOnly("org.springframework.boot:spring-boot-devtools:2.7.1") } @@ -174,7 +193,7 @@ tasks { } else { "npm" }, - "install" + "install", ) } @@ -191,7 +210,7 @@ tasks { "npm" }, "run", - "build" + "build", ) } @@ -202,6 +221,13 @@ tasks { from("$webui/dist/") into("$projectDir/src/main/resources/public/") } + + register("benchmark") { + group = "benchmark" + inputs.files(benchmarkSourceSet.output) + testClassesDirs = benchmarkSourceSet.output.classesDirs + classpath = benchmarkSourceSet.runtimeClasspath + } } springBoot { @@ -228,11 +254,11 @@ sourceSets { } val dbSqlite = mapOf( - "url" to "jdbc:sqlite:${project.buildDir}/generated/flyway/database.sqlite" + "url" to "jdbc:sqlite:${project.buildDir}/generated/flyway/database.sqlite", ) val migrationDirsSqlite = listOf( "$projectDir/src/flyway/resources/db/migration/sqlite", - "$projectDir/src/flyway/kotlin/db/migration/sqlite" + "$projectDir/src/flyway/kotlin/db/migration/sqlite", ) flyway { url = dbSqlite["url"] diff --git a/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/AbstractBenchmark.kt b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/AbstractBenchmark.kt new file mode 100644 index 000000000..ab0011dff --- /dev/null +++ b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/AbstractBenchmark.kt @@ -0,0 +1,55 @@ +package org.gotson.komga.benchmark + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State +import org.openjdk.jmh.results.format.ResultFormatType +import org.openjdk.jmh.runner.Runner +import org.openjdk.jmh.runner.options.Options +import org.openjdk.jmh.runner.options.OptionsBuilder +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.absolutePathString + +@ExtendWith(SpringExtension::class) +@SpringBootTest +@ActiveProfiles("test", "benchmark") +@State(Scope.Benchmark) +abstract class AbstractBenchmark { + @Autowired + private lateinit var benchmarkProperties: BenchmarkProperties + + @Test + fun executeJmhRunner() { + if (this.javaClass.simpleName.contains("jmhType")) return + Files.createDirectories(Paths.get(benchmarkProperties.resultFolder)) + val extension = when (benchmarkProperties.resultFormat) { + ResultFormatType.TEXT -> "txt" + ResultFormatType.CSV -> "csv" + ResultFormatType.SCSV -> "scsv" + ResultFormatType.JSON -> "json" + ResultFormatType.LATEX -> "tex" + } + val resultFile = Paths.get(benchmarkProperties.resultFolder, "${this.javaClass.name}.$extension").absolutePathString() + val opt: Options = OptionsBuilder() + .include("\\." + this.javaClass.simpleName + "\\.") // set the class name regex for benchmarks to search for to the current class + .warmupIterations(benchmarkProperties.warmupIterations) + .measurementIterations(benchmarkProperties.measurementIterations) + .forks(0) // do not use forking or the benchmark methods will not see references stored within its class + .threads(1) // do not use multiple threads + .mode(benchmarkProperties.mode) + .shouldDoGC(true) + .shouldFailOnError(true) + .resultFormat(benchmarkProperties.resultFormat) + .result(resultFile) + .shouldFailOnError(true) + .jvmArgs("-server") + .build() + Runner(opt).run() + } +} diff --git a/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/BenchmarkProperties.kt b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/BenchmarkProperties.kt new file mode 100644 index 000000000..4c79c993f --- /dev/null +++ b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/BenchmarkProperties.kt @@ -0,0 +1,26 @@ +package org.gotson.komga.benchmark + +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.results.format.ResultFormatType +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component +import org.springframework.validation.annotation.Validated +import javax.validation.constraints.Positive +import javax.validation.constraints.PositiveOrZero + +@Component +@ConfigurationProperties(prefix = "benchmark") +@Validated +class BenchmarkProperties { + @PositiveOrZero + var warmupIterations: Int = 1 + + @Positive + var measurementIterations: Int = 1 + + var resultFolder: String = "" + + var resultFormat: ResultFormatType = ResultFormatType.TEXT + + var mode: Mode = Mode.AverageTime +} diff --git a/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/AbstractRestBenchmark.kt b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/AbstractRestBenchmark.kt new file mode 100644 index 000000000..2d6769027 --- /dev/null +++ b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/AbstractRestBenchmark.kt @@ -0,0 +1,46 @@ +package org.gotson.komga.benchmark.rest + +import org.gotson.komga.benchmark.AbstractBenchmark +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.interfaces.api.rest.BookController +import org.gotson.komga.interfaces.api.rest.SeriesController +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.Setup +import org.springframework.beans.factory.annotation.Autowired + +internal const val DEFAULT_PAGE_SIZE = 20 + +abstract class AbstractRestBenchmark : AbstractBenchmark() { + + companion object { + lateinit var userRepository: KomgaUserRepository + lateinit var seriesController: SeriesController + lateinit var bookController: BookController + } + + @Autowired + fun setUserRepository(repository: KomgaUserRepository) { + userRepository = repository + } + + @Autowired + fun setSeriesController(controller: SeriesController) { + seriesController = controller + } + + @Autowired + fun setBookController(controller: BookController) { + bookController = controller + } + + protected lateinit var principal: KomgaPrincipal + + @Setup(Level.Trial) + fun prepareData() { + principal = KomgaPrincipal( + userRepository.findByEmailIgnoreCaseOrNull("admin@example.org") + ?: throw IllegalStateException("no user found"), + ) + } +} diff --git a/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/DashboardBenchmark.kt b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/DashboardBenchmark.kt new file mode 100644 index 000000000..4290d2483 --- /dev/null +++ b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/DashboardBenchmark.kt @@ -0,0 +1,107 @@ +package org.gotson.komga.benchmark.rest + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.gotson.komga.domain.model.Media +import org.gotson.komga.domain.model.ReadStatus +import org.gotson.komga.interfaces.api.rest.dto.ReadProgressUpdateDto +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Setup +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import java.time.LocalDate +import java.util.concurrent.TimeUnit + +@OutputTimeUnit(TimeUnit.MILLISECONDS) +class DashboardBenchmark : AbstractRestBenchmark() { + + companion object { + lateinit var bookLatestReleaseDate: LocalDate + } + + @Setup(Level.Trial) + override fun prepareData() { + super.prepareData() + + // mark some books in progress + bookController.getAllBooks(principal, readStatus = listOf(ReadStatus.IN_PROGRESS), page = Pageable.ofSize(DEFAULT_PAGE_SIZE)).let { page -> + if (page.totalElements < DEFAULT_PAGE_SIZE) { + bookController.getAllBooks(principal, readStatus = listOf(ReadStatus.UNREAD), page = Pageable.ofSize(DEFAULT_PAGE_SIZE)).content.forEach { book -> + bookController.markReadProgress(book.id, ReadProgressUpdateDto(2, false), principal) + } + } + } + + // retrieve most recent book release date + bookLatestReleaseDate = bookController.getAllBooks(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("metadata.releaseDate")))) + .content.firstOrNull()?.metadata?.releaseDate ?: LocalDate.now() + } + + @Benchmark + fun loadDashboard() { + runBlocking { + withContext(Dispatchers.Default) { + launch { getBooksInProgress() } + launch { getBooksOnDeck() } + launch { getBooksLatest() } + launch { getBooksRecentlyReleased() } + launch { getSeriesNew() } + launch { getSeriesUpdated() } + } + } + } + + val pageableBooksInProgress = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by(Sort.Order.desc("readProgress.readDate"))) + + @Benchmark + fun getBooksInProgress() { + bookController.getAllBooks(principal, readStatus = listOf(ReadStatus.IN_PROGRESS), page = pageableBooksInProgress) + } + + val pageableBooksOnDeck = Pageable.ofSize(DEFAULT_PAGE_SIZE) + + @Benchmark + fun getBooksOnDeck() { + bookController.getBooksOnDeck(principal, page = pageableBooksOnDeck) + } + + val pageableBooksLatest = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by(Sort.Order.desc("createdDate"))) + + @Benchmark + fun getBooksLatest() { + bookController.getAllBooks(principal, page = pageableBooksLatest) + } + + val pageableBooksRecentlyReleased = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by(Sort.Order.desc("metadata.releaseDate"))) + + @Benchmark + fun getBooksRecentlyReleased() { + bookController.getAllBooks(principal, releasedAfter = bookLatestReleaseDate.minusMonths(1), page = pageableBooksRecentlyReleased) + } + + val pageableSeriesNew = Pageable.ofSize(DEFAULT_PAGE_SIZE) + + @Benchmark + fun getSeriesNew() { + seriesController.getNewSeries(principal, page = pageableSeriesNew) + } + + val pageableSeriesUpdated = Pageable.ofSize(DEFAULT_PAGE_SIZE) + + @Benchmark + fun getSeriesUpdated() { + seriesController.getUpdatedSeries(principal, page = pageableSeriesUpdated) + } + + val pageableBooksToCheck = Pageable.ofSize(1) + + @Benchmark + fun getBooksToCheck() { + bookController.getAllBooks(principal, mediaStatus = listOf(Media.Status.ERROR, Media.Status.UNSUPPORTED), page = pageableBooksToCheck) + } +} diff --git a/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/PaginationBenchmark.kt b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/PaginationBenchmark.kt new file mode 100644 index 000000000..0ec4d1a72 --- /dev/null +++ b/komga/src/benchmark/kotlin/org/gotson/komga/benchmark/rest/PaginationBenchmark.kt @@ -0,0 +1,47 @@ +package org.gotson.komga.benchmark.rest + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Level +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Param +import org.openjdk.jmh.annotations.Setup +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort +import java.util.concurrent.TimeUnit + +@OutputTimeUnit(TimeUnit.MILLISECONDS) +class PaginationBenchmark : AbstractRestBenchmark() { + + companion object { + private lateinit var biggestSeriesId: String + } + + @Param("20", "100", "500", "1000", "5000") + private var pageSize: Int = DEFAULT_PAGE_SIZE + + @Setup(Level.Trial) + override fun prepareData() { + super.prepareData() + + // find series with most books + biggestSeriesId = seriesController.getAllSeries(principal, page = PageRequest.of(0, 1, Sort.by(Sort.Order.desc("booksCount")))) + .content.first() + .id + } + + @Benchmark + fun getAllSeries() { + seriesController.getAllSeries(principal, page = Pageable.ofSize(pageSize)) + } + + @Benchmark + fun getAllBooks() { + bookController.getAllBooks(principal, page = Pageable.ofSize(pageSize)) + } + + @Benchmark + fun getSeriesBooks() { + seriesController.getAllBooksBySeries(principal, biggestSeriesId, page = Pageable.ofSize(pageSize)) + } +} diff --git a/komga/src/benchmark/resources/application-benchmark.yml b/komga/src/benchmark/resources/application-benchmark.yml new file mode 100644 index 000000000..cdea858b3 --- /dev/null +++ b/komga/src/benchmark/resources/application-benchmark.yml @@ -0,0 +1,24 @@ +application.version: BENCHMARK + +benchmark: + result-folder: ${reportsDir}/benchmark + measurement-iterations: 5 + warmup-iterations: 1 + mode: averagetime + result-format: text +komga: + config-dir: ${rootDir}/config-dir + workspace: diesel + database: + file: \${komga.config-dir}/\${komga.workspace}.sqlite + lucene: + data-directory: \${komga.config-dir}/lucene/\${komga.workspace} +spring: + flyway: + enabled: true + artemis: + embedded: + data-directory: \${komga.config-dir}/artemis/\${komga.workspace} +#logging: +# level: +# org.jooq: DEBUG diff --git a/komga/src/benchmark/resources/junit-platform.properties b/komga/src/benchmark/resources/junit-platform.properties new file mode 100644 index 000000000..2af5bf864 --- /dev/null +++ b/komga/src/benchmark/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default=per_class