xref: /aosp_15_r20/external/leakcanary2/shark-android/src/test/java/shark/HprofRetainedHeapPerfTest.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<lambda>null1 package shark
2 
3 import java.io.File
4 import java.util.EnumSet
5 import java.util.concurrent.CountDownLatch
6 import java.util.concurrent.TimeUnit.SECONDS
7 import kotlin.concurrent.thread
8 import kotlin.math.absoluteValue
9 import kotlin.reflect.KClass
10 import org.assertj.core.api.AbstractIntegerAssert
11 import org.junit.Before
12 import org.junit.Rule
13 import org.junit.Test
14 import org.junit.rules.TemporaryFolder
15 import shark.AndroidReferenceMatchers.Companion.buildKnownReferences
16 import shark.AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON
17 import shark.AndroidReferenceMatchers.REFERENCES
18 import shark.GcRoot.ThreadObject
19 import shark.HprofHeapGraph.Companion.openHeapGraph
20 import shark.OnAnalysisProgressListener.Step.COMPUTING_NATIVE_RETAINED_SIZE
21 import shark.OnAnalysisProgressListener.Step.COMPUTING_RETAINED_SIZE
22 import shark.OnAnalysisProgressListener.Step.EXTRACTING_METADATA
23 import shark.OnAnalysisProgressListener.Step.FINDING_DOMINATORS
24 import shark.OnAnalysisProgressListener.Step.FINDING_PATHS_TO_RETAINED_OBJECTS
25 import shark.OnAnalysisProgressListener.Step.FINDING_RETAINED_OBJECTS
26 import shark.OnAnalysisProgressListener.Step.INSPECTING_OBJECTS
27 import shark.OnAnalysisProgressListener.Step.PARSING_HEAP_DUMP
28 import shark.internal.ObjectDominators
29 
30 private const val ANALYSIS_THREAD = "analysis"
31 
32 class HprofRetainedHeapPerfTest {
33 
34   @get:Rule
35   var tmpFolder = TemporaryFolder()
36 
37   lateinit var folder: File
38 
39   @Before
40   fun setUp() {
41     folder = tmpFolder.newFolder()
42   }
43 
44   @Test fun `freeze retained memory when indexing leak_asynctask_o`() {
45     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
46 
47     val (baselineHeap, heapWithIndex) = runInThread(ANALYSIS_THREAD) {
48       val baselineHeap = dumpHeap("baseline")
49       val hprofIndex = indexRecordsOf(hprofFile)
50       val heapWithIndex = dumpHeapRetaining(hprofIndex)
51       baselineHeap to heapWithIndex
52     }
53 
54     val (analysisRetained, _) = heapWithIndex.retainedHeap(ANALYSIS_THREAD)
55 
56     val retained = analysisRetained - baselineHeap.retainedHeap(ANALYSIS_THREAD).first
57 
58     assertThat(retained).isEqualTo(5.07 MB +-5 % margin)
59   }
60 
61   @Test fun `freeze retained memory when indexing leak_asynctask_m`() {
62     val hprofFile = "leak_asynctask_m.hprof".classpathFile()
63 
64     val (baselineHeap, heapWithIndex) = runInThread(ANALYSIS_THREAD) {
65       val baselineHeap = dumpHeap("baseline")
66       val hprofIndex = indexRecordsOf(hprofFile)
67       val heapWithIndex = dumpHeapRetaining(hprofIndex)
68       baselineHeap to heapWithIndex
69     }
70 
71     val (analysisRetained, _) = heapWithIndex.retainedHeap(ANALYSIS_THREAD)
72 
73     val retained = analysisRetained - baselineHeap.retainedHeap(ANALYSIS_THREAD).first
74 
75     assertThat(retained).isEqualTo(4.9 MB +-5 % margin)
76   }
77 
78   @Test fun `freeze retained memory through analysis steps of leak_asynctask_o`() {
79     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
80     val stepsToHeapDumpFile = mutableMapOf<OnAnalysisProgressListener.Step, File>()
81     val heapAnalyzer = HeapAnalyzer { step ->
82       stepsToHeapDumpFile[step] = dumpHeap(step.name)
83     }
84 
85     // matchers contain large description strings which depending on the VM maybe be reachable
86     // only via matchers (=> thread locals), and otherwise also statically by the enum class that
87     // defines them. So we create a reference outside of the working thread to exclude them from
88     // the retained count and avoid a varying count.
89     val matchers = AndroidReferenceMatchers.appDefaults
90     val baselineHeap = runInThread(ANALYSIS_THREAD) {
91       val baselineHeap = dumpHeap("baseline")
92       heapAnalyzer.analyze(
93         heapDumpFile = hprofFile,
94         leakingObjectFinder = FilteringLeakingObjectFinder(
95           AndroidObjectInspectors.appLeakingObjectFilters
96         ),
97         referenceMatchers = matchers,
98         objectInspectors = AndroidObjectInspectors.appDefaults,
99         metadataExtractor = AndroidMetadataExtractor,
100         computeRetainedHeapSize = true
101       ).apply {
102         check(this is HeapAnalysisSuccess) {
103           "Expected success not $this"
104         }
105       }
106       baselineHeap
107     }
108 
109     val retainedBeforeAnalysis = baselineHeap.retainedHeap(ANALYSIS_THREAD).first
110     val retained = stepsToHeapDumpFile.mapValues {
111       val retainedPair = it.value.retainedHeap(ANALYSIS_THREAD, computeDominators = true)
112       retainedPair.first - retainedBeforeAnalysis to retainedPair.second
113     }
114 
115     assertThat(retained after PARSING_HEAP_DUMP).isEqualTo(5.57 MB +-5 % margin)
116     assertThat(retained after EXTRACTING_METADATA).isEqualTo(5.62 MB +-5 % margin)
117     assertThat(retained after FINDING_RETAINED_OBJECTS).isEqualTo(5.72 MB +-5 % margin)
118     assertThat(retained after FINDING_PATHS_TO_RETAINED_OBJECTS).isEqualTo(7.12 MB +-5 % margin)
119     assertThat(retained after FINDING_DOMINATORS).isEqualTo(7.12 MB +-5 % margin)
120     assertThat(retained after INSPECTING_OBJECTS).isEqualTo(7.13 MB +-5 % margin)
121     assertThat(retained after COMPUTING_NATIVE_RETAINED_SIZE).isEqualTo(7.13 MB +-5 % margin)
122     assertThat(retained after COMPUTING_RETAINED_SIZE).isEqualTo(6.05 MB +-5 % margin)
123   }
124 
125   private fun indexRecordsOf(hprofFile: File): HprofIndex {
126     return HprofIndex.indexRecordsOf(
127       hprofSourceProvider = FileSourceProvider(hprofFile),
128       hprofHeader = HprofHeader.parseHeaderOf(hprofFile)
129     )
130   }
131 
132   private fun dumpHeapRetaining(instance: Any): File {
133     val heapDumpFile = dumpHeap("retaining-${instance::class.java.name}")
134     // Dumb check to prevent instance from being garbage collected.
135     check(instance::class::class.isInstance(KClass::class))
136     return heapDumpFile
137   }
138 
139   private fun dumpHeap(name: String): File {
140     // Dumps the heap in a separate thread to avoid java locals being added to the count of
141     // bytes retained by this thread.
142     return runInThread("heap dump") {
143       val testHprofFile = File(folder, "$name.hprof")
144       if (testHprofFile.exists()) {
145         testHprofFile.delete()
146       }
147       JvmTestHeapDumper.dumpHeap(testHprofFile.absolutePath)
148       testHprofFile
149     }
150   }
151 
152   private fun <T : Any> runInThread(
153     threadName: String,
154     work: () -> T
155   ): T {
156     lateinit var result: T
157     val latch = CountDownLatch(1)
158     thread(name = threadName) {
159       result = work()
160       latch.countDown()
161     }
162     check(latch.await(30, SECONDS))
163     return result
164   }
165 
166   private infix fun Map<OnAnalysisProgressListener.Step, Pair<Bytes, String>>.after(step: OnAnalysisProgressListener.Step): Pair<Bytes, String> {
167     val values = OnAnalysisProgressListener.Step.values()
168     for (nextOrdinal in step.ordinal + 1 until values.size) {
169       val pair = this[values[nextOrdinal]]
170       if (pair != null) {
171         val (nextStepRetained, dominatorTree) = pair
172 
173         return nextStepRetained to "\n$nextStepRetained retained by analysis thread after step ${step.name} not valid\n" + dominatorTree
174       }
175     }
176     error("No step in $this after $step")
177   }
178 
179   private fun File.retainedHeap(
180     threadName: String,
181     computeDominators: Boolean = false
182   ): Pair<Bytes, String> {
183     val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
184 
185     val (analysis, dominatorTree) = openHeapGraph().use { graph ->
186       val analysis = heapAnalyzer.analyze(
187         heapDumpFile = this,
188         graph = graph,
189         referenceMatchers = buildKnownReferences(
190           EnumSet.of(REFERENCES, FINALIZER_WATCHDOG_DAEMON)
191         ),
192         leakingObjectFinder = {
193           setOf(graph.gcRoots.first { gcRoot ->
194             gcRoot is ThreadObject &&
195               graph.objectExists(gcRoot.id) &&
196               graph.findObjectById(gcRoot.id)
197                 .asInstance!!["java.lang.Thread", "name"]!!
198                 .value.readAsJavaString() == threadName
199           }.id)
200         },
201         computeRetainedHeapSize = true
202       )
203       check(analysis is HeapAnalysisSuccess) {
204         "Expected success not $analysis"
205       }
206 
207       val dominatorTree = if (computeDominators) {
208         val weakAndFinalizerRefs = EnumSet.of(REFERENCES, FINALIZER_WATCHDOG_DAEMON)
209         val ignoredRefs = buildKnownReferences(weakAndFinalizerRefs).map { matcher ->
210           matcher as IgnoredReferenceMatcher
211         }
212         ObjectDominators().renderDominatorTree(
213           graph, ignoredRefs, 200, threadName, true
214         )
215       } else ""
216       analysis to dominatorTree
217     }
218 
219     return analysis.applicationLeaks.single().leakTraces.single().retainedHeapByteSize!!.bytes to dominatorTree
220   }
221 
222   class BytesAssert(
223     bytes: Bytes,
224     description: String
225   ) : AbstractIntegerAssert<BytesAssert>(
226     bytes.count, BytesAssert::class.java
227   ) {
228 
229     init {
230       describedAs(description)
231     }
232 
233     fun isEqualTo(expected: BytesWithError): BytesAssert {
234       val errorPercentage = expected.error.percentage.absoluteValue
235       return isBetween(
236         (expected.count * (1 - errorPercentage)).toInt(),
237         (expected.count * (1 + errorPercentage)).toInt()
238       )
239     }
240   }
241 
242   private fun assertThat(bytes: Bytes) = BytesAssert(bytes, "")
243 
244   private fun assertThat(pair: Pair<Bytes, String>) = BytesAssert(pair.first, pair.second)
245 
246   data class Bytes(val count: Int)
247 
248   operator fun Bytes.minus(other: Bytes) = Bytes(count - other.count)
249 
250   private val Int.bytes: Bytes
251     get() = Bytes(this)
252 
253   class BytesWithError(
254     val count: Int,
255     val error: ErrorPercentage
256   )
257 
258   object Margin
259 
260   private val margin
261     get() = Margin
262 
263   class ErrorPercentage(val percentage: Double)
264 
265   infix fun Double.MB(error: ErrorPercentage) =
266     BytesWithError((this * 1_000_000).toInt(), error)
267 
268   operator fun Int.rem(ignored: Margin): ErrorPercentage = ErrorPercentage(this / 100.0)
269 }
270