summaryrefslogtreecommitdiff
path: root/profilers
diff options
context:
space:
mode:
authorPhil Nguyen <philnguyen@google.com>2022-04-19 11:45:14 -0700
committerTreeHugger Robot <treehugger-gerrit@google.com>2022-04-22 22:57:54 +0000
commit03c9b26813dd3f364515ceaa4c0e0b1701db222e (patch)
tree4867663149b876116bd0c5f840d9870aaffbc052 /profilers
parentd4e85963919b2e68642999b8ec94d974113cf938 (diff)
downloadidea-03c9b26813dd3f364515ceaa4c0e0b1701db222e.tar.gz
Implement delegate for cached properties dependent on states
In the codebase for capture details, there's a common problem where there's a property that's expensive to compute that depends on some state (internal or external). The code has been dealing with this by manually modyfing the property at the right time to be in sync with the state. This change implements an alternative to handle this problem. We lazily compute and cache the property, and only recompute the property if the state is different since last time. This is a trade off: it's less error-prone, and it reduces computation if we keep changing the state but never query the property, but there's some overhead in remembering the state. Bug: n/a Test: add new Change-Id: I2e7055b2e75dc4e69091cd5efb5035a596ce095c
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