build: benchmark tooling

This commit is contained in:
Gauthier Roebroeck 2022-07-15 22:21:30 +08:00
parent 0af5f5c4d9
commit 43a1fc7a0a
9 changed files with 337 additions and 4 deletions

1
.gitignore vendored
View file

@ -45,3 +45,4 @@ nbdist/
/komga/src/main/resources/public/
/config-dir/
application-oauth2.yml
/benchmark

View file

@ -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<Test>("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"]

View file

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

View file

@ -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
}

View file

@ -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"),
)
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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

View file

@ -0,0 +1 @@
junit.jupiter.testinstance.lifecycle.default=per_class