Repository navigation
Reduce Compose HTML EAP rendering overhead #5768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| 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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see this function (
It might make sense to have 2 overloads:
which of the existing benchmark workloads "exercises" this function?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Especially I quickly tried adding two overloads on |
||
| 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) | ||
|
|
||
There was a problem hiding this comment.
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:
java.util.BitSetto support more than 64 valuesBut 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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).