Add SlotTable integration benchmark suite
Benchmarks to compare Composition under the old and new SlotTable.
These should demonstrate scenarios in which the old SlotTable performs
poorly and the new SlotTable matches or exceeds the old performance.
Test: SlotTableIntegrationBenchmark
Change-Id: I01b91517e0ac3f74bee37350f65a90ac76a12960
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index a905398..bda7255 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -49,6 +49,19 @@
</intent-filter>
</activity>
<activity
+ android:name=".StaticScrollingContentWithChromeInitialCompositionActivity"
+ android:label="C StaticScrollingWithChrome Init"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".TrivialStartupTracingActivity"
android:label="C TrivialTracing"
android:exported="true">
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
new file mode 100644
index 0000000..87810a2
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/StaticScrollingContentWithChromeInitialCompositionActivity.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomNavigation
+import androidx.compose.material.BottomNavigationItem
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material.icons.filled.Place
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.trace
+
+class StaticScrollingContentWithChromeInitialCompositionActivity : ComponentActivity() {
+
+ private val onlyPerformComposition: Boolean
+ get() = intent.action == "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ if (onlyPerformComposition) {
+ ComposeOnlyLayout {
+ StaticScrollingContentWithChrome(
+ modifier = Modifier
+ .onPlaced { _ ->
+ throw RuntimeException(
+ "Content was placed, but should only be composed"
+ )
+ }
+ .drawWithContent {
+ throw RuntimeException(
+ "Content was drawn, but should only be composed"
+ )
+ }
+ )
+ }
+ } else {
+ StaticScrollingContentWithChrome()
+ }
+ }
+ }
+}
+
+/**
+ * A layout that will compose all of the [content], but will not place
+ * (and therefore not layout or draw) any of its children.
+ *
+ * This is useful for this benchmark as we care about the composition time. A major limitation
+ * of this approach is that any content in a SubcomposeLayout will not be composed
+ * and will not contribute to the overall measured time of this test.
+ */
+@Composable
+private fun ComposeOnlyLayout(
+ content: @Composable () -> Unit
+) {
+ Layout(content) { _, _ -> layout(0, 0) {} }
+}
+
+@Preview
+@Composable
+private fun StaticScrollingContentWithChrome(
+ modifier: Modifier = Modifier
+) = trace(sectionName = "StaticScrollingContentWithChrome") {
+ Column(modifier) {
+ TopBar()
+ ScrollingContent(modifier = Modifier.weight(1f))
+ BottomBar()
+ }
+}
+
+@Composable
+private fun TopBar(modifier: Modifier = Modifier) {
+ TopAppBar(
+ modifier = modifier,
+ title = {
+ Column {
+ Text(
+ "Initial Composition Macrobench",
+ style = MaterialTheme.typography.subtitle1,
+ maxLines = 1
+ )
+ Text(
+ "Static Scrolling Content w/ Chrome",
+ style = MaterialTheme.typography.caption,
+ maxLines = 1
+ )
+ }
+ },
+ navigationIcon = {
+ Button(onClick = {}) {
+ Icon(Icons.Default.Close, "Dismiss")
+ }
+ },
+ actions = {
+ Button(onClick = {}) {
+ Icon(Icons.Default.MoreVert, "Actions")
+ }
+ }
+ )
+}
+
+@Composable
+private fun BottomBar(modifier: Modifier = Modifier) {
+ BottomNavigation(modifier = modifier) {
+ BottomNavigationItem(
+ selected = true,
+ onClick = {},
+ icon = { Icon(Icons.Default.Home, "Home") }
+ )
+ BottomNavigationItem(
+ selected = false,
+ onClick = {},
+ icon = { Icon(Icons.Default.Add, "Add") }
+ )
+ }
+}
+
+@Composable
+private fun ScrollingContent(modifier: Modifier = Modifier) {
+ Column(
+ modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(vertical = 16.dp)
+ ) {
+ Item(
+ color = Color.DarkGray,
+ icon = Icons.Filled.Info,
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .aspectRatio(16f / 9f)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(16.dp))
+ )
+
+ repeat(5) { iteration ->
+ CardGroup(
+ title = "Group ${4 * iteration}",
+ groupIcon = Icons.Filled.Person,
+ groupColor = Color(0xFF1967D2)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 1}",
+ groupIcon = Icons.Filled.Favorite,
+ groupColor = Color(0xFFC5221F)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 2}",
+ groupIcon = Icons.Filled.Star,
+ groupColor = Color(0xFFF29900)
+ )
+
+ CardGroup(
+ title = "Group ${4 * iteration + 3}",
+ groupIcon = Icons.Filled.Place,
+ groupColor = Color(0xFF188038)
+ )
+ }
+ }
+}
+
+@Composable
+private fun CardGroup(
+ title: String,
+ groupIcon: ImageVector,
+ groupColor: Color,
+ modifier: Modifier = Modifier,
+ count: Int = 10
+) {
+ Column(
+ modifier = modifier
+ ) {
+ Text(
+ title,
+ style = MaterialTheme.typography.h6,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ Row(
+ modifier = Modifier
+ .horizontalScroll(rememberScrollState())
+ .padding(horizontal = 12.dp)
+ ) {
+ repeat(count) {
+ Item(
+ color = groupColor,
+ icon = groupIcon,
+ modifier = Modifier
+ .padding(horizontal = 4.dp)
+ .size(64.dp)
+ .clip(RoundedCornerShape(4.dp))
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun Item(
+ color: Color,
+ icon: ImageVector,
+ modifier: Modifier = Modifier
+) {
+ Box(
+ modifier = modifier.background(color),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(icon, null, tint = Color.White)
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
new file mode 100644
index 0000000..b573368
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/main/java/androidx/compose/integration/macrobenchmark/StaticScrollingContentWithChromeInitialCompositionBenchmark.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark
+
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.testutils.createStartupCompilationParams
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class StaticScrollingContentWithChromeInitialCompositionBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun initialComposition() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = startupMode,
+ packageName = "androidx.compose.integration.macrobenchmark.target"
+ ) {
+ action = "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_INITIAL_COMPOSITION_ACTIVITY"
+ }
+
+ @Test
+ fun firstFrame() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = startupMode,
+ packageName = "androidx.compose.integration.macrobenchmark.target"
+ ) {
+ action = "androidx.compose.integration.macrobenchmark.target" +
+ ".STATIC_SCROLLING_CONTENT_WITH_CHROME_FIRST_FRAME_ACTIVITY"
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
+ @JvmStatic
+ fun parameters() = createStartupCompilationParams()
+ }
+}
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
new file mode 100644
index 0000000..9b61a35
--- /dev/null
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SlotTableIntegrationBenchmark.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.benchmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import kotlin.random.Random
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class SlotTableIntegrationBenchmark : ComposeBenchmarkBase() {
+
+ @UiThreadTest
+ @Test
+ fun create() = runBlockingTestWithFrameClock {
+ measureCompose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeManyGroups() = runBlockingTestWithFrameClock {
+ var includeGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ if (includeGroups) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { includeGroups = false }
+ reset { includeGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeAlternatingGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ key(index) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = false }
+ reset { insertAlternatingGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun removeManyReplaceGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(true)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ Pixel(color = Color(
+ red = 0,
+ green = 2 * index,
+ blue = 0
+ ))
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = false }
+ reset { insertAlternatingGroups = true }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertManyGroups() = runBlockingTestWithFrameClock {
+ var includeGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ if (includeGroups) {
+ repeat(100) {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { includeGroups = true }
+ reset { includeGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertAlternatingGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ key(index) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = true }
+ reset { insertAlternatingGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun insertManyReplaceGroups() = runBlockingTestWithFrameClock {
+ var insertAlternatingGroups by mutableStateOf(false)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ repeat(100) { index ->
+ if (index % 2 == 0 || insertAlternatingGroups) {
+ Pixel(color = Color(
+ red = 0,
+ green = 2 * index,
+ blue = 0
+ ))
+ }
+ }
+ }
+ }
+ update { insertAlternatingGroups = true }
+ reset { insertAlternatingGroups = false }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateManyNestedGroups() = runBlockingTestWithFrameClock {
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ val random = remember(seed) { Random(seed) }
+ MatryoshkaLayout(
+ depth = 100,
+ content = {
+ MinimalBox {
+ Pixel(color = Color(random.nextInt()))
+ Pixel(color = Color.Red)
+ Pixel(color = Color.Green)
+ Pixel(color = Color.Blue)
+ }
+ MinimalBox {
+ NonRenderingText("abcdef")
+ }
+ NonRenderingText(
+ text = random.nextString(),
+ textColor = Color(random.nextInt()),
+ textSize = random.nextInt(6, 32).dp,
+ ellipsize = random.nextBoolean(),
+ minLines = random.nextInt(),
+ maxLines = random.nextInt(),
+ )
+ }
+ )
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateDisjointGroups() = runBlockingTestWithFrameClock {
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ MinimalBox {
+ repeat(10) { container ->
+ MinimalBox {
+ MatryoshkaLayout(
+ depth = 100,
+ content = { depth ->
+ if (depth > 50) {
+ val random = Random(seed * container + depth)
+ NonRenderingText(
+ text = random.nextString(),
+ textColor = Color(random.nextInt()),
+ textSize = random.nextInt(6, 32).dp,
+ ellipsize = random.nextBoolean(),
+ minLines = random.nextInt(),
+ maxLines = random.nextInt(),
+ )
+ } else {
+ NonRenderingText("foo")
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun updateDeepCompositionLocalHierarchy() = runBlockingTestWithFrameClock {
+ val PixelColorLocal = compositionLocalOf { Color.Unspecified }
+ var seed by mutableIntStateOf(1337)
+ measureRecomposeSuspending {
+ compose {
+ val random = remember(seed) { Random(seed) }
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ CompositionLocalProvider(
+ PixelColorLocal provides Color(random.nextInt())
+ ) {
+ Pixel(PixelColorLocal.current)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ update { seed++ }
+ }
+ }
+
+ @UiThreadTest
+ @Test
+ fun reverseGroups() = runBlockingTestWithFrameClock {
+ val originalItems = (1..100).toList()
+ var keys by mutableStateOf(originalItems)
+ measureRecomposeSuspending {
+ compose {
+ Column(
+ modifier = Modifier.size(width = 20.dp, height = 300.dp)
+ ) {
+ keys.forEach {
+ key(it) {
+ Pixel(color = Color.Blue)
+ }
+ }
+ }
+ }
+ update { keys = keys.reversed() }
+ reset { keys = originalItems }
+ }
+ }
+}
+
+@Composable
+private fun Pixel(color: Color) {
+ Layout(
+ modifier = Modifier.background(color)
+ ) { _, _ ->
+ layout(1, 1) {}
+ }
+}
+
+@Composable
+private fun NonRenderingText(
+ text: String,
+ textColor: Color = Color.Unspecified,
+ textSize: Dp = Dp.Unspecified,
+ ellipsize: Boolean = false,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ use(text)
+ use(textColor.value.toInt())
+ use(textSize.value)
+ use(ellipsize)
+ use(minLines)
+ use(maxLines)
+ Layout { _, _ ->
+ layout(1, 1) {}
+ }
+}
+
+@Composable
+private fun MinimalBox(
+ modifier: Modifier = Modifier,
+ content: @Composable () -> Unit
+) {
+ Layout(content, modifier, MinimalBoxMeasurePolicy)
+}
+
+@Composable
+private fun MatryoshkaLayout(
+ depth: Int,
+ content: @Composable (depth: Int) -> Unit
+) {
+ if (depth <= 0) {
+ content(0)
+ } else {
+ Layout(
+ content = {
+ content(depth)
+ MatryoshkaLayout(depth - 1, content)
+ },
+ measurePolicy = MinimalBoxMeasurePolicy
+ )
+ }
+}
+
+private val MinimalBoxMeasurePolicy = MeasurePolicy { measurables, constraints ->
+ val placeables = measurables.map { it.measure(constraints) }
+ val (usedWidth, usedHeight) = placeables.fold(
+ initial = IntOffset(0, 0)
+ ) { (maxWidth, maxHeight), placeable ->
+ IntOffset(
+ maxOf(maxWidth, placeable.measuredWidth),
+ maxOf(maxHeight, placeable.measuredHeight)
+ )
+ }
+
+ layout(
+ width = usedWidth,
+ height = usedHeight
+ ) {
+ placeables.forEach { it.place(0, 0) }
+ }
+}
+
+private fun Random.nextString(length: Int = 16) = buildString(length) {
+ repeat(length) { append(nextInt('A'.code, 'z'.code).toChar()) }
+}
+
+@Suppress("UNUSED_PARAMETER") private fun use(value: Any?) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Int) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Long) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Float) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Double) {}
+@Suppress("UNUSED_PARAMETER") private fun use(value: Boolean) {}