<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