summaryrefslogtreecommitdiff
path: root/profilers
diff options
context:
space:
mode:
Diffstat (limited to 'profilers')
-rw-r--r--profilers/src/com/android/tools/profilers/CachedDerivedProperty.kt64
-rw-r--r--profilers/testSrc/com/android/tools/profilers/CachedDerivedPropertyTest.kt78
2 files changed, 142 insertions, 0 deletions
diff --git a/profilers/src/com/android/tools/profilers/CachedDerivedProperty.kt b/profilers/src/com/android/tools/profilers/CachedDerivedProperty.kt
new file mode 100644
index 00000000000..6a0e5a44b89
--- /dev/null
+++ b/profilers/src/com/android/tools/profilers/CachedDerivedProperty.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.profilers
+
+import kotlin.reflect.KProperty
+
+/**
+ * Delegation for derived properties based on `source` that are expensive to compute.
+ * It only recomputes if `source` has changed since last time.
+ *
+ * The implementer of this property has the option to look at the last computation
+ * to incrementally derive the new one instead of computing from scratch.
+ *
+ * `derive` is supposed to be a pure function not dependent on any outside state,
+ * otherwise the cache is unsound.
+ *
+ * Example usage:
+ * ```
+ * val obj = object {
+ * var n: Int = 0
+ * val sumUpToN: Int by CachedDerivedProperty(::n, ::sumUpTo)
+ *
+ * fun sumUpTo(n: Int) = (1 .. n).sum()
+ * }
+ * obj.n = 100 // `sumUpToN` not updated
+ * obj.n = 200 // `sumUpToN` not updated
+ * obj.sumUptoN // `sumUpToN` updated
+ * ```
+ */
+class CachedDerivedProperty<K, V: Any>(private val getSourceProperty: () -> K,
+ private val getDerivedProperty: (K, Pair<K, V>?) -> V) {
+ constructor(getSourceProperty: () -> K, getDerivedProperty: (K) -> V)
+ : this(getSourceProperty, { k, _ -> getDerivedProperty(k) })
+
+ private var lastKey: K? = null
+ private var lastVal: V? = null
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): V {
+ val key = getSourceProperty()
+ return when {
+ key != lastKey -> {
+ val memo = lastKey?.let { lastKey -> lastVal?.let { lastVal -> lastKey to lastVal }}
+ getDerivedProperty(key, memo).also {
+ lastKey = key
+ lastVal = it
+ }
+ }
+ else -> lastVal!!
+ }
+ }
+} \ No newline at end of file
diff --git a/profilers/testSrc/com/android/tools/profilers/CachedDerivedPropertyTest.kt b/profilers/testSrc/com/android/tools/profilers/CachedDerivedPropertyTest.kt
new file mode 100644
index 00000000000..d03fb60fbd2
--- /dev/null
+++ b/profilers/testSrc/com/android/tools/profilers/CachedDerivedPropertyTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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 com.android.tools.profilers
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CachedDerivedPropertyTest {
+
+ @Test
+ fun `derived property only changes when source changes`() {
+ var recompCount = 0
+ val obj = object {
+ var name: String = ""
+ val nameLength: Int by CachedDerivedProperty({name}, { str -> str.length.also { recompCount++ } })
+ }
+
+ assertThat(obj.nameLength).isEqualTo(0)
+ assertThat(recompCount).isEqualTo(1)
+
+ obj.name = "foo"
+ assertThat(obj.nameLength).isEqualTo(3)
+ assertThat(recompCount).isEqualTo(2)
+
+ obj.name = "foo"
+ assertThat(obj.nameLength).isEqualTo(3)
+ assertThat(recompCount).isEqualTo(2)
+
+ obj.name = "bs"
+ assertThat(obj.nameLength).isEqualTo(2)
+ assertThat(recompCount).isEqualTo(3)
+ }
+
+ @Test
+ fun `last result used for incremental computation`() {
+ var log = listOf<Int>()
+ fun sumFromTo(from: Int, to: Int) = (from..to).sum().also {
+ log = (from..to).toList()
+ }
+
+ val obj = object {
+ var bound: Int = 0
+ val sumUpToBound: Int by CachedDerivedProperty({bound}, { bound, cache -> when (cache) {
+ null -> sumFromTo(1, bound)
+ else -> {
+ val (oldBound, oldSum) = cache
+ when {
+ oldBound < bound -> oldSum + sumFromTo(oldBound + 1, bound)
+ else -> oldSum - sumFromTo(bound + 1, oldBound)
+ }
+ }
+ } })
+ }
+
+ assertThat(obj.sumUpToBound).isEqualTo(0)
+
+ obj.bound = 10
+ assertThat(obj.sumUpToBound).isEqualTo((1 .. 10).sum())
+ assertThat(log).isEqualTo((1 .. 10).toList())
+
+ obj.bound = 11
+ assertThat(obj.sumUpToBound).isEqualTo((1 .. 11).sum())
+ assertThat(log).isEqualTo(listOf(11))
+ }
+} \ No newline at end of file