Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions html/eap/benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
# EAP benchmarks

Compare three ways to render HTML:
`RendererPoolBenchmark` compares three ways to render HTML:

- `fresh`: create a renderer for each call.
- `threadLocal`: reuse a renderer on each worker thread.
- `sharedKeyedPool`: reuse the production pool with one key shared by all workers.

Each case checks its HTML output before measurement.

`SsrBenchmark` measures String, Writer and callback rendering with JMH.
`HydrationBenchmark` measures JS/Wasm hydration of JVM-rendered HTML and checks DOM adoption.
See [third-party notices](../../licenses/third_party/README.md) for adapted workloads.

## Run

From the repository root, run (default 4 workers):
From the repository root:

```shell
html/gradlew -p html -Pcompose.html.eap.enabled=true \
:html-eap-benchmarks:benchmark
html/gradlew -p html -Pcompose.html.eap.enabled=true :html-eap-benchmarks:ssrBenchmark
html/gradlew -p html -Pcompose.html.eap.enabled=true :html-eap-benchmarks:hydrationBenchmark
```

Defaults: 1 or 64 rows, two JVM forks, two one-second warmups, and three one-second measurements.
Results are saved under `html/eap/benchmarks/build/reports/benchmarks`.
SSR defaults to String. Choose `-PrenderMode=writer`, `callback` or `all`.
Add `-Pcompose.html.lowering.enabled=true` for JVM compiler lowering.
Use `smokeBenchmark` for a short JVM run or `benchmark` for all JVM cases, including pooling.

For a short smoke test of all strategies and row counts:
SSR uses two forks, each with 5 seconds of warmup and ten 1-second measurements.
Hydration runs JS and Wasm in Chrome with two warmups and five samples, excluding HTML parsing and bundle loading.

```shell
html/gradlew -p html -Pcompose.html.eap.enabled=true \
:html-eap-benchmarks:smokeBenchmark
```
Results: `html/eap/benchmarks/build/reports/benchmarks` for JVM.
`html/eap/benchmarks/build/reports/tests` and test output for hydration (mean, standard deviation and range).

## Add a benchmark

Add an `open` Kotlin class under `src/main/kotlin` with `kotlinx.benchmark` annotations.
Add an `open` Kotlin class under `src/jvmMain/kotlin` with `kotlinx.benchmark` annotations.
Use `@JvmField` on `@Param` properties so JMH can populate them.
Shared workloads live in `src/commonMain/kotlin`. Browser benchmarks in `src/webTest/kotlin`.
107 changes: 88 additions & 19 deletions html/eap/benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,36 +1,69 @@
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)

import kotlinx.benchmark.gradle.JvmBenchmarkTarget
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm")
kotlin("multiplatform")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.compose.html.lowering") apply false
id("org.jetbrains.kotlinx.benchmark") version "0.4.15"
}

if (providers.gradleProperty("compose.html.lowering.enabled").orElse("false").get().toBooleanStrict()) {
apply(plugin = "org.jetbrains.compose.html.lowering")
}

val composeVersion = providers.gradleProperty("compose.version").get()
val browserVersion = providers.gradleProperty("compose.html.eap.kotlinx-browser-common-subset.version").get()
val benchmarkVersion = "0.4.15"

dependencies {
implementation(project(":html-core-eap"))
implementation("org.jetbrains.compose.runtime:runtime:$composeVersion")
implementation("org.jetbrains.compose.html:kotlinx-browser-common-subset:$browserVersion")
implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:$benchmarkVersion")
}
kotlin {
jvm()
js {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
wasmJs {
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
applyDefaultHierarchyTemplate()

// Allow the benchmark to access renderer internals.
tasks.named<KotlinCompile>("compileKotlin") {
friendPaths.from(
project(":html-core-eap").tasks.named<Jar>("jvmJar")
.flatMap { it.archiveFile }
)
sourceSets {
commonMain.dependencies {
implementation(project(":html-core-eap"))
implementation(project(":html-svg-eap"))
implementation("androidx.compose.runtime:runtime:$composeVersion")
implementation("org.jetbrains.compose.html:kotlinx-browser-common-subset:$browserVersion")
}
jvmMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:$benchmarkVersion")
}
val webTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.core)
}
}
}
}

// Match the library's Java compatibility.
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
// Allow the pooling benchmark to access renderer internals.
tasks.named<KotlinCompile>("compileKotlinJvm") {
friendPaths.from(project(":html-core-eap").tasks.named<Jar>("jvmJar").flatMap { it.archiveFile })
}

