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

<lambda>null1 package shark
2 
3 import java.io.File
4 import kotlin.math.floor
5 import org.assertj.core.api.Assertions.assertThat
6 import org.junit.Test
7 import org.nield.kotlinstatistics.median
8 import shark.HprofHeapGraph.Companion.openHeapGraph
9 import shark.PrimitiveType.INT
10 
11 /**
12  * IO reads is the largest factor on Shark's performance so this helps prevents
13  * regressions.
14  */
15 class HprofIOPerfTest {
16 
17   @Test fun `HeapObjectArray#readByteSize() does not read`() {
18     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
19     val arrayId = hprofFile.openHeapGraph().use { graph ->
20       graph.objectArrays.maxBy { it.readRecord().elementIds.size * graph.identifierByteSize }!!.objectId
21     }
22 
23     val source = MetricsDualSourceProvider(hprofFile)
24 
25     val bytesRead = source.openHeapGraph().use { graph ->
26       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
27       graph.findObjectById(arrayId).asObjectArray!!.byteSize
28       bytesReadMetrics.sum()
29     }
30 
31     assertThat(bytesRead).isEqualTo(0)
32   }
33 
34   @Test fun `HeapObjectArray#byteSize correctly reads size of array`() {
35     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
36     hprofFile.openHeapGraph().use { graph ->
37       graph.objectArrays.forEach { array ->
38         assertThat(array.byteSize).isEqualTo(
39           array.readRecord().elementIds.size * graph.identifierByteSize
40         )
41       }
42     }
43   }
44 
45   @Test fun `HeapPrimitiveArray#byteSize does not read`() {
46     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
47     val arrayId = hprofFile.openHeapGraph().use { graph ->
48       graph.primitiveArrays.maxBy { it.readRecord().size * it.primitiveType.byteSize }!!.objectId
49     }
50 
51     val source = MetricsDualSourceProvider(hprofFile)
52 
53     val bytesRead = source.openHeapGraph().use { graph ->
54       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
55       graph.findObjectById(arrayId).asPrimitiveArray!!.byteSize
56       bytesReadMetrics.sum()
57     }
58 
59     assertThat(bytesRead).isEqualTo(0)
60   }
61 
62   @Test fun `HeapPrimitiveArray#readByteSize() correctly reads size of array`() {
63     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
64     hprofFile.openHeapGraph().use { graph ->
65       graph.primitiveArrays.forEach { array ->
66         assertThat(array.byteSize).isEqualTo(
67           array.readRecord().size * array.primitiveType.byteSize
68         )
69       }
70     }
71   }
72 
73   @Test fun `HeapInstance#byteSize reads 0 bytes`() {
74     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
75 
76     val source = MetricsDualSourceProvider(hprofFile)
77 
78     val bytesRead = source.openHeapGraph().use { graph ->
79       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
80       graph.instances.first().byteSize
81       bytesReadMetrics.sum()
82     }
83 
84     assertThat(bytesRead).isEqualTo(0)
85   }
86 
87   @Test fun `consecutive call to HeapObject#readRecord() reads 0 bytes`() {
88     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
89 
90     val source = MetricsDualSourceProvider(hprofFile)
91 
92     val bytesRead = source.openHeapGraph().use { graph ->
93       graph.objects.first().readRecord()
94       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
95       graph.objects.first().readRecord()
96       bytesReadMetrics.sum()
97     }
98 
99     assertThat(bytesRead).isEqualTo(0)
100   }
101 
102   @Test fun `HeapObject#readRecord() reads 0 bytes when reading from LRU`() {
103     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
104 
105     val source = MetricsDualSourceProvider(hprofFile)
106 
107     val bytesRead = source.openHeapGraph().use { graph ->
108       graph.objects.take(HPROF_HEAP_GRAPH_LRU_OBJECT_CACHE_SIZE).forEach { it.readRecord() }
109       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
110       graph.objects.take(HPROF_HEAP_GRAPH_LRU_OBJECT_CACHE_SIZE).forEach { it.readRecord() }
111       bytesReadMetrics.sum()
112     }
113 
114     assertThat(bytesRead).isEqualTo(0)
115   }
116 
117   @Test fun `HeapObject#readRecord() reads bytes when reading evicted object`() {
118     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
119 
120     val source = MetricsDualSourceProvider(hprofFile)
121 
122     val bytesRead = source.openHeapGraph().use { graph ->
123       graph.objects.take(HPROF_HEAP_GRAPH_LRU_OBJECT_CACHE_SIZE + 1).forEach { it.readRecord() }
124       val bytesReadMetrics = source.sourcesMetrics.last().apply { clear() }
125       graph.objects.first().readRecord()
126       bytesReadMetrics.sum()
127     }
128 
129     assertThat(bytesRead).isGreaterThan(0)
130   }
131 
132   @Test fun `analyze() creates 4 separate sources`() {
133     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
134 
135     val metrics = trackAnalyzeIoReadMetrics(hprofFile)
136 
137     // 4 phases: Read headers, fast scan, indexing, then random access for analysis.
138     assertThat(metrics).hasSize(4)
139   }
140 
141   @Test fun `header parsing requires only one segment`() {
142     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
143 
144     val metrics = trackAnalyzeIoReadMetrics(hprofFile)
145 
146     val headerParsingReads = metrics[0]
147     assertThat(headerParsingReads).isEqualTo(listOf(OKIO_SEGMENT_SIZE))
148   }
149 
150   @Test fun `fast scan pre indexing is a full file scan`() {
151     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
152 
153     val metrics = trackAnalyzeIoReadMetrics(hprofFile)
154 
155     val fastScanReads = metrics[1]
156     val expectedReads = fullScanExpectedReads(hprofFile.length())
157     assertThat(fastScanReads).hasSameSizeAs(expectedReads).isEqualTo(expectedReads)
158   }
159 
160   @Test fun `indexing is a full file scan`() {
161     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
162 
163     val metrics = trackAnalyzeIoReadMetrics(hprofFile)
164 
165     val indexingReads = metrics[2]
166     val expectedReads = fullScanExpectedReads(hprofFile.length())
167     assertThat(indexingReads).hasSameSizeAs(expectedReads).isEqualTo(expectedReads)
168   }
169 
170   @Test fun `freeze leak_asynctask_o hprof random access metrics`() {
171     val hprofFile = "leak_asynctask_o.hprof".classpathFile()
172 
173     val metrics = trackAnalyzeRandomAccessMetrics(hprofFile)
174 
175     assertThat(
176       listOf(
177         metrics.first.readsCount, metrics.first.medianBytesRead, metrics.first.totalBytesRead,
178         metrics.second.readsCount, metrics.second.medianBytesRead, metrics.second.totalBytesRead
179       )
180     )
181       .isEqualTo(
182         listOf(
183           25760, 40.0, 1309045, 25765, 40.0, 1309225
184         )
185       )
186   }
187 
188   @Test fun `freeze leak_asynctask_m hprof random access metrics`() {
189     val hprofFile = "leak_asynctask_m.hprof".classpathFile()
190 
191     val metrics = trackAnalyzeRandomAccessMetrics(hprofFile)
192 
193     assertThat(
194       listOf(
195         metrics.first.readsCount, metrics.first.medianBytesRead, metrics.first.totalBytesRead,
196         metrics.second.readsCount, metrics.second.medianBytesRead, metrics.second.totalBytesRead
197       )
198     )
199       .isEqualTo(
200         listOf(
201           22493, 40.0, 2203818, 22498, 40.0, 2203998
202         )
203       )
204   }
205 
206   @Test fun `freeze leak_asynctask_pre_m hprof random access metrics`() {
207     val hprofFile = "leak_asynctask_pre_m.hprof".classpathFile()
208 
209     val metrics = trackAnalyzeRandomAccessMetrics(hprofFile)
210 
211     assertThat(
212       listOf(
213         metrics.first.readsCount, metrics.first.medianBytesRead, metrics.first.totalBytesRead,
214         metrics.second.readsCount, metrics.second.medianBytesRead, metrics.second.totalBytesRead
215       )
216     )
217       .isEqualTo(
218         listOf(
219           16889, 32.0, 768692, 16891, 32.0, 768756
220         )
221       )
222   }
223 
224   class Reads(reads: List<Int>) {
225     val readsCount = reads.size
226     val medianBytesRead = reads.median()
227     val totalBytesRead = reads.sum()
228   }
229 
230   private fun trackAnalyzeRandomAccessMetrics(hprofFile: File): Pair<Reads, Reads> {
231     return trackAnalyzeIoReadMetrics(hprofFile).run {
232       Reads(this[3])
233     } to trackAnalyzeIoReadMetrics(
234       hprofFile,
235       computeRetainedHeapSize = true,
236       printResult = true
237     ).run {
238       Reads(this[3])
239     }
240   }
241 
242   private fun trackAnalyzeIoReadMetrics(
243     hprofFile: File,
244     computeRetainedHeapSize: Boolean = false,
245     printResult: Boolean = false
246   ): List<List<Int>> {
247     val source = MetricsDualSourceProvider(hprofFile)
248     val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
249     val analysis = source.openHeapGraph().use { graph ->
250       heapAnalyzer.analyze(
251         heapDumpFile = hprofFile,
252         graph = graph,
253         leakingObjectFinder = FilteringLeakingObjectFinder(
254           AndroidObjectInspectors.appLeakingObjectFilters
255         ),
256         referenceMatchers = AndroidReferenceMatchers.appDefaults,
257         computeRetainedHeapSize = computeRetainedHeapSize,
258         objectInspectors = AndroidObjectInspectors.appDefaults,
259         metadataExtractor = AndroidMetadataExtractor
260       )
261     }
262     check(analysis is HeapAnalysisSuccess) {
263       "Expected success not $analysis"
264     }
265     if (printResult) {
266       println(analysis)
267     }
268     return source.sourcesMetrics
269   }
270 
271   private fun fullScanExpectedReads(fileLength: Long): List<Int> {
272     val fullReadsCount = floor(fileLength / OKIO_SEGMENT_SIZE.toDouble()).toInt()
273     val remainderBytes = (fileLength - (OKIO_SEGMENT_SIZE * fullReadsCount)).toInt()
274 
275     val finalReads = if (remainderBytes > 0) listOf(remainderBytes, 0) else listOf(0)
276 
277     return List(fullReadsCount) {
278       OKIO_SEGMENT_SIZE
279     } + finalReads
280   }
281 
282   companion object {
283     private const val OKIO_SEGMENT_SIZE = 8192
284     private const val HPROF_HEAP_GRAPH_LRU_OBJECT_CACHE_SIZE = 3000
285   }
286 }
287