From cc32ea36cbad4e080bebb753d3b801e219526be5 Mon Sep 17 00:00:00 2001 From: Phil Nguyen Date: Wed, 25 May 2022 22:47:06 +0000 Subject: Cache tabs if memory allows This change caches the tabs using soft references. This remembers the trees' expansion when switching tabs, and also improves latency. When reusing a previous tab, we need to re-register the listeners that were canceled in `onRemoved` in I41d7331e03ca282e7b1d122a00e7a9b26df2b748 Fixes: 65453906 Test: add new Change-Id: I91e1ae347447dbf5d954141b56539acc8e1c1680 --- .../src/com/android/tools/profilers/SoftHashMap.kt | 47 ++++++++++++++++++++++ .../profilers/cpu/capturedetails/CaptureDetails.kt | 23 +++++++---- .../profilers/cpu/capturedetails/CpuTreeModel.kt | 8 +++- .../com/android/tools/profilers/SoftHashMapTest.kt | 47 ++++++++++++++++++++++ 4 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 profilers/src/com/android/tools/profilers/SoftHashMap.kt create mode 100644 profilers/testSrc/com/android/tools/profilers/SoftHashMapTest.kt (limited to 'profilers') diff --git a/profilers/src/com/android/tools/profilers/SoftHashMap.kt b/profilers/src/com/android/tools/profilers/SoftHashMap.kt new file mode 100644 index 00000000000..158b4688229 --- /dev/null +++ b/profilers/src/com/android/tools/profilers/SoftHashMap.kt @@ -0,0 +1,47 @@ +/* + * 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 java.lang.ref.SoftReference + +/** + * Map that's strong in key and soft in value + */ +class SoftHashMap: MutableMap { + private val content = hashMapOf>() + override val entries get() = content.mapNotNullTo(hashSetOf()) { (k, vRef) -> vRef.get()?.let { v -> entry(k, v) } } + override val keys get() = content.keys + override val size get() = content.size + override val values get() = content.values.mapNotNullTo(hashSetOf(), SoftReference::get) + override fun clear() = content.clear() + override fun isEmpty() = content.isEmpty() + override fun remove(key: K): V? = content.remove(key)?.get() + override fun putAll(from: Map) = from.forEach { (k, v) -> content[k] = SoftReference(v) } + override fun put(key: K, value: V): V? = content.put(key, SoftReference(value))?.get() + override fun get(key: K): V? = content[key]?.get() + override fun containsValue(value: V) = content.values.any { it.get() == value } + override fun containsKey(key: K) = key in content + + companion object { + fun entry(k: K, v: V) = object: MutableMap.MutableEntry { + override var value = v + override val key get() = k + override fun setValue(newValue: V): V = value.also { + value = newValue + } + } + } +} \ No newline at end of file diff --git a/profilers/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetails.kt b/profilers/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetails.kt index 03f7b987854..dedb7317e19 100644 --- a/profilers/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetails.kt +++ b/profilers/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetails.kt @@ -31,7 +31,6 @@ import kotlin.math.max sealed class CaptureDetails(val clockType: ClockType, val capture: CpuCapture) { abstract val type: Type - abstract fun onDestroyed() sealed class ChartDetails(clockType: ClockType, cpuCapture: CpuCapture): CaptureDetails(clockType, cpuCapture) { abstract val node: CaptureNode? @@ -60,8 +59,12 @@ sealed class CaptureDetails(val clockType: ClockType, val capture: CpuCapture) { } } - override fun onDestroyed() { - model?.onDestroyed(); + fun onRemoved() { + model?.onDestroyed() + } + + fun onReattached() { + model?.onReattached() } } @@ -79,7 +82,6 @@ sealed class CaptureDetails(val clockType: ClockType, val capture: CpuCapture) { : ChartDetails(clockType, cpuCapture) { override val node = nodes.firstOrNull() override val type get() = Type.CALL_CHART - override fun onDestroyed() { } } class FlameChart internal constructor(clockType: ClockType, @@ -93,8 +95,17 @@ sealed class CaptureDetails(val clockType: ClockType, val capture: CpuCapture) { override var node: CaptureNode? = null private set val aspect: AspectModel = AspectModel() + private val captureNodes = captureNodes.sortedWith(Comparator.comparingLong(CaptureNode::startGlobal)) init { + onReattached() + } + + fun onRemoved() { + selectionRange.removeDependencies(aspect) + } + + fun onReattached() { if (captureNodes.isNotEmpty()) { val captureNodes = captureNodes.sortedWith(Comparator.comparingLong(CaptureNode::startGlobal)) @@ -165,10 +176,6 @@ sealed class CaptureDetails(val clockType: ClockType, val capture: CpuCapture) { } } - override fun onDestroyed() { - selectionRange.removeDependencies(aspect) - } - /** * Produces a flame chart that is similar to [CallChart], but the identical methods with the same sequence of callers * are combined into one wider bar. It converts it from [TopDownNode] as it's similar to FlameChart. diff --git a/profilers/src/com/android/tools/profilers/cpu/capturedetails/CpuTreeModel.kt b/profilers/src/com/android/tools/profilers/cpu/capturedetails/CpuTreeModel.kt index de7284cd5ee..9e408f83b4f 100644 --- a/profilers/src/com/android/tools/profilers/cpu/capturedetails/CpuTreeModel.kt +++ b/profilers/src/com/android/tools/profilers/cpu/capturedetails/CpuTreeModel.kt @@ -54,8 +54,7 @@ class CpuTreeModel>(val clockType: ClockType, private val range: }) init { - range.addDependency(observer).onChange(Range.Aspect.RANGE, rangeChanged) - rangeChanged() + onReattached() } fun sort(newOrder: Comparator>) { @@ -69,6 +68,11 @@ class CpuTreeModel>(val clockType: ClockType, private val range: range.removeDependencies(observer) } + fun onReattached() { + range.addDependency(observer).onChange(Range.Aspect.RANGE, rangeChanged) + rangeChanged() + } + private fun reload() { val event = TreeModelEvent(this, arrayOf(root), null, null) listeners.asReversed().forEach { it.treeStructureChanged(event) } diff --git a/profilers/testSrc/com/android/tools/profilers/SoftHashMapTest.kt b/profilers/testSrc/com/android/tools/profilers/SoftHashMapTest.kt new file mode 100644 index 00000000000..d57bfa928a2 --- /dev/null +++ b/profilers/testSrc/com/android/tools/profilers/SoftHashMapTest.kt @@ -0,0 +1,47 @@ +/* + * 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 SoftHashMapTest { + @Test + fun `map gives back what was put in (modulo null)`() { + val m = SoftHashMap() + (0 .. 10).forEach { m[it] = it.toString() } + fun check() = (0 .. 10).forEach { assertThat(m[it]).isAnyOf(it.toString(), null) } + check() + repeat(3) { System.gc() } + check() + } + + @Test + fun `soft references are cleared when not enough memory`() { + val m = SoftHashMap() + (0 .. 10).forEach { m[it] = it.toString() } + try { + tailrec fun allocTillDeath(size: Int) { + IntArray(size) + allocTillDeath(size * 2 + 1) + } + allocTillDeath(1) + } catch (_: OutOfMemoryError) { + assertThat(m.keys).isNotEmpty() + assertThat(m.values).isEmpty() + } + } +} -- cgit v1.2.3