<lambda>null1 package leakcanary.internal
2
3 import android.os.Debug
4 import android.os.SystemClock
5 import java.io.File
6 import java.util.UUID
7 import java.util.concurrent.atomic.AtomicBoolean
8 import java.util.concurrent.atomic.AtomicReference
9 import leakcanary.HeapAnalysisConfig
10 import leakcanary.HeapAnalysisInterceptor
11 import leakcanary.HeapAnalysisJob
12 import leakcanary.HeapAnalysisJob.Result
13 import leakcanary.HeapAnalysisJob.Result.Canceled
14 import leakcanary.HeapAnalysisJob.Result.Done
15 import leakcanary.JobContext
16 import okio.buffer
17 import okio.sink
18 import shark.CloseableHeapGraph
19 import shark.ConstantMemoryMetricsDualSourceProvider
20 import shark.DualSourceProvider
21 import shark.HeapAnalysis
22 import shark.HeapAnalysisException
23 import shark.HeapAnalysisFailure
24 import shark.HeapAnalysisSuccess
25 import shark.HeapAnalyzer
26 import shark.HprofHeapGraph
27 import shark.HprofHeapGraph.Companion.openHeapGraph
28 import shark.HprofPrimitiveArrayStripper
29 import shark.OnAnalysisProgressListener
30 import shark.RandomAccessSource
31 import shark.SharkLog
32 import shark.StreamingSourceProvider
33 import shark.ThrowingCancelableFileSourceProvider
34
35 internal class RealHeapAnalysisJob(
36 private val heapDumpDirectoryProvider: () -> File,
37 private val config: HeapAnalysisConfig,
38 private val interceptors: List<HeapAnalysisInterceptor>,
39 override val context: JobContext
40 ) : HeapAnalysisJob, HeapAnalysisInterceptor.Chain {
41
42 private val heapDumpDirectory by lazy {
43 heapDumpDirectoryProvider()
44 }
45
46 private val _canceled = AtomicReference<Canceled?>()
47
48 private val _executed = AtomicBoolean(false)
49
50 private lateinit var executionThread: Thread
51
52 private var interceptorIndex = 0
53
54 private var analysisStep: OnAnalysisProgressListener.Step? = null
55
56 override val executed
57 get() = _executed.get()
58
59 override val canceled
60 get() = _canceled.get() != null
61
62 override val job: HeapAnalysisJob
63 get() = this
64
65 override fun execute(): Result {
66 check(_executed.compareAndSet(false, true)) { "HeapAnalysisJob can only be executed once" }
67 SharkLog.d { "Starting heap analysis job" }
68 executionThread = Thread.currentThread()
69 return proceed()
70 }
71
72 override fun cancel(cancelReason: String) {
73 // If cancel is called several times, we use the first cancel reason.
74 _canceled.compareAndSet(null, Canceled(cancelReason))
75 }
76
77 override fun proceed(): Result {
78 check(Thread.currentThread() == executionThread) {
79 "Interceptor.Chain.proceed() called from unexpected thread ${Thread.currentThread()} instead of $executionThread"
80 }
81 check(interceptorIndex <= interceptors.size) {
82 "Interceptor.Chain.proceed() should be called max once per interceptor"
83 }
84 _canceled.get()?.let {
85 interceptorIndex = interceptors.size + 1
86 return it
87 }
88 if (interceptorIndex < interceptors.size) {
89 val currentInterceptor = interceptors[interceptorIndex]
90 interceptorIndex++
91 return currentInterceptor.intercept(this)
92 } else {
93 interceptorIndex++
94 val result = dumpAndAnalyzeHeap()
95 val analysis = result.analysis
96 analysis.heapDumpFile.delete()
97 if (analysis is HeapAnalysisFailure) {
98 val cause = analysis.exception.cause
99 if (cause is StopAnalysis) {
100 return _canceled.get()!!.run {
101 copy(cancelReason = "$cancelReason (stopped at ${cause.step})")
102 }
103 }
104 }
105 return result
106 }
107 }
108
109 private fun dumpAndAnalyzeHeap(): Done {
110 val filesDir = heapDumpDirectory
111 filesDir.mkdirs()
112 val fileNameBase = "$HPROF_PREFIX${UUID.randomUUID()}"
113 val sensitiveHeapDumpFile = File(filesDir, "$fileNameBase$HPROF_SUFFIX").apply {
114 // Any call to System.exit(0) will run shutdown hooks that will attempt to remove this
115 // file. Note that this is best effort, and won't delete if the VM is killed by the system.
116 deleteOnExit()
117 }
118
119 val heapDumpStart = SystemClock.uptimeMillis()
120 saveHeapDumpTime(heapDumpStart)
121
122 var dumpDurationMillis = -1L
123 var analysisDurationMillis = -1L
124 var heapDumpFile = sensitiveHeapDumpFile
125
126 try {
127 runGc()
128 dumpHeap(sensitiveHeapDumpFile)
129 dumpDurationMillis = SystemClock.uptimeMillis() - heapDumpStart
130
131 val stripDurationMillis =
132 if (config.stripHeapDump) {
133 leakcanary.internal.friendly.measureDurationMillis {
134 val strippedHeapDumpFile = File(filesDir, "$fileNameBase-stripped$HPROF_SUFFIX").apply {
135 deleteOnExit()
136 }
137 heapDumpFile = strippedHeapDumpFile
138 try {
139 stripHeapDump(sensitiveHeapDumpFile, strippedHeapDumpFile)
140 } finally {
141 sensitiveHeapDumpFile.delete()
142 }
143 }
144 } else null
145
146 return analyzeHeapWithStats(heapDumpFile).let { (heapAnalysis, stats) ->
147 when (heapAnalysis) {
148 is HeapAnalysisSuccess -> {
149 val metadata = heapAnalysis.metadata.toMutableMap()
150 metadata["Stats"] = stats
151 if (config.stripHeapDump) {
152 metadata["Hprof stripping duration"] = "$stripDurationMillis ms"
153 }
154 Done(
155 heapAnalysis.copy(
156 dumpDurationMillis = dumpDurationMillis,
157 metadata = metadata
158 ), stripDurationMillis
159 )
160 }
161 is HeapAnalysisFailure -> Done(
162 heapAnalysis.copy(
163 dumpDurationMillis = dumpDurationMillis,
164 analysisDurationMillis = (SystemClock.uptimeMillis() - heapDumpStart) - dumpDurationMillis
165 ), stripDurationMillis
166 )
167 }
168 }
169 } catch (throwable: Throwable) {
170 if (dumpDurationMillis == -1L) {
171 dumpDurationMillis = SystemClock.uptimeMillis() - heapDumpStart
172 }
173 if (analysisDurationMillis == -1L) {
174 analysisDurationMillis = (SystemClock.uptimeMillis() - heapDumpStart) - dumpDurationMillis
175 }
176 return Done(
177 HeapAnalysisFailure(
178 heapDumpFile = heapDumpFile,
179 createdAtTimeMillis = System.currentTimeMillis(),
180 dumpDurationMillis = dumpDurationMillis,
181 analysisDurationMillis = analysisDurationMillis,
182 exception = HeapAnalysisException(throwable)
183 )
184 )
185 }
186 }
187
188 private fun runGc() {
189 // Code taken from AOSP FinalizationTest:
190 // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
191 // java/lang/ref/FinalizationTester.java
192 // System.gc() does not garbage collect every time. Runtime.gc() is
193 // more likely to perform a gc.
194 Runtime.getRuntime()
195 .gc()
196 enqueueReferences()
197 System.runFinalization()
198 }
199
200 private fun enqueueReferences() {
201 // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
202 // references to the appropriate queues.
203 try {
204 Thread.sleep(100)
205 } catch (e: InterruptedException) {
206 throw AssertionError()
207 }
208 }
209
210 private fun saveHeapDumpTime(heapDumpUptimeMillis: Long) {
211 try {
212 Class.forName("leakcanary.KeyedWeakReference")
213 .getDeclaredField("heapDumpUptimeMillis")
214 .apply { isAccessible = true }
215 .set(null, heapDumpUptimeMillis)
216 } catch (ignored: Throwable) {
217 SharkLog.d(ignored) { "KeyedWeakReference.heapDumpUptimeMillis not updated" }
218 }
219 }
220
221 private fun dumpHeap(heapDumpFile: File) {
222 Debug.dumpHprofData(heapDumpFile.absolutePath)
223
224 check(heapDumpFile.exists()) {
225 "File does not exist after dump"
226 }
227
228 check(heapDumpFile.length() > 0L) {
229 "File has length ${heapDumpFile.length()} after dump"
230 }
231 }
232
233 private fun stripHeapDump(
234 sourceHeapDumpFile: File,
235 strippedHeapDumpFile: File
236 ) {
237 val sensitiveSourceProvider =
238 ThrowingCancelableFileSourceProvider(sourceHeapDumpFile) {
239 checkStopAnalysis("stripping heap dump")
240 }
241
242 var openCalls = 0
243 val deletingFileSourceProvider = StreamingSourceProvider {
244 openCalls++
245 sensitiveSourceProvider.openStreamingSource().apply {
246 if (openCalls == 2) {
247 // Using the Unix trick of deleting the file as soon as all readers have opened it.
248 // No new readers/writers will be able to access the file, but all existing
249 // ones will still have access until the last one closes the file.
250 SharkLog.d { "Deleting $sourceHeapDumpFile eagerly" }
251 sourceHeapDumpFile.delete()
252 }
253 }
254 }
255
256 val strippedHprofSink = strippedHeapDumpFile.outputStream().sink().buffer()
257 val stripper = HprofPrimitiveArrayStripper()
258
259 stripper.stripPrimitiveArrays(deletingFileSourceProvider, strippedHprofSink)
260 }
261
262 private fun analyzeHeapWithStats(heapDumpFile: File): Pair<HeapAnalysis, String> {
263 val fileLength = heapDumpFile.length()
264 val analysisSourceProvider = ConstantMemoryMetricsDualSourceProvider(
265 ThrowingCancelableFileSourceProvider(heapDumpFile) {
266 checkStopAnalysis(analysisStep?.name ?: "Reading heap dump")
267 })
268
269 val deletingFileSourceProvider = object : DualSourceProvider {
270 override fun openStreamingSource() = analysisSourceProvider.openStreamingSource()
271
272 override fun openRandomAccessSource(): RandomAccessSource {
273 SharkLog.d { "Deleting $heapDumpFile eagerly" }
274 return analysisSourceProvider.openRandomAccessSource().apply {
275 // Using the Unix trick of deleting the file as soon as all readers have opened it.
276 // No new readers/writers will be able to access the file, but all existing
277 // ones will still have access until the last one closes the file.
278 heapDumpFile.delete()
279 }
280 }
281 }
282
283 return deletingFileSourceProvider.openHeapGraph().use { graph ->
284 val heapAnalysis = analyzeHeap(heapDumpFile, graph)
285 val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
286 val randomAccessStats =
287 "RandomAccess[" +
288 "bytes=${analysisSourceProvider.randomAccessByteReads}," +
289 "reads=${analysisSourceProvider.randomAccessReadCount}," +
290 "travel=${analysisSourceProvider.randomAccessByteTravel}," +
291 "range=${analysisSourceProvider.byteTravelRange}," +
292 "size=$fileLength" +
293 "]"
294 val stats = "$lruCacheStats $randomAccessStats"
295 (heapAnalysis to stats)
296 }
297 }
298
299 private fun analyzeHeap(
300 analyzedHeapDumpFile: File,
301 graph: CloseableHeapGraph
302 ): HeapAnalysis {
303 val stepListener = OnAnalysisProgressListener { step ->
304 analysisStep = step
305 checkStopAnalysis(step.name)
306 SharkLog.d { "Analysis in progress, working on: ${step.name}" }
307 }
308
309 val heapAnalyzer = HeapAnalyzer(stepListener)
310 return heapAnalyzer.analyze(
311 heapDumpFile = analyzedHeapDumpFile,
312 graph = graph,
313 leakingObjectFinder = config.leakingObjectFinder,
314 referenceMatchers = config.referenceMatchers,
315 computeRetainedHeapSize = config.computeRetainedHeapSize,
316 objectInspectors = config.objectInspectors,
317 metadataExtractor = config.metadataExtractor
318 )
319 }
320
321 private fun checkStopAnalysis(step: String) {
322 if (_canceled.get() != null) {
323 throw StopAnalysis(step)
324 }
325 }
326
327 class StopAnalysis(val step: String) : Exception() {
328 override fun fillInStackTrace(): Throwable {
329 // Skip filling in stacktrace.
330 return this
331 }
332 }
333
334 companion object {
335 const val HPROF_PREFIX = "heap-"
336 const val HPROF_SUFFIX = ".hprof"
337 }
338 }
339