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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private const val HtmlDoctype = "<!doctype html>"
* Renders a complete HTML document with optional browser hydration.
*
* [content] must produce exactly one `html` element. Output starts with an HTML doctype.
* Output starts with an HTML doctype.
* Static documents emit no hydration state.
*
* Load client code that calls `hydrateRoot` after [HydrationRoot] and its state element are parsed:
Expand Down Expand Up @@ -113,7 +114,7 @@ internal fun composeHydratedDocument(
): String {
val seenHydrationIds = if (validateStrictly) mutableSetOf<String>() else null
return composeHtmlString(
requireHtmlDocumentRoot = true,
requireHtmlDocumentRoot = validateStrictly,
chunkSink = chunkSink,
chunkSize = chunkSize,
prefix = HtmlDoctype,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,49 +218,56 @@ data class StylePropertyDeclaration(
typealias StylePropertyList = List<StylePropertyDeclaration>
typealias MutableStylePropertyList = MutableList<StylePropertyDeclaration>

// TODO: Investigate an append overload that avoids intermediate strings.
@OptIn(ComposeWebInternalApi::class)
internal fun StyleHolder.toStyleAttributeValue(): String? {
if (properties.isEmpty() && variables.isEmpty()) return null
// Reuse conversions if priority or removal needs a second pass.
// Skip removed values as before.
val values = arrayOfNulls<String>(properties.size + variables.size)
val ordinary = StringBuilder()
if (ordinary.appendOrdinaryDeclarations(properties, "") &&
ordinary.appendOrdinaryDeclarations(variables, "--")
if (ordinary.appendOrdinaryDeclarations(properties, "", values, 0) &&
ordinary.appendOrdinaryDeclarations(variables, "--", values, properties.size)
) return ordinary.toString()
val declarations = properties + variables.map { it.copy(name = "--${it.name}") }
val removedNames = mutableSetOf<String>()
val resetPriorityNames = mutableSetOf<String>()
// Walk backwards to retain fallbacks without repeatedly scanning earlier declarations.
// Empty assignments remove a property. Normal assignments reset its earlier priority.
return declarations.asReversed().filter { declaration ->
val included = BooleanArray(declarations.size)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How large can it be in practice? Not many I assume.

Two thoughts:

  • perhaps the bits in Long (64 bits) are enough to hold the necessary amount of flags
  • Otherwise, for jvm this can use java.util.BitSet to support more than 64 values

But it's fine to keep your current implementation until we see any evidences in the profiler.
adding a comment to consider BitSet for k/jvm would be enough

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our current test workloads, we only have 5-8 declarations. But we can't expect that to be always the case, so we'd still need a fallback with Long.
Long wasn't faster in complete SSR and reduced allocation by 0.5%. I think the simpler implementation without the fallback is worth keeping, or WDYT?

I also tried BitSet - it was about 5% slower.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. TIL: BitSet is slower than BooleanArray (at least in this case).

for (index in declarations.lastIndex downTo 0) {
val declaration = declarations[index]
if (declaration.name in removedNames) continue
val value = values[index] ?: declaration.value.toString().also { values[index] = it }
when {
declaration.name in removedNames -> false
declaration.value.toString().isEmpty() -> {
removedNames.add(declaration.name)
false
}

declaration.important -> declaration.name !in resetPriorityNames
value.isEmpty() -> removedNames.add(declaration.name)
declaration.important -> included[index] = declaration.name !in resetPriorityNames
else -> {
resetPriorityNames.add(declaration.name)
true
included[index] = true
}
}
}.asReversed()
.takeIf { declarations -> declarations.isNotEmpty() }
?.joinToString("; ") { declaration ->
buildString {
append(declaration.name)
append(": ")
append(declaration.value)
if (declaration.important) append(" !important")
}
}
return buildString {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this function (toStyleAttributeValue) has 2 use cases:

  • the resulting String is written into a StringBuilder
  • the resulting String is used as String directly

It might make sense to have 2 overloads:

  • 1: a new variant taking the StringBuilder as a parameter
  • 2: same signature as now but implemented via the 1st overload

which of the existing benchmark workloads "exercises" this function?
Here too, I think no rush to apply my suggestion. Maybe adding a comment in the code is enough.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially fractional-style, but also data-table.

I quickly tried adding two overloads on fractional-style, it had 3.4% less allocation, but 2.7% more render time. It still produces an intermediate CSS String because the SSR caller subsequently HTML-escapes it. I'll add a comment to further look into it in the future.

for (index in declarations.indices) {
if (!included[index]) continue
val declaration = declarations[index]
if (isNotEmpty()) append("; ")
append(declaration.name).append(": ").append(values[index])
if (declaration.important) append(" !important")
}
}.takeIf { it.isNotEmpty() }
}

// Convert each ordinary value once, including numeric CSS carriers.
private fun StringBuilder.appendOrdinaryDeclarations(declarations: StylePropertyList, prefix: String): Boolean {
for (declaration in declarations) {
private fun StringBuilder.appendOrdinaryDeclarations(
declarations: StylePropertyList,
prefix: String,
values: Array<String?>,
offset: Int,
): Boolean {
for (index in declarations.indices) {
val declaration = declarations[index]
if (declaration.important) return false
val value = declaration.value.toString()
values[offset + index] = value
if (value.isEmpty()) return false
if (isNotEmpty()) append("; ")
append(prefix).append(declaration.name).append(": ").append(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ internal fun String.rawTextTagIndex(
}

private fun requireValidRawTextContent(tagName: String, content: String) {
require(tagName in listOf("script", "style", "iframe", "xmp", "noembed", "noframes")) {
require(isHtmlRawTextElement(tagName, HtmlNamespace)) {
"Raw text content is not supported for <$tagName>"
}
require(content.rawTextTagIndex(tagName, closing = true) < 0) {
Expand Down
Loading
Loading