tasks.withType<JavaCompile>().configureEach {
Expand All @@ -40,9 +73,19 @@ tasks.withType<JavaCompile>().configureEach {
benchmark {
configurations {
configureEach {
// Preserve @Fork rather than the runner's default of one fork.
providers.gradleProperty("benchmarks").orNull?.let { include(it) }
advanced("jvmForks", "definedByJmh")
}
register("ssr") {
val method = when (val mode = providers.gradleProperty("renderMode").orElse("string").get()) {
"string" -> "renderToString"
"writer" -> "streamToWriter"
"callback" -> "streamToCallback"
"all" -> ".*"
else -> error("Unknown renderMode '$mode': use string, writer, callback or all")
}
include(".*SsrBenchmark.$method")
}
register("smoke") {
warmups = 1
iterations = 1
Expand All @@ -51,9 +94,35 @@ benchmark {
}
}
targets {
register("main") {
register("jvm") {
this as JvmBenchmarkTarget
jmhVersion = "1.37"
}
}
}

val hydrationFixtures = layout.buildDirectory.dir("generated/hydrationSources")
val jvmMain = kotlin.targets.getByName("jvm").compilations.getByName("main")
val generateHydrationFixtures = tasks.register<JavaExec>("generateHydrationFixtures") {
group = "verification"
dependsOn(jvmMain.compileTaskProvider)
mainClass.set("benchmarks.hydration.GenerateHydrationFixturesKt")
classpath(jvmMain.output.allOutputs, jvmMain.runtimeDependencyFiles)
args(hydrationFixtures.get().asFile.absolutePath)
outputs.dir(hydrationFixtures)
}

kotlin.sourceSets.named("webTest") {
kotlin.srcDir(generateHydrationFixtures)
}

// Each browser benchmark invocation should collect fresh measurements.
listOf("jsBrowserTest", "wasmJsBrowserTest").forEach { name ->
tasks.named(name) { outputs.upToDateWhen { false } }
}

tasks.register("hydrationBenchmark") {
group = "benchmark"
description = "Measures hydration on JS and Wasm in Chrome."
dependsOn("jsBrowserTest", "wasmJsBrowserTest")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2026 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/

package benchmarks.compose

import benchmarks.model.*
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.dom.*
import androidx.compose.runtime.Composable

@Composable
fun ContentArticleDocument(data: ArticleData) {
Html(attrs = { lang("en") }) {
Head {
Meta(attrs = { attr("charset", "UTF-8") })
Title { Text(data.title) }
Meta(attrs = {
attr("name", "description")
attr("content", data.excerpt)
})
}
Body {
ContentArticle(data)
}
}
}

@Composable
fun ContentArticle(data: ArticleData) {
// Breadcrumbs
Nav(attrs = { attr("aria-label", "Breadcrumb") }) {
Ol {
data.breadcrumbs.forEach { crumb ->
Li {
A(href = crumb.url, attrs = {
if (crumb.isCurrent) attr("aria-current", "page")
}) { Text(crumb.label) }
}
}
}
}

Main {
Article {
// Header
Header {
H1 { Text(data.title) }
Div(attrs = { classes("meta") }) {
Span { Text("By ${data.author}") }
TagElement(
elementBuilder = org.jetbrains.compose.web.dom.ElementBuilder.createBuilder("time"),
applyAttrs = { attr("datetime", data.publishDateIso) },
content = { Text(data.publishDateFormatted) }
)
Span { Text("${data.readingTimeMin} min read") }
}
Img(src = data.heroImage.url, alt = data.heroImage.alt, attrs = {
attr("width", data.heroImage.width.toString())
attr("height", data.heroImage.height.toString())
attr("loading", "lazy")
})
}

// Table of Contents
Nav(attrs = { attr("aria-label", "Table of contents") }) {
H2 { Text("Table of Contents") }
Ul {
data.toc.forEach { item ->
Li { A(href = "#${item.targetId}") { Text(item.title) } }
}
}
}

// Content Sections
data.sections.forEach { section ->
Section(attrs = { id(section.id) }) {
H2 { Text(section.title) }
section.blocks.forEach { block ->
when (block.type) {
BlockType.PARAGRAPH -> P { Text(block.content) }
BlockType.CODE -> Pre {
Code(attrs = { attr("data-language", block.language ?: "text") }) {
Text(block.content)
}
}
BlockType.IMAGE -> Img(src = block.url ?: "", alt = block.content, attrs = {
attr("loading", "lazy")
})
BlockType.LINK -> P {
A(href = block.url ?: "", attrs = {
attr("target", "_blank")
attr("rel", "noreferrer")
}) { Text(block.content) }
}
}
}
}
}

// Footer / Tags
Footer {
Ul(attrs = { classes("tags") }) {
data.tags.forEach { tag ->
Li { A(href = "/tags/$tag") { Text(tag) } }
}
}
}
}

Aside(attrs = { attr("aria-label", "Related articles") }) {
H3 { Text("Related Articles") }
Ul {
data.relatedArticles.forEach { article ->
Li {
A(href = article.url) { Text(article.title) }
}
}
}
}
}

// Site Footer
Footer {
P { Text("© 2024 Benchmarks") }
}
}
Loading
Loading