diff options
14 files changed, 171 insertions, 17 deletions
diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/AndroidFrameTimelineTab.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/AndroidFrameTimelineTab.kt index e2f6bac2662..06ea1820b35 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/AndroidFrameTimelineTab.kt +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/AndroidFrameTimelineTab.kt @@ -54,4 +54,8 @@ class AndroidFrameTimelineTab(profilersView: StudioProfilersView, model: Android override fun onRemoved() { // this tab doesn't leak heavy listeners } + + override fun onReattached() { + // this tab doesn't remove anything to restore + } }
\ No newline at end of file diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisChart.java b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisChart.java index ae7209fe254..8f46d8ca098 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisChart.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisChart.java @@ -162,6 +162,11 @@ public class CpuAnalysisChart extends CpuAnalysisTab<CpuAnalysisChartModel<?>> { myActiveDetailsView.onRemoved(); } + @Override + public void onReattached() { + myActiveDetailsView.onReattached(); + } + @VisibleForTesting @NotNull FilterComponent getFilterComponent() { diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisEventsTab.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisEventsTab.kt index 80dc615ff60..47d1d9277d8 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisEventsTab.kt +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisEventsTab.kt @@ -33,4 +33,5 @@ class CpuAnalysisEventsTab(profilersView: StudioProfilersView, } override fun onRemoved() { } + override fun onReattached() { } }
\ No newline at end of file diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisFramesTab.kt b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisFramesTab.kt index e45aaf399af..efb99282d53 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisFramesTab.kt +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisFramesTab.kt @@ -69,6 +69,7 @@ class CpuAnalysisFramesTab(profilersView: StudioProfilersView, } override fun onRemoved() { } + override fun onReattached() { } companion object { val PAGE_SIZE_VALUES = arrayOf(10, 25, 50, 100) diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisPanel.java b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisPanel.java index 4e51de37526..1051b8fb23b 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisPanel.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisPanel.java @@ -20,6 +20,7 @@ import com.android.tools.adtui.common.StudioColorsKt; import com.android.tools.adtui.model.AspectObserver; import com.android.tools.adtui.model.MultiSelectionModel; import com.android.tools.adtui.model.ViewBinder; +import com.android.tools.profilers.SoftHashMap; import com.android.tools.profilers.StudioProfilersView; import com.android.tools.profilers.cpu.CpuCaptureStage; import com.google.common.annotations.VisibleForTesting; @@ -27,6 +28,7 @@ import com.intellij.ui.components.JBTabbedPane; import com.intellij.util.ui.JBUI; import java.awt.BorderLayout; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import javax.swing.JComponent; import javax.swing.JLabel; @@ -185,6 +187,7 @@ public class CpuAnalysisPanel extends AspectObserver { */ private class TabChangeListener implements ChangeListener { private int myLastSelectedIndex = -1; + private Map<CpuAnalysisTabModel<?>, CpuAnalysisTab<?>> myCachedTabs = new SoftHashMap<>(); @Override public void stateChanged(ChangeEvent e) { @@ -200,12 +203,19 @@ public class CpuAnalysisPanel extends AspectObserver { if (myLastSelectedIndex >= 0 && myLastSelectedIndex < myTabView.getTabCount()) { CpuAnalysisTab<?> tab = (CpuAnalysisTab<?>)myTabView.getComponentAt(myLastSelectedIndex); if (tab != null) { + myCachedTabs.put(tab.getModel(), tab); tab.onRemoved(); } myTabView.setComponentAt(myLastSelectedIndex, new JPanel()); } if (newIndex >= 0 && newIndex < myTabView.getTabCount()) { - myTabView.setComponentAt(newIndex, myTabViewsBinder.build(myProfilersView, mySelectedModel.getTabModelAt(newIndex))); + CpuAnalysisTab<?> cachedTab = myCachedTabs.get(mySelectedModel.getTabModelAt(newIndex)); + if (cachedTab != null) { + cachedTab.onReattached(); + myTabView.setComponentAt(newIndex, cachedTab); + } else { + myTabView.setComponentAt(newIndex, myTabViewsBinder.build(myProfilersView, mySelectedModel.getTabModelAt(newIndex))); + } } myLastSelectedIndex = newIndex; } diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisSummaryTab.java b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisSummaryTab.java index 193007539ed..20b333d1fbe 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisSummaryTab.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisSummaryTab.java @@ -43,6 +43,9 @@ public class CpuAnalysisSummaryTab extends CpuAnalysisTab<CpuAnalysisSummaryTabM @Override public void onRemoved() { } + @Override + public void onReattached() { } + private void initComponents() { setLayout(new BorderLayout()); JScrollPane scrollPane = new JBScrollPane(myViewBinder.build(getProfilersView(), getModel()), diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisTab.java b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisTab.java index 7dcd0433d15..8dd633242a9 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisTab.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/analysis/CpuAnalysisTab.java @@ -44,5 +44,13 @@ public abstract class CpuAnalysisTab<T extends CpuAnalysisTabModel> extends JCom return myModel; } + /** + * Clean up (e.g. releasing expensive listeners) when this tab is inactive + */ abstract public void onRemoved(); + + /** + * Restore everything cleaned up in `onRemoved` to prepare for re-attachment + */ + abstract public void onReattached(); } diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetailsView.java b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetailsView.java index 76cdd9405e7..d330b29e91a 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetailsView.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/CaptureDetailsView.java @@ -47,6 +47,7 @@ public abstract class CaptureDetailsView { } public abstract void onRemoved(); + public abstract void onReattached(); @NotNull public abstract JComponent getComponent(); diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/ChartDetailsView.java b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/ChartDetailsView.java index 357a53cb3b4..3c0f0b99fb4 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/ChartDetailsView.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/ChartDetailsView.java @@ -187,9 +187,10 @@ public abstract class ChartDetailsView extends CaptureDetailsView { } @Override - public void onRemoved() { - myCallChart.onDestroyed(); - } + public void onRemoved() { } + + @Override + public void onReattached() { } @NotNull private JPanel createChartPanel() { @@ -273,7 +274,12 @@ public abstract class ChartDetailsView extends CaptureDetailsView { @Override public void onRemoved() { - myFlameChart.onDestroyed(); + myFlameChart.onRemoved(); + } + + @Override + public void onReattached() { + myFlameChart.onReattached(); } @NotNull diff --git a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/TreeDetailsView.java b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/TreeDetailsView.java index 4f437b828e6..d8f50125f96 100644 --- a/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/TreeDetailsView.java +++ b/profilers-ui/src/com/android/tools/profilers/cpu/capturedetails/TreeDetailsView.java @@ -431,7 +431,12 @@ public abstract class TreeDetailsView extends CaptureDetailsView { @Override public void onRemoved() { - myTopDown.onDestroyed(); + myTopDown.onRemoved(); + } + + @Override + public void onReattached() { + myTopDown.onReattached(); } /** @@ -478,7 +483,12 @@ public abstract class TreeDetailsView extends CaptureDetailsView { @Override public void onRemoved() { - myBottomUp.onDestroyed(); + myBottomUp.onRemoved(); + } + + @Override + public void onReattached() { + myBottomUp.onReattached(); } } 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<K: Any, V: Any>: MutableMap<K, V> { + private val content = hashMapOf<K, SoftReference<V>>() + 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<V>::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<out K, V>) = 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<K, V> entry(k: K, v: V) = object: MutableMap.MutableEntry<K, V> { + 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<Aspect> = 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<T: Aggregate<T>>(val clockType: ClockType, private val range: }) init { - range.addDependency(observer).onChange(Range.Aspect.RANGE, rangeChanged) - rangeChanged() + onReattached() } fun sort(newOrder: Comparator<CpuTreeNode<T>>) { @@ -69,6 +68,11 @@ class CpuTreeModel<T: Aggregate<T>>(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<Int, String>() + (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<Int, String>() + (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() + } + } +} |