diff --git a/html/eap/benchmarks/README.md b/html/eap/benchmarks/README.md index 0d0a1315df5..429553103d0 100644 --- a/html/eap/benchmarks/README.md +++ b/html/eap/benchmarks/README.md @@ -1,6 +1,6 @@ # 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. @@ -8,26 +8,31 @@ Compare three ways to render HTML: 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`. diff --git a/html/eap/benchmarks/build.gradle.kts b/html/eap/benchmarks/build.gradle.kts index 5d7c31941b2..0e05c0ffe46 100644 --- a/html/eap/benchmarks/build.gradle.kts +++ b/html/eap/benchmarks/build.gradle.kts @@ -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("compileKotlin") { - friendPaths.from( - project(":html-core-eap").tasks.named("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("compileKotlinJvm") { + friendPaths.from(project(":html-core-eap").tasks.named("jvmJar").flatMap { it.archiveFile }) } tasks.withType().configureEach { @@ -40,9 +73,19 @@ tasks.withType().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 @@ -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("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") +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/ContentArticle.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/ContentArticle.kt new file mode 100644 index 00000000000..0e32cb884bb --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/ContentArticle.kt @@ -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") } + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/DataTable.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/DataTable.kt new file mode 100644 index 00000000000..a36fa0f1bdd --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/DataTable.kt @@ -0,0 +1,167 @@ +/* + * 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 DataTableDocument(data: DataTableData) { + Html(attrs = { lang("en") }) { + Head { + Meta(attrs = { attr("charset", "UTF-8") }) + Title { Text(data.title) } + } + Body { + DataTable(data) + } + } +} + +@Composable +fun DataTable(data: DataTableData) { + Div(attrs = { + id("dashboard") + attr("data-page", "data-table") + }) { + // Header + Header(attrs = { classes("page-header") }) { + H1 { Text(data.title) } + P { Text(data.description) } + } + + // Filters + Div(attrs = { + classes("filters-bar") + attr("role", "search") + }) { + Input(type = InputType.Search) { + id("table-search") + placeholder("Search rows...") + attr("aria-label", "Search rows") + data.searchQuery?.let { attr("value", it) } + } + Div(attrs = { classes("active-filters") }) { + data.activeFilters.forEach { filter -> + Span(attrs = { + classes("filter-badge") + attr("data-filter-key", filter.key) + attr("data-filter-val", filter.value) + }) { Text("${filter.key}: ${filter.value}") } + } + } + } + + // Selection Status Bar + Div(attrs = { + classes("selection-status") + attr("role", "status") + attr("aria-live", "polite") + }) { + Text("${data.selectedRowCount} items selected") + } + + // Table + Table(attrs = { + attr("role", "grid") + attr("aria-rowcount", data.totalRowCount.toString()) + attr("aria-colcount", data.headers.size.toString()) + attr("aria-label", "Data Table") + }) { + Thead { + Tr { + Th(attrs = { attr("scope", "col") }) { + Input(type = InputType.Checkbox) { + attr("aria-label", "Select all rows") + } + } + data.headers.forEach { header -> + Th(attrs = { + attr("scope", "col") + attr("data-column-id", header.id) + attr("role", "columnheader") + if (header.sortable) { + val sortDir = if (header.id == data.sortColumnId) data.sortDirection else "none" + classes("sortable") + attr("aria-sort", sortDir) + if (sortDir != "none") { + classes("sorted-$sortDir") + } + } + }) { Text(header.label) } + } + } + } + Tbody { + data.rows.forEachIndexed { _, row -> + Tr(attrs = { + attr("data-row-id", row.id) + attr("data-status", row.status) + if (row.selected) { + attr("aria-selected", "true") + classes("row-selected") + } else { + attr("aria-selected", "false") + } + }) { + Td { + Input(type = InputType.Checkbox) { + attr("aria-label", "Select row ${row.id}") + if (row.selected) attr("checked", "") + } + } + row.cells.forEach { cell -> + Td(attrs = { + attr("data-column", cell.columnId) + attr("data-value", cell.rawValue) + if (cell.tooltip != null) { + val tooltipId = "tt-${row.id}-${cell.columnId}" + attr("aria-describedby", tooltipId) + } + }) { + if (cell.isStatus) { + Span(attrs = { + classes("status-badge", "status-${cell.rawValue}") + attr("role", "status") + }) { Text(cell.displayValue) } + } else { + Text(cell.displayValue) + } + cell.tooltip?.let { tooltip -> + Span(attrs = { + id("tt-${row.id}-${cell.columnId}") + classes("tooltip-text") + style { property("display", "none") } + }) { Text(tooltip) } + } + } + } + } + } + } + } + + // Pagination + Nav(attrs = { attr("aria-label", "Pagination") }) { + Ul(attrs = { classes("pagination") }) { + data.pages.forEach { page -> + Li { + A(href = "?page=${page.number}", attrs = { + attr("data-page", page.number.toString()) + attr("aria-label", "Page ${page.number}") + if (page.isCurrent) { + attr("aria-current", "page") + classes("current-page") + } + }) { Text(page.number.toString()) } + } + } + } + } + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/FormApp.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/FormApp.kt new file mode 100644 index 00000000000..68752b10e93 --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/FormApp.kt @@ -0,0 +1,187 @@ +/* + * 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 FormAppDocument(data: FormData) { + Html(attrs = { lang("en") }) { + Head { + Meta(attrs = { attr("charset", "UTF-8") }) + Title { Text(data.title) } + } + Body { + FormApp(data) + } + } +} + +@Composable +fun FormApp(data: FormData) { + Div(attrs = { + id("app-container") + attr("role", "main") + }) { + H1 { Text(data.title) } + Form(action = data.actionUrl, attrs = { + method(FormMethod.Post) + attr("aria-label", data.formLabel) + }) { + data.sections.forEach { section -> + Fieldset(attrs = { + attr("aria-labelledby", "legend-${section.id}") + }) { + Legend(attrs = { id("legend-${section.id}") }) { Text(section.title) } + + section.fields.forEach { field -> + Div(attrs = { + classes("form-group") + id("group-${field.id}") + }) { + Label(forId = field.id) { Text(field.label) } + + val hasError = field.errorMessage != null + val ariaDescribedBy = buildString { + if (field.helpText != null) append("help-${field.id} ") + if (hasError) append("error-${field.id}") + }.trim() + + when (field.type) { + FormFieldType.TEXT, FormFieldType.EMAIL, FormFieldType.PASSWORD, FormFieldType.NUMBER, FormFieldType.TEL -> { + Input(type = when(field.type) { + FormFieldType.EMAIL -> InputType.Email + FormFieldType.PASSWORD -> InputType.Password + FormFieldType.NUMBER -> InputType.Number + FormFieldType.TEL -> InputType.Tel + else -> InputType.Text + }) { + id(field.id) + name(field.name) + field.value?.let { attr("value", it) } + field.placeholder?.let { placeholder(it) } + if (field.required) required() + if (field.disabled) disabled() + if (field.readOnly) readOnly() + field.autoComplete?.let { attr("autocomplete", it) } + + field.min?.let { min(it) } + field.max?.let { max(it) } + field.minLength?.let { attr("minlength", it.toString()) } + field.maxLength?.let { attr("maxlength", it.toString()) } + field.pattern?.let { attr("pattern", it) } + field.step?.let { attr("step", it.toString().removeSuffix(".0")) } + + if (field.required) attr("aria-required", "true") + if (hasError) attr("aria-invalid", "true") + if (ariaDescribedBy.isNotEmpty()) attr("aria-describedby", ariaDescribedBy) + } + } + FormFieldType.SELECT -> { + Select(attrs = { + id(field.id) + name(field.name) + if (field.required) required() + if (field.disabled) disabled() + if (field.multiple) multiple() + + if (field.required) attr("aria-required", "true") + if (hasError) attr("aria-invalid", "true") + if (ariaDescribedBy.isNotEmpty()) attr("aria-describedby", ariaDescribedBy) + }) { + field.options?.forEach { option -> + Option(value = option.value, attrs = { + if (option.selected) selected() + if (option.disabled) disabled() + }) { Text(option.label) } + } + } + } + FormFieldType.TEXTAREA -> { + TagElement( + elementBuilder = ElementBuilder.createBuilder("textarea"), + applyAttrs = { + id(field.id) + attr("name", field.name) + field.placeholder?.let { attr("placeholder", it) } + if (field.required) attr("required", "") + if (field.disabled) attr("disabled", "") + if (field.readOnly) attr("readonly", "") + field.minLength?.let { attr("minlength", it.toString()) } + field.maxLength?.let { attr("maxlength", it.toString()) } + if (field.required) attr("aria-required", "true") + if (hasError) attr("aria-invalid", "true") + if (ariaDescribedBy.isNotEmpty()) attr("aria-describedby", ariaDescribedBy) + }, + content = { Text(field.value ?: "") }, + ) + } + FormFieldType.RADIO -> { + Div(attrs = { + attr("role", "radiogroup") + if (field.required) attr("aria-required", "true") + if (hasError) attr("aria-invalid", "true") + if (ariaDescribedBy.isNotEmpty()) attr("aria-describedby", ariaDescribedBy) + }) { + field.options?.forEachIndexed { index, option -> + val optId = "${field.id}-opt-$index" + Div { + Input(type = InputType.Radio) { + id(optId) + name(field.name) + attr("value", option.value) + if (option.selected) attr("checked", "") + if (option.disabled) disabled() + } + Label(forId = optId) { Text(option.label) } + } + } + } + } + FormFieldType.CHECKBOX -> { + Input(type = InputType.Checkbox) { + id(field.id) + name(field.name) + field.value?.let { attr("value", it) } + if (field.checked) attr("checked", "") + if (field.required) required() + if (field.disabled) disabled() + + if (field.required) attr("aria-required", "true") + if (hasError) attr("aria-invalid", "true") + if (ariaDescribedBy.isNotEmpty()) attr("aria-describedby", ariaDescribedBy) + } + } + } + + field.helpText?.let { help -> + Span(attrs = { + id("help-${field.id}") + classes("help-text") + }) { Text(help) } + } + + if (hasError) { + Div(attrs = { + id("error-${field.id}") + classes("error-message") + attr("aria-live", "polite") + }) { Text(field.errorMessage) } + } + } + } + } + } + Button(attrs = { + type(ButtonType.Submit) + if (data.isSubmitting) disabled() + }) { Text("Submit Registration") } + } + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/SvgDashboard.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/SvgDashboard.kt new file mode 100644 index 00000000000..b060d676106 --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/SvgDashboard.kt @@ -0,0 +1,175 @@ +/* + * 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. + */ + +@file:OptIn(org.jetbrains.compose.web.ExperimentalComposeWebSvgApi::class) + +package benchmarks.compose + +import benchmarks.model.* +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.svg.* +import androidx.compose.runtime.Composable + +@Composable +fun SvgDashboardDocument(data: SvgDashboardData) { + Html(attrs = { lang("en") }) { + Head { + Meta(attrs = { attr("charset", "UTF-8") }) + Title { Text(data.title) } + } + Body { + SvgDashboard(data) + } + } +} + +@Composable +fun SvgDashboard(data: SvgDashboardData) { + Div(attrs = { classes("dashboard-container") }) { + Header(attrs = { classes("dashboard-header") }) { + H1 { Text(data.title) } + } + + Div(attrs = { classes("charts-grid") }) { + // Line Chart + Div(attrs = { + classes("chart-wrapper") + attr("aria-label", "Line chart showing ${data.lineChart.title}") + attr("role", "img") + }) { + H2 { Text(data.lineChart.title) } + Svg(viewBox = "0 0 800 400", attrs = { + attr("width", "100%") + attr("height", "100%") + }) { + // Axes + Line(x1 = 50, y1 = 350, x2 = 750, y2 = 350, attrs = { + attr("stroke", "#000") + attr("stroke-width", "2") + }) + Line(x1 = 50, y1 = 50, x2 = 50, y2 = 350, attrs = { + attr("stroke", "#000") + attr("stroke-width", "2") + }) + + // Data Path + Path(d = data.lineChart.pathData, attrs = { + attr("fill", "none") + attr("stroke", "#3b82f6") + attr("stroke-width", "3") + }) + + // Points + data.lineChart.points.forEach { point -> + Circle(cx = point.x, cy = point.y, r = 4, attrs = { + attr("fill", "#1d4ed8") + }) + } + + // Labels + data.lineChart.xLabels.forEach { label -> + SvgText(text = label.text, x = label.x, y = 370, attrs = { + attr("text-anchor", "middle") + attr("font-size", "12") + }) + } + } + } + + // Bar Chart + Div(attrs = { + classes("chart-wrapper") + attr("aria-label", "Bar chart showing ${data.barChart.title}") + attr("role", "img") + }) { + H2 { Text(data.barChart.title) } + Svg(viewBox = "0 0 600 400") { + Defs { + LinearGradient(id = "bar-grad", attrs = { + attr("x1", "0%") + attr("y1", "100%") + attr("x2", "0%") + attr("y2", "0%") + }) { + Stop(attrs = { + attr("offset", "0%") + attr("stop-color", "#34d399") + }) + Stop(attrs = { + attr("offset", "100%") + attr("stop-color", "#059669") + }) + } + } + + data.barChart.bars.forEach { bar -> + Rect(x = bar.x, y = bar.y, width = bar.width, height = bar.height, attrs = { + attr("fill", "url(#bar-grad)") + }) + SvgText(text = bar.value.toString().removeSuffix(".0"), x = bar.x + bar.width / 2, y = bar.y - 10, attrs = { + attr("text-anchor", "middle") + attr("font-size", "12") + }) + } + } + } + + // Pie Chart + Div(attrs = { + classes("chart-wrapper") + attr("aria-label", "Pie chart showing ${data.pieChart.title}") + attr("role", "img") + }) { + H2 { Text(data.pieChart.title) } + Svg(viewBox = "0 0 400 400") { + G(attrs = { attr("transform", "translate(200, 200)") }) { + data.pieChart.slices.forEach { slice -> + Path(d = slice.pathData, attrs = { + attr("fill", slice.color) + attr("stroke", "#fff") + attr("stroke-width", "2") + }) + } + } + } + } + + // Gauge Meter + Div(attrs = { + classes("chart-wrapper") + attr("aria-label", "Gauge showing ${data.gauge.title}") + attr("role", "img") + }) { + H2 { Text(data.gauge.title) } + Svg(viewBox = "0 0 200 150") { + Path(d = "M 20 130 A 80 80 0 0 1 180 130", attrs = { + attr("fill", "none") + attr("stroke", "#e5e7eb") + attr("stroke-width", "20") + }) + Path(d = data.gauge.valuePathData, attrs = { + attr("fill", "none") + attr("stroke", "#ef4444") + attr("stroke-width", "20") + }) + // Needle + Line(x1 = 100, y1 = 130, x2 = data.gauge.needleX, y2 = data.gauge.needleY, attrs = { + attr("stroke", "#374151") + attr("stroke-width", "4") + }) + Circle(cx = 100, cy = 130, r = 8, attrs = { + attr("fill", "#374151") + }) + SvgText(text = data.gauge.valueLabel, x = 100, y = 145, attrs = { + attr("text-anchor", "middle") + attr("font-size", "14") + attr("dominant-baseline", "hanging") + }) + } + } + } + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/TailwindCatalog.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/TailwindCatalog.kt new file mode 100644 index 00000000000..71024528dfb --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/compose/TailwindCatalog.kt @@ -0,0 +1,98 @@ +/* + * 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 TailwindCatalogDocument(data: CatalogData) { + Html(attrs = { lang("en") }) { + Head { + Meta(attrs = { attr("charset", "UTF-8") }) + Title { Text(data.title) } + Link(attrs = { + attr("rel", "stylesheet") + href("/styles.css") + }) + } + Body(attrs = { classes("bg-slate-50", "text-slate-900", "font-sans", "antialiased", "min-h-screen") }) { + TailwindCatalog(data) + } + } +} + +@Composable +fun TailwindCatalog(data: CatalogData) { + Div(attrs = { classes("max-w-7xl", "mx-auto", "px-4", "sm:px-6", "lg:px-8", "py-12") }) { + // Header & Search + Div(attrs = { classes("flex", "flex-col", "md:flex-row", "justify-between", "items-center", "mb-8") }) { + H1(attrs = { classes("text-3xl", "font-extrabold", "tracking-tight", "text-slate-900") }) { Text(data.title) } + Form(action = "/search", attrs = { classes("mt-4", "md:mt-0", "flex", "w-full", "md:w-auto") }) { + Input(type = InputType.Search) { + classes("shadow-sm", "focus:ring-indigo-500", "focus:border-indigo-500", "block", "w-full", "sm:text-sm", "border-gray-300", "rounded-md") + name("q") + placeholder("Search projects...") + } + Button(attrs = { + type(ButtonType.Submit) + classes("ml-3", "inline-flex", "justify-center", "py-2", "px-4", "border", "border-transparent", "shadow-sm", "text-sm", "font-medium", "rounded-md", "text-white", "bg-indigo-600", "hover:bg-indigo-700") + }) { Text("Search") } + } + } + + // Filters + Div(attrs = { classes("flex", "space-x-4", "mb-8", "overflow-x-auto", "pb-2") }) { + data.platforms.forEach { platform -> + A(href = "?platform=${platform.id}", attrs = { + classes("px-3", "py-2", "font-medium", "text-sm", "rounded-md") + if (platform.active) { + classes("bg-indigo-100", "text-indigo-700") + } else { + classes("text-slate-500", "hover:text-slate-700", "bg-slate-100", "hover:bg-slate-200") + } + }) { Text(platform.name) } + } + } + + // Project Grid + data.sections.forEach { section -> + Div(attrs = { classes("mb-12") }) { + H2(attrs = { classes("text-2xl", "font-bold", "text-slate-900", "mb-6") }) { Text(section.title) } + Div(attrs = { classes("grid", "grid-cols-1", "gap-6", "sm:grid-cols-2", "lg:grid-cols-3") }) { + section.projects.forEach { project -> + Div(attrs = { classes("bg-white", "overflow-hidden", "shadow", "rounded-lg", "border", "border-slate-200", "flex", "flex-col") }) { + Div(attrs = { classes("px-4", "py-5", "sm:p-6", "flex-grow") }) { + Div(attrs = { classes("flex", "items-center", "justify-between", "mb-4") }) { + Span(attrs = { classes("text-sm", "font-medium", "text-indigo-600") }) { Text(project.author) } + Span(attrs = { classes("inline-flex", "items-center", "px-2.5", "py-0.5", "rounded-full", "text-xs", "font-medium", "bg-yellow-100", "text-yellow-800") }) { + Text("★ ${project.starCount}") + } + } + H3(attrs = { classes("text-lg", "font-medium", "text-slate-900", "mb-2") }) { Text(project.name) } + P(attrs = { classes("text-sm", "text-slate-500", "line-clamp-3") }) { Text(project.description) } + } + Div(attrs = { classes("bg-slate-50", "px-4", "py-4", "sm:px-6", "flex", "flex-wrap", "gap-2") }) { + project.badges.forEach { badge -> + Span(attrs = { classes("inline-flex", "items-center", "px-2", "py-0.5", "rounded", "text-xs", "font-medium", "bg-slate-100", "text-slate-800", "border", "border-slate-200") }) { + Text(badge) + } + } + } + } + } + } + } + } + + // Footer + Footer(attrs = { classes("mt-12", "border-t", "border-slate-200", "pt-8", "text-center", "pb-12") }) { + P(attrs = { classes("text-sm", "text-slate-500") }) { Text("© Compose HTML Benchmarks.") } + } + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/Hydrate1k.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/Hydrate1k.kt new file mode 100644 index 00000000000..bf1d0290ae6 --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/Hydrate1k.kt @@ -0,0 +1,100 @@ +/* + * 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.hydration + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.jetbrains.compose.web.dom.A +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Table +import org.jetbrains.compose.web.dom.Tbody +import org.jetbrains.compose.web.dom.Td +import org.jetbrains.compose.web.dom.Text +import org.jetbrains.compose.web.dom.Tr + +data class Hydrate1kState(val rows: List) + +data class Hydrate1kRow(val id: Int, val label: String) + +private val adjectives = listOf( + "pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", + "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", + "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy", +) + +private val colours = listOf( + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange", +) + +private val nouns = listOf( + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", + "mouse", "keyboard", +) + +fun hydrate1kFixture(): Hydrate1kState = Hydrate1kState( + rows = (1..1_000).map { id -> + Hydrate1kRow( + id = id, + label = "${adjectives[(id * 17) % adjectives.size]} " + + "${colours[(id * 31) % colours.size]} ${nouns[(id * 47) % nouns.size]}", + ) + }, +) + +@Composable +fun Hydrate1kTable(initialState: Hydrate1kState) { + var rows by remember(initialState) { mutableStateOf(initialState.rows) } + var selectedRowId by remember { mutableStateOf(null) } + + Div(attrs = { classes("container") }) { + Table(attrs = { classes("table", "table-hover", "table-striped", "test-data") }) { + Tbody { + rows.forEach { row -> + key(row.id) { + Tr(attrs = { + if (selectedRowId == row.id) classes("danger") + }) { + Td(attrs = { classes("col-md-1") }) { Text(row.id.toString()) } + Td(attrs = { classes("col-md-4") }) { + A(attrs = { + onClick { event -> + event.preventDefault() + selectedRowId = row.id + } + }) { + Text(row.label) + } + } + Td(attrs = { classes("col-md-1") }) { + A(attrs = { + onClick { event -> + event.preventDefault() + rows = rows.filterNot { it.id == row.id } + } + }) { + Span(attrs = { + classes("glyphicon", "glyphicon-remove") + attr("aria-hidden", "true") + }) + } + } + Td(attrs = { classes("col-md-6") }) + } + } + } + } + } + Span(attrs = { + classes("preloadicon", "glyphicon", "glyphicon-remove") + attr("aria-hidden", "true") + }) + } +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/HydrationWorkloads.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/HydrationWorkloads.kt new file mode 100644 index 00000000000..095388e67a4 --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/hydration/HydrationWorkloads.kt @@ -0,0 +1,25 @@ +/* + * 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.hydration + +import androidx.compose.runtime.Composable +import benchmarks.compose.* +import benchmarks.model.* + +internal val hydrationWorkloads = listOf( + "tailwind-catalog", "form-app", "data-table", "svg-dashboard", "content-article", "hydrate1k", +) + +// JVM fixture generation and browser hydration use exactly the same content and data. +internal fun hydrationContent(workload: String): @Composable () -> Unit = when (workload) { + "tailwind-catalog" -> catalogFixture().let { data -> { TailwindCatalog(data) } } + "form-app" -> formFixture().let { data -> { FormApp(data) } } + "data-table" -> dataTableFixture().let { data -> { DataTable(data) } } + "svg-dashboard" -> svgDashboardFixture().let { data -> { SvgDashboard(data) } } + "content-article" -> articleFixture().let { data -> { ContentArticle(data) } } + "hydrate1k" -> hydrate1kFixture().let { data -> { Hydrate1kTable(data) } } + else -> error("Unknown hydration workload: $workload") +} diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Fixtures.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Fixtures.kt new file mode 100644 index 00000000000..7901103a9ae --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Fixtures.kt @@ -0,0 +1,201 @@ +/* + * 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.model + +object Fixtures { + + fun generateCatalogData(): CatalogData { + val platforms = listOf( + PlatformFilter("jvm", "JVM", true), + PlatformFilter("js", "JS", false), + PlatformFilter("native", "Native", false), + PlatformFilter("wasm", "Wasm", true) + ) + + val sections = (1..8).map { s -> + CatalogSection( + title = "Section $s", + projects = (1..6).map { p -> + CatalogProject( + author = "Author $p", + name = "Project $s-$p", + description = "This is a detailed description for project $s-$p.", + starCount = s * p * 100, + badges = listOf("Kotlin", "Multiplatform", "Library") + ) + } + ) + } + + return CatalogData("KMP Catalog", platforms, sections) + } + + fun generateFormData(): FormData { + return FormData( + title = "Advanced Form", + actionUrl = "/api/submit", + formLabel = "User Registration", + isSubmitting = false, + sections = listOf( + FormSection( + id = "personal", + title = "Personal Information", + fields = listOf( + FormField("f1", "firstName", "First Name", FormFieldType.TEXT, "John", "Enter first name", null, null, true, false, false, false, false, "given-name", null, null, 2, 50, null, null, null), + FormField("f2", "lastName", "Last Name", FormFieldType.TEXT, "Doe", "Enter last name", null, null, true, false, false, false, false, "family-name", null, null, 2, 50, null, null, null), + FormField("f3", "email", "Email", FormFieldType.EMAIL, "john.doe@example.com", "Email address", "We'll never share your email.", null, true, false, false, false, false, "email", null, null, null, null, null, null, null), + FormField("f4", "phone", "Phone", FormFieldType.TEL, null, "+1", null, null, false, false, false, false, false, "tel", null, null, null, null, null, null, null), + FormField("f5", "age", "Age", FormFieldType.NUMBER, "30", null, null, null, false, false, false, false, false, null, "18", "120", null, null, null, 1, null), + FormField("f6", "bio", "Bio", FormFieldType.TEXTAREA, "A brief bio...", null, null, null, false, false, false, false, false, null, null, null, null, 500, null, null, null) + ) + ), + FormSection( + id = "preferences", + title = "Preferences", + fields = listOf( + FormField("p1", "theme", "Theme", FormFieldType.SELECT, null, null, null, null, false, false, false, false, false, null, null, null, null, null, null, null, listOf( + SelectOption("light", "Light", true, false), + SelectOption("dark", "Dark", false, false), + SelectOption("system", "System", false, false) + )), + FormField("p2", "notifications", "Enable Notifications", FormFieldType.CHECKBOX, "true", null, null, null, false, false, false, true, false, null, null, null, null, null, null, null, null), + FormField("p3", "frequency", "Frequency", FormFieldType.RADIO, "daily", null, null, null, false, false, false, true, false, null, null, null, null, null, null, null, null) + ) + ), + FormSection( + id = "security", + title = "Security Settings", + fields = listOf( + FormField("s1", "password", "Password", FormFieldType.PASSWORD, null, "Min 8 characters", null, null, true, false, false, false, false, "new-password", null, null, 8, null, null, null, null), + FormField("s2", "confirmPassword", "Confirm Password", FormFieldType.PASSWORD, null, null, null, null, true, false, false, false, false, "new-password", null, null, 8, null, null, null, null), + FormField("s3", "twoFactor", "Two-Factor Auth", FormFieldType.CHECKBOX, "enabled", null, null, null, false, false, false, false, false, null, null, null, null, null, null, null, null) + ) + ), + FormSection( + id = "additional", + title = "Additional Data", + fields = (1..10).map { i -> + FormField("a$i", "field$i", "Extra Field $i", FormFieldType.TEXT, "Value $i", null, null, null, false, false, false, false, false, null, null, null, null, null, null, null, null) + } + ) + ) + ) + } + + fun generateDataTableData(): DataTableData { + val headers = listOf( + TableHeader("id", "ID", true), + TableHeader("name", "Name", true), + TableHeader("email", "Email", true), + TableHeader("role", "Role", true), + TableHeader("status", "Status", true), + TableHeader("lastLogin", "Last Login", true) + ) + + val rows = (1..50).map { i -> + DataRow( + id = "row-$i", + status = if (i % 3 == 0) "offline" else "online", + selected = i % 10 == 0, + cells = listOf( + DataCell("id", "U$i", "U$i", null, false), + DataCell("name", "User $i", "User $i", "Full name for User $i", false), + DataCell("email", "user$i@example.com", "user$i@example.com", null, false), + DataCell("role", if (i % 5 == 0) "Admin" else "User", if (i % 5 == 0) "Admin" else "User", null, false), + DataCell("status", if (i % 3 == 0) "offline" else "online", if (i % 3 == 0) "Offline" else "Online", null, true), + DataCell("lastLogin", "2023-10-01T12:00:00Z", "Oct 1, 2023", null, false) + ) + ) + } + + val pages = (1..5).map { PageItem(it, it == 1) } + + return DataTableData( + title = "User Directory", + description = "A comprehensive list of all registered users.", + searchQuery = null, + activeFilters = mapOf("role" to "User", "status" to "online"), + selectedRowCount = 5, + totalRowCount = 250, + headers = headers, + sortColumnId = "name", + sortDirection = "ascending", + rows = rows, + pages = pages + ) + } + + fun generateSvgDashboardData(): SvgDashboardData { + val points = (0..20).map { i -> SvgPoint(i * 10.0, 100 - (i % 5) * 20.0 + (i % 3) * 5.0) } + val pathData = "M 0,100 " + points.joinToString(" ") { "L ${it.x},${it.y}" } + val xLabels = (0..5).map { SvgLabel(it * 40.0, "W$it") } + + val lineChart = LineChart("Weekly Traffic", pathData, points, xLabels) + + val bars = (0..9).map { i -> + Bar(i * 20.0, 100 - i * 10.0, 15.0, i * 10.0, i * 100.0) + } + val barChart = BarChart("Revenue", bars) + + val pieChart = PieChart("Device Types", listOf( + PieSliceData("M 50 50 L 100 50 A 50 50 0 0 1 50 100 Z", "#FF0000"), + PieSliceData("M 50 50 L 50 100 A 50 50 0 0 1 0 50 Z", "#00FF00"), + PieSliceData("M 50 50 L 0 50 A 50 50 0 0 1 100 50 Z", "#0000FF") + )) + + val gauge = GaugeChart( + "Server Load", + "M 10 90 A 40 40 0 0 1 90 90", + 75.0, 25.0, "75%" + ) + + return SvgDashboardData("Analytics Dashboard", lineChart, barChart, pieChart, gauge) + } + + fun generateArticleData(): ArticleData { + val sections = (1..6).map { s -> + ContentSection( + id = "sec-$s", + title = "Section $s", + blocks = listOf( + ContentBlock(BlockType.PARAGRAPH, "This is paragraph 1 of section $s. ".repeat(10), null, null), + ContentBlock(BlockType.CODE, "fun hello() { println(\"Hello Section $s\") }", "kotlin", null), + ContentBlock(BlockType.PARAGRAPH, "This is paragraph 2 of section $s. ".repeat(10), null, null), + if (s % 2 == 0) ContentBlock(BlockType.IMAGE, "Image description", null, "https://example.com/img$s.png") else ContentBlock(BlockType.LINK, "Read more", null, "https://example.com") + ) + ) + } + + return ArticleData( + title = "The Future of Compose HTML", + author = "Jane Doe", + excerpt = "An in-depth look at server-side rendering with Compose HTML.", + publishDateIso = "2023-10-15T08:00:00Z", + publishDateFormatted = "October 15, 2023", + readingTimeMin = 12, + heroImage = HeroImage("https://example.com/hero.jpg", "Compose Logo", 1200, 600), + breadcrumbs = listOf( + Breadcrumb("Home", "/", false), + Breadcrumb("Blog", "/blog", false), + Breadcrumb("The Future of Compose HTML", "/blog/compose-html", true) + ), + toc = sections.map { TocItem(it.id, it.title) }, + sections = sections, + tags = listOf("Compose", "Kotlin", "Web", "SSR"), + relatedArticles = listOf( + RelatedArticle("/blog/kotlin-wasm", "Getting Started with Kotlin Wasm"), + RelatedArticle("/blog/kmp", "Kotlin Multiplatform in 2024") + ) + ) + } +} + +// Top-level convenience functions used by renderers, benchmarks, and analyzer +fun catalogFixture(): CatalogData = Fixtures.generateCatalogData() +fun formFixture(): FormData = Fixtures.generateFormData() +fun dataTableFixture(): DataTableData = Fixtures.generateDataTableData() +fun svgDashboardFixture(): SvgDashboardData = Fixtures.generateSvgDashboardData() +fun articleFixture(): ArticleData = Fixtures.generateArticleData() diff --git a/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Models.kt b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Models.kt new file mode 100644 index 00000000000..64a348be434 --- /dev/null +++ b/html/eap/benchmarks/src/commonMain/kotlin/benchmarks/model/Models.kt @@ -0,0 +1,248 @@ +/* + * 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.model + +data class CatalogData( + val title: String, + val platforms: List, + val sections: List +) + +data class PlatformFilter( + val id: String, + val name: String, + val active: Boolean +) + +data class CatalogSection( + val title: String, + val projects: List +) + +data class CatalogProject( + val author: String, + val name: String, + val description: String, + val starCount: Int, + val badges: List +) + +// FormData +data class FormData( + val title: String, + val actionUrl: String, + val formLabel: String, + val isSubmitting: Boolean, + val sections: List +) + +data class FormSection( + val id: String, + val title: String, + val fields: List +) + +enum class FormFieldType { TEXT, EMAIL, PASSWORD, NUMBER, TEL, SELECT, TEXTAREA, RADIO, CHECKBOX } + +data class FormField( + val id: String, + val name: String, + val label: String, + val type: FormFieldType, + val value: String?, + val placeholder: String?, + val helpText: String?, + val errorMessage: String?, + val required: Boolean, + val disabled: Boolean, + val readOnly: Boolean, + val checked: Boolean, + val multiple: Boolean, + val autoComplete: String?, + val min: String?, + val max: String?, + val minLength: Int?, + val maxLength: Int?, + val pattern: String?, + val step: Number?, + val options: List? +) + +data class SelectOption( + val value: String, + val label: String, + val selected: Boolean, + val disabled: Boolean +) + +// DataTableData +data class DataTableData( + val title: String, + val description: String, + val searchQuery: String?, + val activeFilters: Map, + val selectedRowCount: Int, + val totalRowCount: Int, + val headers: List, + val sortColumnId: String, + val sortDirection: String, + val rows: List, + val pages: List +) + +data class TableHeader( + val id: String, + val label: String, + val sortable: Boolean +) + +data class DataRow( + val id: String, + val status: String, + val selected: Boolean, + val cells: List +) + +data class DataCell( + val columnId: String, + val rawValue: String, + val displayValue: String, + val tooltip: String?, + val isStatus: Boolean +) + +data class PageItem( + val number: Int, + val isCurrent: Boolean +) + +// SvgDashboardData +data class SvgDashboardData( + val title: String, + val lineChart: LineChart, + val barChart: BarChart, + val pieChart: PieChart, + val gauge: GaugeChart +) + +data class LineChart( + val title: String, + val pathData: String, + val points: List, + val xLabels: List +) + +data class SvgPoint(val x: Double, val y: Double) +data class SvgLabel(val x: Double, val text: String) + +data class BarChart( + val title: String, + val bars: List +) + +data class Bar( + val x: Double, + val y: Double, + val width: Double, + val height: Double, + val value: Double +) + +data class PieChart( + val title: String, + val slices: List +) + +data class PieSliceData( + val pathData: String, + val color: String +) + +data class GaugeChart( + val title: String, + val valuePathData: String, + val needleX: Double, + val needleY: Double, + val valueLabel: String +) + +// ArticleData +data class ArticleData( + val title: String, + val author: String, + val excerpt: String, + val publishDateIso: String, + val publishDateFormatted: String, + val readingTimeMin: Int, + val heroImage: HeroImage, + val breadcrumbs: List, + val toc: List, + val sections: List, + val tags: List, + val relatedArticles: List +) + +data class HeroImage( + val url: String, + val alt: String, + val width: Int, + val height: Int +) + +data class Breadcrumb( + val label: String, + val url: String, + val isCurrent: Boolean +) + +data class TocItem( + val targetId: String, + val title: String +) + +data class ContentSection( + val id: String, + val title: String, + val blocks: List +) + +enum class BlockType { PARAGRAPH, CODE, IMAGE, LINK } + +data class ContentBlock( + val type: BlockType, + val content: String, + val language: String?, + val url: String? +) + +data class RelatedArticle( + val url: String, + val title: String +) + +// preact-render-to-string benchmark fixtures +data class SearchResultsData( + val items: List, + val footerSections: List +) + +data class SearchResultItem( + val id: Int, + val title: String, + val price: String, + val image: String +) + +data class SearchFooterSection( + val title: String, + val links: List, + val headingHref: String? = null +) + +data class SearchFooterLink( + val label: String, + val href: String +) diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DeepStack.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DeepStack.kt new file mode 100644 index 00000000000..cd57720557f --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DeepStack.kt @@ -0,0 +1,28 @@ +/* + * 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 androidx.compose.runtime.Composable +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.dom.* + + +@Composable +fun DeepStack() { + Div { repeat(10) { DeepStackBranch(1_000) } } +} + +@Composable +private fun DeepStackBranch(depth: Int) { + if (depth == 0) { + Div { Span(attrs = { + classes("foo") + attr("data-testid", "stack") + }) { Text("deep stack") } } + } else { + Div { DeepStackBranch(depth - 1) } + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DynamicText.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DynamicText.kt new file mode 100644 index 00000000000..416ce08cfed --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/DynamicText.kt @@ -0,0 +1,31 @@ +/* + * 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 androidx.compose.runtime.Composable +import benchmarks.model.TextData +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text + + +@Composable +fun DynamicText(data: TextData) { + Div { + data.items.forEach { item -> + Div { + Span(attrs = { + classes("foo") + attr("data-testid", "foo") + }) { Text(item.foo) } + Span(attrs = { + classes("bar") + attr("data-testid", "bar") + }) { Text(item.bar) } + } + } + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/FractionalStyle.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/FractionalStyle.kt new file mode 100644 index 00000000000..68ea2477d86 --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/FractionalStyle.kt @@ -0,0 +1,36 @@ +/* + * 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 androidx.compose.runtime.Composable +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* + +@Composable +fun FractionalStyle() { + Div { + repeat(256) { index -> + // These fractions serialize identically from Kotlin Float and JavaScript Number. + val number = (index + 1) / 8f + val overwrite = index % 2 == 1 + Div({ + attr("data-testid", "fractional-style") + style { + width(number.px) + height((number / 4f).em) + property("opacity", (index % 31 + 1) / 32f) + property("line-height", 1.375f + number / 8f, overwrite) + variable("progress", number / 8f) + if (overwrite) { + property("line-height", 1.625f + number / 8f) + property("margin-left", "1px") + property("margin-left", "") + } + } + }) { Text("$index") } + } + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/RawTextElementsRenderer.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/RawTextElementsRenderer.kt new file mode 100644 index 00000000000..b6ea347a7ef --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/RawTextElementsRenderer.kt @@ -0,0 +1,60 @@ +/* + * 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 androidx.compose.runtime.Composable +import benchmarks.model.CatalogData +import org.jetbrains.compose.web.css.StyleSheet +import org.jetbrains.compose.web.css.Style +import org.jetbrains.compose.web.dom.Body +import org.jetbrains.compose.web.dom.Head +import org.jetbrains.compose.web.dom.Html +import org.jetbrains.compose.web.dom.InlineScript +import org.jetbrains.compose.web.dom.Script + +// Keep the inputs stable so the benchmark measures rendering, not fixture construction. +private val rawTextStyleSheet = object : StyleSheet(usePrefix = false) { + val benchmark by style { + repeat(256) { index -> variable("item-$index", "${index}px") } + } +} + +private val styleHeavyCatalogSheet = object : StyleSheet(usePrefix = false) { + val catalog by style { + repeat(2048) { index -> variable("theme-token-$index", "${index}px") } + } +} + +// Repeated, balanced HTML comment markers exercise the script raw-text validator's +// search from each marker while remaining valid JavaScript and HTML raw text. +private val rawTextScript = InlineScript("const markers = `" + "".repeat(1000) + "`;") + +@Composable +fun RawTextStyle() { + Html { + Head { Style(rawTextStyleSheet) } + Body {} + } +} + +@Composable +fun StyleHeavyCatalog(data: CatalogData) { + Html { + Head { Style(styleHeavyCatalogSheet) } + Body { TailwindCatalog(data) } + } +} + +@Composable +fun RawTextElements() { + Html { + Head { + Style(rawTextStyleSheet) + Script(rawTextScript) + } + Body {} + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/SearchResults.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/SearchResults.kt new file mode 100644 index 00000000000..205e626df02 --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/SearchResults.kt @@ -0,0 +1,86 @@ +/* + * 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 androidx.compose.runtime.Composable +import benchmarks.model.SearchResultsData +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.dom.* + +@Composable +fun SearchResults(data: SearchResultsData) { + Div(attrs = { classes("search-results") }) { + Div { + data.items.forEach { item -> + Div(attrs = { classes("search-results-item") }) { + H2 { Text(item.title) } + Div(attrs = { classes("lvpic", "pic", "img", "left") }) { + Div(attrs = { classes("lvpicinner", "full-width", "picW") }) { + A(href = "/buy/${item.id}", attrs = { classes("img", "imgWr2") }) { + Img(src = item.image, alt = item.title, attrs = { attr("loading", "lazy") }) + } + } + } + Span(attrs = { classes("price") }) { Text(item.price) } + Button(attrs = { + classes("buy-now") + type(ButtonType.Button) + }) { Text("Buy now!") } + } + } + } + SearchFooter(data) + } +} + +@Composable +private fun SearchFooter(data: SearchResultsData) { + Footer(attrs = { + id("glbfooter") + attr("role", "contentinfo") + classes("gh-w") + }) { + Div { + Div(attrs = { id("rtm_html_1650") }) { + Div(attrs = { id("rtm_html_1651") }) + H2(attrs = { classes("gh-ar-hdn") }) { Text("Additional site navigation") } + Div(attrs = { + id("gf-BIG") + classes("gffoot") + }) { + Table(attrs = { classes("gf-t") }) { + Tbody { + Tr { + data.footerSections.forEach { section -> + Td { + Ul { + Li(attrs = { classes("gf-li") }) { + H3(attrs = { classes("gf-bttl") }) { + if (section.headingHref == null) { + Text(section.title) + } else { + A(href = section.headingHref, attrs = { classes("gf-bttl", "thrd") }) { + Text(section.title) + } + } + } + } + section.links.forEach { link -> + Li(attrs = { classes("gf-li") }) { + A(href = link.href, attrs = { classes("thrd") }) { Text(link.label) } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/StaticText.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/StaticText.kt new file mode 100644 index 00000000000..be35bb0b537 --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/compose/StaticText.kt @@ -0,0 +1,33 @@ +/* + * 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 androidx.compose.runtime.Composable +import benchmarks.model.BAVARIA_FOO +import benchmarks.model.BAVARIA_BAR +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.dom.* + +// Adapted from preact-render-to-string benchmarks. + + +@Composable +fun StaticText() { + Div { + repeat(1_000) { + Div { + Span(attrs = { + classes("foo") + attr("data-testid", "foo") + }) { Text(BAVARIA_FOO) } + Span(attrs = { + classes("bar") + attr("data-testid", "bar") + }) { Text(BAVARIA_BAR) } + } + } + } +} diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/hydration/GenerateHydrationFixtures.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/hydration/GenerateHydrationFixtures.kt new file mode 100644 index 00000000000..8ba97a42c51 --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/hydration/GenerateHydrationFixtures.kt @@ -0,0 +1,28 @@ +/* + * 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.hydration + +import java.io.File +import org.jetbrains.compose.web.composeHtmlToString + +fun main(args: Array) { + val source = File(args.single(), "HydrationFixtures.kt") + source.parentFile.mkdirs() + source.writeText(buildString { + appendLine("// Generated from the JVM renderer; do not edit.") + appendLine("package benchmarks.hydration") + appendLine("internal val hydrationHtml = mapOf(") + for (workload in hydrationWorkloads) { + val html = composeHtmlToString(content = hydrationContent(workload)) + appendLine(" ${workload.quoted()} to ${html.quoted()},") + } + appendLine(")") + }) +} + +private fun String.quoted(): String = "\"" + replace("\\", "\\\\") + .replace("\"", "\\\"").replace("$", "\\$") + .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + "\"" diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/SearchResultsFixture.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/SearchResultsFixture.kt new file mode 100644 index 00000000000..7b758b7e46d --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/SearchResultsFixture.kt @@ -0,0 +1,184 @@ +/* + * 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.model + +fun searchResultsFixture(): SearchResultsData = SearchResultsData( + items = listOf( + searchResultItem(0, "2012 Nike Air Jordan Retro 4 Bred (Black/Cement-Fire Red) Mens Size 8.5", "\$120.83", 1), + searchResultItem(1, "Nike Air Max 90 Jacquard Sneakers Shoes Size 10.5 New 1 95", "\$126.89", 2), + searchResultItem(2, "jordan 17", "\$181.70", 3), + searchResultItem(3, "Nike Free Run", "\$56.01", 2), + searchResultItem(4, "Nike Zoom Structure+ 15, Royal Blue/ Orange, Size 9.5", "\$65.13", 5), + searchResultItem(5, "Men's Nike Dunk Sz. 12", "\$141.69", 4), + searchResultItem(6, "DS Lebron X 10 Low size 14 BRAND NEW", "\$117.21", 5), + searchResultItem(7, "2008 NIKE AIR JORDAN VI 6 RINGS 322992-061 SIZE 10", "\$175.01", 3), + searchResultItem(8, "Men's Air Jordan 1 Trek Boot Sz. 12 Item#", "\$153.49", 3), + searchResultItem(9, "RARE NiKE AiR FORCE 1 BLACK / BLACK / OLD ROYAL BLUE Hi TOP SZ 11.5", "\$125.71", 4), + searchResultItem(10, "Air Jordan 12 ( XII ) size 12 Retro \" Flu Game \" 2009 130690 065", "\$100.93", 4), + searchResultItem(11, "Air Jordan retro 13 white and red size 10.5", "\$98.51", 3), + searchResultItem(12, "Nike Men's Athletic Running Shoes size 10,", "\$200.45", 3), + searchResultItem(13, "Nike Internationalist Premium Khaki/Brown/Canvas 10.5us", "\$157.84", 6), + searchResultItem(14, "NEW Nike Air Jordan 6 VI Retro VARSITY RED SPIZIKE History of Jordan 23 sz 11", "\$133.01", 5), + searchResultItem(15, "Nike Sb Dunk Wheat Mids 11.5 Jedi Bison Hulk Tiff Wpf", "\$129.09", 3), + searchResultItem(16, "NIke Airmax QD Shoes, Size 11 Black, Waffle Skin, Fitsole 2", "\$158.68", 4), + searchResultItem(17, "Nike Air Penny 5 Orlando - Very Good Condition - Size 8.5", "\$146.56", 3), + searchResultItem(18, "Men Nike Zoom HyperRev 2015 Midnight Navy New Basketball Shoes size 10.5", "\$175.86", 1), + searchResultItem(19, "Mens Nike Flight Size 9.5", "\$146.21", 3), + searchResultItem(20, "Nike Jordan 453980-110 White Courtside Flight Men's Basketball Shoes Size 9.5", "\$66.19", 3), + searchResultItem(21, "NIKE AIR JORDAN 1 I RETRO HI CARMINE WHITE OG 555088-123 size 11.5 NIB", "\$69.01", 5), + searchResultItem(22, "Nike Men's Blazer, size 11.5, Athletic Sneakers", "\$83.42", 1), + searchResultItem(23, "Nike Air Jordan 1 Retro High OG \"Black/White\" new in box size 12", "\$171.77", 1), + searchResultItem(24, "MEN NIKE AIR FORCE 1 AFL WHITE LOW SIZE 9 M", "\$153.90", 2), + searchResultItem(25, "Nike Flight AC Mens Shoes Size 10", "\$104.49", 2), + searchResultItem(26, "Nike Authentic Air Jordan Retro 14 \"Black Toes\" Size 12 Never worn!!!", "\$136.38", 2), + searchResultItem(27, "Nike Air Yeezy 2 Red October Size 11 Plat Solar NRG SP II", "\$81.04", 5), + searchResultItem(28, "Men's Nike Superbad Strike D Cleats Sz. 8 Item#316", "\$148.92", 5), + searchResultItem(29, "nike air jordan son of mars low red", "\$56.12", 6), + searchResultItem(30, "Men's Air Max Size 13", "\$131.95", 2), + searchResultItem(31, "Nike Free 5.0 + Mens Running Shoes Black White Metallic Grey Sneakers 579959 002", "\$132.21", 5), + searchResultItem(32, "New! Mens Nike Dart 10 Running Sneakers Shoes - 4E wide - black", "\$88.73", 4), + searchResultItem(33, "NEW Nike AIR MONARCH IV Mens White Black Red Lace Up Running Training Shoes", "\$59.00", 2), + searchResultItem(34, "Nike Flystepper 2K3 Gym Red - Size 12", "\$139.20", 4), + searchResultItem(35, "Inspired by Nike Jordan XI 11 Gamma Blue Basketball Shoes Mens Size 12", "\$84.83", 6), + searchResultItem(36, "Mems Nike Air Force 1 Size 14 White", "\$140.75", 4), + searchResultItem(37, "Nike Zoom Structure 17 Men's Running Shoes Size 9", "\$195.60", 1), + searchResultItem(38, "Nike Lebron 11 Everglades size 12.5", "\$189.79", 2), + searchResultItem(39, "Nike REVOLUTION 2 Mens Black Volt 042 Lace Up Running Training Athletic Shoes", "\$112.04", 4), + searchResultItem(40, "Nike Free Run 4.0 V2 Mens 8.5", "\$65.58", 5), + searchResultItem(41, "Nike Hyperfuse Men Sneakers. Size 11.5", "\$137.08", 2), + searchResultItem(42, "Air Jordan 1 Retro High OG Laney Size 11 Deadstock", "\$164.46", 1), + searchResultItem(43, "Mens Nike Air Command Force University Red White Black Grey 684715-600", "\$52.98", 6), + searchResultItem(44, "Nike Air Max 1", "\$82.75", 2), + searchResultItem(45, "Jordan Bred 13's Retro 2004", "\$191.06", 3), + searchResultItem(46, "Nike City Court VII Mens White Leather Walking Shoes - NWD- Size 6 M - 15 M", "\$180.38", 3), + searchResultItem(47, "Nike sb dunk high \"northern lights\" sz 11", "\$82.97", 1), + searchResultItem(48, "Jordan Bred 13's Retro 2004", "\$191.68", 1), + searchResultItem(49, "RARE \$110 Mens Bart Simpson NIKE DUNK basketball shoes 11.5 bought @ Nike Store", "\$177.60", 5), + searchResultItem(50, "Nike Zoom Structure 17 Men's Running Shoes Size 11.5", "\$189.69", 1), + searchResultItem(51, "Nike sneakers size 8.5", "\$152.39", 5), + searchResultItem(52, "NIKE KD VII 7 ALL STAR Size 7", "\$131.27", 6), + searchResultItem(53, "Nike City Court VII Mens White Leather Walking Shoes - NWD- Size 6 M - 15 M", "\$100.45", 3), + searchResultItem(54, "Nike sb dunk high \"northern lights\" sz 11", "\$200.31", 5), + searchResultItem(55, "Nike Air Lebron XI/11 Kings Pride Size 11", "\$110.41", 2), + searchResultItem(56, "Nike Air JORDAN B-2RUE 312523 107 Wht/Mts-Cool Gray-Chambray size 10.5 NIB NEW", "\$191.23", 3), + searchResultItem(57, "Nike Lebron 11 Kings Pride Size 7.5", "\$175.46", 2), + searchResultItem(58, "nike air jordan son of mars low red", "\$79.27", 6), + searchResultItem(59, "Nike Air Jordan Retro 12 Flu Game Black Red Size 10", "\$122.74", 6), + searchResultItem(60, "Nike Baseball Training Shoes Size 9", "\$188.47", 4), + searchResultItem(61, "Jordan Retro Nike Free Runs 5.0 NIB Size 9 Running", "\$168.14", 6), + searchResultItem(62, "NIKE AIR FORCE 1 Ones High Top Black Men 12 Sneakers Shoes", "\$98.38", 1), + searchResultItem(63, "Nike+ Livestrong Running Shoes", "\$84.66", 3), + searchResultItem(64, "Nike Air Max Motion Mens Sz Running Shoes Cool Grey Crimson Sneakers 631767 006", "\$183.62", 2), + searchResultItem(65, "nike roshe run", "\$197.89", 5), + searchResultItem(66, "Nike Dual Fusion 11 Mens", "\$96.54", 5), + searchResultItem(67, "NEW! MENS NIKE REVOLUTION 2 BLACK/WHITE/RED RUNNING TRAINING SHOES SIZE", "\$142.79", 3), + searchResultItem(68, "Nike flyknit one", "\$121.40", 1), + searchResultItem(69, "2005 Mens Nike Air Jordan 4 Retro LS White Yellow & Black Size 12 [314254-171]", "\$190.97", 5), + searchResultItem(70, "Nike Jordan VII 7 Retro White/Bronze-Red-Black 2006 Cardinal 304775 SZ 13", "\$179.78", 5), + searchResultItem(71, "Nike Air Jordan Retro 1 I High MELO Black Gold OG Carmelo 332550-026 Sz:4y-13", "\$86.12", 4), + searchResultItem(72, "Men's Jordan 6 Retro Sport Blue size 12 deadstock limited nike basketball shoe", "\$157.74", 1), + searchResultItem(73, "Nike Air Jordan 4 Retro White/Legend Blue/Midnight Navy SIZE 10 123456789", "\$136.69", 6), + searchResultItem(74, "NIKE AIR FOAMPOSITE 1 ONE ROYAL BLUE ROUGH", "\$179.67", 6), + searchResultItem(75, "Nike Running Shoes", "\$83.20", 3), + searchResultItem(76, "air max 90 independence day Size 9.5", "\$54.31", 6), + searchResultItem(77, "New Nike Air Alvord 9 Black/Grey/Sil Mens Athletic Shoe - 443841 001 - Medium", "\$151.13", 1), + searchResultItem(78, "Men's Nike Black Running Shoes Zoom Pegasus 31 Flash Size 8.", "\$67.95", 3), + searchResultItem(79, "Air Jordan 4 Legend Blue", "\$121.95", 4), + searchResultItem(80, "Nike Hyperfuse Vapor Elite Football Cleats 534771-419 Shoes US 15", "\$112.58", 2), + searchResultItem(81, "NIKE AIR MAX BLACK MEN'S 10", "\$182.55", 2), + searchResultItem(82, "nike sb when pigs fly \"Concept\"", "\$127.56", 5), + searchResultItem(83, "NEW Men's Nike Air Max TN Blue size 12", "\$176.13", 2), + searchResultItem(84, "NIKE 554889 001 NIKE DUAL FUSION TRAINER IV MEN'S PREMIUM RUNNING SHOESsz 13", "\$150.83", 4), + searchResultItem(85, "Well worn running shoes", "\$138.85", 1), + searchResultItem(86, "Nike Air Jordan 15 XV Basketball Shoes Size 10 Men 136029 Flint Grey White 2000", "\$194.95", 2), + searchResultItem(87, "mens nike shox Size 11.5", "\$175.89", 3), + searchResultItem(88, "2014 Nike Air Huarache Triple Black White Anthracite Grey QS 318429-003 8-13", "\$93.45", 4), + searchResultItem(89, "Nike Air Force One AF1 Low White Size 9 Basketball shoe", "\$149.28", 3), + searchResultItem(90, "Nike Dunk Low Pro SB - Poison - Size 14 (RARE 2008 Dunks!!!!!!!)", "\$195.60", 3), + searchResultItem(91, "Air Jordan Retro 7 French Blue", "\$121.49", 4), + searchResultItem(92, "White-Out Foamposite", "\$117.42", 5), + searchResultItem(93, "NEW Nike AIR MONARCH IV Mens Black Comfort Lace Up Running Training Shoes", "\$123.18", 6), + searchResultItem(94, "NIKE AIR MAX MEN'S 10", "\$161.46", 5), + searchResultItem(95, "Nike Kobe 8 Yoth", "\$152.88", 2), + searchResultItem(96, "Nike Roshe Run Rosherun Comfort Sneakers New, Dark Loden 511881-303 \"Swoosh Pack", "\$70.39", 5), + searchResultItem(97, "Men's Nike Lunar Kayoss Sz 11", "\$77.91", 3), + searchResultItem(98, "mens nike shox Size 11.5", "\$137.71", 2), + searchResultItem(99, "Nike Dunk Low Pro SB - Poison - Size 14 (RARE 2008 Dunks!!!!!!!)", "\$158.39", 3), + ), + footerSections = searchFooterSections, +) + +private fun searchResultItem(id: Int, title: String, price: String, image: Int) = SearchResultItem( + id, title, price, + "/isomorphic-ui-benchmarks/static/images/test-image-${image.toString().padStart(2, '0')}.jpg", +) + +private val searchFooterSections = listOf( + searchFooterSection("Buy", + "Registration" to "http://pages.ebay.com/help/account/registration.html", + "eBay Money Back Guarantee" to "http://pages.ebay.com/ebay-money-back-guarantee/", + "Bidding & buying help" to "http://pages.ebay.com/help/buy/basics.html", + "Stores" to "http://stores.ebay.com", "eBay Local" to "http://www.ebay.com/local", + "eBay guides" to "http://www.ebay.com/gds").copy(headingHref = "http://www.ebay.com/sch/allcategories/all-categories"), + searchFooterSection("Sell", + "Start selling" to "http://www.ebay.com/sl/sell", + "Learn to sell" to "http://pages.ebay.com/sellerinformation/howtosell/sellingbasics.html", + "Business sellers" to "http://pages.ebay.com/sellerinformation/ebayforbusiness/essentials.html", + "Affiliates" to "https://www.ebaypartnernetwork.com/files/hub/en-US/index.html", + "Mobile apps" to "http://anywhere.ebay.com/mobile/", "Downloads" to "http://anywhere.ebay.com", + "Developers" to "http://developer.ebay.com", "Security center" to "http://pages.ebay.com/securitycenter/index.html", + "eBay official time" to "http://viv.ebay.com/ws/eBayISAPI.dll?EbayTime", + "Site map" to "http://pages.ebay.com/sitemap.html", "eBay Classifieds" to "http://www.ebayclassifiedsgroup.com/", + "StubHub" to "http://www.stubhub.com", "Close5" to "https://www.close5.com", + "See all companies" to "https://www.ebayinc.com/our-company/our-other-businesses/").copy(headingHref = "http://www.ebay.com/sl/sell"), + searchFooterSection("Stay connected", "eBay's Blogs" to "http://www.ebay.com/stories/", + "Facebook" to "https://www.facebook.com/eBay", "Twitter" to "http://twitter.com/#!/eBay", + "Google+" to "https://plus.google.com/+eBay/posts"), + searchFooterSection("About eBay", "Company info" to "https://www.ebayinc.com/our-company/", + "News" to "https://www.ebayinc.com/stories/news/", "Investors" to "https://investors.ebayinc.com", + "Careers" to "https://careers.ebayinc.com/", "Government relations" to "http://www.ebaymainstreet.com", + "Advertise with us" to "http://cc.ebay.com", "Policies" to "http://pages.ebay.com/help/policies/overview.html", + "Verified Rights Owner (VeRO) Program" to "http://pages.ebay.com/help/policies/programs-vero-ov.html", + "Tell us what you think" to "http://qu.ebay.com/survey?srvName=globalheader+%28footer-US%29").copy(headingHref = "http://www.ebayinc.com"), + searchFooterSection("Help & Contact", "Resolution Center" to "http://resolutioncenter.ebay.com/", + "Seller Information Center" to "http://pages.ebay.com/sellerinformation/index.html", + "Contact us" to "http://ocsnext.ebay.com/ocs/cuhome").copy(headingHref = "http://ocs.ebay.com/ws/eBayISAPI.dll?CustomerSupport"), + searchFooterSection("Community", "Announcements" to "http://announcements.ebay.com", + "Answer center" to "http://pages.ebay.com/community/answercenter/index.html", + "Discussion boards" to "http://forums.ebay.com", "eBay Giving Works" to "http://givingworks.ebay.com", + "eBay Celebrity" to "http://givingworks.ebay.com/browse/celebrities", + "Groups" to "http://groups.ebay.com/groups/EbayGroups/1?redirected=1", + "eBay top shared" to "http://www.ebay.com/ets/eBayTopShared").copy(headingHref = "http://community.ebay.com"), + searchFooterSection("eBay Sites", *listOf( + "United States" to "http://www.ebay.com", "Australia" to "http://www.ebay.com.au", + "Austria" to "http://www.ebay.at", "Belgium" to "http://www.ebay.be", "Canada" to "http://www.ebay.ca", + "China" to "http://www.ebay.cn", "France" to "http://www.ebay.fr", "Germany" to "http://www.ebay.de", + "Hong Kong" to "http://www.ebay.com.hk", "India" to "http://www.ebay.in", "Ireland" to "http://www.ebay.ie", + "Italy" to "http://www.ebay.it", "Japan" to "http://www.ebay.co.jp", + "Korea" to "http://global.gmarket.co.kr/Home/Main", "Malaysia" to "http://www.ebay.com.my", + "Netherlands" to "http://www.ebay.nl", "Philippines" to "http://www.ebay.ph", "Poland" to "http://www.ebay.pl", + "Singapore" to "http://www.ebay.com.sg", "Spain" to "http://www.ebay.es", "Sweden" to "http://www.ebay.se", + "Switzerland" to "http://www.ebay.ch", "Taiwan" to "http://www.ebay.com.tw", + "Thailand" to "http://www.ebay.co.th", "Turkey" to "http://www.gittigidiyor.com", + "United Kingdom" to "http://www.ebay.co.uk", "Vietnam" to "http://www.ebay.vn" + ).toTypedArray()), + searchFooterSection("Legal", "About eBay" to "http://www.ebayinc.com", + "Announcements" to "http://announcements.ebay.com", "Community" to "http://community.ebay.com", + "Security Center" to "http://pages.ebay.com/securitycenter/index.html", + "Resolution Center" to "http://resolutioncenter.ebay.com/", + "Seller Information Center" to "http://pages.ebay.com/sellerinformation/index.html", + "Policies" to "http://pages.ebay.com/help/policies/overview.html", + "Affiliates" to "https://www.ebaypartnernetwork.com/files/hub/en-US/index.html", + "Help & Contact" to "http://ocs.ebay.com/ws/eBayISAPI.dll?CustomerSupport", + "Site Map" to "http://pages.ebay.com/sitemap.html", + "User Agreement" to "http://pages.ebay.com/help/policies/user-agreement.html", + "Privacy" to "http://pages.ebay.com/help/policies/privacy-policy.html", + "Cookies" to "http://pages.ebay.com/help/account/cookies-web-beacons.html", + "AdChoice" to "http://cgi6.ebay.com/ws/eBayISAPI.dll?AdChoiceLandingPage&partner=0") +) + +private fun searchFooterSection(title: String, vararg links: Pair) = + SearchFooterSection(title, links.map { SearchFooterLink(it.first, it.second) }) diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextData.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextData.kt new file mode 100644 index 00000000000..efb883c3f3c --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextData.kt @@ -0,0 +1,13 @@ +/* + * 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.model + +// Prose adapted from preact-render-to-string, shared by the constant and runtime-text workloads. +const val BAVARIA_FOO = "Bavaria ipsum dolor sit amet gwiss Charivari Auffisteign koa. Umma pfenningguat vui huift vui back mas Landla Bradwurschtsemmal, Fingahaggln. Wolpern ja, wo samma denn wea nia ausgähd, kummt nia hoam baddscher i moan oiwei! Kloan pfenningguat is Charivari Bussal, hallelujah sog i, luja. Liberalitas Bavariae hod Schorsch om auf’n Gipfe gwiss naa. Und ja, wo samma denn Ohrwaschl hoggd auffi Spotzerl Diandldrahn, oba? Is sog i und glei wirds no fui lustiga Biaschlegl ma nimma ned woar gscheckate, pfenningguat! Gstanzl dei Schorsch Radi i mog di fei hea Reiwadatschi fensdaln dei glei a Hoiwe. Bitt umananda ghupft wia gsprunga Gschicht kimmt, oamoi obandeln. Sog i helfgod amoi hallelujah sog i, luja i hob di narrisch gean, Brodzeid. Wolln a Maß und no a Maß Gaudi obandln eana boarischer hallelujah sog i, luja Maßkruag greaßt eich nachad, Schmankal." +const val BAVARIA_BAR = "Dei um Godds wujn naa Watschnbaam Obazda Trachtnhuat, Vergeltsgott Schneid Schbozal. Om auf’n Gipfe Ramasuri um Godds wujn eana. Wos sammawiedaguad sei Weißwiaschd da, hog di hi is des liab des umananda Brezn Sauakraud Diandldrahn. Vo de weida pfundig Kirwa de Sonn Hetschapfah Watschnpladdla auf gehds beim Schichtl Meidromml auffi lem und lem lossn! Watschnpladdla wolln measi obandeln griasd eich midnand Oachkatzlschwoaf is ma Wuascht sammawiedaguad aasgem. A so a Schmarn Weibaleid naa, des basd scho. Abfieseln helfgod Sauwedda middn ded schoo. A bissal wos gehd ollaweil Sauwedda is Servas wiavui wo hi o’ha, a liabs Deandl pfiad de nix. Maßkruag etza so spernzaln. Weiznglasl Bradwurschtsemmal da, Schdeckalfisch: Mei Musi bitt des wiad a Mordsgaudi kumm geh Biakriagal Greichats obacht?" + +data class TextData(val items: List) +data class TextPair(val foo: String, val bar: String) diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextFixtures.kt b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextFixtures.kt new file mode 100644 index 00000000000..ace0edd029d --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/benchmarks/model/TextFixtures.kt @@ -0,0 +1,17 @@ +/* + * 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.model + +// Prepares the text data before measuring rendering. +fun textFixture( + escaped: Boolean = false, count: Int = 1_000, + foo: String = BAVARIA_FOO, bar: String = BAVARIA_BAR, +): TextData = TextData( + (1..count).map { index -> + val prefix = if (escaped) " & \"quoted\" 'single' 😀 café α: " else "Item $index: " + TextPair(prefix + foo, prefix + bar) + }, +) diff --git a/html/eap/benchmarks/src/main/kotlin/org/jetbrains/compose/web/RendererPoolBenchmark.kt b/html/eap/benchmarks/src/jvmMain/kotlin/org/jetbrains/compose/web/RendererPoolBenchmark.kt similarity index 100% rename from html/eap/benchmarks/src/main/kotlin/org/jetbrains/compose/web/RendererPoolBenchmark.kt rename to html/eap/benchmarks/src/jvmMain/kotlin/org/jetbrains/compose/web/RendererPoolBenchmark.kt diff --git a/html/eap/benchmarks/src/jvmMain/kotlin/org/jetbrains/compose/web/SsrBenchmark.kt b/html/eap/benchmarks/src/jvmMain/kotlin/org/jetbrains/compose/web/SsrBenchmark.kt new file mode 100644 index 00000000000..43cde7c222c --- /dev/null +++ b/html/eap/benchmarks/src/jvmMain/kotlin/org/jetbrains/compose/web/SsrBenchmark.kt @@ -0,0 +1,111 @@ +/* + * 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 org.jetbrains.compose.web + +import androidx.compose.runtime.Composable +import benchmarks.compose.* +import benchmarks.model.* +import java.io.StringWriter +import java.io.Writer +import kotlinx.benchmark.* +import kotlinx.browser.dom.Element +import org.openjdk.jmh.annotations.Fork +import org.jetbrains.compose.web.dom.TagElement +import org.jetbrains.compose.web.dom.Text + +/** Renders the same fixtures to a String, a Writer, or String chunks. */ +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(BenchmarkTimeUnit.MICROSECONDS) +@Warmup(iterations = 1, time = 5) +@Measurement(iterations = 10, time = 1) +@Fork(value = 2, jvmArgsAppend = ["-Xms256m", "-Xmx256m", "-Xss16m"]) +open class SsrBenchmark { + @Param( + "tailwind-catalog", "form-app", "data-table", "svg-dashboard", "content-article", + "static-text", "dynamic-text", "dynamic-text-escaped", "search-results", "deep-stack", + "raw-text-style", "raw-text-elements", "fractional-style", "style-heavy-catalog", + "stream-pre", "stream-textarea", + ) + @JvmField + var workload: String = "" + + private lateinit var content: @Composable () -> Unit + private lateinit var key: String + private var streamedLength = 0 + private val sink: (String) -> Unit = { streamedLength += it.length } + private val writer = object : Writer() { + override fun write(buffer: CharArray, offset: Int, length: Int) { + streamedLength += length + } + + override fun flush() {} + override fun close() {} + } + + @Setup + open fun setup() { + key = "ssr-$workload" + // Construct fixtures and composable lambdas outside measurement. + content = when (workload) { + "tailwind-catalog" -> catalogFixture().let { data -> { TailwindCatalogDocument(data) } } + "form-app" -> formFixture().let { data -> { FormAppDocument(data) } } + "data-table" -> dataTableFixture().let { data -> { DataTableDocument(data) } } + "svg-dashboard" -> svgDashboardFixture().let { data -> { SvgDashboardDocument(data) } } + "content-article" -> articleFixture().let { data -> { ContentArticleDocument(data) } } + "static-text" -> { { StaticText() } } + "dynamic-text", "dynamic-text-escaped" -> textFixture(escaped = workload == "dynamic-text-escaped") + .let { data -> { DynamicText(data) } } + "search-results" -> searchResultsFixture().let { data -> { SearchResults(data) } } + "deep-stack" -> { { DeepStack() } } + "raw-text-style" -> { { RawTextStyle() } } + "raw-text-elements" -> { { RawTextElements() } } + "fractional-style" -> { { FractionalStyle() } } + "style-heavy-catalog" -> catalogFixture().let { data -> { StyleHeavyCatalog(data) } } + "stream-pre" -> streamingContent("pre") + "stream-textarea" -> streamingContent("textarea") + else -> error("Unknown workload: $workload") + } + + // Check complete output before measuring streams with non-retaining sinks. + val expected = renderToString() + val output = StringWriter() + composeHtmlToStream(writer = output, key = key, content = content) + check(output.toString() == expected) { "Writer output differs for $workload" } + val chunks = StringBuilder() + composeHtmlToStream(sink = { chunks.append(it) }, key = key, content = content) + check(chunks.toString() == expected) { "Callback output differs for $workload" } + check(streamToWriter() == expected.length) + check(streamToCallback() == expected.length) + } + + @Benchmark + open fun renderToString(): String = composeHtmlToString(key = key, content = content) + + @Benchmark + open fun streamToWriter(): Int { + streamedLength = 0 + composeHtmlToStream(writer = writer, key = key, content = content) + return streamedLength + } + + @Benchmark + open fun streamToCallback(): Int { + streamedLength = 0 + composeHtmlToStream(sink = sink, key = key, content = content) + return streamedLength + } +} + +private fun streamingContent(tag: String): @Composable () -> Unit { + val text = "a&😀α".repeat(512) + return { + TagElement(tag, null) { + Text("\n") + repeat(256) { Text(text) } + } + } +} diff --git a/html/eap/benchmarks/src/webTest/kotlin/benchmarks/hydration/HydrationBenchmark.kt b/html/eap/benchmarks/src/webTest/kotlin/benchmarks/hydration/HydrationBenchmark.kt new file mode 100644 index 00000000000..8d47ddce2a3 --- /dev/null +++ b/html/eap/benchmarks/src/webTest/kotlin/benchmarks/hydration/HydrationBenchmark.kt @@ -0,0 +1,77 @@ +/* + * 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. + */ + +@file:OptIn( + kotlin.js.ExperimentalWasmJsInterop::class, + org.jetbrains.compose.web.internal.runtime.ComposeWebInternalApi::class, +) + +package benchmarks.hydration + +import kotlinx.browser.dom.HTMLElement +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.promise +import kotlinx.coroutines.withTimeout +import org.jetbrains.compose.web.hydrateComposable +import org.jetbrains.compose.web.internal.runtime.browserDocument +import kotlin.math.roundToInt +import kotlin.math.sqrt +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import kotlin.test.assertTrue +import kotlin.time.measureTimedValue + +class HydrationBenchmark { + @Test fun catalog() = benchmark("tailwind-catalog") + @Test fun form() = benchmark("form-app") + @Test fun table() = benchmark("data-table") + @Test fun svg() = benchmark("svg-dashboard") + @Test fun article() = benchmark("content-article") + @Test fun hydrate1k() = benchmark("hydrate1k") + + private fun benchmark(workload: String) = MainScope().promise { + val html = hydrationHtml.getValue(workload) + val content = hydrationContent(workload) + val samples = mutableListOf() + repeat(7) { iteration -> + val root = browserDocument.createElement("div") as HTMLElement + root.innerHTML = html + browserDocument.body!!.appendChild(root) + val nodes = root.querySelectorAll("*") + val original = List(nodes.length) { nodes.item(it)!! } + val (composition, elapsed) = measureTimedValue { + hydrateComposable(root, onHydrationMismatch = { throw it }) { content() } + } + try { + assertTrue(original.isNotEmpty(), workload) + val hydrated = root.querySelectorAll("*") + assertEquals(original.size, hydrated.length, workload) + original.forEachIndexed { index, node -> assertSame(node, hydrated.item(index), workload) } + if (workload == "hydrate1k" && iteration == 6) { + assertEquals(1_000, root.querySelectorAll("tbody tr").length) + (root.querySelector("tbody tr:nth-child(2) td:nth-child(3) a") as HTMLElement).click() + withTimeout(5_000) { + while (root.querySelectorAll("tbody tr").length != 999) delay(1) + } + assertEquals("3", root.querySelector("tbody tr:nth-child(2) td")!!.textContent) + } + if (iteration >= 2) samples += elapsed.inWholeNanoseconds / 1_000_000.0 + } finally { + composition.dispose() + root.parentNode!!.removeChild(root) + } + } + val mean = samples.average() + val stddev = sqrt(samples.sumOf { (it - mean) * (it - mean) } / (samples.size - 1)) + println( + "HYDRATION $workload: mean=${mean.rounded()} ms, stddev=${stddev.rounded()} ms, " + + "range=${samples.min()}..${samples.max()} ms, n=${samples.size}; samples=$samples" + ) + } +} + +private fun Double.rounded(): Double = (this * 1_000).roundToInt() / 1_000.0 diff --git a/html/eap/compiler-plugin/README.md b/html/eap/compiler-plugin/README.md index 37755f4537e..449e6452a82 100644 --- a/html/eap/compiler-plugin/README.md +++ b/html/eap/compiler-plugin/README.md @@ -5,6 +5,17 @@ Prepares static HTML at compile time to reduce work during server-side rendering ## Activate Use the [Compose HTML EAP runtime](../README.md#published-artifacts). +Add the development repository to `settings.gradle.kts`: + +```kotlin +pluginManagement { + repositories { + maven("https://packages.jetbrains.team/maven/p/cmp/dev") + gradlePluginPortal() + } +} +``` + Add the plugin to your module's `build.gradle.kts` alongside your existing Kotlin and Compose compiler plugins: ```kotlin diff --git a/html/licenses/third_party/README.md b/html/licenses/third_party/README.md index 21f23958a4a..fbdce4514a1 100644 --- a/html/licenses/third_party/README.md +++ b/html/licenses/third_party/README.md @@ -3,3 +3,12 @@ Following third-party dependencies exist in this module: * highlight.js (version 10.7.2): * BSD 3-Clause "New" or "Revised" License [bsd-3-revised.txt](bsd-3-revised.txt) * https://github.com/highlightjs/highlight.js/blob/main/LICENSE + +The EAP benchmarks also contain the following benchmark adaptations: + +* `static-text`, `dynamic-text`, `dynamic-text-escaped`, `search-results`, and `deep-stack`: + * MIT License [preact-render-to-string.txt](preact-render-to-string.txt) + * https://github.com/preactjs/preact-render-to-string +* `hydrate1k`: + * MIT License [preact-benchmarks.txt](preact-benchmarks.txt) + * https://github.com/preactjs/benchmarks diff --git a/html/licenses/third_party/preact-benchmarks.txt b/html/licenses/third_party/preact-benchmarks.txt new file mode 100644 index 00000000000..6b87c281ed5 --- /dev/null +++ b/html/licenses/third_party/preact-benchmarks.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Preact + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/html/licenses/third_party/preact-render-to-string.txt b/html/licenses/third_party/preact-render-to-string.txt new file mode 100644 index 00000000000..3291625f1a9 --- /dev/null +++ b/html/licenses/third_party/preact-render-to-string.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.