<lambda>null1 package leakcanary.internal
2 
3 import java.io.File
4 import java.io.IOException
5 import leakcanary.EventListener
6 import leakcanary.EventListener.Event.HeapAnalysisDone
7 import leakcanary.EventListener.Event.HeapAnalysisDone.HeapAnalysisFailed
8 import leakcanary.EventListener.Event.HeapAnalysisDone.HeapAnalysisSucceeded
9 import leakcanary.EventListener.Event.HeapAnalysisProgress
10 import leakcanary.EventListener.Event.HeapDump
11 import leakcanary.LeakCanary
12 import leakcanary.internal.activity.LeakActivity
13 import leakcanary.internal.activity.db.HeapAnalysisTable
14 import leakcanary.internal.activity.db.LeakTable
15 import leakcanary.internal.activity.db.ScopedLeaksDb
16 import shark.ConstantMemoryMetricsDualSourceProvider
17 import shark.HeapAnalysis
18 import shark.HeapAnalysisException
19 import shark.HeapAnalysisFailure
20 import shark.HeapAnalysisSuccess
21 import shark.HeapAnalyzer
22 import shark.HprofHeapGraph
23 import shark.HprofHeapGraph.Companion.openHeapGraph
24 import shark.OnAnalysisProgressListener
25 import shark.OnAnalysisProgressListener.Step.PARSING_HEAP_DUMP
26 import shark.OnAnalysisProgressListener.Step.REPORTING_HEAP_ANALYSIS
27 import shark.ProguardMappingReader
28 import shark.ThrowingCancelableFileSourceProvider
29 
30 /**
31  * This should likely turn into a public API but probably better to do once it's
32  * coroutine based to supports cleaner cancellation + publishing progress.
33  */
34 internal object AndroidDebugHeapAnalyzer {
35 
36   private const val PROGUARD_MAPPING_FILE_NAME = "leakCanaryObfuscationMapping.txt"
37 
38   private val application = InternalLeakCanary.application
39 
40   /**
41    * Runs the heap analysis on the current thread and then sends a
42    * [EventListener.Event.HeapAnalysisDone] event with the result (from the current thread as well).
43    */
44   fun runAnalysisBlocking(
45     heapDumped: HeapDump,
46     isCanceled: () -> Boolean = { false },
47     progressEventListener: (HeapAnalysisProgress) -> Unit
48   ): HeapAnalysisDone<*> {
49     val progressListener = OnAnalysisProgressListener { step ->
50       val percent = (step.ordinal * 1.0) / OnAnalysisProgressListener.Step.values().size
51       progressEventListener(HeapAnalysisProgress(heapDumped.uniqueId, step, percent))
52     }
53 
54     val heapDumpFile = heapDumped.file
55     val heapDumpDurationMillis = heapDumped.durationMillis
56     val heapDumpReason = heapDumped.reason
57 
58     val heapAnalysis = if (heapDumpFile.exists()) {
59       analyzeHeap(heapDumpFile, progressListener, isCanceled)
60     } else {
61       missingFileFailure(heapDumpFile)
62     }
63 
64     val fullHeapAnalysis = when (heapAnalysis) {
65       is HeapAnalysisSuccess -> heapAnalysis.copy(
66         dumpDurationMillis = heapDumpDurationMillis,
67         metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
68       )
69       is HeapAnalysisFailure -> {
70         val failureCause = heapAnalysis.exception.cause!!
71         if (failureCause is OutOfMemoryError) {
72           heapAnalysis.copy(
73             dumpDurationMillis = heapDumpDurationMillis,
74             exception = HeapAnalysisException(
75               RuntimeException(
76                 """
77               Not enough memory to analyze heap. You can:
78               - Kill the app then restart the analysis from the LeakCanary activity.
79               - Increase the memory available to your debug app with largeHeap=true: https://developer.android.com/guide/topics/manifest/application-element#largeHeap
80               - Set up LeakCanary to run in a separate process: https://square.github.io/leakcanary/recipes/#running-the-leakcanary-analysis-in-a-separate-process
81               - Download the heap dump from the LeakCanary activity then run the analysis from your computer with shark-cli: https://square.github.io/leakcanary/shark/#shark-cli
82             """.trimIndent(), failureCause
83               )
84             )
85           )
86         } else {
87           heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
88         }
89       }
90     }
91     progressListener.onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
92 
93     val analysisDoneEvent = ScopedLeaksDb.writableDatabase(application) { db ->
94       val id = HeapAnalysisTable.insert(db, heapAnalysis)
95       when (fullHeapAnalysis) {
96         is HeapAnalysisSuccess -> {
97           val showIntent = LeakActivity.createSuccessIntent(application, id)
98           val leakSignatures = fullHeapAnalysis.allLeaks.map { it.signature }.toSet()
99           val leakSignatureStatuses = LeakTable.retrieveLeakReadStatuses(db, leakSignatures)
100           val unreadLeakSignatures = leakSignatureStatuses.filter { (_, read) ->
101             !read
102           }.keys
103             // keys returns LinkedHashMap$LinkedKeySet which isn't Serializable
104             .toSet()
105           HeapAnalysisSucceeded(
106             heapDumped.uniqueId,
107             fullHeapAnalysis,
108             unreadLeakSignatures,
109             showIntent
110           )
111         }
112         is HeapAnalysisFailure -> {
113           val showIntent = LeakActivity.createFailureIntent(application, id)
114           HeapAnalysisFailed(heapDumped.uniqueId, fullHeapAnalysis, showIntent)
115         }
116       }
117     }
118     LeakCanary.config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
119     return analysisDoneEvent
120   }
121 
122   private fun analyzeHeap(
123     heapDumpFile: File,
124     progressListener: OnAnalysisProgressListener,
125     isCanceled: () -> Boolean
126   ): HeapAnalysis {
127     val config = LeakCanary.config
128     val heapAnalyzer = HeapAnalyzer(progressListener)
129     val proguardMappingReader = try {
130       ProguardMappingReader(application.assets.open(PROGUARD_MAPPING_FILE_NAME))
131     } catch (e: IOException) {
132       null
133     }
134 
135     progressListener.onAnalysisProgress(PARSING_HEAP_DUMP)
136 
137     val sourceProvider =
138       ConstantMemoryMetricsDualSourceProvider(ThrowingCancelableFileSourceProvider(heapDumpFile) {
139         if (isCanceled()) {
140           throw RuntimeException("Analysis canceled")
141         }
142       })
143 
144     val closeableGraph = try {
145       sourceProvider.openHeapGraph(proguardMapping = proguardMappingReader?.readProguardMapping())
146     } catch (throwable: Throwable) {
147       return HeapAnalysisFailure(
148         heapDumpFile = heapDumpFile,
149         createdAtTimeMillis = System.currentTimeMillis(),
150         analysisDurationMillis = 0,
151         exception = HeapAnalysisException(throwable)
152       )
153     }
154     return closeableGraph
155       .use { graph ->
156         val result = heapAnalyzer.analyze(
157           heapDumpFile = heapDumpFile,
158           graph = graph,
159           leakingObjectFinder = config.leakingObjectFinder,
160           referenceMatchers = config.referenceMatchers,
161           computeRetainedHeapSize = config.computeRetainedHeapSize,
162           objectInspectors = config.objectInspectors,
163           metadataExtractor = config.metadataExtractor
164         )
165         if (result is HeapAnalysisSuccess) {
166           val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats()
167           val randomAccessStats =
168             "RandomAccess[" +
169               "bytes=${sourceProvider.randomAccessByteReads}," +
170               "reads=${sourceProvider.randomAccessReadCount}," +
171               "travel=${sourceProvider.randomAccessByteTravel}," +
172               "range=${sourceProvider.byteTravelRange}," +
173               "size=${heapDumpFile.length()}" +
174               "]"
175           val stats = "$lruCacheStats $randomAccessStats"
176           result.copy(metadata = result.metadata + ("Stats" to stats))
177         } else result
178       }
179   }
180 
181   private fun missingFileFailure(
182     heapDumpFile: File
183   ): HeapAnalysisFailure {
184     val deletedReason = LeakDirectoryProvider.hprofDeleteReason(heapDumpFile)
185     val exception = IllegalStateException(
186       "Hprof file $heapDumpFile missing, deleted because: $deletedReason"
187     )
188     return HeapAnalysisFailure(
189       heapDumpFile = heapDumpFile,
190       createdAtTimeMillis = System.currentTimeMillis(),
191       analysisDurationMillis = 0,
192       exception = HeapAnalysisException(exception)
193     )
194   }
195 }
196