<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