<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