<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