mirror of
https://github.com/gotson/komga.git
synced 2025-12-15 21:12:27 +01:00
build: benchmark tooling
This commit is contained in:
parent
0af5f5c4d9
commit
43a1fc7a0a
9 changed files with 337 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -45,3 +45,4 @@ nbdist/
|
|||
/komga/src/main/resources/public/
|
||||
/config-dir/
|
||||
application-oauth2.yml
|
||||
/benchmark
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
24
komga/src/benchmark/resources/application-benchmark.yml
Normal file
24
komga/src/benchmark/resources/application-benchmark.yml
Normal 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
|
||||
1
komga/src/benchmark/resources/junit-platform.properties
Normal file
1
komga/src/benchmark/resources/junit-platform.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
junit.jupiter.testinstance.lifecycle.default=per_class
|
||||
Loading…
Reference in a new issue