diff options
author | Phil Nguyen <philnguyen@google.com> | 2022-04-19 11:45:14 -0700 |
---|---|---|
committer | TreeHugger Robot <treehugger-gerrit@google.com> | 2022-04-22 22:57:54 +0000 |
commit | 03c9b26813dd3f364515ceaa4c0e0b1701db222e (patch) | |
tree | 4867663149b876116bd0c5f840d9870aaffbc052 /profilers | |
parent | d4e85963919b2e68642999b8ec94d974113cf938 (diff) | |
download | idea-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.kt | 64 | ||||
-rw-r--r-- | profilers/testSrc/com/android/tools/profilers/CachedDerivedPropertyTest.kt | 78 |
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 |