xref: /aosp_15_r20/external/leakcanary2/shark-android/src/test/java/shark/LruCacheTuning.kt (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)

<lambda>null1 package shark
2 
3 import okio.buffer
4 import okio.source
5 import shark.AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON
6 import shark.AndroidReferenceMatchers.REFERENCES
7 import shark.HprofHeapGraph.Companion.openHeapGraph
8 import java.io.File
9 import java.io.FileWriter
10 import java.util.EnumSet
11 import java.util.UUID
12 
13 /**
14  * Tests different values for the lru cache size when performing the leak analysss, measuring IO
15  * reads and memory retained by the cache for each cache size, and outputting the result as
16  * a CSV.
17  *
18  * Data saved at https://docs.google.com/spreadsheets/d/14BRd1CJO2_WRBqNQRdfDUDLhr3_2R5N461a74XA4pjE/edit?usp=sharing
19  */
20 fun main() {
21   val tmpHeapDumpFolder = createTemporaryFolder()
22 
23   val lruCacheSizes = 100..20000 step 500
24   val files = listOf("leak_asynctask_o", "leak_asynctask_m")
25   val computeRetainedHeapSizeList = listOf(false, true)
26 
27   val stats = mutableListOf<MutableList<Int>>()
28   for (lruCacheSize in lruCacheSizes) {
29     val row = mutableListOf<Int>()
30     row.add(lruCacheSize)
31     stats.add(row)
32   }
33 
34   for (filename in files) {
35     val hprofFile = "$filename.hprof".classpathFile()
36     val bytes = hprofFile.inputStream().source().buffer().readByteArray()
37     lruCacheSizes.forEachIndexed { index, lruCacheSize ->
38       val row = stats[index]
39       for (computeRetainedHeapSize in computeRetainedHeapSizeList) {
40         val (randomAccessReads, lruRetainedSize) = trackAnalyzeMetrics(
41           hprofFile, bytes, tmpHeapDumpFolder, computeRetainedHeapSize, lruCacheSize
42         )
43         val bytesRead = randomAccessReads.sum()
44         val readCount = randomAccessReads.size
45         row.add(bytesRead)
46         row.add(readCount)
47         row.add(lruRetainedSize)
48       }
49     }
50   }
51 
52   tmpHeapDumpFolder.recursiveDelete()
53 
54   FileWriter("lru_cache_tuning.csv").use {
55     with(it) {
56       append("lru_size")
57       for (filename in files) {
58         for (computeRetainedHeapSize in computeRetainedHeapSizeList) {
59           listOf("bytes_read", "read_count", "lru_retained").forEach { column ->
60             append(",${filename}_size_${computeRetainedHeapSize}_$column")
61           }
62         }
63       }
64       append('\n')
65       for (statRow in stats) {
66         append(statRow.joinToString(",", postfix = "\n"))
67       }
68     }
69   }
70 }
71 
trackAnalyzeMetricsnull72 private fun trackAnalyzeMetrics(
73   hprofFile: File,
74   bytes: ByteArray,
75   tmpHeapDumpFolder: File,
76   computeRetainedHeapSize: Boolean,
77   lruCacheSize: Int
78 ): Pair<List<Int>, Int> {
79   println(
80     "Analysing ${hprofFile.name} computeRetainedHeapSize=$computeRetainedHeapSize lruCacheSize=$lruCacheSize"
81   )
82 
83   val source = MetricsDualSourceProvider(ByteArraySourceProvider(bytes))
84   val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
85   val heapAfterAnalysis = withLruCacheSize(lruCacheSize) {
86     source.openHeapGraph().use { graph ->
87       val analysis = heapAnalyzer.analyze(
88         heapDumpFile = hprofFile,
89         graph = graph,
90         leakingObjectFinder = FilteringLeakingObjectFinder(
91           AndroidObjectInspectors.appLeakingObjectFilters
92         ),
93         referenceMatchers = AndroidReferenceMatchers.appDefaults,
94         computeRetainedHeapSize = computeRetainedHeapSize,
95         objectInspectors = AndroidObjectInspectors.appDefaults,
96         metadataExtractor = AndroidMetadataExtractor
97       )
98       check(analysis is HeapAnalysisSuccess) {
99         "Expected success not $analysis"
100       }
101       tmpHeapDumpFolder.dumpHeap()
102     }
103   }
104   val randomAccessReads = source.sourcesMetrics[3]
105 
106   val lruCacheAnalysis = heapAnalyzer.analyze(
107     heapDumpFile = heapAfterAnalysis,
108     referenceMatchers = AndroidReferenceMatchers.buildKnownReferences(
109       EnumSet.of(REFERENCES, FINALIZER_WATCHDOG_DAEMON)
110     ),
111     leakingObjectFinder = { graph ->
112       setOf(graph.findClassByName("shark.internal.LruCache")!!.instances.single().objectId)
113     },
114     computeRetainedHeapSize = true
115   )
116   check(lruCacheAnalysis is HeapAnalysisSuccess) {
117     "Expected success not $lruCacheAnalysis"
118   }
119   val lruRetainedSize =
120     lruCacheAnalysis.allLeaks.single().leakTraces.single().retainedHeapByteSize!!
121 
122   println(
123     "${randomAccessReads.sum()} bytes in ${randomAccessReads.size} reads, retaining $lruRetainedSize bytes in cache"
124   )
125 
126   return randomAccessReads to lruRetainedSize
127 }
128 
withLruCacheSizenull129 private fun <T> withLruCacheSize(
130   lruCacheSize: Int,
131   block: () -> T
132 ): T {
133   val sizeBefore = HprofHeapGraph.INTERNAL_LRU_CACHE_SIZE
134   HprofHeapGraph.INTERNAL_LRU_CACHE_SIZE = lruCacheSize
135   try {
136     return block()
137   } finally {
138     HprofHeapGraph.INTERNAL_LRU_CACHE_SIZE = sizeBefore
139   }
140 }
141 
createTemporaryFoldernull142 private fun createTemporaryFolder(): File {
143   val createdFolder = File.createTempFile("shark", "", null)
144   createdFolder.delete()
145   createdFolder.mkdir()
146   return createdFolder
147 }
148 
recursiveDeletenull149 private fun File.recursiveDelete() {
150   val files = listFiles()
151   if (files != null) {
152     for (each in files) {
153       each.recursiveDelete()
154     }
155   }
156   delete()
157 }
158 
dumpHeapnull159 private fun File.dumpHeap(): File {
160   val testHprofFile = File(this, "${UUID.randomUUID()}.hprof")
161   JvmTestHeapDumper.dumpHeap(testHprofFile.absolutePath)
162   return testHprofFile
163 }
164