diff --git a/benchmark/.gitignore b/benchmark/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/benchmark/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts new file mode 100644 index 0000000..4e334b6 --- /dev/null +++ b/benchmark/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.android.test) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.benchmark" + compileSdk = properties["android.compileSdk"].toString().toInt() + + compileOptions { + sourceCompatibility = JavaVersion.toVersion(properties["jvm.version"].toString()) + targetCompatibility = JavaVersion.toVersion(properties["jvm.version"].toString()) + } + + kotlinOptions { + jvmTarget = properties["jvm.version"].toString() + } + + defaultConfig { + minSdk = properties["android.minSdk"].toString().toInt() + targetSdk = properties["android.targetSdk"].toString().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" + } + + buildTypes { + // This benchmark buildType is used for benchmarking, and should function like your + // release build (for example, with minification on). It"s signed with a debug key + // for easy local/CI testing. + create("benchmark") { + isDebuggable = true +// isMinifyEnabled = true + signingConfig = getByName("debug").signingConfig + matchingFallbacks += listOf("release") + } + } + + targetProjectPath = ":sample:android" + experimentalProperties["android.experimental.self-instrumenting"] = true +} + +dependencies { + implementation(libs.androidx.test.ext.junit) + implementation(libs.androidx.test.espresso.core) + implementation(libs.ui.automator) + implementation(libs.macrobenchmark.junit) +} + +androidComponents { + beforeVariants(selector().all()) { + it.enable = it.buildType == "benchmark" + } +} \ No newline at end of file diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml new file mode 100644 index 0000000..227314e --- /dev/null +++ b/benchmark/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt b/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt new file mode 100644 index 0000000..ecfc76a --- /dev/null +++ b/benchmark/src/main/java/com/example/benchmark/TreemapChartBenchmark.kt @@ -0,0 +1,103 @@ +package com.example.benchmark + +import androidx.benchmark.macro.CompilationMode +import androidx.benchmark.macro.FrameTimingMetric +import androidx.benchmark.macro.MacrobenchmarkScope +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TreemapChartBenchmark { + + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun showSimpleChart() = measureRepeated { + startActivityAndWait() + + device.wait(Until.hasObject(By.text("4")), 1000) + device.wait(Until.hasObject(By.text("2")), 1000) + device.wait(Until.hasObject(By.text("1")), 1000) + } + + @Test + fun showComplexChart() = benchmarkRule.measureRepeated( + packageName = "by.overpass.treemapchart.sample.android", + metrics = listOf(FrameTimingMetric()), + compilationMode = CompilationMode.DEFAULT, + startupMode = StartupMode.COLD, + iterations = 5, + ) { + startActivityAndWait() + + device.findObject(By.text("Show more complex chart")) + .click() + + device.wait( + Until.hasObject( + By.text( + """ + |Cars + |12.11% + """.trimMargin(), + ), + ), + 5000, + ) + } + + @Test + fun showComplexChartDetailPopup() = benchmarkRule.measureRepeated( + packageName = "by.overpass.treemapchart.sample.android", + metrics = listOf(FrameTimingMetric()), + compilationMode = CompilationMode.DEFAULT, + startupMode = StartupMode.COLD, + iterations = 5, + ) { + startActivityAndWait() + + device.findObject(By.text("Show more complex chart")) + .click() + + device.wait( + Until.hasObject( + By.text( + """ + |Cars + |12.11% + """.trimMargin(), + ), + ), + 5000, + ) + device.findObject( + By.text( + """ + |Cars + |12.11% + """.trimMargin(), + ), + ).click() + + device.wait(Until.hasObject(By.text("Exports value")), 5000) + device.wait(Until.hasObject(By.text("$88.6B")), 5000) + } + + private fun measureRepeated(block: MacrobenchmarkScope.() -> Unit) { + benchmarkRule.measureRepeated( + packageName = "by.overpass.treemapchart.sample.android", + metrics = listOf(FrameTimingMetric()), + compilationMode = CompilationMode.DEFAULT, + startupMode = StartupMode.COLD, + iterations = 5, + measureBlock = block, + ) + } +} diff --git a/sample/android/build.gradle.kts b/sample/android/build.gradle.kts index 6ab894d..7668007 100644 --- a/sample/android/build.gradle.kts +++ b/sample/android/build.gradle.kts @@ -31,7 +31,13 @@ android { isDebuggable = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } -// create("benchmark") { + create("benchmark") { + initWith(buildTypes.getByName("release")) + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + isDebuggable = false + } + // create("benchmark") { // initWith(release) // signingConfig = signingConfigs.getByName("debug") // matchingFallbacks.add("release") diff --git a/sample/android/src/main/AndroidManifest.xml b/sample/android/src/main/AndroidManifest.xml index 47f7281..08fcce2 100644 --- a/sample/android/src/main/AndroidManifest.xml +++ b/sample/android/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -7,12 +8,16 @@ - + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index e5520ca..462afe2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,7 +23,7 @@ include(":treemap-chart") include(":treemap-chart-compose") include(":sample:shared") include(":sample:android") -//include(":sample:android:macrobenchmark") include(":sample:desktop") include(":sample:web") include(":sample:web-wasm") +include(":benchmark") diff --git a/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt b/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt deleted file mode 100644 index 7ea1d78..0000000 --- a/treemap-chart/src/commonMain/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurer2.kt +++ /dev/null @@ -1,195 +0,0 @@ -package by.overpass.treemapchart.core.measure.squarified - -import androidx.compose.runtime.Stable -import by.overpass.treemapchart.core.measure.LayoutOrientation -import by.overpass.treemapchart.core.measure.TreemapChartMeasurer -import by.overpass.treemapchart.core.measure.TreemapNode -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -/** - * An implementation of [TreemapChartMeasurer] that keeps the aspect ratio of the nodes - * as close to 1 as possible - * Uses the squarified treemap algorithm: http://www.win.tue.nl/~vanwijk/stm.pdf - * Implementation inspired by https://github.com/tasubo/javafx-chart-treemap - */ -@Stable -class SquarifiedMeasurer2 : TreemapChartMeasurer { - - private var height = 0.0 - private var width = 0.0 - private var heightLeft = 0.0 - private var widthLeft = 0.0 - private var left = 0.0 - private var top = 0.0 - private var layoutOrientation = LayoutOrientation.VERTICAL - - override fun measureNodes(values: List, width: Int, height: Int): List { - val children = mutableListOf() - setupSizeAndValues(children, width.toDouble(), height.toDouble(), values) - return measureNodes(children) - } - - private fun setupSizeAndValues( - children: MutableList, - width: Double, - height: Double, - values: List, - ) { - this.width = width - this.height = height - this.left = 0.0 - this.top = 0.0 - children.clear() - for (value in values) { - val treemapElement = TreemapElement(value) - children.add(treemapElement) - } - layoutOrientation = if (width > height) { - LayoutOrientation.VERTICAL - } else { - LayoutOrientation.HORIZONTAL - } - scaleArea(children) - } - - private fun measureNodes(children: List): List { - val treemapNodeList: MutableList = ArrayList() - heightLeft = height - widthLeft = width - squarify(ArrayList(children), ArrayList(), minimumSide()) - for (child in children) { - val treemapNode = TreemapNode( - child.width.toInt(), - child.height.toInt(), - child.left.toInt(), - child.top.toInt() - ) - treemapNodeList.add(treemapNode) - check(child.top <= height) { "Top is bigger than height" } - check(child.left <= width) { "Left is bigger than width" } - } - return treemapNodeList - } - - private fun squarify(children: List, row: List, w: Double) { - val remainPopped = ArrayDeque(children) - val c = remainPopped.removeFirst() - val concatRow = ArrayList(row) - concatRow.add(c) - val remaining = ArrayList(remainPopped) - val worstConcat = worst(concatRow, w) - val worstRow = worst(row, w) - if (row.isEmpty() || worstRow > worstConcat || isDoubleEqual(worstRow, worstConcat)) { - if (remaining.isEmpty()) { - layoutRow(concatRow, w) - } else { - squarify(remaining, concatRow, w) - } - } else { - layoutRow(row, w) - squarify(children, ArrayList(), minimumSide()) - } - } - - private fun worst(ch: List, w: Double): Double { - if (ch.isEmpty()) { - return Double.MAX_VALUE - } - var areaSum = 0.0 - var maxArea = 0.0 - var minArea = Double.MAX_VALUE - for (item in ch) { - val area = item.area - areaSum += area - minArea = min(minArea, area) - maxArea = max(maxArea, area) - } - val sqw = w * w - val sqAreaSum = areaSum * areaSum - return max( - sqw * maxArea / sqAreaSum, - sqAreaSum / (sqw * minArea) - ) - } - - private fun layoutRow(row: List, w: Double) { - var totalArea = 0.0 - for (element in row) { - val area = element.area - totalArea += area - } - if (layoutOrientation == LayoutOrientation.VERTICAL) { - val rowWidth = totalArea / w - var topItem = 0.0 - for (element in row) { - val area = element.area - val h = area / rowWidth - element.top = top + topItem - element.left = left - element.width = rowWidth - element.height = h - topItem += h - } - widthLeft -= rowWidth - //this.heightLeft -= w; - left += rowWidth - val minimumSide = minimumSide() - if (!isDoubleEqual(minimumSide, heightLeft)) { - changeLayout() - } - } else { - val rowHeight = totalArea / w - var rowLeft = 0.0 - for (item in row) { - val area = item.area - val wi = area / rowHeight - item.top = top - item.left = left + rowLeft - item.height = rowHeight - item.width = wi - rowLeft += wi - } - //this.widthLeft -= rowHeight; - heightLeft -= rowHeight - top += rowHeight - val minimumSide = minimumSide() - if (!isDoubleEqual(minimumSide, widthLeft)) { - changeLayout() - } - } - } - - private fun changeLayout() { - layoutOrientation = - if (layoutOrientation == LayoutOrientation.HORIZONTAL) { - LayoutOrientation.VERTICAL - } else { - LayoutOrientation.HORIZONTAL - } - } - - private fun isDoubleEqual(one: Double, two: Double): Boolean { - val eps = 0.00001 - return abs(one - two) < eps - } - - private fun minimumSide(): Double { - return min(heightLeft, widthLeft) - } - - private fun scaleArea(children: List) { - val areaGiven = width * height - var areaTotalTaken = 0.0 - for (child in children) { - val area = child.area - areaTotalTaken += area - } - val ratio = areaTotalTaken / areaGiven - for (child in children) { - val area = child.area / ratio - child.area = area - } - } -} diff --git a/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt b/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt index 6653c03..b5fcc71 100644 --- a/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt +++ b/treemap-chart/src/commonTest/kotlin/by/overpass/treemapchart/core/measure/squarified/SquarifiedMeasurerTest.kt @@ -8,7 +8,6 @@ import kotlin.time.measureTimedValue class SquarifiedMeasurerTest { private val squarifiedMeasurer = SquarifiedMeasurer() - private val squarifiedMeasurer2 = SquarifiedMeasurer2() private val values = listOf(6.0, 6.0, 4.0, 3.0, 2.0, 2.0, 1.0) @Test @@ -50,44 +49,4 @@ class SquarifiedMeasurerTest { assertEquals(expectedNodes, actual.value) println("Time: ${actual.duration}") } - - @Test - fun nodesAreMeasuredCorrectlyWithSquarifiedAlgorithmVerticalScreen2() { - val expectedNodes = listOf( - TreemapNode(width = 540, height = 960, offsetX = 0, offsetY = 0), - TreemapNode(width = 540, height = 960, offsetX = 540, offsetY = 0), - TreemapNode(width = 630, height = 548, offsetX = 0, offsetY = 960), - TreemapNode(width = 630, height = 411, offsetX = 0, offsetY = 1508), - TreemapNode(width = 450, height = 384, offsetX = 630, offsetY = 960), - TreemapNode(width = 450, height = 384, offsetX = 630, offsetY = 1344), - TreemapNode(width = 450, height = 192, offsetX = 630, offsetY = 1728), - ) - - val actual = measureTimedValue { - squarifiedMeasurer2.measureNodes(values, 1080, 1920) - } - - assertEquals(expectedNodes, actual.value) - println("Time: ${actual.duration}") - } - - @Test - fun nodesAreMeasuredCorrectlyWithSquarifiedAlgorithmHorizontalScreen2() { - val expectedNodes = listOf( - TreemapNode(width = 960, height = 540, offsetX = 0, offsetY = 0), - TreemapNode(width = 960, height = 540, offsetX = 0, offsetY = 540), - TreemapNode(width = 548, height = 630, offsetX = 960, offsetY = 0), - TreemapNode(width = 411, height = 630, offsetX = 1508, offsetY = 0), - TreemapNode(width = 384, height = 450, offsetX = 960, offsetY = 630), - TreemapNode(width = 384, height = 450, offsetX = 1344, offsetY = 630), - TreemapNode(width = 192, height = 450, offsetX = 1728, offsetY = 630), - ) - - val actual = measureTimedValue { - squarifiedMeasurer2.measureNodes(values, 1920, 1080) - } - - assertEquals(expectedNodes, actual.value) - println("Time: ${actual.duration}") - } }