diff options
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 |