mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Integrate Android Studio updates and tests for memory snapshot analysis
GitOrigin-RevId: 5a66bb48a91a1ced2b239eb7b3995b161595caa5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a05c28aa47
commit
428bd427e4
@@ -20,10 +20,11 @@ import com.intellij.diagnostic.hprof.analysis.AnalysisContext;
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalyzeClassloaderReferencesGraph;
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalyzeGraph;
|
||||
import com.intellij.diagnostic.hprof.analysis.HProfAnalysis;
|
||||
import com.intellij.diagnostic.hprof.util.ListProvider;
|
||||
import com.intellij.openapi.application.ModalityState;
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator;
|
||||
import com.intellij.openapi.progress.ProgressIndicator;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import kotlin.jvm.functions.Function3;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
@@ -81,7 +82,7 @@ public final class Analyzer {
|
||||
|
||||
Path hprofPath;
|
||||
ProgressIndicator progress;
|
||||
Function2<AnalysisContext, ProgressIndicator, String> analysisCallback;
|
||||
Function3<AnalysisContext, ListProvider, ProgressIndicator, String> analysisCallback;
|
||||
boolean onlyStrongReferences = false;
|
||||
boolean includeClassesAsRoots = true;
|
||||
int index = 0;
|
||||
@@ -95,13 +96,13 @@ public final class Analyzer {
|
||||
if (args[index].equals("-plugin")) {
|
||||
index++;
|
||||
String pluginId = args[index];
|
||||
analysisCallback = (analysisContext, progressIndicator) -> new AnalyzeClassloaderReferencesGraph(analysisContext, pluginId).analyze(progressIndicator);
|
||||
analysisCallback = (analysisContext, listProvider, progressIndicator) -> new AnalyzeClassloaderReferencesGraph(analysisContext, listProvider, pluginId).analyze(progressIndicator).getMainReport().toString();
|
||||
onlyStrongReferences = true;
|
||||
includeClassesAsRoots = false;
|
||||
index++;
|
||||
}
|
||||
else {
|
||||
analysisCallback = (analysisContext, progressIndicator) -> new AnalyzeGraph(analysisContext).analyze(progressIndicator);
|
||||
analysisCallback = (analysisContext, listProvider, progressIndicator) -> new AnalyzeGraph(analysisContext, listProvider).analyze(progressIndicator).getMainReport().toString();
|
||||
}
|
||||
hprofPath = Paths.get(args[index]);
|
||||
|
||||
|
||||
+28
-10
@@ -20,7 +20,8 @@ class AnalysisConfig(
|
||||
val histogramOptions: HistogramOptions = HistogramOptions(),
|
||||
val disposerOptions: DisposerOptions = DisposerOptions(),
|
||||
val traverseOptions: TraverseOptions = TraverseOptions(),
|
||||
val metaInfoOptions: MetaInfoOptions = MetaInfoOptions()
|
||||
val metaInfoOptions: MetaInfoOptions = MetaInfoOptions(),
|
||||
val dominatorTreeOptions: DominatorTreeOptions = DominatorTreeOptions()
|
||||
) {
|
||||
|
||||
class PerClassOptions(
|
||||
@@ -30,23 +31,21 @@ class AnalysisConfig(
|
||||
)
|
||||
|
||||
class TreeDisplayOptions(
|
||||
val minimumObjectSize: Int = 20_000_000,
|
||||
val minimumObjectCount: Int = 20_000,
|
||||
val minimumObjectSize: Int = 30_000_000,
|
||||
val minimumObjectCount: Int = 50_000,
|
||||
val minimumSubgraphSize: Long = 100_000_000,
|
||||
val minimumObjectCountPercent: Int = 10,
|
||||
val minimumObjectCountPercent: Int = 15,
|
||||
val maximumTreeDepth: Int = 80,
|
||||
val maximumIndent: Int = 40,
|
||||
val minimumPaths: Int = 2,
|
||||
val headLimit: Int = 100,
|
||||
val tailLimit: Int = 25,
|
||||
val showFieldNames: Boolean = false,
|
||||
val smartIndent: Boolean = true,
|
||||
val showSize: Boolean = true
|
||||
) {
|
||||
companion object {
|
||||
fun default() = TreeDisplayOptions()
|
||||
fun all(showFieldNames: Boolean = false,
|
||||
smartIndent: Boolean = true,
|
||||
fun all(smartIndent: Boolean = true,
|
||||
showSize: Boolean = true) =
|
||||
TreeDisplayOptions(minimumObjectSize = 0,
|
||||
minimumObjectCount = 0,
|
||||
@@ -55,7 +54,6 @@ class AnalysisConfig(
|
||||
headLimit = Int.MAX_VALUE,
|
||||
tailLimit = 0,
|
||||
minimumPaths = Int.MAX_VALUE,
|
||||
showFieldNames = showFieldNames,
|
||||
smartIndent = smartIndent,
|
||||
showSize = showSize)
|
||||
}
|
||||
@@ -64,28 +62,48 @@ class AnalysisConfig(
|
||||
class HistogramOptions(
|
||||
val includeByCount: Boolean = true,
|
||||
val includeBySize: Boolean = true,
|
||||
val includeSummary: Boolean = true,
|
||||
val classByCountLimit: Int = 50,
|
||||
val classBySizeLimit: Int = 10
|
||||
)
|
||||
|
||||
class DisposerOptions(
|
||||
val includeDisposerTree: Boolean = true,
|
||||
val includeDisposerTreeSummary: Boolean = true,
|
||||
val includeDisposedObjectsSummary: Boolean = true,
|
||||
val includeDisposedObjectsDetails: Boolean = true,
|
||||
val disposedObjectsDetailsTreeDisplayOptions: TreeDisplayOptions = TreeDisplayOptions(minimumSubgraphSize = 5_000_000,
|
||||
headLimit = 70,
|
||||
tailLimit = 5)
|
||||
tailLimit = 5),
|
||||
val disposerTreeSummaryOptions: DisposerTreeSummaryOptions = DisposerTreeSummaryOptions()
|
||||
)
|
||||
|
||||
class DisposerTreeSummaryOptions(
|
||||
val maxDepth: Int = 20,
|
||||
val headLimit: Int = 400,
|
||||
val nodeCutoff: Int = 20
|
||||
)
|
||||
|
||||
class TraverseOptions(
|
||||
val onlyStrongReferences: Boolean = false,
|
||||
val includeClassesAsRoots: Boolean = true
|
||||
val includeClassesAsRoots: Boolean = true,
|
||||
val includeDisposerRelationships: Boolean = true,
|
||||
val includeFieldInformation: Boolean = true
|
||||
)
|
||||
|
||||
class MetaInfoOptions(
|
||||
val include: Boolean = true
|
||||
)
|
||||
|
||||
class DominatorTreeOptions(
|
||||
val includeDominatorTree: Boolean = true,
|
||||
val maxDominatorIterations: Int = 2_000,
|
||||
val minNodeSize: Int = 100_000,
|
||||
val maxDepth: Int = 30,
|
||||
val headLimit: Int = 5_000,
|
||||
val diskSpaceThreshold: Long = 500_000_000L
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun getDefaultConfig(nominatedClasses: List<String>) = AnalysisConfig(PerClassOptions(nominatedClasses))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.util.IntList
|
||||
import com.intellij.diagnostic.hprof.util.UByteList
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
|
||||
|
||||
class AnalysisContext(
|
||||
@@ -32,4 +34,6 @@ class AnalysisContext(
|
||||
) {
|
||||
val classStore = navigator.classStore
|
||||
val disposedObjectsIDs = IntOpenHashSet()
|
||||
val disposerParentToChildren = Int2ObjectOpenHashMap<IntArrayList>()
|
||||
var disposerTreeObjectId = 0
|
||||
}
|
||||
+9
-7
@@ -1,13 +1,16 @@
|
||||
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.intellij.diagnostic.hprof.analysis
|
||||
|
||||
import com.intellij.diagnostic.hprof.util.AnalysisReport
|
||||
import com.intellij.diagnostic.hprof.util.ListProvider
|
||||
import com.intellij.ide.plugins.cl.PluginClassLoader
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
|
||||
class AnalyzeClassloaderReferencesGraph(analysisContext: AnalysisContext, val pluginId: String) : AnalyzeGraph(analysisContext) {
|
||||
override fun analyze(progress: ProgressIndicator): String {
|
||||
traverseInstanceGraph(progress)
|
||||
class AnalyzeClassloaderReferencesGraph(analysisContext: AnalysisContext, listProvider: ListProvider, val pluginId: String)
|
||||
: AnalyzeGraph(analysisContext, listProvider) {
|
||||
override fun analyze(progress: ProgressIndicator): AnalysisReport = AnalysisReport().apply {
|
||||
traverseInstanceGraph(progress, this)
|
||||
|
||||
val navigator = analysisContext.navigator
|
||||
for (l in 1..navigator.instanceCount) {
|
||||
@@ -19,15 +22,14 @@ class AnalyzeClassloaderReferencesGraph(analysisContext: AnalysisContext, val pl
|
||||
if (navigator.getStringInstanceFieldValue() == pluginId) {
|
||||
val objectId = analysisContext.parentList[l.toInt()]
|
||||
if (objectId == 0) {
|
||||
return ""
|
||||
return@apply
|
||||
}
|
||||
|
||||
val gcRootPathsTree = GCRootPathsTree(analysisContext, AnalysisConfig.TreeDisplayOptions.all(showFieldNames = true, showSize = false), null)
|
||||
val gcRootPathsTree = GCRootPathsTree(analysisContext, AnalysisConfig.TreeDisplayOptions.all(showSize = false), null)
|
||||
gcRootPathsTree.registerObject(l.toInt())
|
||||
return gcRootPathsTree.printTree()
|
||||
mainReport.append(gcRootPathsTree.printTree())
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
+389
-158
@@ -15,22 +15,29 @@
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.analysis
|
||||
|
||||
import com.intellij.util.ExceptionUtil
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Stack
|
||||
import java.util.function.LongConsumer
|
||||
import com.intellij.diagnostic.hprof.classstore.ClassDefinition
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.util.TreeNode
|
||||
import com.intellij.diagnostic.hprof.util.TreeVisualizer
|
||||
import com.intellij.diagnostic.hprof.util.TruncatingPrintBuffer
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toPaddedShortStringAsSize
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsCount
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsSize
|
||||
import com.intellij.diagnostic.hprof.util.TruncatingPrintBuffer
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList
|
||||
import it.unimi.dsi.fastutil.longs.LongList
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
|
||||
import it.unimi.dsi.fastutil.longs.*
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
|
||||
import java.util.*
|
||||
|
||||
internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
data class ClassGrouping(val childClass: ClassDefinition,
|
||||
val parentClass: ClassDefinition?,
|
||||
val rootClass: ClassDefinition)
|
||||
class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
|
||||
private var prepareException: Exception? = null
|
||||
|
||||
data class Grouping(val childClass: ClassDefinition,
|
||||
val parentClass: ClassDefinition?,
|
||||
val rootClass: ClassDefinition)
|
||||
|
||||
class InstanceStats {
|
||||
private val parentIds = LongArrayList()
|
||||
@@ -54,124 +61,342 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
)
|
||||
}
|
||||
|
||||
fun prepareDisposerTreeSection(): String {
|
||||
fun prepareDisposerChildren() {
|
||||
prepareException = null
|
||||
val result = analysisContext.disposerParentToChildren
|
||||
result.clear()
|
||||
|
||||
if (!analysisContext.classStore.containsClass("com.intellij.openapi.util.Disposer")) {
|
||||
return ""
|
||||
}
|
||||
val nav = analysisContext.navigator
|
||||
|
||||
val result = StringBuilder()
|
||||
|
||||
nav.goToStaticField("com.intellij.openapi.util.Disposer", "ourTree")
|
||||
assert(!nav.isNull())
|
||||
nav.goToInstanceField("com.intellij.openapi.util.ObjectTree", "myRootNode")
|
||||
val rootObjectNodeIds:LongList = getObjectNodeChildrenIds(nav)
|
||||
|
||||
val groupingToObjectStats = HashMap<ClassGrouping, InstanceStats>()
|
||||
val maxTreeDepth = 200
|
||||
val tooDeepObjectClasses = HashSet<ClassDefinition>()
|
||||
for (i in 0 until rootObjectNodeIds.size) {
|
||||
val rootObjectNodeId = rootObjectNodeIds.getLong(i)
|
||||
nav.goTo(rootObjectNodeId)
|
||||
if (nav.isNull()) continue
|
||||
val rootObjectId = nav.getInstanceFieldObjectId("com.intellij.openapi.util.ObjectNode", "myObject")
|
||||
val rootObjectClass = nav.getClassForObjectId(rootObjectId)
|
||||
nav.goTo(rootObjectNodeId)
|
||||
visitObjectTreeRecursively(nav, rootObjectNodeId, 0L, null, rootObjectId, rootObjectClass,
|
||||
tooDeepObjectClasses, maxTreeDepth, 0, groupingToObjectStats)
|
||||
}
|
||||
|
||||
TruncatingPrintBuffer(400, 0, result::appendLine).use { buffer ->
|
||||
groupingToObjectStats
|
||||
.entries
|
||||
.asSequence()
|
||||
.sortedByDescending { it.value.objectCount() }
|
||||
.groupBy { it.key.rootClass }
|
||||
.forEach { (rootClass, entries) ->
|
||||
buffer.println("Root: ${rootClass.name}")
|
||||
TruncatingPrintBuffer(100, 0, buffer::println).use { buffer ->
|
||||
entries.forEach { (classGrouping, instanceStats) ->
|
||||
printDisposerTreeReportLine(buffer, classGrouping, instanceStats)
|
||||
}
|
||||
}
|
||||
buffer.println()
|
||||
}
|
||||
}
|
||||
|
||||
if (tooDeepObjectClasses.size > 0) {
|
||||
result.appendLine("Skipped analysis of objects too deep in disposer tree:")
|
||||
tooDeepObjectClasses.forEach {
|
||||
result.appendLine(" * ${nav.classStore.getShortPrettyNameForClass(it)}")
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
// list of ids of ObjectNodes stored in the current ObjectNode.myChildren
|
||||
private fun getObjectNodeChildrenIds(nav: ObjectNavigator): LongList {
|
||||
nav.goToInstanceField("com.intellij.openapi.util.ObjectNode", "myChildren")
|
||||
if (nav.getClass().name == Collections.emptyList<Any>().javaClass.name) {
|
||||
// no children
|
||||
return LongList.of()
|
||||
}
|
||||
else {
|
||||
nav.goToInstanceField("com.intellij.util.SmartList", "myElem")
|
||||
if (nav.isNull()) return LongList.of()
|
||||
if (nav.getClass().isArray()) {
|
||||
return nav.getReferencesCopy()
|
||||
}
|
||||
else {
|
||||
// myElem is ObjectNode
|
||||
return LongList.of(nav.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visit ObjectTree starting with the ObjectNode pointed to by currentNodeId,
|
||||
// descending recursively through the "ObjectNode.myChildren" references,
|
||||
// gathering statistics into "groupingToObjectStats" along the way
|
||||
private fun visitObjectTreeRecursively(nav: ObjectNavigator,
|
||||
currentNodeId: Long,
|
||||
parentObjectId: Long,
|
||||
parentObjectClass: ClassDefinition?,
|
||||
rootObjectId: Long,
|
||||
rootObjectClass: ClassDefinition,
|
||||
tooDeepObjectClasses: HashSet<ClassDefinition>,
|
||||
maxTreeDepth: Int,
|
||||
currentTreeDepth: Int,
|
||||
groupingToObjectStats: HashMap<ClassGrouping, InstanceStats>) {
|
||||
nav.goTo(currentNodeId)
|
||||
if (nav.isNull()) return
|
||||
val currentObjectId = nav.getInstanceFieldObjectId("com.intellij.openapi.util.ObjectNode", "myObject")
|
||||
val currentObjectClass = nav.getClassForObjectId(currentObjectId)
|
||||
|
||||
groupingToObjectStats
|
||||
.getOrPut(ClassGrouping(currentObjectClass, parentObjectClass, rootObjectClass)) { InstanceStats() }
|
||||
.registerObject(parentObjectId, rootObjectId)
|
||||
|
||||
if (currentTreeDepth >= maxTreeDepth) {
|
||||
tooDeepObjectClasses.add(currentObjectClass)
|
||||
return
|
||||
}
|
||||
try {
|
||||
val nav = analysisContext.navigator
|
||||
|
||||
nav.goTo(currentNodeId)
|
||||
val childrenNodeIds = getObjectNodeChildrenIds(nav)
|
||||
for (i in 0 until childrenNodeIds.size) {
|
||||
val childNodeId = childrenNodeIds.getLong(i)
|
||||
visitObjectTreeRecursively(nav, childNodeId, currentObjectId, currentObjectClass, rootObjectId, rootObjectClass,
|
||||
tooDeepObjectClasses, maxTreeDepth, currentTreeDepth + 1, groupingToObjectStats)
|
||||
nav.goToStaticField("com.intellij.openapi.util.Disposer", "ourTree")
|
||||
analysisContext.disposerTreeObjectId = nav.id.toInt()
|
||||
|
||||
goToArrayOfDisposableObjectNodes(nav)
|
||||
nav.getReferencesCopy().forEach {
|
||||
if (it == 0L) return@forEach
|
||||
|
||||
nav.goTo(it)
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
val objectNodeParentId = nav.getInstanceFieldObjectId(null, "myParent")
|
||||
val childId = nav.getInstanceFieldObjectId(null, "myObject")
|
||||
nav.goTo(objectNodeParentId)
|
||||
|
||||
val parentId = if (nav.isNull()) {
|
||||
0L
|
||||
}
|
||||
else {
|
||||
nav.getInstanceFieldObjectId(null, "myObject")
|
||||
}
|
||||
|
||||
val childrenList = if (result.containsKey(parentId.toInt())) {
|
||||
result.get(parentId.toInt())
|
||||
}
|
||||
else {
|
||||
val list = IntArrayList()
|
||||
result.put(parentId.toInt(), list)
|
||||
list
|
||||
}
|
||||
childrenList.add(childId.toInt())
|
||||
}
|
||||
result.values.forEach(IntArrayList::trim)
|
||||
}
|
||||
catch (ex: Exception) {
|
||||
prepareException = ex
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToArrayOfDisposableObjectNodes(nav: ObjectNavigator) {
|
||||
nav.goToStaticField("com.intellij.openapi.util.Disposer", "ourTree")
|
||||
|
||||
analysisContext.disposerTreeObjectId = nav.id.toInt()
|
||||
|
||||
verifyClassIsObjectTree(nav.getClass())
|
||||
|
||||
if (nav.isNull()) {
|
||||
throw ObjectNavigator.NavigationException("Disposer.ourTree == null")
|
||||
}
|
||||
nav.goToInstanceField(null, "myObject2NodeMap")
|
||||
if (nav.getClass().name == "gnu.trove.THashMap") {
|
||||
nav.goToInstanceField("gnu.trove.THashMap", "_values")
|
||||
}
|
||||
else {
|
||||
nav.goToInstanceField("it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap", "value")
|
||||
}
|
||||
if (nav.isNull()) {
|
||||
throw ObjectNavigator.NavigationException("Collection of children is null")
|
||||
}
|
||||
if (!nav.getClass().isArray()) {
|
||||
throw ObjectNavigator.NavigationException("Invalid type of map values collection: ${nav.getClass().name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyClassIsObjectNode(clazzObjectTree: ClassDefinition) {
|
||||
if (clazzObjectTree.undecoratedName != "com.intellij.openapi.util.objectTree.ObjectNode" &&
|
||||
clazzObjectTree.undecoratedName != "com.intellij.openapi.util.ObjectNode") {
|
||||
throw ObjectNavigator.NavigationException("Wrong type, expected ObjectNode: ${clazzObjectTree.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyClassIsObjectTree(clazzObjectTree: ClassDefinition) {
|
||||
if (clazzObjectTree.undecoratedName != "com.intellij.openapi.util.objectTree.ObjectTree" &&
|
||||
clazzObjectTree.undecoratedName != "com.intellij.openapi.util.ObjectTree") {
|
||||
throw ObjectNavigator.NavigationException("Wrong type, expected ObjectTree: ${clazzObjectTree.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private class DisposerNode(val className: String) : TreeNode {
|
||||
var count = 0
|
||||
var subtreeSize = 0
|
||||
var filteredSubtreeSize = 0
|
||||
val children = HashMap<String, DisposerNode>()
|
||||
|
||||
fun equals(other: DisposerNode): Boolean = className == other.className
|
||||
|
||||
override fun equals(other: Any?): Boolean = other != null && other is DisposerNode && equals(other)
|
||||
override fun hashCode() = className.hashCode()
|
||||
override fun description(): String = "[$subtreeSize] $count $className"
|
||||
override fun children(): Collection<TreeNode> = children.values.sortedByDescending { it.subtreeSize }
|
||||
|
||||
fun addInstance() {
|
||||
count++
|
||||
}
|
||||
|
||||
fun getChildForClassName(name: String): DisposerNode = children.getOrPut(name, { DisposerNode(name) })
|
||||
}
|
||||
|
||||
private enum class SubTreeUpdaterOperation { PROCESS_CHILDREN, UPDATE_SIZE }
|
||||
|
||||
fun prepareDisposerTreeSummarySection(options: AnalysisConfig.DisposerTreeSummaryOptions): String = buildString {
|
||||
TruncatingPrintBuffer(options.headLimit, 0, this::appendLine).use { buffer ->
|
||||
if (!analysisContext.classStore.containsClass("com.intellij.openapi.util.Disposer")) {
|
||||
return@buildString
|
||||
}
|
||||
|
||||
prepareException?.let {
|
||||
buffer.println(ExceptionUtil.getThrowableText(it))
|
||||
return@buildString
|
||||
}
|
||||
|
||||
val nav = analysisContext.navigator
|
||||
val objectId2Children = Long2ObjectOpenHashMap<LongArrayList>()
|
||||
val topLevelObjectIds = LongArrayList()
|
||||
|
||||
// Build a map: object -> list of its disposable children
|
||||
// Collect top-level objects (i.e. have no parent)
|
||||
try {
|
||||
goToArrayOfDisposableObjectNodes(nav)
|
||||
|
||||
nav.getReferencesCopy().forEach { childId ->
|
||||
if (childId == 0L) return@forEach
|
||||
|
||||
nav.goTo(childId)
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
|
||||
val objectNodeParentId = nav.getInstanceFieldObjectId(null, "myParent")
|
||||
val objectId = nav.getInstanceFieldObjectId(null, "myObject")
|
||||
nav.goTo(objectNodeParentId)
|
||||
|
||||
if (!objectId2Children.containsKey(objectId))
|
||||
objectId2Children[objectId] = LongArrayList()
|
||||
|
||||
if (nav.isNull()) {
|
||||
topLevelObjectIds.add(objectId)
|
||||
}
|
||||
else {
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
val parentId = nav.getInstanceFieldObjectId(null, "myObject")
|
||||
|
||||
var children = objectId2Children.get(parentId)
|
||||
if (children == null) {
|
||||
children = LongArrayList()
|
||||
objectId2Children[parentId] = children
|
||||
}
|
||||
children.add(objectId)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
val rootNode = DisposerNode("<root>")
|
||||
|
||||
data class StackObject(val node: DisposerNode, val childrenIds: LongCollection)
|
||||
|
||||
val stack = Stack<StackObject>()
|
||||
stack.push(StackObject(rootNode, topLevelObjectIds))
|
||||
|
||||
while (!stack.empty()) {
|
||||
val (currentNode, childrenIds) = stack.pop()
|
||||
|
||||
val nodeToChildren = HashMap<DisposerNode, LongArrayList>()
|
||||
childrenIds.forEach(LongConsumer {
|
||||
val childClassName = nav.getClassForObjectId(it).name
|
||||
val childNode = currentNode.getChildForClassName(childClassName)
|
||||
childNode.addInstance()
|
||||
nodeToChildren.getOrPut(childNode, { LongArrayList() }).addAll(objectId2Children[it])
|
||||
})
|
||||
nodeToChildren.forEach { (node, children) -> stack.push(StackObject(node, children)) }
|
||||
}
|
||||
|
||||
// Update subtree size
|
||||
data class SubtreeSizeUpdateStackObject(val node: DisposerNode, val operation: SubTreeUpdaterOperation)
|
||||
|
||||
val nodeStack = Stack<SubtreeSizeUpdateStackObject>()
|
||||
nodeStack.push(SubtreeSizeUpdateStackObject(rootNode, SubTreeUpdaterOperation.PROCESS_CHILDREN))
|
||||
|
||||
while (!nodeStack.isEmpty()) {
|
||||
val (currentNode, operation) = nodeStack.pop()
|
||||
if (operation == SubTreeUpdaterOperation.PROCESS_CHILDREN) {
|
||||
currentNode.subtreeSize = currentNode.count
|
||||
currentNode.filteredSubtreeSize = currentNode.count
|
||||
nodeStack.push(SubtreeSizeUpdateStackObject(currentNode, SubTreeUpdaterOperation.UPDATE_SIZE))
|
||||
currentNode.children.values.forEach {
|
||||
nodeStack.push(SubtreeSizeUpdateStackObject(it, SubTreeUpdaterOperation.PROCESS_CHILDREN))
|
||||
}
|
||||
}
|
||||
else {
|
||||
assert(operation == SubTreeUpdaterOperation.UPDATE_SIZE)
|
||||
|
||||
currentNode.children.values.forEach { currentNode.subtreeSize += it.subtreeSize }
|
||||
currentNode.children.entries.removeIf { it.value.filteredSubtreeSize < options.nodeCutoff }
|
||||
currentNode.children.values.forEach { currentNode.filteredSubtreeSize += it.subtreeSize }
|
||||
}
|
||||
}
|
||||
val visualizer = TreeVisualizer()
|
||||
buffer.println("Cutoff: ${options.nodeCutoff}")
|
||||
buffer.println("Count of disposable objects: ${rootNode.subtreeSize}")
|
||||
buffer.println()
|
||||
rootNode.children().forEach {
|
||||
visualizer.visualizeTree(it, buffer, analysisContext.config.disposerOptions.disposerTreeSummaryOptions)
|
||||
buffer.println()
|
||||
}
|
||||
}
|
||||
catch (ex: Exception) {
|
||||
buffer.println(ExceptionUtil.getThrowableText(ex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareDisposerTreeSection(): String = buildString {
|
||||
if (!analysisContext.classStore.containsClass("com.intellij.openapi.util.Disposer")) {
|
||||
return@buildString
|
||||
}
|
||||
|
||||
prepareException?.let {
|
||||
appendLine(ExceptionUtil.getThrowableText(it))
|
||||
return@buildString
|
||||
}
|
||||
|
||||
val nav = analysisContext.navigator
|
||||
|
||||
try {
|
||||
goToArrayOfDisposableObjectNodes(nav)
|
||||
|
||||
val groupingToObjectStats = HashMap<Grouping, InstanceStats>()
|
||||
val maxTreeDepth = 200
|
||||
val tooDeepObjectClasses = HashSet<ClassDefinition>()
|
||||
nav.getReferencesCopy().forEach {
|
||||
if (it == 0L) return@forEach
|
||||
|
||||
nav.goTo(it)
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
val objectNodeParentId = nav.getInstanceFieldObjectId(null, "myParent")
|
||||
val objectNodeObjectId = nav.getInstanceFieldObjectId(null, "myObject")
|
||||
nav.goTo(objectNodeParentId)
|
||||
|
||||
val parentId =
|
||||
if (nav.isNull())
|
||||
0L
|
||||
else {
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
nav.getInstanceFieldObjectId(null, "myObject")
|
||||
}
|
||||
|
||||
val parentClass =
|
||||
if (parentId == 0L)
|
||||
null
|
||||
else {
|
||||
nav.goTo(parentId)
|
||||
nav.getClass()
|
||||
}
|
||||
|
||||
nav.goTo(objectNodeObjectId)
|
||||
val objectClass = nav.getClass()
|
||||
|
||||
val rootClass: ClassDefinition
|
||||
val rootId: Long
|
||||
|
||||
if (parentId == 0L) {
|
||||
rootClass = objectClass
|
||||
rootId = objectNodeObjectId
|
||||
}
|
||||
else {
|
||||
var rootObjectNodeId = objectNodeParentId
|
||||
var rootObjectId: Long
|
||||
var iterationCount = 0
|
||||
do {
|
||||
nav.goTo(rootObjectNodeId)
|
||||
verifyClassIsObjectNode(nav.getClass())
|
||||
rootObjectNodeId = nav.getInstanceFieldObjectId(null, "myParent")
|
||||
rootObjectId = nav.getInstanceFieldObjectId(null, "myObject")
|
||||
iterationCount++
|
||||
}
|
||||
while (rootObjectNodeId != 0L && iterationCount < maxTreeDepth)
|
||||
|
||||
if (iterationCount >= maxTreeDepth) {
|
||||
tooDeepObjectClasses.add(objectClass)
|
||||
rootId = parentId
|
||||
rootClass = parentClass!!
|
||||
}
|
||||
else {
|
||||
nav.goTo(rootObjectId)
|
||||
rootId = rootObjectId
|
||||
rootClass = nav.getClass()
|
||||
}
|
||||
}
|
||||
|
||||
groupingToObjectStats
|
||||
.getOrPut(Grouping(objectClass, parentClass, rootClass)) { InstanceStats() }
|
||||
.registerObject(parentId, rootId)
|
||||
}
|
||||
|
||||
TruncatingPrintBuffer(400, 0, this::appendLine).use { buffer ->
|
||||
groupingToObjectStats
|
||||
.entries
|
||||
.sortedByDescending { it.value.objectCount() }
|
||||
.groupBy { it.key.rootClass }
|
||||
.forEach { (rootClass, entries) ->
|
||||
buffer.println("Root: ${rootClass.name}")
|
||||
TruncatingPrintBuffer(100, 0, buffer::println).use { buffer ->
|
||||
entries.forEach { (mapping, groupedObjects) ->
|
||||
printDisposerTreeReportLine(buffer, mapping, groupedObjects)
|
||||
}
|
||||
}
|
||||
buffer.println()
|
||||
}
|
||||
}
|
||||
|
||||
if (tooDeepObjectClasses.size > 0) {
|
||||
appendLine("Skipped analysis of objects too deep in disposer tree:")
|
||||
tooDeepObjectClasses.forEach {
|
||||
appendLine(" * ${nav.classStore.getShortPrettyNameForClass(it)}")
|
||||
}
|
||||
}
|
||||
} catch (ex : Exception) {
|
||||
appendLine(ExceptionUtil.getThrowableText(ex))
|
||||
}
|
||||
}
|
||||
|
||||
private fun printDisposerTreeReportLine(buffer: TruncatingPrintBuffer,
|
||||
classGrouping: ClassGrouping,
|
||||
instanceStats: InstanceStats) {
|
||||
val (sourceClass, parentClass, rootClass) = classGrouping
|
||||
mapping: Grouping,
|
||||
groupedObjects: InstanceStats) {
|
||||
val (sourceClass, parentClass, rootClass) = mapping
|
||||
val nav = analysisContext.navigator
|
||||
|
||||
val objectCount = instanceStats.objectCount()
|
||||
val parentCount = instanceStats.parentCount()
|
||||
val objectCount = groupedObjects.objectCount()
|
||||
val parentCount = groupedObjects.parentCount()
|
||||
|
||||
// Ignore 1-1 mappings
|
||||
if (parentClass != null && objectCount == parentCount)
|
||||
@@ -183,7 +408,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
}
|
||||
else {
|
||||
val parentClassName = nav.classStore.getShortPrettyNameForClass(parentClass)
|
||||
val rootCount = instanceStats.rootCount()
|
||||
val rootCount = groupedObjects.rootCount()
|
||||
if (rootClass != parentClass || rootCount != parentCount) {
|
||||
parentString = "<-- $parentCount $parentClassName [...] $rootCount"
|
||||
}
|
||||
@@ -199,45 +424,53 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
val disposedObjectsIDs = analysisContext.disposedObjectsIDs
|
||||
disposedObjectsIDs.clear()
|
||||
|
||||
val nav = analysisContext.navigator
|
||||
val parentList = analysisContext.parentList
|
||||
|
||||
if (!nav.classStore.containsClass("com.intellij.openapi.util.Disposer")) {
|
||||
if (prepareException != null) {
|
||||
return
|
||||
}
|
||||
|
||||
nav.goToStaticField("com.intellij.openapi.util.Disposer", "ourTree")
|
||||
assert(!nav.isNull())
|
||||
nav.goToInstanceField("com.intellij.openapi.util.ObjectTree", "myDisposedObjects")
|
||||
nav.goToInstanceField("com.intellij.util.containers.WeakHashMap", "myMap")
|
||||
nav.goToInstanceField("com.intellij.util.containers.RefHashMap\$MyMap", "keys")
|
||||
val weakKeyClass = nav.classStore["com.intellij.util.containers.WeakHashMap\$WeakKey"]
|
||||
try {
|
||||
val nav = analysisContext.navigator
|
||||
val parentList = analysisContext.parentList
|
||||
|
||||
nav.getReferencesCopy().forEach {
|
||||
if (it == 0L) {
|
||||
return@forEach
|
||||
}
|
||||
nav.goTo(it, ObjectNavigator.ReferenceResolution.ALL_REFERENCES)
|
||||
if (nav.getClass() != weakKeyClass) {
|
||||
return@forEach
|
||||
}
|
||||
nav.goToInstanceField("com.intellij.util.containers.WeakHashMap\$WeakKey", "referent")
|
||||
if (nav.id == 0L) {
|
||||
return@forEach
|
||||
if (!nav.classStore.containsClass("com.intellij.openapi.util.Disposer")) {
|
||||
return
|
||||
}
|
||||
|
||||
val leakId = nav.id.toInt()
|
||||
if (parentList[leakId] == 0) {
|
||||
// If there is no parent, then the object does not have a strong-reference path to GC root
|
||||
return@forEach
|
||||
nav.goToStaticField("com.intellij.openapi.util.Disposer", "ourTree")
|
||||
if (nav.isNull()) {
|
||||
throw ObjectNavigator.NavigationException("ourTree is null")
|
||||
}
|
||||
disposedObjectsIDs.add(leakId)
|
||||
verifyClassIsObjectTree(nav.getClass())
|
||||
nav.goToInstanceField(null, "myDisposedObjects")
|
||||
nav.goToInstanceField("com.intellij.util.containers.WeakHashMap", "myMap")
|
||||
nav.goToInstanceField("com.intellij.util.containers.RefHashMap\$MyMap", "key")
|
||||
val weakKeyClass = nav.classStore.getClassIfExists("com.intellij.util.containers.WeakHashMap\$WeakKey")
|
||||
|
||||
nav.getReferencesCopy().forEach {
|
||||
if (it == 0L) {
|
||||
return@forEach
|
||||
}
|
||||
nav.goTo(it, ObjectNavigator.ReferenceResolution.ALL_REFERENCES)
|
||||
if (nav.getClass() != weakKeyClass) {
|
||||
return@forEach
|
||||
}
|
||||
nav.goToInstanceField("com.intellij.util.containers.WeakHashMap\$WeakKey", "referent")
|
||||
if (nav.id == 0L) return@forEach
|
||||
|
||||
val leakId = nav.id.toInt()
|
||||
if (parentList.get(leakId) == 0) {
|
||||
// If there is no parent, then the object does not have a strong-reference path to GC root
|
||||
return@forEach
|
||||
}
|
||||
disposedObjectsIDs.add(leakId)
|
||||
}
|
||||
}
|
||||
catch (navEx: ObjectNavigator.NavigationException) {
|
||||
prepareException = navEx
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareDisposedObjectsSection(): String {
|
||||
val result = StringBuilder()
|
||||
|
||||
fun prepareDisposedObjectsSection(): String = buildString {
|
||||
val leakedInstancesByClass = HashMap<ClassDefinition, LongList>()
|
||||
val countByClass = Object2IntOpenHashMap<ClassDefinition>()
|
||||
var totalCount = 0
|
||||
@@ -268,7 +501,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
|
||||
if (disposerOptions.includeDisposedObjectsSummary) {
|
||||
// Print counts of disposed-but-strong-referenced objects
|
||||
TruncatingPrintBuffer(100, 0, result::appendln).use { buffer ->
|
||||
TruncatingPrintBuffer(100, 0, this::appendLine).use { buffer ->
|
||||
buffer.println("Count of disposed-but-strong-referenced objects: $totalCount")
|
||||
entries
|
||||
.sortedByDescending { it.value }
|
||||
@@ -278,13 +511,12 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
buffer.println(" ${entry.value} ${entry.key.prettyName}")
|
||||
}
|
||||
}
|
||||
result.appendln()
|
||||
appendLine()
|
||||
}
|
||||
|
||||
val disposedTree = GCRootPathsTree(analysisContext, AnalysisConfig.TreeDisplayOptions.all(), null)
|
||||
val iterator = disposedObjectsIDs.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
disposedTree.registerObject(iterator.nextInt())
|
||||
for (disposedObjectsID in disposedObjectsIDs) {
|
||||
disposedTree.registerObject(disposedObjectsID)
|
||||
}
|
||||
|
||||
val disposedDominatorNodesByClass = disposedTree.getDisposedDominatorNodes()
|
||||
@@ -305,7 +537,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
}
|
||||
|
||||
if (disposerOptions.includeDisposedObjectsSummary) {
|
||||
TruncatingPrintBuffer(30, 0, result::appendln).use { buffer ->
|
||||
TruncatingPrintBuffer(30, 0, this::appendLine).use { buffer ->
|
||||
buffer.println("Disposed-but-strong-referenced dominator object count: $allDominatorsCount")
|
||||
buffer.println(
|
||||
"Disposed-but-strong-referenced dominator sub-graph size: ${toShortStringAsSize(allDominatorsSubgraphSize)}")
|
||||
@@ -316,7 +548,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
" ${toPaddedShortStringAsSize(entry.size)} - ${toShortStringAsCount(entry.count)} ${entry.classDefinition.name}")
|
||||
}
|
||||
}
|
||||
result.appendln()
|
||||
appendLine()
|
||||
}
|
||||
|
||||
if (disposerOptions.includeDisposedObjectsDetails) {
|
||||
@@ -325,7 +557,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
disposedDominatorClassSizeList
|
||||
)
|
||||
|
||||
TruncatingPrintBuffer(700, 0, result::appendln).use { buffer ->
|
||||
TruncatingPrintBuffer(700, 0, this::appendLine).use { buffer ->
|
||||
instancesListInOrder
|
||||
.forEach { instances ->
|
||||
nav.goTo(instances.getLong(0))
|
||||
@@ -339,7 +571,6 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun getInstancesListInPriorityOrder(
|
||||
@@ -363,7 +594,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
// Alternate between class with most instances leaked and class with most bytes leaked
|
||||
|
||||
// Prepare instance count class list by priority
|
||||
val classOrderByInstanceCount = ArrayDeque<ClassDefinition>(
|
||||
val classOrderByInstanceCount = ArrayDeque(
|
||||
classToLeakedIdsListCopy
|
||||
.entries
|
||||
.sortedByDescending { it.value.size }
|
||||
@@ -371,7 +602,7 @@ internal class AnalyzeDisposer(private val analysisContext: AnalysisContext) {
|
||||
)
|
||||
|
||||
// Prepare dominator bytes count class list by priority
|
||||
val classOrderByByteCount = ArrayDeque<ClassDefinition>(
|
||||
val classOrderByByteCount = ArrayDeque(
|
||||
disposedDominatorReportEntries
|
||||
.sortedByDescending { it.size }
|
||||
.map { it.classDefinition }
|
||||
|
||||
+621
-70
@@ -20,25 +20,39 @@ import com.intellij.diagnostic.DiagnosticBundle
|
||||
import com.intellij.diagnostic.hprof.classstore.ClassDefinition
|
||||
import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.util.*
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.sectionHeader
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toPaddedShortStringAsCount
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toPaddedShortStringAsSize
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsCount
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsSize
|
||||
import com.intellij.diagnostic.hprof.util.PartialProgressIndicator
|
||||
import com.intellij.diagnostic.hprof.visitors.HistogramVisitor
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import it.unimi.dsi.fastutil.ints.*
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
|
||||
import it.unimi.dsi.fastutil.ints.IntSet
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayList
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
fun analyzeGraph(analysisContext: AnalysisContext, progress: ProgressIndicator): String {
|
||||
return AnalyzeGraph(analysisContext).analyze(progress)
|
||||
fun analyzeGraph(analysisContext: AnalysisContext, listProvider: ListProvider, progress: ProgressIndicator): String {
|
||||
return AnalyzeGraph(analysisContext, listProvider).analyze(progress).mainReport.toString()
|
||||
}
|
||||
|
||||
open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
open class AnalyzeGraph(protected val analysisContext: AnalysisContext, private val listProvider: ListProvider) {
|
||||
|
||||
private val unreachableDisposableObjects = IntArrayList()
|
||||
private var strongRefHistogram: Histogram? = null
|
||||
private var softWeakRefHistogram: Histogram? = null
|
||||
private var traverseReport: String? = null
|
||||
private var dominatorFlameGraph: String? = null
|
||||
|
||||
private val parentList = analysisContext.parentList
|
||||
|
||||
@@ -52,60 +66,71 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
|
||||
private val nominatedInstances = HashMap<ClassDefinition, IntSet>()
|
||||
|
||||
open fun analyze(progress: ProgressIndicator): String {
|
||||
val sb = StringBuilder()
|
||||
|
||||
open fun analyze(progress: ProgressIndicator): AnalysisReport = AnalysisReport().apply {
|
||||
val includePerClassSection = analysisContext.config.perClassOptions.classNames.isNotEmpty()
|
||||
|
||||
val traverseProgress =
|
||||
if (includePerClassSection) PartialProgressIndicator(progress, 0.0, 0.5) else progress
|
||||
|
||||
traverseInstanceGraph(traverseProgress)
|
||||
|
||||
val analyzeDisposer = AnalyzeDisposer(analysisContext)
|
||||
analyzeDisposer.prepareDisposerChildren()
|
||||
|
||||
traverseInstanceGraph(traverseProgress, this)
|
||||
|
||||
analyzeDisposer.computeDisposedObjectsIDs()
|
||||
|
||||
// Histogram section
|
||||
val histogramOptions = analysisContext.config.histogramOptions
|
||||
if (histogramOptions.includeByCount || histogramOptions.includeBySize) {
|
||||
sb.appendLine(sectionHeader("Histogram"))
|
||||
sb.append(prepareHistogramSection())
|
||||
mainReport.appendLine(sectionHeader("Histogram"))
|
||||
mainReport.append(prepareHistogramSection())
|
||||
}
|
||||
if (histogramOptions.includeSummary) {
|
||||
mainReport.appendLine(sectionHeader("Heap summary"))
|
||||
mainReport.append(traverseReport)
|
||||
}
|
||||
|
||||
// Per-class section
|
||||
if (includePerClassSection) {
|
||||
val perClassProgress = PartialProgressIndicator(progress, 0.5, 0.5)
|
||||
sb.appendLine(sectionHeader("Instances of each nominated class"))
|
||||
sb.append(preparePerClassSection(perClassProgress))
|
||||
mainReport.appendLine(sectionHeader("Instances of each nominated class"))
|
||||
mainReport.append(preparePerClassSection(perClassProgress))
|
||||
}
|
||||
|
||||
// Disposer sections
|
||||
if (config.disposerOptions.includeDisposerTree) {
|
||||
sb.appendLine(sectionHeader("Disposer tree"))
|
||||
sb.append(analyzeDisposer.prepareDisposerTreeSection())
|
||||
mainReport.appendLine(sectionHeader("Disposer tree"))
|
||||
mainReport.append(analyzeDisposer.prepareDisposerTreeSection())
|
||||
}
|
||||
if (config.disposerOptions.includeDisposerTreeSummary) {
|
||||
mainReport.appendLine(sectionHeader("Disposer tree summary"))
|
||||
mainReport.append(analyzeDisposer.prepareDisposerTreeSummarySection(config.disposerOptions.disposerTreeSummaryOptions))
|
||||
}
|
||||
if (config.disposerOptions.includeDisposedObjectsSummary || config.disposerOptions.includeDisposedObjectsDetails) {
|
||||
sb.appendLine(sectionHeader("Disposed objects"))
|
||||
sb.append(analyzeDisposer.prepareDisposedObjectsSection())
|
||||
mainReport.appendLine(sectionHeader("Disposed objects"))
|
||||
mainReport.append(analyzeDisposer.prepareDisposedObjectsSection())
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
// Dominator tree flame graph
|
||||
if (config.dominatorTreeOptions.includeDominatorTree) {
|
||||
mainReport.appendLine(sectionHeader("Dominator tree flame graph"))
|
||||
mainReport.append(dominatorFlameGraph)
|
||||
}
|
||||
}
|
||||
|
||||
private fun preparePerClassSection(progress: PartialProgressIndicator): String {
|
||||
val sb = StringBuilder()
|
||||
private fun preparePerClassSection(progress: PartialProgressIndicator) = buildString {
|
||||
val histogram = analysisContext.histogram
|
||||
val perClassOptions = analysisContext.config.perClassOptions
|
||||
|
||||
if (perClassOptions.includeClassList) {
|
||||
sb.appendLine("Nominated classes:")
|
||||
appendLine("Nominated classes:")
|
||||
perClassOptions.classNames.forEach { name ->
|
||||
val (classDefinition, totalInstances, totalBytes) =
|
||||
histogram.entries.find { entry -> entry.classDefinition.name == name } ?: return@forEach
|
||||
val prettyName = classDefinition.prettyName
|
||||
sb.appendLine(" --> [${toShortStringAsCount(totalInstances)}/${toShortStringAsSize(totalBytes)}] " + prettyName)
|
||||
appendLine(" --> [${toShortStringAsCount(totalInstances)}/${toShortStringAsSize(totalBytes)}] " + prettyName)
|
||||
}
|
||||
sb.appendLine()
|
||||
appendLine()
|
||||
}
|
||||
|
||||
val nav = analysisContext.navigator
|
||||
@@ -118,22 +143,20 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
progress.fraction = counter.toDouble() / nominatedInstances.size
|
||||
progress.text2 = DiagnosticBundle.message("hprof.analysis.progress", set.size, classDefinition.prettyName)
|
||||
stopwatch.reset().start()
|
||||
sb.appendLine("CLASS: ${classDefinition.prettyName} (${set.size} objects)")
|
||||
appendLine("CLASS: ${classDefinition.prettyName} (${set.size} objects)")
|
||||
val referenceRegistry = GCRootPathsTree(analysisContext, perClassOptions.treeDisplayOptions, classDefinition)
|
||||
set.forEach { objectId ->
|
||||
referenceRegistry.registerObject(objectId)
|
||||
}
|
||||
set.clear()
|
||||
sb.append(referenceRegistry.printTree())
|
||||
append(referenceRegistry.printTree())
|
||||
if (config.metaInfoOptions.include) {
|
||||
sb.appendLine("Report for ${classDefinition.prettyName} created in $stopwatch")
|
||||
appendLine("Report for ${classDefinition.prettyName} created in $stopwatch")
|
||||
}
|
||||
sb.appendLine()
|
||||
appendLine()
|
||||
counter++
|
||||
}
|
||||
progress.fraction = 1.0
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun prepareHistogramSection(): String {
|
||||
@@ -158,6 +181,7 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
enum class WalkGraphPhase {
|
||||
StrongReferencesNonLocalVariables,
|
||||
StrongReferencesLocalVariables,
|
||||
DisposerTree,
|
||||
SoftReferences,
|
||||
WeakReferences,
|
||||
CleanerFinalizerReferences,
|
||||
@@ -166,8 +190,11 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
|
||||
private val config = analysisContext.config
|
||||
|
||||
protected fun traverseInstanceGraph(progress: ProgressIndicator): String {
|
||||
val result = StringBuilder()
|
||||
protected fun traverseInstanceGraph(progress: ProgressIndicator, report: AnalysisReport) {
|
||||
val traverseOptions = config.traverseOptions
|
||||
val onlyStrongReferences = traverseOptions.onlyStrongReferences
|
||||
val includeDisposerRelationships = traverseOptions.includeDisposerRelationships
|
||||
val includeFieldInformation = traverseOptions.includeFieldInformation
|
||||
|
||||
val nav = analysisContext.navigator
|
||||
val classStore = analysisContext.classStore
|
||||
@@ -180,7 +207,7 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
|
||||
val nominatedClassNames = config.perClassOptions.classNames
|
||||
nominatedClassNames.forEach {
|
||||
nominatedInstances.put(classStore.get(it), IntOpenHashSet())
|
||||
nominatedInstances[classStore[it]] = IntOpenHashSet()
|
||||
}
|
||||
|
||||
progress.text2 = DiagnosticBundle.message("analyze.graph.progress.details.collect.roots")
|
||||
@@ -217,12 +244,8 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
}
|
||||
|
||||
toVisit.addAll(rootsSet)
|
||||
rootsSet.clear()
|
||||
rootsSet.trim()
|
||||
|
||||
var leafCounter = 0
|
||||
result.appendLine("Roots count: ${toVisit.size}")
|
||||
result.appendLine("Classes count: ${classStore.size()}")
|
||||
|
||||
progress.text2 = DiagnosticBundle.message("analyze.graph.progress.details.traversing.instance.graph")
|
||||
|
||||
@@ -238,6 +261,7 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
var visitedCount = 0
|
||||
var strongRefVisitedCount = 0
|
||||
var softWeakVisitedCount = 0
|
||||
var edgeCount = 0
|
||||
|
||||
var finalizableBytes = 0L
|
||||
var softBytes = 0L
|
||||
@@ -248,11 +272,18 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
val cleanerObjects = IntArrayList()
|
||||
val sunMiscCleanerClass = classStore.getClassIfExists("sun.misc.Cleaner")
|
||||
val finalizerClass = classStore.getClassIfExists("java.lang.ref.Finalizer")
|
||||
val onlyStrongReferences = config.traverseOptions.onlyStrongReferences
|
||||
|
||||
while (!toVisit.isEmpty) {
|
||||
for (i in 0 until toVisit.size) {
|
||||
val id = toVisit.getInt(i)
|
||||
val id = toVisit[i]
|
||||
|
||||
// Disposer.ourTree is only visited during DisposerTree phase to give opportunity for
|
||||
if (includeDisposerRelationships &&
|
||||
id == analysisContext.disposerTreeObjectId &&
|
||||
phase < WalkGraphPhase.DisposerTree) {
|
||||
continue
|
||||
}
|
||||
|
||||
nav.goTo(id.toLong(), ObjectNavigator.ReferenceResolution.ALL_REFERENCES)
|
||||
val currentObjectClass = nav.getClass()
|
||||
|
||||
@@ -272,41 +303,64 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
nav.copyReferencesTo(references)
|
||||
val currentObjectIsArray = currentObjectClass.isArray()
|
||||
|
||||
// Postpone any soft references encountered before the phase that handles them
|
||||
if (phase < WalkGraphPhase.SoftReferences && nav.getSoftReferenceId() != 0L) {
|
||||
// Postpone soft references
|
||||
if (!onlyStrongReferences) {
|
||||
softReferenceIdToParentMap.put(nav.getSoftReferenceId().toInt(), id)
|
||||
}
|
||||
references[nav.getSoftWeakReferenceIndex()] = 0L
|
||||
}
|
||||
|
||||
// Postpone any weak references encountered before the phase that handles them
|
||||
if (phase < WalkGraphPhase.WeakReferences && nav.getWeakReferenceId() != 0L) {
|
||||
// Postpone weak references
|
||||
if (!onlyStrongReferences) {
|
||||
weakReferenceIdToParentMap.put(nav.getWeakReferenceId().toInt(), id)
|
||||
}
|
||||
references[nav.getSoftWeakReferenceIndex()] = 0L
|
||||
}
|
||||
|
||||
val size = nav.getObjectSize()
|
||||
val nonDisposerReferences = references.size
|
||||
|
||||
// Inline children from the disposer tree
|
||||
if (includeDisposerRelationships && analysisContext.disposerParentToChildren.contains(id)) {
|
||||
if (phase >= WalkGraphPhase.DisposerTree) {
|
||||
unreachableDisposableObjects.add(id)
|
||||
}
|
||||
analysisContext.disposerParentToChildren[id].forEach {
|
||||
references.add(it.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
for (j in 0 until references.size) {
|
||||
val referenceId = references.getLong(j).toInt()
|
||||
|
||||
if (referenceId != 0) edgeCount++
|
||||
if (addIdToListAndSetParentIfOrphan(toVisit2, referenceId, id)) {
|
||||
if (!currentObjectIsArray && j <= 254) {
|
||||
refIndexList[referenceId] = j + 1
|
||||
if (includeFieldInformation) {
|
||||
refIndexList.set(referenceId, when {
|
||||
currentObjectIsArray -> RefIndexUtil.ARRAY_ELEMENT
|
||||
j >= nonDisposerReferences -> RefIndexUtil.DISPOSER_CHILD
|
||||
j < RefIndexUtil.MAX_FIELD_INDEX -> j + 1
|
||||
else -> RefIndexUtil.FIELD_OMITTED // Too many reference fields
|
||||
})
|
||||
}
|
||||
isLeaf = false
|
||||
}
|
||||
}
|
||||
|
||||
// Ordered list of visited nodes. Parent is always before its children.
|
||||
visitedList[visitedCount++] = id
|
||||
val size = nav.getObjectSize()
|
||||
|
||||
// Store size of the object. Later pass will update the size by adding sizes of children
|
||||
// Size in DWORDs to support graph sizes up to 10GB.
|
||||
var sizeDivBy4 = (size + 3) / 4
|
||||
if (sizeDivBy4 == 0) sizeDivBy4 = 1
|
||||
sizesList[id] = sizeDivBy4
|
||||
sizesList.set(id, sizeDivBy4)
|
||||
|
||||
// Update histogram (separately for Strong-references and other reachable objects)
|
||||
var histogramEntries: HashMap<ClassDefinition, HistogramVisitor.InternalHistogramEntry>
|
||||
if (phase == WalkGraphPhase.StrongReferencesNonLocalVariables || phase == WalkGraphPhase.StrongReferencesLocalVariables) {
|
||||
if (phase == WalkGraphPhase.StrongReferencesNonLocalVariables || phase == WalkGraphPhase.StrongReferencesLocalVariables ||
|
||||
phase == WalkGraphPhase.DisposerTree) {
|
||||
histogramEntries = strongRefHistogramEntries
|
||||
if (isLeaf) {
|
||||
leafCounter++
|
||||
@@ -316,12 +370,8 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
else {
|
||||
histogramEntries = reachableNonStrongHistogramEntries
|
||||
when (phase) {
|
||||
WalkGraphPhase.CleanerFinalizerReferences -> {
|
||||
finalizableBytes += size
|
||||
}
|
||||
WalkGraphPhase.SoftReferences -> {
|
||||
softBytes += size
|
||||
}
|
||||
WalkGraphPhase.CleanerFinalizerReferences -> finalizableBytes += size
|
||||
WalkGraphPhase.SoftReferences -> softBytes += size
|
||||
else -> {
|
||||
assert(phase == WalkGraphPhase.WeakReferences)
|
||||
weakBytes += size
|
||||
@@ -333,13 +383,17 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
HistogramVisitor.InternalHistogramEntry(currentObjectClass)
|
||||
}.addInstance(size.toLong())
|
||||
}
|
||||
|
||||
// Update process
|
||||
progress.fraction = (1.0 * visitedInstancesCount / nav.instanceCount)
|
||||
|
||||
// Prepare next level of objects for processing
|
||||
toVisit.clear()
|
||||
val tmp = toVisit
|
||||
toVisit = toVisit2
|
||||
toVisit2 = tmp
|
||||
|
||||
// Handle state transitions
|
||||
// If no more object to visit at this phase, transition to the next
|
||||
while (toVisit.size == 0 && phase != WalkGraphPhase.Finished) {
|
||||
// Next state
|
||||
phase = WalkGraphPhase.values()[phase.ordinal + 1]
|
||||
@@ -348,39 +402,73 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
WalkGraphPhase.StrongReferencesLocalVariables ->
|
||||
frameRootsSet.forEach { id ->
|
||||
addIdToListAndSetParentIfOrphan(toVisit, id, id)
|
||||
true
|
||||
}
|
||||
WalkGraphPhase.CleanerFinalizerReferences -> {
|
||||
toVisit.addAll(cleanerObjects)
|
||||
cleanerObjects.clear()
|
||||
}
|
||||
WalkGraphPhase.SoftReferences -> {
|
||||
for (entry in softReferenceIdToParentMap.int2IntEntrySet().fastIterator()) {
|
||||
addIdToListAndSetParentIfOrphan(toVisit, entry.intKey, entry.intValue)
|
||||
for (entry in softReferenceIdToParentMap.int2IntEntrySet()) {
|
||||
val softId = entry.intKey
|
||||
val parentId = entry.intValue
|
||||
if (addIdToListAndSetParentIfOrphan(toVisit, softId, parentId)) {
|
||||
refIndexList[softId] = RefIndexUtil.SOFT_REFERENCE
|
||||
}
|
||||
}
|
||||
// No need to store the list anymore
|
||||
softReferenceIdToParentMap.clear()
|
||||
softReferenceIdToParentMap.trim()
|
||||
}
|
||||
WalkGraphPhase.WeakReferences -> {
|
||||
for (entry in weakReferenceIdToParentMap.int2IntEntrySet().fastIterator()) {
|
||||
addIdToListAndSetParentIfOrphan(toVisit, entry.intKey, entry.intValue)
|
||||
weakReferenceIdToParentMap.int2IntEntrySet().forEach {
|
||||
val weakId = it.intKey
|
||||
val parentId = it.intValue
|
||||
if (addIdToListAndSetParentIfOrphan(toVisit, weakId, parentId)) {
|
||||
refIndexList[weakId] = RefIndexUtil.WEAK_REFERENCE
|
||||
}
|
||||
}
|
||||
// No need to store the list anymore
|
||||
weakReferenceIdToParentMap.clear()
|
||||
weakReferenceIdToParentMap.trim()
|
||||
}
|
||||
WalkGraphPhase.DisposerTree -> {
|
||||
if (analysisContext.disposerTreeObjectId != 0) {
|
||||
toVisit.add(analysisContext.disposerTreeObjectId)
|
||||
}
|
||||
}
|
||||
else -> Unit // No work for other state transitions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.dominatorTreeOptions.includeDominatorTree) {
|
||||
val usableDiskSpace = File(FileUtil.getTempDirectory()).usableSpace
|
||||
if (usableDiskSpace - estimateDominatorTempFilesSize(visitedCount, edgeCount) > config.dominatorTreeOptions.diskSpaceThreshold) {
|
||||
try {
|
||||
rootsSet.addAll(frameRootsSet)
|
||||
computeDominatorFlameGraph(nav, rootsSet, sizesList, edgeCount, report)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
val baos = ByteArrayOutputStream()
|
||||
val pw = PrintWriter(baos)
|
||||
e.printStackTrace(pw)
|
||||
pw.flush()
|
||||
dominatorFlameGraph = baos.toString()
|
||||
}
|
||||
}
|
||||
else {
|
||||
dominatorFlameGraph = "Omitted due to low disk space"
|
||||
}
|
||||
}
|
||||
rootsSet.clear()
|
||||
|
||||
// Assert that any postponed objects have been handled
|
||||
assert(cleanerObjects.isEmpty)
|
||||
assert(softReferenceIdToParentMap.isEmpty())
|
||||
assert(weakReferenceIdToParentMap.isEmpty())
|
||||
|
||||
result.appendLine("Finalizable size: ${toShortStringAsSize(finalizableBytes)}")
|
||||
result.appendLine("Soft-reachable size: ${toShortStringAsSize(softBytes)}")
|
||||
result.appendLine("Weak-reachable size: ${toShortStringAsSize(weakBytes)}")
|
||||
|
||||
// Histograms are accessible publicly after traversal is complete
|
||||
strongRefHistogram = Histogram(
|
||||
strongRefHistogramEntries
|
||||
.values
|
||||
@@ -395,8 +483,11 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
.sortedByDescending { it.totalInstances },
|
||||
softWeakVisitedCount.toLong())
|
||||
|
||||
// Update size field in non-leaves to reflect the size of the whole subtree. Right after traversal
|
||||
// the size field is initialized to the size of the given object.
|
||||
// Traverses objects in the reverse order, so a child is always visited before its parent.
|
||||
// This assures size field is correctly set for a given before its added to the size field of the parent.
|
||||
val stopwatchUpdateSizes = Stopwatch.createStarted()
|
||||
// Update sizes for non-leaves
|
||||
var index = visitedCount - 1
|
||||
while (index >= 0) {
|
||||
val id = visitedList[index]
|
||||
@@ -408,12 +499,470 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
}
|
||||
stopwatchUpdateSizes.stop()
|
||||
|
||||
if (config.metaInfoOptions.include) {
|
||||
result.appendLine("Analysis completed! Visited instances: $visitedInstancesCount, time: $stopwatch")
|
||||
result.appendLine("Update sizes time: $stopwatchUpdateSizes")
|
||||
result.appendLine("Leaves found: $leafCounter")
|
||||
traverseReport = buildString {
|
||||
if (config.metaInfoOptions.include) {
|
||||
appendLine("Analysis completed! Visited instances: $visitedInstancesCount, time: $stopwatch")
|
||||
appendLine("Update sizes time: $stopwatchUpdateSizes")
|
||||
appendLine("Leaves found: $leafCounter")
|
||||
}
|
||||
|
||||
appendLine("Class count: ${classStore.size()}")
|
||||
|
||||
// Adds summary of object count by reachability
|
||||
appendLine("Finalizable size: ${toShortStringAsSize(finalizableBytes)}")
|
||||
appendLine("Soft-reachable size: ${toShortStringAsSize(softBytes)}")
|
||||
appendLine("Weak-reachable size: ${toShortStringAsSize(weakBytes)}")
|
||||
appendLine("Reachable only from disposer tree: ${unreachableDisposableObjects.size}")
|
||||
TruncatingPrintBuffer(10, 0, this::appendLine).use { buffer ->
|
||||
val unreachableChildren = IntOpenHashSet()
|
||||
unreachableDisposableObjects.forEach { id ->
|
||||
analysisContext.disposerParentToChildren[id]?.let { unreachableChildren.addAll(it) }
|
||||
}
|
||||
unreachableDisposableObjects.forEach { id ->
|
||||
if (unreachableChildren.contains(id)) {
|
||||
return@forEach
|
||||
}
|
||||
buffer.println(" * ${nav.getClassForObjectId(id.toLong()).name} (${toShortStringAsSize(sizesList.get(id).toLong() * 4)})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun estimateDominatorTempFilesSize(objectCount: Int, edgeCount: Int): Long {
|
||||
return 20L * objectCount + 10L * edgeCount
|
||||
}
|
||||
|
||||
private fun computeDominatorFlameGraph(nav: ObjectNavigator, rootsSet: IntSet, sizesList: IntList, edgeCount: Int, report: AnalysisReport) {
|
||||
val totalStopwatch = Stopwatch.createUnstarted()
|
||||
val postorderStopwatch = Stopwatch.createUnstarted()
|
||||
val incomingEdgesStopwatch = Stopwatch.createUnstarted()
|
||||
val sortIncomingEdgesStopwatch = Stopwatch.createUnstarted()
|
||||
val dominatorsStopwatch = Stopwatch.createUnstarted()
|
||||
val retainedSizesStopwatch = Stopwatch.createUnstarted()
|
||||
val flameGraphStopwatch = Stopwatch.createUnstarted()
|
||||
|
||||
totalStopwatch.start()
|
||||
postorderStopwatch.start()
|
||||
val objectCount = nav.instanceCount.toInt()
|
||||
// this is slightly oversized; it only needs to be large enough to contain all strongly-referenced objects
|
||||
val postorderList = listProvider.createIntList("postorderList", (objectCount + 2).toLong())
|
||||
val postorderNumbers = listProvider.createIntList("postorderNumbers", (objectCount + 2).toLong())
|
||||
|
||||
// compute postorder numbers
|
||||
val nodeStack = listProvider.createIntList("dominatorBuf1", (objectCount+2).toLong())
|
||||
val childrenStack = listProvider.createIntList("childStack", (edgeCount + rootsSet.size).toLong())
|
||||
val childrenStackOffsets = listProvider.createIntList("dominatorBuf2", (objectCount + 2).toLong())
|
||||
val childrenStackSizes = listProvider.createIntList("dominatorBuf3", (objectCount + 2).toLong())
|
||||
var csEntries = 0
|
||||
var poEdgeCount = rootsSet.size
|
||||
for (id in rootsSet) {
|
||||
childrenStack[csEntries++] = id
|
||||
}
|
||||
|
||||
childrenStackSizes[0] = csEntries
|
||||
childrenStackOffsets[0] = 0
|
||||
childrenStackOffsets[1] = csEntries
|
||||
|
||||
var nsSize = 0
|
||||
var csSize = 1
|
||||
var maxPonum = 1
|
||||
var maxStackDepth = 0
|
||||
val refList = LongArrayList()
|
||||
while (csSize != 0) {
|
||||
if (childrenStackSizes[csSize-1] > 0) {
|
||||
val child = childrenStack[childrenStackOffsets[csSize-1] + childrenStackSizes[csSize-1] - 1]
|
||||
childrenStackSizes[csSize-1]--
|
||||
if (postorderNumbers[child] == 0) {
|
||||
postorderNumbers[child] = -1
|
||||
nodeStack[nsSize++] = child
|
||||
nav.goTo(child.toLong())
|
||||
nav.copyReferencesTo(refList)
|
||||
var refsAdded = 0
|
||||
for (i in 0 until refList.size) {
|
||||
if (refList[i] != 0L) {
|
||||
childrenStack[csEntries++] = refList[i].toInt()
|
||||
refsAdded++
|
||||
poEdgeCount++
|
||||
}
|
||||
}
|
||||
childrenStackSizes[csSize] = refsAdded
|
||||
childrenStackOffsets[csSize+1] = childrenStackOffsets[csSize] + refsAdded
|
||||
csSize++
|
||||
maxStackDepth = max(csSize, maxStackDepth)
|
||||
}
|
||||
} else {
|
||||
csSize--
|
||||
if (csSize == 0) break
|
||||
csEntries = childrenStackOffsets[csSize]
|
||||
if (nsSize != 0) {
|
||||
val n = nodeStack[nsSize - 1]
|
||||
postorderList[maxPonum] = n
|
||||
postorderNumbers[n] = maxPonum++
|
||||
nsSize--
|
||||
}
|
||||
}
|
||||
}
|
||||
val rootPonum = maxPonum
|
||||
postorderNumbers[objectCount+1] = rootPonum
|
||||
postorderStopwatch.stop()
|
||||
|
||||
incomingEdgesStopwatch.start()
|
||||
/* To accelerate the iterative dominance calculation, we can take advantage of the fact that an object's idom can only change
|
||||
* if at least one of its successors idoms have changed since the last time it was computed. After the first couple iterations,
|
||||
* most idoms stay the same, so much time is wasted on recomputing idoms that are guaranteed not to change.
|
||||
*
|
||||
* Drawing inspiration from garbage collector card tables, we keep a bitmap that records which objects need to be updated. Each
|
||||
* bit corresponds to a range of postorder numbers (probably 128 or 256), to keep the card table small and rapidly scannable. If
|
||||
* that bit is set, every object in that range will have its idom recomputed. This is roughly 5x faster than the simple approach.
|
||||
*/
|
||||
|
||||
// cards represent as small of a power-of-2 sized range as possible while ensuring that card indices can fit in a short
|
||||
val cardBits = if (maxPonum < 65536) 0 else (Math.log(maxPonum / 65536.0) / Math.log(2.0) + 1).toInt()
|
||||
val cardSize = 1 shl cardBits
|
||||
val ncards = (maxPonum + cardSize - 1) / cardSize
|
||||
val ncardChunks = (ncards + 63) / 64
|
||||
val dirtyCards = LongArray(ncardChunks)
|
||||
dirtyCards.fill(-1) // initially everything needs to have its idom computed
|
||||
// outgoingCardRefs contains, for each object p, a list of which cards must be marked dirty when p's idom changes, i.e., the cards
|
||||
// containing the successors of p. These lists can contain duplicates if some of the successors are in the same card, but removing
|
||||
// them is significantly more costly than just performing the redundant card updates.
|
||||
val outgoingCardRefs = listProvider.createUShortList("cardRefs", poEdgeCount.toLong())
|
||||
// outgoingCardListOffsets[p] is the index in outgoingCardRefs where the list for object 'p' begins
|
||||
val outgoingCardListOffsets = nodeStack
|
||||
outgoingCardListOffsets.clear(maxPonum+1)
|
||||
|
||||
val edgeListOffsets = childrenStackOffsets
|
||||
edgeListOffsets.clear(maxPonum+1)
|
||||
|
||||
// first count the number of incoming edges for each object and fill the card lists
|
||||
val references = LongArrayList()
|
||||
var ncardrefs = 0
|
||||
for (i in 1 until maxPonum) {
|
||||
val id = postorderList[i]
|
||||
nav.goTo(id.toLong())
|
||||
nav.copyReferencesTo(references)
|
||||
if (rootsSet.contains(id)) {
|
||||
edgeListOffsets[i]++
|
||||
}
|
||||
outgoingCardListOffsets[i] = ncardrefs
|
||||
for (j in 0 until references.size) {
|
||||
if (references[j] != 0L) {
|
||||
val target = postorderNumbers[references[j].toInt()]
|
||||
edgeListOffsets[target]++
|
||||
outgoingCardRefs[ncardrefs++] = target shr cardBits
|
||||
}
|
||||
}
|
||||
}
|
||||
outgoingCardListOffsets[maxPonum] = ncardrefs
|
||||
// compute the partial sums of the incoming edge counts, in place
|
||||
var next: Int
|
||||
var psum = edgeListOffsets[0]
|
||||
for (i in 0 until maxPonum) {
|
||||
next = edgeListOffsets[i + 1]
|
||||
edgeListOffsets[i + 1] = psum
|
||||
psum += next
|
||||
}
|
||||
edgeListOffsets[0] = 0
|
||||
|
||||
// concatenated incoming edge lists for each object, in postorder traversal order;
|
||||
// edgeListOffsets[p] gives the starting index for the list for the object with postorder number p
|
||||
val incomingEdges: IntList
|
||||
if (poEdgeCount < edgeCount + rootsSet.size) {
|
||||
// if childrenStack is big enough, reuse it
|
||||
incomingEdges = childrenStack
|
||||
incomingEdges.clear(poEdgeCount)
|
||||
} else {
|
||||
incomingEdges = listProvider.createIntList("incoming", poEdgeCount.toLong())
|
||||
}
|
||||
// stores the number of edges added to each incoming edge list so far
|
||||
val edgeListIndices = childrenStackSizes
|
||||
edgeListIndices.clear(maxPonum+1)
|
||||
|
||||
fun getEdge(ponum: Int, index: Int) = incomingEdges[edgeListOffsets[ponum] + index]
|
||||
|
||||
fun addEdge(ponum: Int, parent: Int) {
|
||||
val nextFreeSlot = edgeListOffsets[ponum] + edgeListIndices[ponum]
|
||||
incomingEdges[nextFreeSlot] = parent
|
||||
edgeListIndices[ponum]++
|
||||
}
|
||||
|
||||
fun numIncomingEdges(ponum: Int) = edgeListOffsets[ponum + 1] - edgeListOffsets[ponum]
|
||||
|
||||
// now that the incoming edges have been counted and the offsets determined, fill in the lists
|
||||
for (i in 1 until maxPonum) {
|
||||
val id = postorderList[i]
|
||||
nav.goTo(id.toLong())
|
||||
nav.copyReferencesTo(references)
|
||||
if (rootsSet.contains(id)) {
|
||||
addEdge(i, rootPonum)
|
||||
}
|
||||
for (j in 0 until references.size) {
|
||||
if (references[j] != 0L) {
|
||||
val target = postorderNumbers[references[j].toInt()]
|
||||
addEdge(target, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
incomingEdgesStopwatch.stop()
|
||||
sortIncomingEdgesStopwatch.start()
|
||||
for (i in 0 until maxPonum-1) {
|
||||
val size = edgeListOffsets[i+1] - edgeListOffsets[i]
|
||||
if (size <= 1) continue
|
||||
val arr = IntArray(size)
|
||||
for (j in 0 until size) {
|
||||
arr[j] = incomingEdges[edgeListOffsets[i] + j]
|
||||
}
|
||||
Arrays.sort(arr)
|
||||
for (j in 0 until size) {
|
||||
incomingEdges[edgeListOffsets[i] + j] = arr[j]
|
||||
}
|
||||
}
|
||||
sortIncomingEdgesStopwatch.stop()
|
||||
|
||||
/* Compute DOM sets by iteratively applying this recurrence until a fixed point is reached:
|
||||
* DOM(n) = intersection(DOM(p_1), DOM(p_2), ..., DOM(p_k))
|
||||
* where p_i is the i'th predecessor of n, and initially DOM(ROOT) = {ROOT}, and for n != ROOT, DOM(n) is the entire node set.
|
||||
* DOM(n) can be represented more compactly by only storing the immediate dominator for each node, which forms a tree with idoms
|
||||
* as the parent pointers. The DOM set for a node n consists of all of the nodes in the path from n up to the root.
|
||||
* This iteration is most efficient when done in reverse-postorder (if the graph is reducible in the sense of flow-graph reducibility,
|
||||
* it is guaranteed to terminate in 2 iterations. Unfortunately heaps tend not to be reducible, and while it can take hundreds of
|
||||
* iterations to quiesce completely, very few idoms are changed after the first couple dozen)
|
||||
*/
|
||||
dominatorsStopwatch.start()
|
||||
val idomList = edgeListIndices // edgeListIndices is no longer needed, so we can reuse this buffer for the idoms and save some space
|
||||
idomList.clear(maxPonum+1)
|
||||
idomList[rootPonum] = rootPonum
|
||||
|
||||
/* Find the intersection of the DOM sets for nodes a and b. This will be the common prefix of the paths from the root to a and b,
|
||||
* so will be represented by the postorder number for the nearest common ancestor of a and b. Since idom(n) > n, the NCA can be
|
||||
* found efficiently by walking up the tree from a and b, at each step moving only the one with the smaller postorder number,
|
||||
* until either the two are equal or the root is reached.
|
||||
*/
|
||||
var intersectCalls = 0
|
||||
fun intersect(a: Int, b: Int): Int {
|
||||
intersectCalls++
|
||||
var i = a
|
||||
var j = b
|
||||
while (i != j) {
|
||||
while (i < j) {
|
||||
if (i == 0) return j
|
||||
i = idomList[i]
|
||||
}
|
||||
while (j < i) {
|
||||
if (j == 0) return i
|
||||
j = idomList[j]
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// compute the intersection of the idoms of the predecessors of object i; return true if the idom for i changed.
|
||||
var rootSkipIntersectCalls = 0
|
||||
fun updateIdom(i: Int): Boolean {
|
||||
val numIncoming = numIncomingEdges(i)
|
||||
var newIdom = getEdge(i, numIncoming - 1)
|
||||
var j = numIncoming - 2
|
||||
while (j >= 0) {
|
||||
val parent = getEdge(i, j)
|
||||
if (parent == 0) break
|
||||
val intersection = intersect(newIdom, parent)
|
||||
if (intersection != 0) {
|
||||
newIdom = intersection
|
||||
}
|
||||
if (newIdom == rootPonum) {
|
||||
rootSkipIntersectCalls += j
|
||||
break
|
||||
}
|
||||
j--
|
||||
}
|
||||
if (newIdom != idomList[i]) {
|
||||
idomList[i] = newIdom
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun markCard(cardNumber: Int, value: Boolean) {
|
||||
val chunk = cardNumber shr 6
|
||||
val card = cardNumber and 0x3F
|
||||
if (value) {
|
||||
dirtyCards[chunk] = dirtyCards[chunk] or (1L shl card)
|
||||
} else {
|
||||
dirtyCards[chunk] = dirtyCards[chunk] and (1L shl card).inv()
|
||||
}
|
||||
}
|
||||
|
||||
// scan through the card table in reverse order, recomputing idoms for any dirty cards. Do this repeatedly until either a fixed point
|
||||
// or the iteration cap is reached
|
||||
var nchanged = 1
|
||||
var iter = 0
|
||||
var idomUpdates = 0
|
||||
var prevIntersectCalls = 0
|
||||
var updateCallsSavedByCardTable = 0L
|
||||
while (nchanged != 0 && iter < config.dominatorTreeOptions.maxDominatorIterations) {
|
||||
val iterationStopwatch = Stopwatch.createStarted()
|
||||
var nupdates = 0
|
||||
nchanged = 0
|
||||
var chunkIndex = ncardChunks - 1
|
||||
while (chunkIndex >= 0) {
|
||||
val chunk = dirtyCards[chunkIndex]
|
||||
if (chunk == 0L) { // quickly skip chunks with no set bits - this allows skipping thousands of objects at a time
|
||||
updateCallsSavedByCardTable += cardSize * 64
|
||||
chunkIndex--
|
||||
continue
|
||||
}
|
||||
var card = 63
|
||||
while (card >= 0) {
|
||||
if ((chunk and (1L shl card)) != 0L) {
|
||||
val basePonum = cardSize * (chunkIndex * 64 + card)
|
||||
var i = cardSize - 1
|
||||
markCard(chunkIndex * 64 + card, false) // we are about to recompute everything in this card. mark it clean
|
||||
while (i >= 0) {
|
||||
val ponum = basePonum + i
|
||||
if (ponum == 0) break
|
||||
if (ponum >= rootPonum) {
|
||||
i--
|
||||
continue
|
||||
}
|
||||
if (idomList[ponum] == rootPonum) { // if the idom is already the root, it can't change any more - no need to recompute
|
||||
i--
|
||||
continue
|
||||
}
|
||||
|
||||
idomUpdates++
|
||||
nupdates++
|
||||
if (updateIdom(ponum)) { // idom changed - need to mark successor cards dirty
|
||||
var chidx = outgoingCardListOffsets[ponum]
|
||||
while (chidx < outgoingCardListOffsets[ponum+1]) {
|
||||
val targetCard = outgoingCardRefs[chidx]
|
||||
markCard(targetCard, true)
|
||||
chidx++
|
||||
}
|
||||
nchanged++
|
||||
}
|
||||
i--
|
||||
}
|
||||
} else {
|
||||
updateCallsSavedByCardTable += cardSize
|
||||
}
|
||||
card--
|
||||
}
|
||||
chunkIndex--
|
||||
}
|
||||
LOG.debug("iteration $iter recomputed $nupdates and changed $nchanged idoms in $iterationStopwatch; called intersect() ${intersectCalls - prevIntersectCalls} times")
|
||||
prevIntersectCalls = intersectCalls
|
||||
iter++
|
||||
}
|
||||
dominatorsStopwatch.stop()
|
||||
|
||||
retainedSizesStopwatch.start()
|
||||
val retainedSizes = outgoingCardListOffsets // reuse this buffer, it is no longer needed
|
||||
for (i in 1 until maxPonum) {
|
||||
retainedSizes[i] = sizesList[postorderList[i]]
|
||||
}
|
||||
for (i in 1 until maxPonum) {
|
||||
retainedSizes[idomList[i]] += retainedSizes[i]
|
||||
}
|
||||
retainedSizesStopwatch.stop()
|
||||
|
||||
flameGraphStopwatch.start()
|
||||
val idomTreeChildren = mutableMapOf<Int, MutableList<Int>>()
|
||||
for (i in 1 until maxPonum) {
|
||||
if (retainedSizes[i] * 4L >= config.dominatorTreeOptions.minNodeSize) {
|
||||
idomTreeChildren.computeIfAbsent(idomList[i]) { mutableListOf() }.add(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun signatureFor(ponum: Int): String {
|
||||
return if (ponum == rootPonum) "root" else nav.getClassForObjectId(postorderList[ponum].toLong()).name.dropLastWhile { it == ';' }
|
||||
}
|
||||
|
||||
/* Produce a flame graph of the idom tree in a relatively compact format that can be easily translated
|
||||
* to async-profiler format. The tree is truncated where the retained size of a node falls below minNodeSize,
|
||||
* or if the depth gets above maxDepth (very long chains can be created with linked lists, for example).
|
||||
*
|
||||
* The format is as follows (all numbers are written out as text):
|
||||
* Line 1: number N of strings in string pool
|
||||
* <N lines>: one line per string in the string pool
|
||||
* 1 line per node, with 3 space-separated integer fields:
|
||||
* - index of node name in the string pool
|
||||
* - retained size of this node
|
||||
* - number of child nodes
|
||||
*/
|
||||
|
||||
val stringToIndex = mutableMapOf<String, Int>()
|
||||
val indexToString = mutableListOf<String>()
|
||||
|
||||
fun addStringToPool(s: String): Int {
|
||||
if (!stringToIndex.contains(s)) {
|
||||
stringToIndex[s] = stringToIndex.size
|
||||
indexToString.add(s)
|
||||
}
|
||||
return stringToIndex[s]!!
|
||||
}
|
||||
|
||||
var renderedNodes = 0
|
||||
fun StringBuilder.dumpCompressedFlameGraph(poNumber: Int, depth: Int = 0) {
|
||||
val signatureIndex = addStringToPool(signatureFor(poNumber))
|
||||
val children = idomTreeChildren[poNumber]
|
||||
if (depth < config.dominatorTreeOptions.maxDepth && children != null) {
|
||||
val childrenSize = children.sumBy { p -> retainedSizes[p] }
|
||||
appendLine("$signatureIndex ${retainedSizes[poNumber] - childrenSize} ${children.size}")
|
||||
renderedNodes++
|
||||
children.sortedByDescending { p -> retainedSizes[p] }.forEach { p ->
|
||||
if (renderedNodes >= config.dominatorTreeOptions.headLimit) return@forEach
|
||||
dumpCompressedFlameGraph(p, depth + 1)
|
||||
}
|
||||
} else {
|
||||
appendLine("$signatureIndex ${retainedSizes[poNumber]} 0")
|
||||
renderedNodes++
|
||||
}
|
||||
}
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.dumpCompressedFlameGraph(rootPonum)
|
||||
sb.insert(0, buildString {
|
||||
appendLine(indexToString.size)
|
||||
indexToString.forEach {
|
||||
appendLine(it)
|
||||
}
|
||||
})
|
||||
dominatorFlameGraph = sb.toString()
|
||||
|
||||
flameGraphStopwatch.stop()
|
||||
totalStopwatch.stop()
|
||||
|
||||
fun Double.round(): String {
|
||||
val s = toString()
|
||||
if (!s.contains('.')) return s
|
||||
return s.substring(0, min(s.indexOf(".")+3, s.length))
|
||||
}
|
||||
|
||||
report.metaInfo.apply {
|
||||
appendLine("Dominator phase total time: $totalStopwatch")
|
||||
appendLine(" Compute postorder numbers: $postorderStopwatch")
|
||||
appendLine(" edgeCount = $edgeCount, poEdgeCount = $poEdgeCount, rootsSet size = ${rootsSet.size}, maxPonum = $maxPonum; max stack depth = $maxStackDepth")
|
||||
appendLine(" Compute incoming edges + card refs: $incomingEdgesStopwatch")
|
||||
appendLine(" Sort incoming edges: $sortIncomingEdgesStopwatch")
|
||||
appendLine(" Dominator computation: $dominatorsStopwatch")
|
||||
appendLine(" $iter iterations")
|
||||
appendLine(
|
||||
" $idomUpdates idom updates (${(idomUpdates.toDouble() / maxPonum).round()}x per node); $intersectCalls calls to intersect (${(intersectCalls.toDouble() / idomUpdates).round()} per idomUpdate)")
|
||||
appendLine(" card table is $ncards bits x $cardSize obj/bit, saved $updateCallsSavedByCardTable idom updates")
|
||||
appendLine(" aborting updateIdom when root is hit saved $rootSkipIntersectCalls intersect calls")
|
||||
appendLine(" Compute retained sizes: $retainedSizesStopwatch")
|
||||
appendLine(" Emit flame graph: $flameGraphStopwatch")
|
||||
appendLine(" retained size cutoff: ${config.dominatorTreeOptions.minNodeSize}")
|
||||
appendLine(" depth cutoff: ${config.dominatorTreeOptions.maxDepth}")
|
||||
appendLine(
|
||||
" pruned tree contains $renderedNodes nodes ${if (renderedNodes > config.dominatorTreeOptions.headLimit) "(truncated to ${config.dominatorTreeOptions.headLimit})" else ""}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun IntList.clear(size: Int) {
|
||||
for (i in 0 until size) {
|
||||
this[i] = 0
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,7 +973,7 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
*
|
||||
* @return true if object was added to the list.
|
||||
*/
|
||||
private fun addIdToListAndSetParentIfOrphan(list: IntList, id: Int, parentId: Int = id): Boolean {
|
||||
private fun addIdToListAndSetParentIfOrphan(list: IntArrayList, id: Int, parentId: Int = id): Boolean {
|
||||
if (id != 0 && getParentIdForObjectId(id.toLong()) == 0L) {
|
||||
setParentForObjectId(id.toLong(), parentId.toLong())
|
||||
list.add(id)
|
||||
@@ -461,5 +1010,7 @@ open class AnalyzeGraph(protected val analysisContext: AnalysisContext) {
|
||||
return result ?: throw IllegalStateException("Graph not analyzed.")
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private val LOG = Logger.getInstance(AnalyzeGraph::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ package com.intellij.diagnostic.hprof.analysis
|
||||
import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.histogram.HistogramEntry
|
||||
|
||||
internal class ClassNomination(private val histogram: Histogram,
|
||||
class ClassNomination(private val histogram: Histogram,
|
||||
private val classLimitPerCategory: Int) {
|
||||
fun nominateClasses(): List<HistogramEntry> {
|
||||
val resultClasses = HashSet<HistogramEntry>()
|
||||
|
||||
+58
-87
@@ -22,6 +22,7 @@ import com.intellij.diagnostic.hprof.util.HeapReportUtils.STRING_PADDING_FOR_COU
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.STRING_PADDING_FOR_SIZE
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsCount
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsSize
|
||||
import com.intellij.diagnostic.hprof.util.RefIndexUtil
|
||||
import com.intellij.diagnostic.hprof.util.TruncatingPrintBuffer
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||
@@ -39,6 +40,30 @@ internal class GCRootPathsTree(
|
||||
// If all objects are of the same class and not arrays then instance size can be computed only once.
|
||||
private val objectSizeStrategy = ObjectSizeCalculationStrategy.getBestStrategyForClass(allObjectsOfClass)
|
||||
|
||||
private enum class Status {
|
||||
None,
|
||||
Warning,
|
||||
LastInPath;
|
||||
|
||||
companion object {
|
||||
fun getStatus(refIndex: Int, lastInPath: Boolean, disposed: Boolean): Status = when {
|
||||
lastInPath -> LastInPath
|
||||
disposed -> Warning
|
||||
refIndex == RefIndexUtil.SOFT_REFERENCE -> Warning
|
||||
refIndex == RefIndexUtil.WEAK_REFERENCE -> Warning
|
||||
else -> None
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = getStatusCharacter().toString()
|
||||
|
||||
fun getStatusCharacter(): Char = when (this) {
|
||||
None -> ' '
|
||||
Warning -> '!'
|
||||
LastInPath -> '*'
|
||||
}
|
||||
}
|
||||
|
||||
interface ObjectSizeCalculationStrategy {
|
||||
fun calculateObjectSize(nav: ObjectNavigator, id: Int): Int
|
||||
|
||||
@@ -93,25 +118,24 @@ internal class GCRootPathsTree(
|
||||
|
||||
val gcPath = IntArrayList()
|
||||
val fieldsPath = IntArrayList()
|
||||
val sizesPath = IntArrayList()
|
||||
var objectIterationId = objectId
|
||||
var parentId = parentMapping[objectIterationId]
|
||||
var count = 0
|
||||
|
||||
fieldsPath.add(0)
|
||||
val maxTreeDepth = treeDisplayOptions.maximumTreeDepth
|
||||
while (count < maxTreeDepth && parentId != objectIterationId) {
|
||||
gcPath.add(objectIterationId)
|
||||
if (treeDisplayOptions.showFieldNames) {
|
||||
fieldsPath.add(refIndexMapping[objectIterationId])
|
||||
}
|
||||
else {
|
||||
fieldsPath.add(0)
|
||||
}
|
||||
fieldsPath.add(refIndexMapping[objectIterationId])
|
||||
sizesPath.add(sizesMapping[objectIterationId])
|
||||
objectIterationId = parentId
|
||||
parentId = parentMapping[objectIterationId]
|
||||
count++
|
||||
}
|
||||
|
||||
// No field for a root node
|
||||
fieldsPath.add(RefIndexUtil.ROOT)
|
||||
|
||||
if (parentId != objectIterationId) {
|
||||
// Object ignored as its GC-root path is too long
|
||||
countOfIgnoredObjects++
|
||||
@@ -119,26 +143,16 @@ internal class GCRootPathsTree(
|
||||
}
|
||||
|
||||
gcPath.add(objectIterationId)
|
||||
sizesPath.add(sizesMapping[objectIterationId])
|
||||
|
||||
assert(gcPath.size == fieldsPath.size)
|
||||
|
||||
val size = objectSizeStrategy.calculateObjectSize(nav, objectId)
|
||||
|
||||
var currentNode: Node = topNode
|
||||
for (i in gcPath.size - 1 downTo 0) {
|
||||
val id = gcPath.getInt(i)
|
||||
var classDefinition = nav.getClassForObjectId(id.toLong())
|
||||
var fieldName: String? = null
|
||||
if (fieldsPath.getInt(i) != 0) {
|
||||
if (classDefinition.name == "java.lang.Class") {
|
||||
classDefinition = nav.classStore[id.toLong()]
|
||||
fieldName = classDefinition.getClassFieldName(fieldsPath.getInt(i) - 1)
|
||||
}
|
||||
else {
|
||||
fieldName = classDefinition.getRefField(nav.classStore, fieldsPath.getInt(i) - 1).name
|
||||
}
|
||||
}
|
||||
currentNode = currentNode.addEdge(id, size, sizesMapping[id], classDefinition, fieldName, disposedObjectsIDsSet.contains(id))
|
||||
val classDefinition = nav.getClassForObjectId(id.toLong())
|
||||
currentNode = currentNode.addEdge(id, size, sizesPath.getInt(i), classDefinition, fieldsPath.getInt(i).toByte(), disposedObjectsIDsSet.contains(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,11 +179,11 @@ internal class GCRootPathsTree(
|
||||
objectSize: Int,
|
||||
subgraphSizeInDwords: Int,
|
||||
classDefinition: ClassDefinition,
|
||||
fieldName: String?,
|
||||
refIndex: Byte,
|
||||
disposed: Boolean): Node
|
||||
}
|
||||
|
||||
data class Edge(val classDefinition: ClassDefinition, val fieldName: String?, val disposed: Boolean)
|
||||
data class Edge(val classDefinition: ClassDefinition, val refIndex: Byte, val disposed: Boolean)
|
||||
|
||||
class RegularNode : Node {
|
||||
|
||||
@@ -184,14 +198,14 @@ internal class GCRootPathsTree(
|
||||
objectSize: Int,
|
||||
subgraphSizeInDwords: Int,
|
||||
classDefinition: ClassDefinition,
|
||||
fieldName: String?,
|
||||
refIndex: Byte,
|
||||
disposed: Boolean): Node {
|
||||
var localEdges = edges
|
||||
if (localEdges == null) {
|
||||
localEdges = HashMap(1)
|
||||
edges = localEdges
|
||||
}
|
||||
val node = localEdges.getOrPut(Edge(classDefinition, fieldName, disposed)) { RegularNode() }
|
||||
val node = localEdges.getOrPut(Edge(classDefinition, refIndex, disposed)) { RegularNode() }
|
||||
node.pathsCount++
|
||||
if (node.pathsSize + objectSize.toLong() > Int.MAX_VALUE) {
|
||||
node.pathsSize = Int.MAX_VALUE
|
||||
@@ -237,7 +251,7 @@ internal class GCRootPathsTree(
|
||||
objectSize: Int,
|
||||
subgraphSizeInDwords: Int,
|
||||
classDefinition: ClassDefinition,
|
||||
fieldName: String?,
|
||||
refIndex: Byte,
|
||||
disposed: Boolean): Node {
|
||||
val nullableNode = edges.get(objectId)?.first
|
||||
val node: RegularNode
|
||||
@@ -247,7 +261,7 @@ internal class GCRootPathsTree(
|
||||
}
|
||||
else {
|
||||
val newNode = RegularNode()
|
||||
val pair = Pair(newNode, Edge(classDefinition, fieldName, disposed))
|
||||
val pair = Pair(newNode, Edge(classDefinition, refIndex, disposed))
|
||||
newNode.instances.add(objectId)
|
||||
edges.put(objectId, pair)
|
||||
node = newNode
|
||||
@@ -273,47 +287,13 @@ internal class GCRootPathsTree(
|
||||
}
|
||||
|
||||
data class StackEntry(
|
||||
val parentClass: ClassDefinition?,
|
||||
val edge: Edge,
|
||||
val node: RegularNode,
|
||||
val indent: String,
|
||||
val nextIndent: String
|
||||
)
|
||||
|
||||
private class SoftWeakClassCache(private val classStore: ClassStore) {
|
||||
val softClasses = mutableSetOf<ClassDefinition>()
|
||||
val weakClasses = mutableSetOf<ClassDefinition>()
|
||||
val nonSoftWeakClasses = mutableSetOf<ClassDefinition>()
|
||||
|
||||
fun getSoftWeakDescriptor(classDefinition: ClassDefinition): String? {
|
||||
if (softClasses.contains(classDefinition))
|
||||
return "soft"
|
||||
if (weakClasses.contains(classDefinition))
|
||||
return "weak"
|
||||
if (nonSoftWeakClasses.contains(classDefinition))
|
||||
return null
|
||||
|
||||
var definition = classDefinition
|
||||
while (!classStore.isSoftOrWeakReferenceClass(definition)) {
|
||||
val superclassDefinition = definition.getSuperClass(classStore)
|
||||
if (superclassDefinition == null) {
|
||||
nonSoftWeakClasses.add(classDefinition)
|
||||
return null
|
||||
}
|
||||
definition = superclassDefinition
|
||||
}
|
||||
if (definition == classStore.weakReferenceClass) {
|
||||
weakClasses.add(classDefinition)
|
||||
return "weak"
|
||||
}
|
||||
else {
|
||||
assert(definition == classStore.softReferenceClass)
|
||||
softClasses.add(classDefinition)
|
||||
return "soft"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun createHotPathReport(treeDisplayOptions: AnalysisConfig.TreeDisplayOptions, rootReasonGetter: (Int) -> String): String {
|
||||
val rootList = mutableListOf<Triple<Int, RegularNode, Edge>>()
|
||||
val result = StringBuilder()
|
||||
@@ -328,23 +308,21 @@ internal class GCRootPathsTree(
|
||||
treeDisplayOptions.minimumObjectCount,
|
||||
(Math.ceil(totalInstanceCount / 100.0) * treeDisplayOptions.minimumObjectCountPercent).toInt())
|
||||
|
||||
val softWeakClassCache = SoftWeakClassCache(classStore)
|
||||
|
||||
// Show paths from roots that have at least minimumObjectCountPercent%, minimumObjectCount objects or size of all reported objects
|
||||
// in the subtree is more than minimumObjectSize.
|
||||
// Always show at least two paths.
|
||||
rootList
|
||||
.sortedByDescending { it.second.pathsSize }
|
||||
.filterIndexed { index, (_, node, _) ->
|
||||
index < treeDisplayOptions.minimumPaths ||
|
||||
node.pathsCount >= minimumObjectsForReport ||
|
||||
node.pathsSize >= treeDisplayOptions.minimumObjectSize
|
||||
}
|
||||
.sortedByDescending { it.second.pathsSize }
|
||||
.forEachIndexed { index, (rootObjectId, rootNode, rootEdge) ->
|
||||
val rootReasonString = rootReasonGetter(rootObjectId)
|
||||
val rootPercent = (100.0 * rootNode.pathsCount / totalInstanceCount).toInt()
|
||||
|
||||
result.appendln("Root ${index + 1}:")
|
||||
result.appendLine("Root ${index + 1}:")
|
||||
printReportLine(printFunc,
|
||||
treeDisplayOptions,
|
||||
rootNode.pathsCount,
|
||||
@@ -352,8 +330,7 @@ internal class GCRootPathsTree(
|
||||
rootNode.pathsSize,
|
||||
rootNode.totalSizeInDwords.toLong() * 4,
|
||||
1,
|
||||
false,
|
||||
null,
|
||||
Status.getStatus(RefIndexUtil.ROOT, false, false),
|
||||
false,
|
||||
null,
|
||||
"",
|
||||
@@ -362,14 +339,12 @@ internal class GCRootPathsTree(
|
||||
TruncatingPrintBuffer(treeDisplayOptions.headLimit, treeDisplayOptions.tailLimit, printFunc).use { buffer ->
|
||||
// Iterate over the hot path
|
||||
val stack = ArrayDeque<StackEntry>()
|
||||
stack.push(StackEntry(rootEdge, rootNode, "", ""))
|
||||
stack.push(StackEntry(null, rootEdge, rootNode, "", ""))
|
||||
|
||||
while (!stack.isEmpty()) {
|
||||
val (edge, node, indent, nextIndent) = stack.pop()
|
||||
val (classDefinition, fieldName, disposed) = edge
|
||||
|
||||
// Soft/weak referents don't have a parent field set to differentiate them from other (strong-referencing) fields.
|
||||
val softWeakDescriptor = if (fieldName == null) softWeakClassCache.getSoftWeakDescriptor(classDefinition) else null
|
||||
val (parentClass, edge, node, indent, nextIndent) = stack.pop()
|
||||
val (classDefinition, refIndexByte, disposed) = edge
|
||||
val refIndex = java.lang.Byte.toUnsignedInt(refIndexByte)
|
||||
|
||||
printReportLine(buffer::println,
|
||||
treeDisplayOptions,
|
||||
@@ -378,10 +353,9 @@ internal class GCRootPathsTree(
|
||||
node.pathsSize,
|
||||
node.totalSizeInDwords.toLong() * 4,
|
||||
node.instances.size,
|
||||
node.edges == null,
|
||||
softWeakDescriptor,
|
||||
Status.getStatus(refIndex, node.edges == null, disposed),
|
||||
disposed,
|
||||
fieldName,
|
||||
RefIndexUtil.getFieldDescription(refIndex, parentClass, classStore),
|
||||
indent,
|
||||
classDefinition.prettyName)
|
||||
|
||||
@@ -400,7 +374,7 @@ internal class GCRootPathsTree(
|
||||
|
||||
if (childrenToReport.size == 1 && treeDisplayOptions.smartIndent) {
|
||||
// No indentation for a single child
|
||||
stack.push(StackEntry(childrenToReport[0].key, childrenToReport[0].value, nextIndent, nextIndent))
|
||||
stack.push(StackEntry(classDefinition, childrenToReport[0].key, childrenToReport[0].value, nextIndent, nextIndent))
|
||||
}
|
||||
else {
|
||||
// Don't report too deep paths
|
||||
@@ -408,13 +382,13 @@ internal class GCRootPathsTree(
|
||||
printReportLine(buffer::println,
|
||||
treeDisplayOptions,
|
||||
null, null, null, null,
|
||||
null, true, null, null, null,
|
||||
null, Status.LastInPath, null, null,
|
||||
nextIndent, "\\-[...]")
|
||||
else {
|
||||
// Add indentation only if there are 2+ children
|
||||
childrenToReport.forEachIndexed { index, e ->
|
||||
if (index == 0) stack.push(StackEntry(e.key, e.value, "$nextIndent\\-", "$nextIndent "))
|
||||
else stack.push(StackEntry(e.key, e.value, "$nextIndent+-", "$nextIndent| "))
|
||||
if (index == 0) stack.push(StackEntry(classDefinition, e.key, e.value, "$nextIndent\\-", "$nextIndent "))
|
||||
else stack.push(StackEntry(classDefinition, e.key, e.value, "$nextIndent+-", "$nextIndent| "))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,8 +405,7 @@ internal class GCRootPathsTree(
|
||||
instanceSize: Int?,
|
||||
subgraphSize: Long?,
|
||||
instanceCount: Int?,
|
||||
lastInPath: Boolean,
|
||||
softWeakDescriptor: String?,
|
||||
status: Status,
|
||||
disposed: Boolean?,
|
||||
fieldName: String?,
|
||||
indent: String,
|
||||
@@ -441,18 +414,16 @@ internal class GCRootPathsTree(
|
||||
val percentString = (percent?.let { "$it%" } ?: "").padStart(4)
|
||||
val instanceSizeString = (instanceSize?.let { toShortStringAsSize(it.toLong()) } ?: "").padStart(STRING_PADDING_FOR_SIZE)
|
||||
val instanceCountString = (instanceCount ?: "").toString().padStart(10)
|
||||
val status = if (lastInPath) "*" else if (softWeakDescriptor != null || disposed == true) "!" else " "
|
||||
val fieldNameString = if (fieldName != null) ".$fieldName" else ""
|
||||
val fieldNameString = if (fieldName != null) "$fieldName: " else ""
|
||||
val disposedString = if (disposed == true) " (disposed)" else ""
|
||||
val softWeakString = if (softWeakDescriptor != null) " ($softWeakDescriptor)" else ""
|
||||
val subgraphSizeString = (subgraphSize?.let { toShortStringAsSize(it) } ?: "").padStart(STRING_PADDING_FOR_SIZE)
|
||||
|
||||
if (treeDisplayOptions.showSize) {
|
||||
printFunc(
|
||||
"[$pathsCountString/$percentString/$instanceSizeString] $subgraphSizeString $instanceCountString $status $indent$text$fieldNameString$disposedString$softWeakString")
|
||||
"[$pathsCountString/$percentString/$instanceSizeString] $subgraphSizeString $instanceCountString $status $indent$fieldNameString$text$disposedString")
|
||||
}
|
||||
else {
|
||||
printFunc("$status $indent$text$fieldNameString$disposedString$softWeakString")
|
||||
printFunc("$status $indent$text$fieldNameString$disposedString")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-11
@@ -21,11 +21,9 @@ import com.intellij.diagnostic.hprof.classstore.HProfMetadata
|
||||
import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.parser.HProfEventBasedParser
|
||||
import com.intellij.diagnostic.hprof.util.FileBackedIntList
|
||||
import com.intellij.diagnostic.hprof.util.FileBackedUByteList
|
||||
import com.intellij.diagnostic.hprof.util.*
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.sectionHeader
|
||||
import com.intellij.diagnostic.hprof.util.HeapReportUtils.toShortStringAsCount
|
||||
import com.intellij.diagnostic.hprof.util.PartialProgressIndicator
|
||||
import com.intellij.diagnostic.hprof.visitors.RemapIDsVisitor
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import org.jetbrains.annotations.NonNls
|
||||
@@ -34,9 +32,9 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
internal class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
private val tempFilenameSupplier: TempFilenameSupplier,
|
||||
private val analysisCallback: (AnalysisContext, ProgressIndicator) -> String) {
|
||||
private val analysisCallback: (AnalysisContext, ListProvider, ProgressIndicator) -> String) {
|
||||
|
||||
interface TempFilenameSupplier {
|
||||
fun getTempFilePath(type: String): Path
|
||||
@@ -61,7 +59,6 @@ internal class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
|
||||
private fun openTempEmptyFileChannel(@NonNls type: String): FileChannel {
|
||||
val tempPath = tempFilenameSupplier.getTempFilePath(type)
|
||||
|
||||
val tempChannel = FileChannel.open(tempPath,
|
||||
StandardOpenOption.READ,
|
||||
StandardOpenOption.WRITE,
|
||||
@@ -73,6 +70,12 @@ internal class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
return tempChannel
|
||||
}
|
||||
|
||||
val fileBackedListProvider = object: ListProvider {
|
||||
override fun createUByteList(name: String, size: Long) = FileBackedUByteList.createEmpty(openTempEmptyFileChannel(name), size)
|
||||
override fun createUShortList(name: String, size: Long) = FileBackedUShortList.createEmpty(openTempEmptyFileChannel(name), size)
|
||||
override fun createIntList(name: String, size: Long) = FileBackedIntList.createEmpty(openTempEmptyFileChannel(name), size)
|
||||
}
|
||||
|
||||
fun analyze(progress: ProgressIndicator): String {
|
||||
val result = StringBuilder()
|
||||
val totalStopwatch = Stopwatch.createStarted()
|
||||
@@ -129,10 +132,10 @@ internal class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
|
||||
prepareFilesStopwatch.stop()
|
||||
|
||||
val parentList = FileBackedIntList.createEmpty(openTempEmptyFileChannel("parents"), navigator.instanceCount + 1)
|
||||
val sizesList = FileBackedIntList.createEmpty(openTempEmptyFileChannel("sizes"), navigator.instanceCount + 1)
|
||||
val visitedList = FileBackedIntList.createEmpty(openTempEmptyFileChannel("visited"), navigator.instanceCount + 1)
|
||||
val refIndexList = FileBackedUByteList.createEmpty(openTempEmptyFileChannel("refIndex"), navigator.instanceCount + 1)
|
||||
val parentList = fileBackedListProvider.createIntList("parents", navigator.instanceCount + 1)
|
||||
val sizesList = fileBackedListProvider.createIntList("sizes", navigator.instanceCount + 1)
|
||||
val visitedList = fileBackedListProvider.createIntList("visited", navigator.instanceCount + 1)
|
||||
val refIndexList = fileBackedListProvider.createUByteList("refIndex", navigator.instanceCount + 1)
|
||||
|
||||
analysisStopwatch.start()
|
||||
|
||||
@@ -150,7 +153,7 @@ internal class HProfAnalysis(private val hprofFileChannel: FileChannel,
|
||||
histogram
|
||||
)
|
||||
|
||||
val analysisReport = analysisCallback(analysisContext, PartialProgressIndicator(progress, 0.4, 0.4))
|
||||
val analysisReport = analysisCallback(analysisContext, fileBackedListProvider, PartialProgressIndicator(progress, 0.4, 0.4))
|
||||
if (analysisReport.isNotBlank()) {
|
||||
result.appendln(analysisReport)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ class ClassDefinition(val name: String,
|
||||
val prettyName: String
|
||||
get() = computePrettyName(name)
|
||||
|
||||
val undecoratedName: String
|
||||
get() = name.substringBefore('!')
|
||||
|
||||
companion object {
|
||||
const val OBJECT_PREAMBLE_SIZE = 8
|
||||
const val ARRAY_PREAMBLE_SIZE = 12
|
||||
|
||||
+14
-6
@@ -34,6 +34,8 @@ abstract class ObjectNavigator(val classStore: ClassStore, val instanceCount: Lo
|
||||
|
||||
data class RootObject(val id: Long, val reason: RootReason)
|
||||
|
||||
class NavigationException(message: String) : RuntimeException(message)
|
||||
|
||||
abstract val id: Long
|
||||
|
||||
abstract fun createRootsIterator(): Iterator<RootObject>
|
||||
@@ -54,17 +56,19 @@ abstract class ObjectNavigator(val classStore: ClassStore, val instanceCount: Lo
|
||||
abstract fun getWeakReferenceId(): Long
|
||||
abstract fun getSoftWeakReferenceIndex(): Int
|
||||
|
||||
fun goToInstanceField(@NonNls className: String, @NonNls fieldName: String) {
|
||||
fun goToInstanceField(@NonNls className: String?, @NonNls fieldName: String) {
|
||||
val objectId = getInstanceFieldObjectId(className, fieldName)
|
||||
goTo(objectId, ReferenceResolution.ALL_REFERENCES)
|
||||
}
|
||||
|
||||
fun getInstanceFieldObjectId(@NonNls className: String, @NonNls name: String): Long {
|
||||
fun getInstanceFieldObjectId(@NonNls className: String?, @NonNls name: String): Long {
|
||||
val refs = getReferencesCopy()
|
||||
assert(className == getClass().name.substringBeforeLast('!')) { "Expected $className, got ${getClass().name}" }
|
||||
if (className != null && className != getClass().undecoratedName) {
|
||||
throw NavigationException("Expected $className, got ${getClass().undecoratedName}")
|
||||
}
|
||||
val indexOfField = getClass().allRefFieldNames(classStore).indexOfFirst { it == name }
|
||||
if (indexOfField == -1) {
|
||||
throw IllegalStateException("Can't find field $name in class $className")
|
||||
throw NavigationException("Missing field $name in ${getClass().name}")
|
||||
}
|
||||
return refs.getLong(indexOfField)
|
||||
}
|
||||
@@ -74,8 +78,12 @@ abstract class ObjectNavigator(val classStore: ClassStore, val instanceCount: Lo
|
||||
goTo(objectId, ReferenceResolution.ALL_REFERENCES)
|
||||
}
|
||||
|
||||
private fun getStaticFieldObjectId(className: String, fieldName: String) =
|
||||
classStore[className].objectStaticFields.first { it.name == fieldName }.value
|
||||
private fun getStaticFieldObjectId(className: String, fieldName: String): Long {
|
||||
val staticField =
|
||||
classStore[className].objectStaticFields.firstOrNull { it.name == fieldName }
|
||||
?: throw NavigationException("Missing static field $fieldName in class $className")
|
||||
return staticField.value
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createOnAuxiliaryFiles(parser: HProfEventBasedParser,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
class AnalysisReport(val mainReport: StringBuilder = StringBuilder(), val metaInfo: StringBuilder = StringBuilder())
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
class FileBackedUShortList(private val buffer: ByteBuffer) : UShortList {
|
||||
|
||||
override operator fun get(index: Int): Int {
|
||||
buffer.position(index * 2)
|
||||
return java.lang.Short.toUnsignedInt(buffer.short)
|
||||
}
|
||||
|
||||
override operator fun set(index: Int, value: Int) {
|
||||
assert(value in 0..65535)
|
||||
buffer.position(index * 2)
|
||||
buffer.putShort(value.toShort())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createEmpty(channel: FileChannel, size: Long): UShortList {
|
||||
FileBackedHashMap.createEmptyFile(channel, size * 2)
|
||||
return FileBackedUShortList(channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
import com.intellij.diagnostic.hprof.parser.*
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.DataOutput
|
||||
import java.io.DataOutputStream
|
||||
|
||||
class HprofWriter(
|
||||
private val dos: DataOutputStream,
|
||||
private val idSize: Int,
|
||||
timestamp: Long
|
||||
) : Closeable {
|
||||
|
||||
init {
|
||||
if (idSize != 4 && idSize != 8) {
|
||||
throw IllegalArgumentException("idSize can only be 4 or 8")
|
||||
}
|
||||
dos.writeNullTerminatedString("JAVA PROFILE 1.0.1")
|
||||
dos.writeInt(idSize)
|
||||
dos.writeLong(timestamp)
|
||||
}
|
||||
|
||||
private val stringToIdMap = Object2LongOpenHashMap<String>()
|
||||
private val idToStringMap = Long2ObjectOpenHashMap<String>()
|
||||
private var nextStringId = 1L
|
||||
private val subtagsBaos = ByteArrayOutputStream()
|
||||
private var subtagsStream = DataOutputStream(subtagsBaos)
|
||||
|
||||
override fun close() {
|
||||
flushHeapObjects()
|
||||
dos.close()
|
||||
}
|
||||
|
||||
private fun DataOutput.writeNullTerminatedString(s: String) {
|
||||
this.write(s.toByteArray())
|
||||
this.write(0)
|
||||
}
|
||||
|
||||
fun writeStringInUTF8(id: Long, s: String) {
|
||||
val bytes = s.toByteArray()
|
||||
writeRecordHeader(RecordType.StringInUTF8, bytes.size + idSize)
|
||||
dos.writeId(id)
|
||||
dos.write(bytes)
|
||||
}
|
||||
|
||||
fun writeLoadClass(serialNumber: Int, classObjectId: Long, stackTraceSerialNumber: Int, classNameId: Long) {
|
||||
writeRecordHeader(RecordType.LoadClass, 8 + idSize * 2)
|
||||
dos.writeInt(serialNumber)
|
||||
dos.writeId(classObjectId)
|
||||
dos.writeInt(stackTraceSerialNumber)
|
||||
dos.writeId(classNameId)
|
||||
}
|
||||
|
||||
fun flushHeapObjects() {
|
||||
writeHeapDumpRecords()
|
||||
}
|
||||
|
||||
private fun writeHeapDumpRecords() {
|
||||
if (subtagsBaos.size() > 0) {
|
||||
subtagsStream.close()
|
||||
writeRecordHeader(RecordType.HeapDump, subtagsBaos.size())
|
||||
subtagsBaos.writeTo(dos)
|
||||
subtagsBaos.reset()
|
||||
subtagsStream = DataOutputStream(subtagsBaos)
|
||||
writeRecordHeader(RecordType.HeapDumpEnd, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeRootUnknown(id: Long) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.RootUnknown)
|
||||
writeId(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeRootJavaFrame(objectId: Long, threadSerialNumber: Int, frameNumber: Int) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.RootJavaFrame)
|
||||
writeId(objectId)
|
||||
writeInt(threadSerialNumber)
|
||||
writeInt(frameNumber)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeClassDump(
|
||||
classObjectId: Long,
|
||||
stackTraceSerialNumber: Int,
|
||||
superClassObjectId: Long,
|
||||
classLoaderObjectId: Long,
|
||||
signersObjectId: Long,
|
||||
protectionDomainObjectId: Long,
|
||||
instanceSize: Int,
|
||||
constantPool: Array<ConstantPoolEntry>,
|
||||
staticFields: Array<StaticFieldEntry>,
|
||||
instanceFields: Array<InstanceFieldEntry>
|
||||
) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.ClassDump)
|
||||
writeId(classObjectId)
|
||||
writeInt(stackTraceSerialNumber)
|
||||
writeId(superClassObjectId)
|
||||
writeId(classLoaderObjectId)
|
||||
writeId(signersObjectId)
|
||||
writeId(protectionDomainObjectId)
|
||||
writeId(0)
|
||||
writeId(0)
|
||||
writeInt(instanceSize)
|
||||
writeShort(constantPool.size)
|
||||
for (entry in constantPool) {
|
||||
writeShort(entry.constantPoolIndex)
|
||||
writeByte(entry.type.typeId)
|
||||
writeValue(entry.value, entry.type)
|
||||
}
|
||||
writeShort(staticFields.size)
|
||||
for (entry in staticFields) {
|
||||
writeId(entry.fieldNameStringId)
|
||||
writeByte(entry.type.typeId)
|
||||
writeValue(entry.value, entry.type)
|
||||
}
|
||||
writeShort(instanceFields.size)
|
||||
for (entry in instanceFields) {
|
||||
writeId(entry.fieldNameStringId)
|
||||
writeByte(entry.type.typeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writeInstanceDump(
|
||||
objectId: Long,
|
||||
stackTraceSerialNumber: Int,
|
||||
classObjectId: Long,
|
||||
bytes: ByteArray
|
||||
) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.InstanceDump)
|
||||
writeId(objectId)
|
||||
writeInt(stackTraceSerialNumber)
|
||||
writeId(classObjectId)
|
||||
writeInt(bytes.count())
|
||||
write(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeObjectArrayDump(
|
||||
arrayObjectId: Long,
|
||||
stackTraceSerialNumber: Int,
|
||||
arrayClassObjectId: Long,
|
||||
elementIds: LongArray
|
||||
) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.ObjectArrayDump)
|
||||
writeId(arrayObjectId)
|
||||
writeInt(stackTraceSerialNumber)
|
||||
writeInt(elementIds.count())
|
||||
writeId(arrayClassObjectId)
|
||||
elementIds.forEach { id ->
|
||||
writeId(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writePrimitiveArrayDump(
|
||||
arrayObjectId: Long,
|
||||
stackTraceSerialNumber: Int,
|
||||
elementType: Type,
|
||||
elements: ByteArray,
|
||||
elementsCount: Int
|
||||
) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.PrimitiveArrayDump)
|
||||
writeId(arrayObjectId)
|
||||
writeInt(stackTraceSerialNumber)
|
||||
writeInt(elementsCount)
|
||||
assert(elementType != Type.OBJECT)
|
||||
writeByte(elementType.typeId)
|
||||
assert(elements.size == elementsCount * elementType.size)
|
||||
write(elements)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreateStringId(s: String): Long {
|
||||
val id = stringToIdMap.getLong(s)
|
||||
if (id == 0L) {
|
||||
val newId = nextStringId++
|
||||
writeStringInUTF8(newId, s)
|
||||
idToStringMap.put(newId, s)
|
||||
stringToIdMap.put(s, newId)
|
||||
return newId
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
|
||||
private fun writeRecordHeader(recordType: RecordType, length: Int) {
|
||||
with(dos) {
|
||||
writeByte(recordType.value)
|
||||
writeInt(0) // timestamp, not supported
|
||||
writeInt(length)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeHeapDumpRecordHeader(heapDumpRecordType: HeapDumpRecordType) {
|
||||
with(subtagsStream) {
|
||||
writeByte(heapDumpRecordType.value)
|
||||
}
|
||||
}
|
||||
|
||||
fun writeRootGlobalJNI(objectId: Long, jniGlobalRefId: Long) {
|
||||
with(subtagsStream) {
|
||||
writeHeapDumpRecordHeader(HeapDumpRecordType.RootGlobalJNI)
|
||||
writeId(objectId)
|
||||
writeId(jniGlobalRefId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DataOutputStream.writeId(id: Long) {
|
||||
when (idSize) {
|
||||
4 -> this.writeInt(id.toInt())
|
||||
8 -> this.writeLong(id)
|
||||
else -> assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DataOutputStream.writeValue(value: Long, type: Type) {
|
||||
if (type == Type.OBJECT) {
|
||||
writeId(value)
|
||||
}
|
||||
else {
|
||||
when (type.size) {
|
||||
1 -> writeByte(value.toInt())
|
||||
2 -> writeShort(value.toInt())
|
||||
4 -> writeInt(value.toInt())
|
||||
8 -> writeLong(value)
|
||||
else -> assert(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun writeStackTrace(stackTraceSerialNumber: Int, threadSerialNumber: Long, stackFrameIds: LongArray) {
|
||||
with (dos) {
|
||||
val length = 3 * 4 + idSize * stackFrameIds.size
|
||||
writeRecordHeader(RecordType.StackTrace, length)
|
||||
writeInt(stackTraceSerialNumber)
|
||||
writeInt(threadSerialNumber.toInt())
|
||||
writeInt(stackFrameIds.size)
|
||||
for (frameId in stackFrameIds) {
|
||||
writeId(frameId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun writeStackFrame(stackFrameId: Long,
|
||||
methodNameStringId: Long,
|
||||
methodSignatureStringId: Long,
|
||||
sourceFilenameStringId: Long,
|
||||
classSerialNumber: Int,
|
||||
lineNumber: Int)
|
||||
{
|
||||
with (dos) {
|
||||
val length = (4 * idSize) + (2 * 4)
|
||||
writeRecordHeader(RecordType.StackFrame, length)
|
||||
writeId(stackFrameId)
|
||||
writeId(methodNameStringId)
|
||||
writeId(methodSignatureStringId)
|
||||
writeId(sourceFilenameStringId)
|
||||
writeInt(classSerialNumber)
|
||||
writeInt(lineNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
interface ListProvider {
|
||||
fun createUByteList(name: String, size: Long): UByteList
|
||||
fun createUShortList(name: String, size: Long): UShortList
|
||||
fun createIntList(name: String, size: Long): IntList
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
import com.intellij.diagnostic.hprof.classstore.ClassDefinition
|
||||
import com.intellij.diagnostic.hprof.classstore.ClassStore
|
||||
|
||||
object RefIndexUtil {
|
||||
|
||||
fun getFieldDescription(refIndex: Int,
|
||||
classDefinition: ClassDefinition?,
|
||||
classStore: ClassStore): String? {
|
||||
return when (refIndex) {
|
||||
ROOT -> "(root)"
|
||||
SOFT_REFERENCE -> "(soft)"
|
||||
WEAK_REFERENCE -> "(weak)"
|
||||
ARRAY_ELEMENT -> "[]"
|
||||
DISPOSER_CHILD -> "(disposer-tree)"
|
||||
FIELD_OMITTED -> null
|
||||
else -> classDefinition?.getRefField(classStore, refIndex - 1)?.name ?: "(field_$refIndex)"
|
||||
}
|
||||
}
|
||||
|
||||
const val FIELD_OMITTED = 0
|
||||
const val MAX_FIELD_INDEX = 250
|
||||
|
||||
const val ROOT = 251
|
||||
const val SOFT_REFERENCE = 252
|
||||
const val WEAK_REFERENCE = 253
|
||||
const val ARRAY_ELEMENT = 254
|
||||
const val DISPOSER_CHILD = 255
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
interface TreeNode {
|
||||
fun description(): String
|
||||
fun children(): Collection<TreeNode>
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalysisConfig
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import java.util.*
|
||||
|
||||
class TreeVisualizer {
|
||||
|
||||
private class IndentStore {
|
||||
private val map = Int2ObjectOpenHashMap<String>()
|
||||
|
||||
fun getIndentForDepth(size: Int): String {
|
||||
if (map.containsKey(size)) {
|
||||
return map[size]
|
||||
}
|
||||
val indent = " ".repeat(size)
|
||||
map[size] = indent
|
||||
return indent
|
||||
}
|
||||
}
|
||||
|
||||
fun visualizeTree(root: TreeNode, buffer: TruncatingPrintBuffer, options: AnalysisConfig.DisposerTreeSummaryOptions) {
|
||||
data class StackItem(val node: TreeNode, val depth: Int)
|
||||
|
||||
val nodeStack = Stack<StackItem>().apply { push(StackItem(root, 0)) }
|
||||
val indentStore = IndentStore()
|
||||
while (!nodeStack.empty()) {
|
||||
val (node, depth) = nodeStack.pop()
|
||||
val indent = indentStore.getIndentForDepth(depth)
|
||||
buffer.println(indent + node.description())
|
||||
val children = node.children().reversed()
|
||||
|
||||
if (depth >= options.maxDepth && children.isNotEmpty()) {
|
||||
buffer.println(indentStore.getIndentForDepth(depth + 1) + "[...]")
|
||||
}
|
||||
else {
|
||||
children.forEach { child -> nodeStack.push(StackItem(child, depth + 1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof.util
|
||||
|
||||
interface UShortList {
|
||||
operator fun get(index: Int): Int
|
||||
operator fun set(index: Int, value: Int)
|
||||
}
|
||||
+4
-1
@@ -58,7 +58,10 @@ class CollectRootReasonsVisitor(private val threadsMap: Long2ObjectMap<ThreadInf
|
||||
else {
|
||||
RootReason.createJavaFrameReason("Unknown location")
|
||||
}
|
||||
roots.put(objectId, rootReason)
|
||||
// Java frame has a lower priority - if won't override any other GC-root reasons.
|
||||
if (!roots.containsKey(objectId)) {
|
||||
roots.put(objectId, rootReason)
|
||||
}
|
||||
}
|
||||
|
||||
override fun visitRootNativeStack(objectId: Long, threadSerialNumber: Long) {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.util.function.LongUnaryOperator
|
||||
|
||||
internal abstract class RemapIDsVisitor : HProfVisitor() {
|
||||
abstract class RemapIDsVisitor : HProfVisitor() {
|
||||
private var currentID = 0
|
||||
|
||||
override fun preVisit() {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
package com.intellij.ide.plugins
|
||||
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory
|
||||
import com.intellij.openapi.util.registry.RegistryManager
|
||||
import com.intellij.configurationStore.jdomSerializer
|
||||
import com.intellij.configurationStore.runInAutoSaveDisabledMode
|
||||
import com.intellij.configurationStore.saveProjectsAndApp
|
||||
@@ -63,6 +62,7 @@ import com.intellij.openapi.util.IconLoader
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.objectTree.ThrowableInterner
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.util.registry.RegistryManager
|
||||
import com.intellij.openapi.vfs.newvfs.FileAttribute
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import com.intellij.openapi.wm.impl.IdeFrameImpl
|
||||
@@ -1063,8 +1063,8 @@ private fun loadModules(
|
||||
|
||||
private fun analyzeSnapshot(hprofPath: String, pluginId: PluginId): String {
|
||||
FileChannel.open(Paths.get(hprofPath), StandardOpenOption.READ).use { channel ->
|
||||
val analysis = HProfAnalysis(channel, SystemTempFilenameSupplier()) { analysisContext, progressIndicator ->
|
||||
AnalyzeClassloaderReferencesGraph(analysisContext, pluginId.idString).analyze(progressIndicator)
|
||||
val analysis = HProfAnalysis(channel, SystemTempFilenameSupplier()) { analysisContext, listProvider, progressIndicator ->
|
||||
AnalyzeClassloaderReferencesGraph(analysisContext, listProvider, pluginId.idString).analyze(progressIndicator).mainReport.toString()
|
||||
}
|
||||
analysis.onlyStrongReferences = true
|
||||
analysis.includeClassesAsRoots = false
|
||||
|
||||
@@ -71,5 +71,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
|
||||
<orderEntry type="library" scope="TEST" name="Objenesis" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework.impl" scope="TEST" />
|
||||
<orderEntry type="library" name="kotlin-reflect" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 7/ 644B] [ 7/ 644B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] MyTestClass!2
|
||||
3: [ 1/ 8B] [ 1/ 8B] MyTestClass!1
|
||||
Total - All: 9 660B 3 classes (Total instances: 9)
|
||||
Total - Strong-ref: 9 660B 3 classes (Total instances: 9)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 7
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] MyTestClass!1
|
||||
--> [1/8B] MyTestClass!2
|
||||
|
||||
CLASS: MyTestClass!1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Global JNI
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!1
|
||||
|
||||
CLASS: MyTestClass!2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!2
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 7/ 756B] [ 7/ 756B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] MyTestClass!2
|
||||
3: [ 1/ 8B] [ 1/ 8B] MyTestClass!1
|
||||
Total - All: 9 772B 3 classes (Total instances: 9)
|
||||
Total - Strong-ref: 9 772B 3 classes (Total instances: 9)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 7
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] MyTestClass!1
|
||||
--> [1/8B] MyTestClass!2
|
||||
|
||||
CLASS: MyTestClass!1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Global JNI
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!1
|
||||
|
||||
CLASS: MyTestClass!2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!2
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 7/ 812B] [ 7/ 812B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] MyTestClass!2
|
||||
3: [ 1/ 8B] [ 1/ 8B] MyTestClass!1
|
||||
Total - All: 9 828B 3 classes (Total instances: 9)
|
||||
Total - Strong-ref: 9 828B 3 classes (Total instances: 9)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 7
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] MyTestClass!1
|
||||
--> [1/8B] MyTestClass!2
|
||||
|
||||
CLASS: MyTestClass!1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Global JNI
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!1
|
||||
|
||||
CLASS: MyTestClass!2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): MyTestClass!2
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
============== DISPOSER TREE SUMMARY ==============
|
||||
Cutoff: 0
|
||||
Count of disposable objects: 9
|
||||
|
||||
[7] 1 MyDisposableRoot
|
||||
[5] 3 MyDisposableChild
|
||||
[2] 2 MyDisposableGrandchild
|
||||
[1] 1 com.intellij.openapi.util.Disposer$1
|
||||
|
||||
[2] 1 com.intellij.openapi.util.Disposer$1
|
||||
[1] 1 MyDisposableChild
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
============ DOMINATOR TREE FLAME GRAPH ===========
|
||||
9
|
||||
root
|
||||
A
|
||||
B
|
||||
E
|
||||
G
|
||||
C
|
||||
D
|
||||
F
|
||||
java.lang.Class
|
||||
0 15 14
|
||||
1 8 4
|
||||
2 6 1
|
||||
3 4 1
|
||||
4 4 0
|
||||
5 4 0
|
||||
6 2 0
|
||||
7 2 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
8 27 0
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
============ DOMINATOR TREE FLAME GRAPH ===========
|
||||
9
|
||||
root
|
||||
A
|
||||
B
|
||||
E
|
||||
G
|
||||
C
|
||||
D
|
||||
F
|
||||
java.lang.Class
|
||||
0 8 14
|
||||
1 8 4
|
||||
2 6 1
|
||||
3 4 1
|
||||
4 4 0
|
||||
5 4 0
|
||||
6 2 0
|
||||
7 2 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
8 29 0
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 6/ 648B] [ 6/ 648B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] C1
|
||||
Total - All: 7 656B 2 classes (Total instances: 7)
|
||||
Total - Strong-ref: 7 656B 2 classes (Total instances: 7)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 6
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] C1
|
||||
|
||||
CLASS: C1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C1
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 6/ 696B] [ 6/ 696B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] C1
|
||||
Total - All: 7 704B 2 classes (Total instances: 7)
|
||||
Total - Strong-ref: 7 704B 2 classes (Total instances: 7)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 6
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] C1
|
||||
|
||||
CLASS: C1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C1
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 9/ 828B] [ 9/ 828B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] C1
|
||||
3: [ 1/ 8B] [ 1/ 8B] C2
|
||||
Total - All: 11 844B 3 classes (Total instances: 11)
|
||||
Total - Strong-ref: 11 844B 3 classes (Total instances: 11)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 9
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] C1
|
||||
--> [1/8B] C2
|
||||
|
||||
CLASS: C1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C1
|
||||
|
||||
CLASS: C2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Java Frame: com.android.tools.idea.diagnostics.hprof.HProfBuilder.com.android.tools.idea.diagnostics.hprof.HProfBuilder.addStackTrace
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C2
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 9/ 972B] [ 9/ 972B] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] C1
|
||||
3: [ 1/ 8B] [ 1/ 8B] C2
|
||||
Total - All: 11 988B 3 classes (Total instances: 11)
|
||||
Total - Strong-ref: 11 988B 3 classes (Total instances: 11)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 9
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] C1
|
||||
--> [1/8B] C2
|
||||
|
||||
CLASS: C1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C1
|
||||
|
||||
CLASS: C2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Java Frame: com.intellij.diagnostic.hprof.HProfBuilder.com.intellij.diagnostic.hprof.HProfBuilder.addStackTrace
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C2
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 9/1.04KB] [ 9/1.04KB] java.lang.Class
|
||||
2: [ 1/ 8B] [ 1/ 8B] C1
|
||||
3: [ 1/ 8B] [ 1/ 8B] C2
|
||||
Total - All: 11 1.06KB 3 classes (Total instances: 11)
|
||||
Total - Strong-ref: 11 1.06KB 3 classes (Total instances: 11)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 9
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [1/8B] C1
|
||||
--> [1/8B] C2
|
||||
|
||||
CLASS: C1 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Unknown
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C1
|
||||
|
||||
CLASS: C2 (1 objects)
|
||||
Root 1:
|
||||
[ 1/100%/ 8B] 8B 1 ROOT: Java Frame: com.android.tools.idea.diagnostics.hprof.HProfBuilder.com.android.tools.idea.diagnostics.hprof.HProfBuilder.addStackTrace
|
||||
[ 1/100%/ 8B] 8B 1 * (root): C2
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 18/1.65KB] [ 18/1.65KB] java.lang.Class
|
||||
2: [ 12/ 192B] [ 9/ 144B] TestString
|
||||
3: [ 8/ 192B] [ 6/ 144B] TestClassB
|
||||
4: [ 4/ 128B] [ 3/ 96B] TestClassA
|
||||
5: [ 3/ 60B] [ 3/ 60B] java.lang.String
|
||||
6: [ 3/ 100B] [ 3/ 100B] char[]
|
||||
7: [ 3/ 48B] [ 3/ 48B] MyRef
|
||||
8: [ 2/ 16B] [ 2/ 16B] java.lang.ref.WeakReference
|
||||
9: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Lock
|
||||
10: [ 1/ 16B] [ 1/ 16B] java.util.Arrays$ArrayList
|
||||
11: [ 1/ 44B] [ 1/ 44B] java.lang.Object[]
|
||||
12: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Null
|
||||
Total - All: 57 2.46KB 12 classes (Total instances: 57)
|
||||
Total - Strong-ref: 51 2.34KB 12 classes (Total instances: 51)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 18
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 128B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [8/192B] TestClassB
|
||||
--> [4/128B] TestClassA
|
||||
--> [12/192B] TestString
|
||||
|
||||
CLASS: TestClassB (8 objects)
|
||||
Root 1:
|
||||
[ 6/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 6/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 6/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 2/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 2/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 2/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * \-a3b: TestClassB
|
||||
Root 2:
|
||||
[ 2/ 25%/ 48B] 292B 1 ROOT: Unknown
|
||||
[ 2/ 25%/ 48B] 292B 1 (root): TestClassA
|
||||
[ 1/ 12%/ 24B] 96B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 96B 1 * \-a3b: TestClassB
|
||||
|
||||
CLASS: TestClassA (4 objects)
|
||||
Root 1:
|
||||
[ 3/ 75%/ 96B] 524B 1 ROOT: Global JNI
|
||||
[ 3/ 75%/ 96B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 3/ 75%/ 96B] 508B 1 a: java.lang.Object[]
|
||||
[ 1/ 25%/ 32B] 176B 1 +-[]: MyRef
|
||||
[ 1/ 25%/ 32B] 160B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 144B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 128B 1 * | referent: TestClassA
|
||||
[ 1/ 25%/ 32B] 128B 1 * +-[]: TestClassA
|
||||
[ 1/ 25%/ 32B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 1/ 25%/ 32B] 128B 1 * (weak): TestClassA
|
||||
Root 2:
|
||||
[ 1/ 25%/ 32B] 292B 1 ROOT: Unknown
|
||||
[ 1/ 25%/ 32B] 292B 1 * (root): TestClassA
|
||||
|
||||
CLASS: TestString (12 objects)
|
||||
Root 1:
|
||||
[ 9/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 9/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 9/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 3/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 3/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 3/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 3/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 3/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * \-a1string: TestString
|
||||
Root 2:
|
||||
[ 3/ 25%/ 48B] 292B 1 ROOT: Unknown
|
||||
[ 3/ 25%/ 48B] 292B 1 (root): TestClassA
|
||||
[ 1/ 8%/ 16B] 96B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 72B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 96B 1 +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 72B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 68B 1 * \-a1string: TestString
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 18/1.94KB] [ 18/1.94KB] java.lang.Class
|
||||
2: [ 12/ 192B] [ 9/ 144B] TestString
|
||||
3: [ 8/ 192B] [ 6/ 144B] TestClassB
|
||||
4: [ 4/ 128B] [ 3/ 96B] TestClassA
|
||||
5: [ 3/ 66B] [ 3/ 66B] java.lang.String
|
||||
6: [ 3/ 68B] [ 3/ 68B] byte[]
|
||||
7: [ 3/ 48B] [ 3/ 48B] MyRef
|
||||
8: [ 2/ 16B] [ 2/ 16B] java.lang.ref.WeakReference
|
||||
9: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Lock
|
||||
10: [ 1/ 16B] [ 1/ 16B] java.util.Arrays$ArrayList
|
||||
11: [ 1/ 44B] [ 1/ 44B] java.lang.Object[]
|
||||
12: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Null
|
||||
Total - All: 57 2.73KB 12 classes (Total instances: 57)
|
||||
Total - Strong-ref: 51 2.6KB 12 classes (Total instances: 51)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 18
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 128B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [8/192B] TestClassB
|
||||
--> [4/128B] TestClassA
|
||||
--> [12/192B] TestString
|
||||
|
||||
CLASS: TestClassB (8 objects)
|
||||
Root 1:
|
||||
[ 6/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 6/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 6/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 2/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 2/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 2/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * \-a3b: TestClassB
|
||||
Root 2:
|
||||
[ 2/ 25%/ 48B] 272B 1 ROOT: Unknown
|
||||
[ 2/ 25%/ 48B] 272B 1 (root): TestClassA
|
||||
[ 1/ 12%/ 24B] 88B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 88B 1 * \-a3b: TestClassB
|
||||
|
||||
CLASS: TestClassA (4 objects)
|
||||
Root 1:
|
||||
[ 3/ 75%/ 96B] 524B 1 ROOT: Global JNI
|
||||
[ 3/ 75%/ 96B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 3/ 75%/ 96B] 508B 1 a: java.lang.Object[]
|
||||
[ 1/ 25%/ 32B] 176B 1 +-[]: MyRef
|
||||
[ 1/ 25%/ 32B] 160B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 144B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 128B 1 * | referent: TestClassA
|
||||
[ 1/ 25%/ 32B] 128B 1 * +-[]: TestClassA
|
||||
[ 1/ 25%/ 32B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 1/ 25%/ 32B] 128B 1 * (weak): TestClassA
|
||||
Root 2:
|
||||
[ 1/ 25%/ 32B] 272B 1 ROOT: Unknown
|
||||
[ 1/ 25%/ 32B] 272B 1 * (root): TestClassA
|
||||
|
||||
CLASS: TestString (12 objects)
|
||||
Root 1:
|
||||
[ 9/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 9/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 9/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 3/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 3/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | \-a1string: TestString
|
||||
[ 3/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 3/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 3/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * b2string: TestString
|
||||
Root 2:
|
||||
[ 3/ 25%/ 48B] 272B 1 ROOT: Unknown
|
||||
[ 3/ 25%/ 48B] 272B 1 (root): TestClassA
|
||||
[ 1/ 8%/ 16B] 88B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 64B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 88B 1 +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 64B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 64B 1 * \-a1string: TestString
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 2147483647 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [ 18/2.08KB] [ 18/2.08KB] java.lang.Class
|
||||
2: [ 12/ 192B] [ 9/ 144B] TestString
|
||||
3: [ 8/ 192B] [ 6/ 144B] TestClassB
|
||||
4: [ 4/ 128B] [ 3/ 96B] TestClassA
|
||||
5: [ 3/ 63B] [ 3/ 63B] java.lang.String
|
||||
6: [ 3/ 68B] [ 3/ 68B] byte[]
|
||||
7: [ 3/ 48B] [ 3/ 48B] MyRef
|
||||
8: [ 2/ 16B] [ 2/ 16B] java.lang.ref.WeakReference
|
||||
9: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Lock
|
||||
10: [ 1/ 16B] [ 1/ 16B] java.util.Arrays$ArrayList
|
||||
11: [ 1/ 44B] [ 1/ 44B] java.lang.Object[]
|
||||
12: [ 1/ 8B] [ 1/ 8B] java.lang.ref.ReferenceQueue$Null
|
||||
Total - All: 57 2.87KB 12 classes (Total instances: 57)
|
||||
Total - Strong-ref: 51 2.74KB 12 classes (Total instances: 51)
|
||||
Unreachable objects: 0 0B
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 18
|
||||
Finalizable size: 0B
|
||||
Soft-reachable size: 0B
|
||||
Weak-reachable size: 128B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [8/192B] TestClassB
|
||||
--> [4/128B] TestClassA
|
||||
--> [12/192B] TestString
|
||||
|
||||
CLASS: TestClassB (8 objects)
|
||||
Root 1:
|
||||
[ 6/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 6/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 6/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 2/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 2/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 2/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * | +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * | \-a3b: TestClassB
|
||||
[ 2/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 2/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 12%/ 24B] 40B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 40B 1 * \-a3b: TestClassB
|
||||
Root 2:
|
||||
[ 2/ 25%/ 48B] 272B 1 ROOT: Unknown
|
||||
[ 2/ 25%/ 48B] 272B 1 (root): TestClassA
|
||||
[ 1/ 12%/ 24B] 88B 1 * +-a2b: TestClassB
|
||||
[ 1/ 12%/ 24B] 88B 1 * \-a3b: TestClassB
|
||||
|
||||
CLASS: TestClassA (4 objects)
|
||||
Root 1:
|
||||
[ 3/ 75%/ 96B] 524B 1 ROOT: Global JNI
|
||||
[ 3/ 75%/ 96B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 3/ 75%/ 96B] 508B 1 a: java.lang.Object[]
|
||||
[ 1/ 25%/ 32B] 176B 1 +-[]: MyRef
|
||||
[ 1/ 25%/ 32B] 160B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 144B 1 | referent: MyRef
|
||||
[ 1/ 25%/ 32B] 128B 1 * | referent: TestClassA
|
||||
[ 1/ 25%/ 32B] 128B 1 * +-[]: TestClassA
|
||||
[ 1/ 25%/ 32B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 1/ 25%/ 32B] 128B 1 * (weak): TestClassA
|
||||
Root 2:
|
||||
[ 1/ 25%/ 32B] 272B 1 ROOT: Unknown
|
||||
[ 1/ 25%/ 32B] 272B 1 * (root): TestClassA
|
||||
|
||||
CLASS: TestString (12 objects)
|
||||
Root 1:
|
||||
[ 9/ 75%/ 144B] 524B 1 ROOT: Global JNI
|
||||
[ 9/ 75%/ 144B] 524B 1 (root): java.util.Arrays$ArrayList
|
||||
[ 9/ 75%/ 144B] 508B 1 a: java.lang.Object[]
|
||||
[ 3/ 25%/ 48B] 176B 1 +-[]: MyRef
|
||||
[ 3/ 25%/ 48B] 160B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 144B 1 | referent: MyRef
|
||||
[ 3/ 25%/ 48B] 128B 1 | referent: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 3/ 25%/ 48B] 128B 1 +-[]: TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 | +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * | +-a1string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 | \-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 3/ 25%/ 48B] 152B 1 \-[]: java.lang.ref.WeakReference
|
||||
[ 3/ 25%/ 48B] 128B 1 ! (weak): TestClassA
|
||||
[ 1/ 8%/ 16B] 40B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 40B 1 +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 16B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 16B 1 * \-a1string: TestString
|
||||
Root 2:
|
||||
[ 3/ 25%/ 48B] 272B 1 ROOT: Unknown
|
||||
[ 3/ 25%/ 48B] 272B 1 (root): TestClassA
|
||||
[ 1/ 8%/ 16B] 88B 1 +-a2b: TestClassB
|
||||
[ 1/ 8%/ 16B] 64B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 88B 1 +-a3b: TestClassB
|
||||
[ 1/ 8%/ 16B] 64B 1 * | b2string: TestString
|
||||
[ 1/ 8%/ 16B] 64B 1 * \-a1string: TestString
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
==================== HISTOGRAM ====================
|
||||
Histogram. Top 50 by instance count [All-objects] [Only-strong-ref]:
|
||||
1: [3.45K/ 235KB] [1.92K/ 173KB] char[]
|
||||
2: [3.43K/68.7KB] [1.91K/38.2KB] java.lang.String
|
||||
3: [ 711/71.1KB] [ 711/71.1KB] java.lang.Class
|
||||
4: [ 632/61.7KB] [ 335/21.5KB] java.lang.Object[]
|
||||
5: [ 622/32.3KB] [ 34/1.76KB] java.util.LinkedHashMap$Entry
|
||||
6: [ 425/ 132KB] [ 408/ 129KB] byte[]
|
||||
7: [ 358/12.8KB] [ 354/12.7KB] java.util.HashMap$Node
|
||||
8: [ 290/20.1KB] [ 128/10.3KB] java.lang.String[]
|
||||
9: [ 256/3.07KB] [ 256/3.07KB] java.lang.Integer
|
||||
10: [ 157/5.65KB] [ 157/5.65KB] java.util.Hashtable$Entry
|
||||
11: [ 136/ 7.4KB] [ 19/5.48KB] int[]
|
||||
12: [ 109/11.4KB] [ 0/ 0B] java.lang.reflect.Field
|
||||
13: [ 106/5.08KB] [ 83/3.98KB] java.lang.ref.SoftReference
|
||||
14: [ 105/3.78KB] [ 104/3.74KB] java.util.concurrent.ConcurrentHashMap$Node
|
||||
15: [ 84/ 672B] [ 73/ 584B] java.lang.Object
|
||||
16: [ 74/7.69KB] [ 74/7.69KB] java.net.URL
|
||||
17: [ 48/2.68KB] [ 0/ 0B] java.lang.ref.Finalizer
|
||||
18: [ 39/2.18KB] [ 39/2.18KB] sun.util.locale.LocaleObjectCache$CacheEntry
|
||||
19: [ 37/1.66KB] [ 37/1.66KB] java.io.ObjectStreamField
|
||||
20: [ 35/2.83KB] [ 35/2.83KB] sun.misc.URLClassPath$JarLoader
|
||||
21: [ 34/ 816B] [ 34/ 816B] java.io.ExpiringCache$Entry
|
||||
22: [ 32/1.79KB] [ 30/1.68KB] java.util.HashMap
|
||||
23: [ 24/16.5KB] [ 22/ 8.2KB] java.util.HashMap$Node[]
|
||||
24: [ 24/ 192B] [ 24/ 192B] java.lang.ref.ReferenceQueue$Lock
|
||||
25: [ 23/1.93KB] [ 0/ 0B] java.lang.Class$ReflectionData
|
||||
26: [ 22/ 704B] [ 22/ 704B] java.lang.ref.ReferenceQueue
|
||||
27: [ 22/ 2.5KB] [ 0/ 0B] java.lang.reflect.Constructor
|
||||
28: [ 21/1.93KB] [ 20/1.84KB] java.util.concurrent.ConcurrentHashMap
|
||||
29: [ 21/ 420B] [ 17/ 340B] java.util.jar.Attributes$Name
|
||||
30: [ 19/ 532B] [ 19/ 532B] java.util.Locale$LocaleKey
|
||||
31: [ 19/ 684B] [ 19/ 684B] java.util.Locale
|
||||
32: [ 19/ 855B] [ 19/ 855B] sun.util.locale.BaseLocale$Key
|
||||
33: [ 19/ 836B] [ 19/ 836B] sun.util.locale.BaseLocale
|
||||
34: [ 17/1.17KB] [ 14/ 966B] sun.nio.cs.UTF_8$Encoder
|
||||
35: [ 16/ 304B] [ 1/ 92B] java.lang.Class[]
|
||||
36: [ 16/3.77KB] [ 15/3.63KB] java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
37: [ 15/ 2.1KB] [ 15/ 2.1KB] java.util.WeakHashMap$Entry[]
|
||||
38: [ 15/ 960B] [ 15/ 960B] java.util.WeakHashMap
|
||||
39: [ 15/ 540B] [ 15/ 540B] java.io.File
|
||||
40: [ 14/ 238B] [ 14/ 238B] sun.misc.MetaIndex
|
||||
41: [ 14/ 336B] [ 14/ 336B] java.util.ArrayList
|
||||
42: [ 13/ 780B] [ 8/ 480B] java.security.AccessControlContext
|
||||
43: [ 13/ 637B] [ 0/ 0B] java.util.zip.ZipFile$ZipFileInputStream
|
||||
44: [ 13/ 452B] [ 13/ 452B] java.io.ObjectStreamField[]
|
||||
45: [ 12/ 492B] [ 12/ 492B] java.util.zip.ZipCoder
|
||||
46: [ 12/ 288B] [ 12/ 288B] java.util.ArrayDeque
|
||||
47: [ 12/ 984B] [ 12/ 984B] java.util.jar.JarFile
|
||||
48: [ 11/ 308B] [ 0/ 0B] sun.reflect.NativeConstructorAccessorImpl
|
||||
49: [ 11/ 220B] [ 0/ 0B] java.lang.reflect.Constructor[]
|
||||
50: [ 11/ 176B] [ 0/ 0B] sun.reflect.DelegatingConstructorAccessorImpl
|
||||
[...removed 228 lines...]
|
||||
Total - All: 12.1K 763KB 278 classes (Total instances: 12115)
|
||||
Total - Strong-ref: 7.48K 549KB 251 classes (Total instances: 7485)
|
||||
|
||||
Top 10 by size:
|
||||
1: [3.45K/ 235KB] [1.92K/ 173KB] char[]
|
||||
2: [ 425/ 132KB] [ 408/ 129KB] byte[]
|
||||
3: [ 711/71.1KB] [ 711/71.1KB] java.lang.Class
|
||||
4: [3.43K/68.7KB] [1.91K/38.2KB] java.lang.String
|
||||
5: [ 632/61.7KB] [ 335/21.5KB] java.lang.Object[]
|
||||
6: [ 622/32.3KB] [ 34/1.76KB] java.util.LinkedHashMap$Entry
|
||||
7: [ 290/20.1KB] [ 128/10.3KB] java.lang.String[]
|
||||
8: [ 24/16.5KB] [ 22/ 8.2KB] java.util.HashMap$Node[]
|
||||
9: [ 358/12.8KB] [ 354/12.7KB] java.util.HashMap$Node
|
||||
10: [ 109/11.4KB] [ 0/ 0B] java.lang.reflect.Field
|
||||
Unreachable objects: 1.37K 85.0KB
|
||||
=================== HEAP SUMMARY ==================
|
||||
Class count: 702
|
||||
Finalizable size: 8.36KB
|
||||
Soft-reachable size: 120KB
|
||||
Weak-reachable size: 0B
|
||||
Reachable only from disposer tree: 0
|
||||
======== INSTANCES OF EACH NOMINATED CLASS ========
|
||||
Nominated classes:
|
||||
--> [3.45K/235KB] char[]
|
||||
--> [425/132KB] byte[]
|
||||
--> [711/71.1KB] java.lang.Class
|
||||
--> [3.43K/68.7KB] java.lang.String
|
||||
--> [632/61.7KB] java.lang.Object[]
|
||||
--> [622/32.3KB] java.util.LinkedHashMap$Entry
|
||||
--> [35/2.83KB] sun.misc.URLClassPath$JarLoader
|
||||
--> [39/2.18KB] sun.util.locale.LocaleObjectCache$CacheEntry
|
||||
--> [17/1.17KB] sun.nio.cs.UTF_8$Encoder
|
||||
--> [19/855B] sun.util.locale.BaseLocale$Key
|
||||
--> [19/836B] sun.util.locale.BaseLocale
|
||||
--> [6/426B] java.nio.DirectByteBuffer
|
||||
|
||||
CLASS: char[] (3113 objects)
|
||||
Root 1:
|
||||
[1.17K/ 37%/46.2KB] 120KB 1 ROOT: Static field: java.util.ResourceBundle.cacheList
|
||||
[1.17K/ 37%/46.2KB] 120KB 1 (root): java.util.concurrent.ConcurrentHashMap
|
||||
[1.17K/ 37%/46.2KB] 120KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[1.17K/ 37%/46.2KB] 119KB 1 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[1.16K/ 37%/46.1KB] 119KB 1 val: java.util.ResourceBundle$BundleReference
|
||||
[1.16K/ 37%/46.1KB] 119KB 1 ! (soft): sun.util.resources.TimeZoneNames
|
||||
[1.16K/ 37%/46.1KB] 119KB 1 lookup: java.util.LinkedHashMap
|
||||
[1.16K/ 37%/45.8KB] 118KB 1 table: java.util.HashMap$Node[]
|
||||
[1.16K/ 37%/45.8KB] 108KB 422 []: java.util.LinkedHashMap$Entry
|
||||
[ 501/ 16%/19.4KB] 37.2KB 122 value: java.lang.String[]
|
||||
[ 501/ 16%/19.4KB] 29.9KB 501 []: java.lang.String
|
||||
[ 501/ 16%/19.4KB] 19.9KB 501 * value: char[]
|
||||
Root 2:
|
||||
[ 600/ 19%/24.4KB] 41.8KB 1 ROOT: Static field: sun.util.calendar.ZoneInfoFile.regions
|
||||
[ 600/ 19%/24.4KB] 41.8KB 1 (root): java.lang.String[]
|
||||
[ 600/ 19%/24.4KB] 37.0KB 600 []: java.lang.String
|
||||
[ 600/ 19%/24.4KB] 25.0KB 600 * value: char[]
|
||||
|
||||
CLASS: byte[] (425 objects)
|
||||
Root 1:
|
||||
[ 385/ 90%/96.2KB] 99.4KB 1 ROOT: Static field: sun.util.calendar.ZoneInfoFile.ruleArray
|
||||
[ 385/ 90%/96.2KB] 99.4KB 1 (root): byte[][]
|
||||
[ 385/ 90%/96.2KB] 96.3KB 385 * []: byte[]
|
||||
Root 2:
|
||||
[ 3/ 0%/8.35KB] 25.1KB 1 ROOT: Static field: java.lang.System.err
|
||||
[ 3/ 0%/8.35KB] 25.1KB 1 (root): java.io.PrintStream
|
||||
[ 2/ 0%/8.21KB] 8.44KB 1 charOut: java.io.OutputStreamWriter
|
||||
[ 2/ 0%/8.21KB] 8.41KB 1 se: sun.nio.cs.StreamEncoder
|
||||
[ 1/ 0%/ 8.2KB] 8.25KB 1 bb: java.nio.HeapByteBuffer
|
||||
[ 1/ 0%/ 8.2KB] 8.2KB 1 * hb: byte[]
|
||||
|
||||
CLASS: java.lang.Class (711 objects)
|
||||
Root 1:
|
||||
[ 1/ 0%/ 100B] 100B 1 ROOT: Sticky class
|
||||
[ 1/ 0%/ 100B] 100B 1 * (root): java.lang.Class
|
||||
Root 2:
|
||||
[ 1/ 0%/ 100B] 100B 1 ROOT: Class definition: [Lsun.launcher.LauncherHelper;
|
||||
[ 1/ 0%/ 100B] 100B 1 * (root): java.lang.Class
|
||||
|
||||
CLASS: java.lang.String (3097 objects)
|
||||
Root 1:
|
||||
[1.17K/ 37%/23.4KB] 120KB 1 ROOT: Static field: java.util.ResourceBundle.cacheList
|
||||
[1.17K/ 37%/23.4KB] 120KB 1 (root): java.util.concurrent.ConcurrentHashMap
|
||||
[1.17K/ 37%/23.4KB] 120KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[1.17K/ 37%/23.4KB] 119KB 1 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[1.16K/ 37%/23.3KB] 119KB 1 val: java.util.ResourceBundle$BundleReference
|
||||
[1.16K/ 37%/23.3KB] 119KB 1 ! (soft): sun.util.resources.TimeZoneNames
|
||||
[1.16K/ 37%/23.3KB] 119KB 1 lookup: java.util.LinkedHashMap
|
||||
[1.16K/ 37%/23.2KB] 118KB 1 table: java.util.HashMap$Node[]
|
||||
[1.16K/ 37%/23.2KB] 108KB 422 []: java.util.LinkedHashMap$Entry
|
||||
[ 501/ 16%/10.0KB] 37.2KB 122 value: java.lang.String[]
|
||||
[ 501/ 16%/10.0KB] 29.9KB 501 * []: java.lang.String
|
||||
Root 2:
|
||||
[ 600/ 19%/12.0KB] 41.8KB 1 ROOT: Static field: sun.util.calendar.ZoneInfoFile.regions
|
||||
[ 600/ 19%/12.0KB] 41.8KB 1 (root): java.lang.String[]
|
||||
[ 600/ 19%/12.0KB] 37.0KB 600 * []: java.lang.String
|
||||
|
||||
CLASS: java.lang.Object[] (335 objects)
|
||||
Root 1:
|
||||
[ 292/ 87%/17.2KB] 24.4KB 1 ROOT: Static field: java.nio.charset.Charset.standardProvider
|
||||
[ 292/ 87%/17.2KB] 24.4KB 1 (root): sun.nio.cs.StandardCharsets
|
||||
[ 212/ 63%/14.2KB] 18.9KB 1 aliasMap: sun.nio.cs.StandardCharsets$Aliases
|
||||
[ 212/ 63%/14.2KB] 18.8KB 1 ht: java.lang.Object[]
|
||||
[ 211/ 62%/6.05KB] 10.6KB 193 []: java.lang.Object[]
|
||||
[ 18/ 5%/ 512B] 788B 17 []: java.lang.Object[]
|
||||
[ 1/ 0%/ 28B] 28B 1 * []: java.lang.Object[]
|
||||
Root 2:
|
||||
[ 23/ 6%/3.12KB] 54.2KB 1 ROOT: Static field: java.lang.ClassLoader.scl
|
||||
[ 23/ 6%/3.12KB] 54.2KB 1 (root): sun.misc.Launcher$AppClassLoader
|
||||
[ 14/ 4%/2.24KB] 31.4KB 1 ucp: sun.misc.URLClassPath
|
||||
[ 12/ 3%/1.72KB] 18.1KB 1 loaders: java.util.ArrayList
|
||||
[ 12/ 3%/1.72KB] 18.1KB 1 elementData: java.lang.Object[]
|
||||
[ 11/ 3%/1.54KB] 13.8KB 11 []: sun.misc.URLClassPath$JarLoader
|
||||
[ 11/ 3%/1.54KB] 9.3KB 11 jar: java.util.jar.JarFile
|
||||
[ 11/ 3%/1.54KB] 2.07KB 11 inflaterCache: java.util.ArrayDeque
|
||||
[ 11/ 3%/1.54KB] 1.81KB 11 * elements: java.lang.Object[]
|
||||
|
||||
CLASS: java.util.LinkedHashMap$Entry (622 objects)
|
||||
Root 1:
|
||||
[ 588/ 94%/30.5KB] 120KB 1 ROOT: Static field: java.util.ResourceBundle.cacheList
|
||||
[ 588/ 94%/30.5KB] 120KB 1 (root): java.util.concurrent.ConcurrentHashMap
|
||||
[ 588/ 94%/30.5KB] 120KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[ 588/ 94%/30.5KB] 119KB 1 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[ 588/ 94%/30.5KB] 119KB 1 val: java.util.ResourceBundle$BundleReference
|
||||
[ 588/ 94%/30.5KB] 119KB 1 ! (soft): sun.util.resources.TimeZoneNames
|
||||
[ 588/ 94%/30.5KB] 119KB 1 lookup: java.util.LinkedHashMap
|
||||
[ 584/ 93%/30.3KB] 118KB 1 table: java.util.HashMap$Node[]
|
||||
[ 584/ 93%/30.3KB] 110KB 447 []: java.util.LinkedHashMap$Entry
|
||||
[ 54/ 8%/ 2.8KB] 8.81KB 53 next: java.util.LinkedHashMap$Entry
|
||||
[ 1/ 0%/ 52B] 108B 1 * next: java.util.LinkedHashMap$Entry
|
||||
Root 2:
|
||||
[ 34/ 5%/1.76KB] 16.9KB 1 ROOT: Static field: java.io.File.fs
|
||||
[ 34/ 5%/1.76KB] 16.9KB 1 (root): java.io.UnixFileSystem
|
||||
[ 32/ 5%/1.66KB] 15.6KB 1 cache: java.io.ExpiringCache
|
||||
[ 32/ 5%/1.66KB] 15.6KB 1 map: java.io.ExpiringCache$1
|
||||
[ 27/ 4%/ 1.4KB] 13.2KB 1 table: java.util.HashMap$Node[]
|
||||
[ 27/ 4%/ 1.4KB] 12.7KB 22 []: java.util.LinkedHashMap$Entry
|
||||
[ 2/ 0%/ 104B] 1.01KB 2 * before: java.util.LinkedHashMap$Entry
|
||||
|
||||
CLASS: sun.misc.URLClassPath$JarLoader (35 objects)
|
||||
Root 1:
|
||||
[ 30/ 85%/2.43KB] 54.2KB 1 ROOT: Static field: java.lang.ClassLoader.scl
|
||||
[ 30/ 85%/2.43KB] 54.2KB 1 (root): sun.misc.Launcher$AppClassLoader
|
||||
[ 20/ 57%/1.62KB] 31.4KB 1 ucp: sun.misc.URLClassPath
|
||||
[ 20/ 57%/1.62KB] 18.1KB 1 loaders: java.util.ArrayList
|
||||
[ 20/ 57%/1.62KB] 18.1KB 1 elementData: java.lang.Object[]
|
||||
[ 20/ 57%/1.62KB] 17.7KB 20 * []: sun.misc.URLClassPath$JarLoader
|
||||
Root 2:
|
||||
[ 5/ 14%/ 405B] 9.42KB 1 ROOT: Static field: sun.misc.Launcher$BootClassPathHolder.bcp
|
||||
[ 5/ 14%/ 405B] 9.42KB 1 (root): sun.misc.URLClassPath
|
||||
[ 5/ 14%/ 405B] 5.03KB 1 loaders: java.util.ArrayList
|
||||
[ 5/ 14%/ 405B] 5.01KB 1 elementData: java.lang.Object[]
|
||||
[ 5/ 14%/ 405B] 4.92KB 5 * []: sun.misc.URLClassPath$JarLoader
|
||||
|
||||
CLASS: sun.util.locale.LocaleObjectCache$CacheEntry (39 objects)
|
||||
Root 1:
|
||||
[ 19/ 48%/1.06KB] 2.7KB 1 ROOT: Static field: java.util.Locale.LOCALECACHE
|
||||
[ 19/ 48%/1.06KB] 2.7KB 1 (root): java.util.Locale$Cache
|
||||
[ 19/ 48%/1.06KB] 2.64KB 1 map: java.util.concurrent.ConcurrentHashMap
|
||||
[ 19/ 48%/1.06KB] 2.54KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[ 19/ 48%/1.06KB] 2.28KB 16 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[ 16/ 41%/ 896B] 896B 16 * val: sun.util.locale.LocaleObjectCache$CacheEntry
|
||||
Root 2:
|
||||
[ 19/ 48%/1.06KB] 6.73KB 1 ROOT: Static field: sun.util.locale.BaseLocale.CACHE
|
||||
[ 19/ 48%/1.06KB] 6.73KB 1 (root): sun.util.locale.BaseLocale$Cache
|
||||
[ 19/ 48%/1.06KB] 6.66KB 1 map: java.util.concurrent.ConcurrentHashMap
|
||||
[ 19/ 48%/1.06KB] 6.57KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[ 19/ 48%/1.06KB] 6.3KB 12 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[ 12/ 30%/ 672B] 672B 12 * val: sun.util.locale.LocaleObjectCache$CacheEntry
|
||||
|
||||
CLASS: sun.nio.cs.UTF_8$Encoder (17 objects)
|
||||
Root 1:
|
||||
[ 12/ 70%/ 828B] 54.2KB 1 ROOT: Static field: java.lang.ClassLoader.scl
|
||||
[ 12/ 70%/ 828B] 54.2KB 1 (root): sun.misc.Launcher$AppClassLoader
|
||||
[ 11/ 64%/ 759B] 31.4KB 1 ucp: sun.misc.URLClassPath
|
||||
[ 11/ 64%/ 759B] 18.1KB 1 loaders: java.util.ArrayList
|
||||
[ 11/ 64%/ 759B] 18.1KB 1 elementData: java.lang.Object[]
|
||||
[ 11/ 64%/ 759B] 13.8KB 11 []: sun.misc.URLClassPath$JarLoader
|
||||
[ 11/ 64%/ 759B] 9.3KB 11 jar: java.util.jar.JarFile
|
||||
[ 11/ 64%/ 759B] 1.45KB 11 zc: java.util.zip.ZipCoder
|
||||
[ 11/ 64%/ 759B] 968B 11 * enc: sun.nio.cs.UTF_8$Encoder
|
||||
Root 2:
|
||||
[ 1/ 5%/ 69B] 25.1KB 1 ROOT: Static field: java.lang.System.err
|
||||
[ 1/ 5%/ 69B] 25.1KB 1 (root): java.io.PrintStream
|
||||
[ 1/ 5%/ 69B] 8.44KB 1 charOut: java.io.OutputStreamWriter
|
||||
[ 1/ 5%/ 69B] 8.41KB 1 se: sun.nio.cs.StreamEncoder
|
||||
[ 1/ 5%/ 69B] 88B 1 * encoder: sun.nio.cs.UTF_8$Encoder
|
||||
|
||||
CLASS: sun.util.locale.BaseLocale$Key (19 objects)
|
||||
Root 1:
|
||||
[ 19/100%/ 855B] 6.73KB 1 ROOT: Static field: sun.util.locale.BaseLocale.CACHE
|
||||
[ 19/100%/ 855B] 6.73KB 1 (root): sun.util.locale.BaseLocale$Cache
|
||||
[ 19/100%/ 855B] 6.66KB 1 map: java.util.concurrent.ConcurrentHashMap
|
||||
[ 19/100%/ 855B] 6.57KB 1 table: java.util.concurrent.ConcurrentHashMap$Node[]
|
||||
[ 19/100%/ 855B] 6.3KB 12 []: java.util.concurrent.ConcurrentHashMap$Node
|
||||
[ 12/ 63%/ 540B] 2.88KB 12 * key: sun.util.locale.BaseLocale$Key
|
||||
|
||||
CLASS: sun.util.locale.BaseLocale (19 objects)
|
||||
Root 1:
|
||||
[ 1/ 5%/ 44B] 116B 1 ROOT: Static field: java.util.Locale.FRANCE
|
||||
[ 1/ 5%/ 44B] 116B 1 (root): java.util.Locale
|
||||
[ 1/ 5%/ 44B] 80B 1 * baseLocale: sun.util.locale.BaseLocale
|
||||
Root 2:
|
||||
[ 1/ 5%/ 44B] 80B 1 ROOT: Static field: java.util.Locale.ROOT
|
||||
[ 1/ 5%/ 44B] 80B 1 (root): java.util.Locale
|
||||
[ 1/ 5%/ 44B] 44B 1 * baseLocale: sun.util.locale.BaseLocale
|
||||
|
||||
CLASS: java.nio.DirectByteBuffer (6 objects)
|
||||
Root 1:
|
||||
[ 1/ 16%/ 79B] 216B 1 ROOT: Static field: sun.misc.PerfCounter$CoreCounters.zfc
|
||||
[ 1/ 16%/ 79B] 216B 1 (root): sun.misc.PerfCounter
|
||||
[ 1/ 16%/ 79B] 128B 1 lb: java.nio.DirectLongBufferU
|
||||
[ 1/ 16%/ 79B] 72B 1 * att: java.nio.DirectByteBuffer
|
||||
Root 2:
|
||||
[ 1/ 16%/ 79B] 260B 1 ROOT: Static field: sun.misc.PerfCounter$CoreCounters.rcbt
|
||||
[ 1/ 16%/ 79B] 260B 1 (root): sun.misc.PerfCounter
|
||||
[ 1/ 16%/ 79B] 128B 1 lb: java.nio.DirectLongBufferU
|
||||
[ 1/ 16%/ 79B] 72B 1 * att: java.nio.DirectByteBuffer
|
||||
|
||||
================== DISPOSER TREE ==================
|
||||
============== DISPOSER TREE SUMMARY ==============
|
||||
================= DISPOSED OBJECTS ================
|
||||
Count of disposed-but-strong-referenced objects: 0
|
||||
|
||||
Disposed-but-strong-referenced dominator object count: 0
|
||||
Disposed-but-strong-referenced dominator sub-graph size: 0B
|
||||
|
||||
============ DOMINATOR TREE FLAME GRAPH ===========
|
||||
1
|
||||
root
|
||||
0 153607 0
|
||||
|
||||
Binary file not shown.
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.util.FileBackedHashMap
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
class FileBackedHashMapTest {
|
||||
|
||||
private lateinit var map: FileBackedHashMap
|
||||
private lateinit var channel: FileChannel
|
||||
private val tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
tmpFolder.create()
|
||||
channel = FileChannel.open(tmpFolder.newFile().toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)
|
||||
map = FileBackedHashMap.createEmpty(channel, 10, 4, 4)
|
||||
map.put(10).putInt(110)
|
||||
map.put(11).putInt(111)
|
||||
map.put(20).putInt(120)
|
||||
map.put(9).putInt(19)
|
||||
map.put(1000).putInt(11000)
|
||||
map.put(1234).putInt(11234)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
channel.close()
|
||||
tmpFolder.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get() {
|
||||
assertEquals(110, map[10]!!.int)
|
||||
assertEquals(111, map[11]!!.int)
|
||||
assertEquals(110, map[10]!!.int)
|
||||
assertEquals(19, map[9]!!.int)
|
||||
assertEquals(11000, map[1000]!!.int)
|
||||
assertNull(map[8])
|
||||
assertNull(map[18])
|
||||
assertNull(map[0])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun put() {
|
||||
map.put(1000).putInt(12000)
|
||||
assertEquals(12000, map[1000]!!.int)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun containsKey() {
|
||||
assertTrue(map.containsKey(10))
|
||||
assertTrue(map.containsKey(11))
|
||||
assertTrue(map.containsKey(9))
|
||||
assertFalse(map.containsKey(18))
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.util.FileBackedIntList
|
||||
import com.intellij.diagnostic.hprof.util.IntList
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
class FileBackedIntListTest {
|
||||
|
||||
private lateinit var list: IntList
|
||||
private lateinit var channel: FileChannel
|
||||
private val tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
tmpFolder.create()
|
||||
channel = FileChannel.open(tmpFolder.newFile().toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE)
|
||||
list = FileBackedIntList.createEmpty(channel, 10)
|
||||
list[0] = 90
|
||||
list[5] = 95
|
||||
list[9] = 99
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
channel.close()
|
||||
tmpFolder.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun get() {
|
||||
assertEquals(list[0], 90)
|
||||
assertEquals(list[9], 99)
|
||||
assertEquals(list[5], 95)
|
||||
assertEquals(list[0], 90)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFromEmptyIndex() {
|
||||
assertEquals(list[1], 0)
|
||||
assertEquals(list[2], 0)
|
||||
}
|
||||
|
||||
@Test(expected = java.nio.BufferUnderflowException::class)
|
||||
fun getOutOfBounds() {
|
||||
list[10]
|
||||
}
|
||||
|
||||
@Test
|
||||
fun set() {
|
||||
list[0] = Int.MAX_VALUE
|
||||
assertEquals(Int.MAX_VALUE, list[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.parser.ConstantPoolEntry
|
||||
import com.intellij.diagnostic.hprof.parser.InstanceFieldEntry
|
||||
import com.intellij.diagnostic.hprof.parser.StaticFieldEntry
|
||||
import com.intellij.diagnostic.hprof.parser.Type
|
||||
import com.intellij.diagnostic.hprof.util.HprofWriter
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.lang.Boolean
|
||||
import java.lang.Byte
|
||||
import java.lang.Double
|
||||
import java.lang.Float
|
||||
import java.lang.Short
|
||||
import java.lang.ref.SoftReference
|
||||
import java.lang.ref.WeakReference
|
||||
import java.lang.reflect.Array
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.Any
|
||||
import kotlin.ByteArray
|
||||
import kotlin.IllegalArgumentException
|
||||
import kotlin.IllegalStateException
|
||||
import kotlin.Int
|
||||
import kotlin.Long
|
||||
import kotlin.LongArray
|
||||
import kotlin.String
|
||||
import kotlin.arrayOf
|
||||
import kotlin.toRawBits
|
||||
|
||||
class HProfBuilder(dos: DataOutputStream, val classNameMapping: ((Class<*>) -> String?)) {
|
||||
private val objectToIdMap = Reference2LongOpenHashMap<Any>()
|
||||
private val stringToIdMap = Object2LongOpenHashMap<String>()
|
||||
private val classObjectIdToClassSerialNumber = Long2IntOpenHashMap()
|
||||
|
||||
private val classToObjectWithStatics = HashMap<Class<*>, Any>()
|
||||
|
||||
private var nextStringId = 1L
|
||||
private var nextObjectId = 1L
|
||||
private var nextStackFrameId = 1L
|
||||
private var nextStackTraceSerialNumberId = 1
|
||||
private var nextClassSerialNumber = 1
|
||||
|
||||
private val idSize = 8
|
||||
|
||||
private val writer = HprofWriter(dos, idSize, System.currentTimeMillis())
|
||||
|
||||
init {
|
||||
addObject(Class::class.java)
|
||||
addObject(SoftReference::class.java)
|
||||
addObject(WeakReference::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Static fields are ignored when the class is added to the builder. This method allows to use instance fields of an object to
|
||||
* act as static fields of another class.
|
||||
*
|
||||
* @param clazz Class for which static fields will be added
|
||||
* @param o Object which instance fields will serve as static fields
|
||||
*/
|
||||
fun registerStaticsForClass(clazz: Class<*>, o: Any) {
|
||||
if (objectToIdMap.containsKey(clazz)) {
|
||||
throw IllegalStateException("Static fields for class can only been registered before the class object is added to the builder.")
|
||||
}
|
||||
classToObjectWithStatics[clazz] = o
|
||||
}
|
||||
|
||||
public val internalWriter: HprofWriter
|
||||
get() = writer
|
||||
|
||||
fun addRootGlobalJNI(o: Any) {
|
||||
val id = addObject(o)
|
||||
writer.writeRootGlobalJNI(id, 0)
|
||||
}
|
||||
|
||||
fun addRootUnknown(o: Any) {
|
||||
val id = addObject(o)
|
||||
writer.writeRootUnknown(id)
|
||||
}
|
||||
|
||||
fun addRootJavaFrame(o: Any, threadSerialNumber: Int, frameIndex: Int) {
|
||||
val id = addObject(o)
|
||||
writer.writeRootJavaFrame(id, threadSerialNumber, frameIndex)
|
||||
}
|
||||
|
||||
fun addStackTrace(thread: Thread, topFramesCount: Int): Int {
|
||||
val stackTrace = thread.stackTrace
|
||||
val stackFrameIds = LongArray(kotlin.math.min(topFramesCount, stackTrace.size))
|
||||
for (i in stackFrameIds.indices) {
|
||||
val ste = stackTrace[i]
|
||||
val stackFrameId = nextStackFrameID()
|
||||
stackFrameIds[i] = stackFrameId
|
||||
val classObjectId = addObject(Class.forName(ste.className))
|
||||
val classSerialNumber = getClassSerialNumber(classObjectId)
|
||||
writer.writeStackFrame(stackFrameId,
|
||||
addString(ste.className + "." + ste.methodName),
|
||||
addString("()"),
|
||||
0,
|
||||
classSerialNumber,
|
||||
if (ste.isNativeMethod) -1 else ste.lineNumber)
|
||||
}
|
||||
val stackTraceSerialNumber = nextStackTraceSerialNumberID()
|
||||
writer.writeStackTrace(stackTraceSerialNumber, thread.id, stackFrameIds)
|
||||
return stackTraceSerialNumber
|
||||
}
|
||||
|
||||
fun addObject(o: Any?): Long {
|
||||
if (o == null) {
|
||||
return 0
|
||||
}
|
||||
if (objectToIdMap.containsKey(o)) {
|
||||
return objectToIdMap.getLong(o)
|
||||
}
|
||||
val objectID = nextObjectID()
|
||||
objectToIdMap.put(o, objectID)
|
||||
|
||||
val oClass: Class<*> = o.javaClass
|
||||
addObject(oClass)
|
||||
|
||||
when {
|
||||
o is Class<*> -> addClass(objectID, o)
|
||||
oClass.isArray && oClass.componentType.isPrimitive -> addPrimitiveArray(objectID, o)
|
||||
oClass.isArray -> addObjectArray(objectID, o)
|
||||
else -> addInstanceObject(objectID, o)
|
||||
}
|
||||
return objectID
|
||||
}
|
||||
|
||||
private fun addPrimitiveArray(id: Long, o: Any) {
|
||||
// Contents of primitive arrays are not yet supported
|
||||
val elementType = Type.getType(o.javaClass.name)
|
||||
val elementSize = elementType.size
|
||||
|
||||
val arraySize = Array.getLength(o)
|
||||
val bytes = ByteArray(arraySize * elementSize)
|
||||
var curr = 0
|
||||
for (i in 0 until arraySize) {
|
||||
val value =
|
||||
when (elementType) {
|
||||
Type.BYTE -> Array.getByte(o, i).toLong()
|
||||
Type.SHORT -> Array.getShort(o, i).toLong()
|
||||
Type.INT -> Array.getInt(o, i).toLong()
|
||||
Type.OBJECT -> throw IllegalStateException()
|
||||
Type.BOOLEAN -> if (Array.getBoolean(o, i)) 1L else 0L
|
||||
Type.CHAR -> Array.getChar(o, i).toLong()
|
||||
Type.FLOAT -> Float.floatToRawIntBits(Array.getFloat(o, i)).toLong()
|
||||
Type.DOUBLE -> Double.doubleToRawLongBits(Array.getDouble(o, i))
|
||||
Type.LONG -> Array.getLong(o, i)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
serializeToBytes(bytes, curr, value, elementType.size)
|
||||
curr += elementType.size
|
||||
}
|
||||
|
||||
writer.writePrimitiveArrayDump(id,
|
||||
0,
|
||||
elementType,
|
||||
bytes,
|
||||
arraySize)
|
||||
}
|
||||
|
||||
private fun serializeToBytes(bytes: ByteArray, offset: Int, value: Long, count: Int): Int {
|
||||
var result = offset
|
||||
for (i in count - 1 downTo 0) {
|
||||
bytes[result++] = ((value shl (i * 8)) and 0xff).toByte()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun addObjectArray(id: Long, o: Any) {
|
||||
val arrayClassObjectId = addObject(o.javaClass)
|
||||
val length = Array.getLength(o)
|
||||
val elements = LongArray(length)
|
||||
|
||||
for (i in 0 until length) {
|
||||
val element = Array.get(o, i)
|
||||
val elementID = addObject(element)
|
||||
elements[i] = elementID
|
||||
}
|
||||
|
||||
writer.writeObjectArrayDump(id,
|
||||
0,
|
||||
arrayClassObjectId,
|
||||
elements)
|
||||
}
|
||||
|
||||
private fun addInstanceObject(id: Long, o: Any) {
|
||||
var oClass: Class<*> = o.javaClass
|
||||
val classObjectId = addObject(oClass)
|
||||
val baos = ByteArrayOutputStream()
|
||||
DataOutputStream(baos).use { dos ->
|
||||
do {
|
||||
oClass.declaredFields.filter { !Modifier.isStatic(it.modifiers) }.forEach { field ->
|
||||
field.isAccessible = true
|
||||
|
||||
when (field.type) {
|
||||
java.lang.Long.TYPE -> dos.writeLong(field.getLong(o))
|
||||
Integer.TYPE -> dos.writeInt(field.getInt(o))
|
||||
Short.TYPE -> dos.writeShort(field.getShort(o).toInt())
|
||||
Character.TYPE -> dos.writeChar(field.getChar(o).toInt())
|
||||
Byte.TYPE -> dos.writeByte(field.getByte(o).toInt())
|
||||
Boolean.TYPE -> dos.writeBoolean(field.getBoolean(o))
|
||||
Double.TYPE -> dos.writeDouble(field.getDouble(o))
|
||||
Float.TYPE -> dos.writeFloat(field.getFloat(o))
|
||||
else -> {
|
||||
val refObject = field.get(o)
|
||||
val refObjectId = addObject(refObject)
|
||||
when (idSize) {
|
||||
8 -> dos.writeLong(refObjectId)
|
||||
4 -> dos.writeInt(refObjectId.toInt())
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
oClass = oClass.superclass ?: break
|
||||
}
|
||||
while (true)
|
||||
}
|
||||
val bytes = baos.toByteArray()
|
||||
writer.writeInstanceDump(id, 0, classObjectId, bytes)
|
||||
}
|
||||
|
||||
private fun addClass(id: Long, oClass: Class<*>) {
|
||||
val superClassObjectId = addObject(oClass.superclass)
|
||||
|
||||
val mappedClassName = classNameMapping(oClass) ?: oClass.name
|
||||
val className = mappedClassName.replace('.', '/')
|
||||
val classNameStringId = addString(className)
|
||||
|
||||
objectToIdMap.put(oClass, id)
|
||||
|
||||
val classSerialNumber = nextClassSerialNumber()
|
||||
classObjectIdToClassSerialNumber.put(id, classSerialNumber)
|
||||
writer.writeLoadClass(classSerialNumber, id, 0, classNameStringId)
|
||||
|
||||
var instanceSize = 0
|
||||
val instanceFields = mutableListOf<InstanceFieldEntry>()
|
||||
oClass.declaredFields.filter { !Modifier.isStatic(it.modifiers) }.forEach { field ->
|
||||
instanceSize += when (field.type) {
|
||||
java.lang.Long.TYPE -> 8
|
||||
Integer.TYPE -> 4
|
||||
Short.TYPE -> 2
|
||||
Character.TYPE -> 2
|
||||
Byte.TYPE -> 1
|
||||
Boolean.TYPE -> 1
|
||||
Double.TYPE -> 8
|
||||
Float.TYPE -> 4
|
||||
else -> idSize
|
||||
}
|
||||
when (field.type) {
|
||||
java.lang.Long.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.LONG))
|
||||
Integer.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.INT))
|
||||
Short.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.SHORT))
|
||||
Character.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.CHAR))
|
||||
Byte.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.BYTE))
|
||||
Boolean.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.BOOLEAN))
|
||||
Double.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.DOUBLE))
|
||||
Float.TYPE -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.FLOAT))
|
||||
else -> instanceFields.add(InstanceFieldEntry(addString(field.name), Type.OBJECT))
|
||||
}
|
||||
}
|
||||
|
||||
// Constants and static fields not supported yet.
|
||||
val constantPool = arrayOf<ConstantPoolEntry>()
|
||||
val staticFields = mutableListOf<StaticFieldEntry>()
|
||||
|
||||
if (classToObjectWithStatics.contains(oClass)) {
|
||||
val statics = classToObjectWithStatics[oClass]!!
|
||||
statics.javaClass.declaredFields.filter { !Modifier.isStatic(it.modifiers) && Modifier.isPublic(it.modifiers) }.forEach { field ->
|
||||
when (field.type) {
|
||||
java.lang.Long.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.LONG, field.getLong(statics)))
|
||||
Integer.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.INT, field.getInt(statics).toLong()))
|
||||
Short.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.SHORT, field.getShort(statics).toLong()))
|
||||
Character.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.CHAR, field.getChar(statics).toLong()))
|
||||
Byte.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.BYTE, field.getByte(statics).toLong()))
|
||||
Boolean.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.BOOLEAN, if (field.getBoolean(statics)) 1 else 0))
|
||||
Double.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.DOUBLE, field.getDouble(statics).toRawBits()))
|
||||
Float.TYPE -> staticFields.add(StaticFieldEntry(addString(field.name), Type.FLOAT, field.getFloat(statics).toRawBits().toLong()))
|
||||
else -> staticFields.add(StaticFieldEntry(addString(field.name), Type.OBJECT, addObject(field.get(statics))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.writeClassDump(id, 0, superClassObjectId, 0, 0, 0,
|
||||
instanceSize,
|
||||
constantPool,
|
||||
staticFields.toTypedArray(),
|
||||
instanceFields.toTypedArray())
|
||||
}
|
||||
|
||||
private fun nextObjectID() = nextObjectId++
|
||||
private fun nextStackFrameID() = nextStackFrameId++
|
||||
private fun nextStackTraceSerialNumberID() = nextStackTraceSerialNumberId++
|
||||
private fun nextClassSerialNumber() = nextClassSerialNumber++
|
||||
|
||||
private fun addString(string: String): Long {
|
||||
if (stringToIdMap.contains(string)) return stringToIdMap.getLong(string)
|
||||
val id = nextStringID()
|
||||
writer.writeStringInUTF8(id, string)
|
||||
stringToIdMap.put(string, id)
|
||||
return id
|
||||
}
|
||||
|
||||
private fun getClassSerialNumber(classObjectId: Long) = classObjectIdToClassSerialNumber[classObjectId]
|
||||
|
||||
private fun nextStringID() = nextStringId++
|
||||
|
||||
fun create() {
|
||||
writer.flushHeapObjects()
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.analysis.HProfAnalysis
|
||||
import com.intellij.diagnostic.hprof.analysis.analyzeGraph
|
||||
import com.intellij.diagnostic.hprof.classstore.HProfMetadata
|
||||
import com.intellij.diagnostic.hprof.navigator.RootReason
|
||||
import com.intellij.diagnostic.hprof.parser.HProfEventBasedParser
|
||||
import com.intellij.openapi.progress.util.AbstractProgressIndicatorBase
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
class HProfEventBasedParserTest {
|
||||
|
||||
private var file: Path = Path.of(PlatformTestUtil.getPlatformTestDataPath(), "diagnostic/sample.hprof")
|
||||
private val channel: FileChannel = FileChannel.open(file, StandardOpenOption.READ)
|
||||
private val parser = HProfEventBasedParser(channel)
|
||||
private val tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
tmpFolder.create()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
channel.close()
|
||||
parser.close()
|
||||
tmpFolder.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIdSize() {
|
||||
assertEquals(8, parser.idSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCollectRoots() {
|
||||
val hprofMetadata = HProfMetadata.create(parser)
|
||||
val roots = hprofMetadata.roots
|
||||
var javaFrameRootsCount = 0
|
||||
val counts = Object2IntOpenHashMap<RootReason>()
|
||||
roots.values.forEach {
|
||||
if (it.javaFrame) {
|
||||
javaFrameRootsCount++
|
||||
} else {
|
||||
if (!counts.containsKey(it)) {
|
||||
counts.put(it, 0)
|
||||
}
|
||||
counts.addTo(it, 1)
|
||||
}
|
||||
}
|
||||
assertEquals(2, counts.getInt(RootReason.rootGlobalJNI))
|
||||
assertEquals(16, javaFrameRootsCount)
|
||||
assertEquals(1, counts.getInt(RootReason.rootLocalJNI))
|
||||
assertEquals(2, counts.getInt(RootReason.rootMonitorUsed))
|
||||
assertEquals(0, counts.getInt(RootReason.rootNativeStack))
|
||||
assertEquals(621, counts.getInt(RootReason.rootStickyClass))
|
||||
assertEquals(0, counts.getInt(RootReason.rootThreadBlock))
|
||||
assertEquals(6, counts.getInt(RootReason.rootThreadObject))
|
||||
assertEquals(0, counts.getInt(RootReason.rootUnknown))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassStore() {
|
||||
val hprofMetadata = HProfMetadata.create(parser)
|
||||
val classStore = hprofMetadata.classStore
|
||||
assertEquals(702, classStore.size())
|
||||
|
||||
val stringDef = classStore["java.lang.String"]
|
||||
val objectDef = classStore["java.lang.Object"]
|
||||
|
||||
assertEquals("java.lang.String", stringDef.name)
|
||||
assertEquals("java.lang.Object", objectDef.name)
|
||||
|
||||
assertEquals(0, stringDef.constantFields.size)
|
||||
assertEquals(12, stringDef.instanceSize)
|
||||
assertEquals(1, stringDef.refInstanceFields.size)
|
||||
assertEquals(2, stringDef.objectStaticFields.size)
|
||||
|
||||
assertEquals(objectDef.id, stringDef.superClassId)
|
||||
assertEquals(28991101024, objectDef.id)
|
||||
|
||||
assertEquals(0, objectDef.superClassId)
|
||||
|
||||
assertEquals("value", stringDef.refInstanceFields[0].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReport() {
|
||||
val analysis = HProfAnalysis(channel, object : HProfAnalysis.TempFilenameSupplier {
|
||||
override fun getTempFilePath(type: String): Path {
|
||||
return tmpFolder.newFile().toPath()
|
||||
}
|
||||
}, ::analyzeGraph)
|
||||
analysis.setIncludeMetaInfo(false)
|
||||
val progress = object : AbstractProgressIndicatorBase() {
|
||||
}
|
||||
progress.isIndeterminate = false
|
||||
val generatedReport = analysis.analyze(progress)
|
||||
val baselinePath = getBaselinePath("sample-report.txt")
|
||||
val baselineReport = getBaselineContents(baselinePath)
|
||||
|
||||
assertEquals("Report doesn't match the baseline from file:\n$baselinePath",
|
||||
baselineReport, generatedReport)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of the baseline file, with system-dependent line endings
|
||||
*/
|
||||
private fun getBaselineContents(path: Path): String {
|
||||
return String(Files.readAllBytes(path), StandardCharsets.UTF_8)
|
||||
.replace(Regex("(\r\n|\n)"), System.lineSeparator())
|
||||
}
|
||||
|
||||
private fun getBaselinePath(fileName: String) =
|
||||
Path.of(PlatformTestUtil.getPlatformTestDataPath(), "diagnostic/$fileName")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalysisConfig
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalysisContext
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalyzeGraph
|
||||
import com.intellij.diagnostic.hprof.analysis.ClassNomination
|
||||
import com.intellij.diagnostic.hprof.classstore.HProfMetadata
|
||||
import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.parser.HProfEventBasedParser
|
||||
import com.intellij.diagnostic.hprof.util.IntList
|
||||
import com.intellij.diagnostic.hprof.util.ListProvider
|
||||
import com.intellij.diagnostic.hprof.util.UByteList
|
||||
import com.intellij.diagnostic.hprof.util.UShortList
|
||||
import com.intellij.diagnostic.hprof.visitors.RemapIDsVisitor
|
||||
import com.intellij.openapi.progress.util.AbstractProgressIndicatorBase
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import org.junit.Assert
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
open class HProfScenarioRunner(private val tmpFolder: TemporaryFolder,
|
||||
private val remapInMemory: Boolean) {
|
||||
|
||||
val regex = Regex("^com\\.intellij\\.diagnostic\\.hprof\\..*\\\$.*\\\$")
|
||||
|
||||
open fun adjustConfig(config: AnalysisConfig): AnalysisConfig = config
|
||||
|
||||
open fun mapClassName(clazz: Class<*>): String {
|
||||
// Simplify inner class names
|
||||
return clazz.name.replace(regex, "")
|
||||
}
|
||||
|
||||
fun run(scenario: HProfBuilder.() -> Unit,
|
||||
baselineFileName: String,
|
||||
nominatedClassNames: List<String>?) {
|
||||
val hprofFile = tmpFolder.newFile()
|
||||
HProfTestUtils.createHProfOnFile(hprofFile,
|
||||
scenario,
|
||||
{ c -> mapClassName(c) })
|
||||
compareReportToBaseline(hprofFile, baselineFileName, nominatedClassNames)
|
||||
}
|
||||
|
||||
private fun compareReportToBaseline(hprofFile: File, baselineFileName: String, nominatedClassNames: List<String>? = null) {
|
||||
FileChannel.open(hprofFile.toPath(), StandardOpenOption.READ).use { hprofChannel ->
|
||||
|
||||
val progress = object : AbstractProgressIndicatorBase() {
|
||||
}
|
||||
progress.isIndeterminate = false
|
||||
|
||||
val parser = HProfEventBasedParser(hprofChannel)
|
||||
val hprofMetadata = HProfMetadata.create(parser)
|
||||
val histogram = Histogram.create(parser, hprofMetadata.classStore)
|
||||
val nominatedClasses = ClassNomination(histogram, 5).nominateClasses()
|
||||
|
||||
val remapIDsVisitor = if (remapInMemory)
|
||||
RemapIDsVisitor.createMemoryBased()
|
||||
else
|
||||
RemapIDsVisitor.createFileBased(openTempEmptyFileChannel(), histogram.instanceCount)
|
||||
|
||||
parser.accept(remapIDsVisitor, "id mapping")
|
||||
parser.setIdRemappingFunction(remapIDsVisitor.getRemappingFunction())
|
||||
hprofMetadata.remapIds(remapIDsVisitor.getRemappingFunction())
|
||||
|
||||
val navigator = ObjectNavigator.createOnAuxiliaryFiles(
|
||||
parser,
|
||||
openTempEmptyFileChannel(),
|
||||
openTempEmptyFileChannel(),
|
||||
hprofMetadata,
|
||||
histogram.instanceCount
|
||||
)
|
||||
|
||||
val parentList = MemoryBackedIntList(navigator.instanceCount.toInt() + 1)
|
||||
val sizesList = MemoryBackedIntList(navigator.instanceCount.toInt() + 1)
|
||||
val visitedList = MemoryBackedIntList(navigator.instanceCount.toInt() + 1)
|
||||
val refIndexList = MemoryBackedUByteList(navigator.instanceCount.toInt() + 1)
|
||||
|
||||
val nominatedClassNamesLocal = nominatedClassNames ?: nominatedClasses.map { it.classDefinition.name }
|
||||
val analysisConfig = AnalysisConfig(
|
||||
perClassOptions = AnalysisConfig.PerClassOptions(
|
||||
classNames = nominatedClassNamesLocal,
|
||||
treeDisplayOptions = AnalysisConfig.TreeDisplayOptions.all()
|
||||
),
|
||||
histogramOptions = AnalysisConfig.HistogramOptions(
|
||||
includeByCount = true,
|
||||
includeBySize = false,
|
||||
classByCountLimit = Int.MAX_VALUE
|
||||
),
|
||||
disposerOptions = AnalysisConfig.DisposerOptions(
|
||||
includeDisposerTree = false,
|
||||
includeDisposerTreeSummary = false,
|
||||
includeDisposedObjectsDetails = false,
|
||||
includeDisposedObjectsSummary = false
|
||||
),
|
||||
metaInfoOptions = AnalysisConfig.MetaInfoOptions(
|
||||
include = false
|
||||
),
|
||||
dominatorTreeOptions = AnalysisConfig.DominatorTreeOptions(
|
||||
includeDominatorTree = false
|
||||
)
|
||||
).let { adjustConfig(it) }
|
||||
val analysisContext = AnalysisContext(
|
||||
navigator,
|
||||
analysisConfig,
|
||||
parentList,
|
||||
sizesList,
|
||||
visitedList,
|
||||
refIndexList,
|
||||
histogram
|
||||
)
|
||||
|
||||
val analysisReport = AnalyzeGraph(analysisContext, memoryBackedListProvider).analyze(progress).mainReport.toString()
|
||||
|
||||
val baselinePath = getBaselinePath(baselineFileName)
|
||||
val baseline = getBaselineContents(baselinePath)
|
||||
Assert.assertEquals("Report doesn't match the baseline from file:\n$baselinePath",
|
||||
baseline,
|
||||
analysisReport)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of the baseline file, with system-dependent line endings
|
||||
*/
|
||||
private fun getBaselineContents(path: Path): String {
|
||||
return String(Files.readAllBytes(path), StandardCharsets.UTF_8)
|
||||
.replace(Regex("(\r\n|\n)"), System.lineSeparator())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns path to a baseline file. Baselines may be different for different runtime versions.
|
||||
*/
|
||||
private fun getBaselinePath(fileName: String): Path {
|
||||
val javaSpecString = System.getProperty("java.specification.version")
|
||||
val filenameWithPath = "diagnostic/analysis-baseline/$fileName"
|
||||
val file = File(filenameWithPath)
|
||||
|
||||
val name = file.nameWithoutExtension
|
||||
val extension = if (file.extension != "") "." + file.extension else ""
|
||||
|
||||
val javaSpecSpecificFileName = File(file.parent, "$name.$javaSpecString$extension").toString()
|
||||
val javaSpecSpecificFile = Path.of(PlatformTestUtil.getPlatformTestDataPath(), javaSpecSpecificFileName);
|
||||
|
||||
if (Files.exists(javaSpecSpecificFile)) {
|
||||
return javaSpecSpecificFile
|
||||
}
|
||||
|
||||
return Path.of(PlatformTestUtil.getPlatformTestDataPath(), filenameWithPath)
|
||||
}
|
||||
|
||||
class MemoryBackedIntList(size: Int) : IntList {
|
||||
private val array = IntArray(size)
|
||||
|
||||
override fun get(index: Int): Int = array[index]
|
||||
override fun set(index: Int, value: Int) {
|
||||
array[index] = value
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryBackedUShortList(size: Int) : UShortList {
|
||||
private val array = ShortArray(size)
|
||||
|
||||
override fun get(index: Int): Int = array[index].toInt()
|
||||
override fun set(index: Int, value: Int) {
|
||||
array[index] = value.toShort()
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryBackedUByteList(size: Int) : UByteList {
|
||||
private val array = ShortArray(size)
|
||||
|
||||
override fun get(index: Int): Int = array[index].toInt()
|
||||
override fun set(index: Int, value: Int) {
|
||||
array[index] = value.toShort()
|
||||
}
|
||||
}
|
||||
|
||||
object memoryBackedListProvider: ListProvider {
|
||||
override fun createUByteList(name: String, size: Long) = MemoryBackedUByteList(size.toInt())
|
||||
override fun createUShortList(name: String, size: Long) = MemoryBackedUShortList(size.toInt())
|
||||
override fun createIntList(name: String, size: Long) = MemoryBackedIntList(size.toInt())
|
||||
}
|
||||
|
||||
private fun openTempEmptyFileChannel(): FileChannel {
|
||||
return FileChannel.open(tmpFolder.newFile().toPath(),
|
||||
StandardOpenOption.READ,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
StandardOpenOption.DELETE_ON_CLOSE)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object HProfTestUtils {
|
||||
fun createHProfOnFile(file: File,
|
||||
scenario: HProfBuilder.() -> Unit,
|
||||
classNameMapping: ((Class<*>) -> String)) {
|
||||
FileOutputStream(file).use { fos ->
|
||||
HProfBuilder(DataOutputStream(fos), classNameMapping).apply(scenario).create()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.analysis.AnalysisConfig
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class HeapAnalysisTest {
|
||||
|
||||
private val tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
private var remapInMemory: Boolean = true
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
tmpFolder.create()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
tmpFolder.delete()
|
||||
}
|
||||
|
||||
private fun runHProfScenario(scenario: HProfBuilder.() -> Unit,
|
||||
baselineFileName: String,
|
||||
nominatedClassNames: List<String>? = null,
|
||||
classNameMapping: ((Class<*>) -> String)? = null) {
|
||||
object : HProfScenarioRunner(tmpFolder, remapInMemory) {
|
||||
override fun mapClassName(clazz: Class<*>): String = classNameMapping?.invoke(clazz) ?: super.mapClassName(clazz)
|
||||
}.run(scenario, baselineFileName, nominatedClassNames)
|
||||
}
|
||||
|
||||
//@Test
|
||||
fun testPathsThroughDifferentFields() {
|
||||
class MyRef(val referent: Any)
|
||||
class TestString(val s: String)
|
||||
class TestClassB(private val b1string: TestString, private val b2string: TestString)
|
||||
class TestClassA {
|
||||
private val a1string = TestString("TestString")
|
||||
private val a2b = TestClassB(a1string, TestString("TestString2"))
|
||||
private val a3b = TestClassB(a1string, TestString("TestString3"))
|
||||
}
|
||||
|
||||
ReferenceStore().use { refStore ->
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
val a = TestClassA()
|
||||
addRootGlobalJNI(listOf(MyRef(MyRef(MyRef(a))),
|
||||
TestClassA(),
|
||||
refStore.createWeakReference(TestClassA()),
|
||||
refStore.createWeakReference(a)))
|
||||
addRootUnknown(TestClassA())
|
||||
}
|
||||
runHProfScenario(scenario, "testPathsThroughDifferentFields.txt",
|
||||
listOf("TestClassB",
|
||||
"TestClassA",
|
||||
"TestString"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassNameClash() {
|
||||
class MyTestClass1
|
||||
class MyTestClass2
|
||||
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
addRootUnknown(MyTestClass1())
|
||||
addRootGlobalJNI(MyTestClass2())
|
||||
|
||||
}
|
||||
val classNameMapping: (Class<*>) -> String = { c ->
|
||||
if (c == MyTestClass1::class.java ||
|
||||
c == MyTestClass2::class.java) {
|
||||
"MyTestClass"
|
||||
}
|
||||
else {
|
||||
c.name
|
||||
}
|
||||
}
|
||||
|
||||
runHProfScenario(scenario, "testClassNameClash.txt",
|
||||
listOf("MyTestClass!1",
|
||||
"MyTestClass!2"),
|
||||
classNameMapping)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJavaFrameGCRootPriority() {
|
||||
class C1
|
||||
class C2
|
||||
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
val o1 = C1()
|
||||
val o2 = C2()
|
||||
addRootUnknown(o1)
|
||||
val threadSerialNumber = addStackTrace(Thread.currentThread(), 2)
|
||||
// This java frame should be overshadowed by root unknown
|
||||
addRootJavaFrame(o1, threadSerialNumber, 1)
|
||||
// This objects sole reference is from a frame
|
||||
addRootJavaFrame(o2, threadSerialNumber, 1)
|
||||
}
|
||||
runHProfScenario(scenario, "testJavaFrameGCRootPriority.txt",
|
||||
listOf("C1", "C2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIgnoreRootWithNoMatchingObject() {
|
||||
class C1
|
||||
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
addRootUnknown(C1())
|
||||
internalWriter.writeRootUnknown(999_999_999) // id without matching object
|
||||
}
|
||||
remapInMemory = false
|
||||
runHProfScenario(scenario, "testIgnoreRootWithNoMatchingObject.txt",
|
||||
listOf("C1"))
|
||||
}
|
||||
|
||||
//@Test
|
||||
fun testDisposerTreeSummarySection() {
|
||||
val objectTree = ObjectTreeTestWrapper()
|
||||
|
||||
open class MyDisposable : Disposable {
|
||||
override fun dispose() {
|
||||
}
|
||||
}
|
||||
|
||||
class MyDisposableRoot : MyDisposable()
|
||||
class MyDisposableChild : MyDisposable()
|
||||
class MyDisposableGrandchild : MyDisposable()
|
||||
|
||||
val root = MyDisposableRoot()
|
||||
val child1 = MyDisposableChild()
|
||||
val child2 = MyDisposableChild()
|
||||
|
||||
objectTree.register(root, child1)
|
||||
objectTree.register(root, MyDisposableChild())
|
||||
objectTree.register(root, child2)
|
||||
|
||||
objectTree.register(child1, MyDisposableGrandchild())
|
||||
objectTree.register(child2, MyDisposableGrandchild())
|
||||
objectTree.register(root, Disposer.newDisposable())
|
||||
|
||||
objectTree.register(Disposer.newDisposable(), MyDisposableChild())
|
||||
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
addDisposer(this, objectTree)
|
||||
}
|
||||
object : HProfScenarioRunner(tmpFolder, remapInMemory) {
|
||||
override fun adjustConfig(config: AnalysisConfig): AnalysisConfig = configWithDisposerTreeSummaryOnly()
|
||||
}.run(scenario, "testDisposerTreeSummarySection.txt", null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDominatorTreeFlameGraph() {
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
abstract class N
|
||||
class A(val b: N, val c : N, val d: N): N()
|
||||
class B(val e: N, val f: N): N()
|
||||
class C(val f: N): N()
|
||||
class D(): N()
|
||||
class E(val g: N): N()
|
||||
class F: N()
|
||||
class G(var b: N?): N()
|
||||
val g = G(null)
|
||||
val f = F()
|
||||
val e = E(g)
|
||||
val d = D()
|
||||
val c = C(f)
|
||||
val b = B(e, f)
|
||||
val a = A(b, c, d)
|
||||
g.b = b
|
||||
addRootGlobalJNI(a)
|
||||
}
|
||||
object : HProfScenarioRunner(tmpFolder, remapInMemory) {
|
||||
override fun adjustConfig(config: AnalysisConfig): AnalysisConfig = configWithDominatorTreeOnly()
|
||||
}.run(scenario, "testDominatorTreeFlameGraph.txt", null)
|
||||
}
|
||||
|
||||
private fun configWithDisposerTreeSummaryOnly() = AnalysisConfig(
|
||||
AnalysisConfig.PerClassOptions(
|
||||
classNames = listOf(),
|
||||
includeClassList = false,
|
||||
),
|
||||
AnalysisConfig.HistogramOptions(includeByCount = false,
|
||||
includeBySize = false,
|
||||
includeSummary = false),
|
||||
AnalysisConfig.DisposerOptions(
|
||||
includeDisposerTree = false,
|
||||
includeDisposerTreeSummary = true,
|
||||
includeDisposedObjectsSummary = false,
|
||||
includeDisposedObjectsDetails = false,
|
||||
disposerTreeSummaryOptions = AnalysisConfig.DisposerTreeSummaryOptions(
|
||||
nodeCutoff = 0
|
||||
)
|
||||
),
|
||||
dominatorTreeOptions = AnalysisConfig.DominatorTreeOptions(
|
||||
includeDominatorTree = false,
|
||||
)
|
||||
)
|
||||
|
||||
private fun configWithDominatorTreeOnly() = AnalysisConfig(
|
||||
AnalysisConfig.PerClassOptions(
|
||||
classNames = listOf(),
|
||||
includeClassList = false,
|
||||
),
|
||||
AnalysisConfig.HistogramOptions(includeByCount = false,
|
||||
includeBySize = false,
|
||||
includeSummary = false),
|
||||
AnalysisConfig.DisposerOptions(
|
||||
includeDisposerTree = false,
|
||||
includeDisposerTreeSummary = false,
|
||||
includeDisposedObjectsSummary = false,
|
||||
includeDisposedObjectsDetails = false,
|
||||
disposerTreeSummaryOptions = AnalysisConfig.DisposerTreeSummaryOptions(
|
||||
nodeCutoff = 0
|
||||
)
|
||||
),
|
||||
dominatorTreeOptions = AnalysisConfig.DominatorTreeOptions(
|
||||
includeDominatorTree = true,
|
||||
minNodeSize = 0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Adds disposer to the hprof. Disposer is simplified and contains only ourTree static field, all other static fields are omitted.
|
||||
*
|
||||
* @param objectTree @code{ObjectTreeTestWrapper} which replaces @code{Disposer.ourTree}
|
||||
*/
|
||||
private fun addDisposer(builder: HProfBuilder, objectTree: ObjectTreeTestWrapper) {
|
||||
class DisposerStatics {
|
||||
@JvmField
|
||||
var ourTree = objectTree.getTree()
|
||||
}
|
||||
builder.registerStaticsForClass(Disposer::class.java, DisposerStatics())
|
||||
builder.addObject(Disposer::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper class to keep strong references to objects referenced by newly created weak references.
|
||||
*/
|
||||
private class ReferenceStore : AutoCloseable {
|
||||
private val set = HashSet<Any?>()
|
||||
|
||||
fun <T> createWeakReference(obj: T): WeakReference<T> {
|
||||
set.add(obj)
|
||||
return WeakReference(obj)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
set.clear()
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
class ObjectLayoutAssumptionTest {
|
||||
|
||||
@Test
|
||||
fun disposerAssumptions() {
|
||||
// Disposer class is in correct package
|
||||
val disposerClass = Disposer::class
|
||||
assertEquals("com.intellij.openapi.util.Disposer", disposerClass.qualifiedName)
|
||||
|
||||
// Disposer.ourTree validation
|
||||
val ourTreeClass =
|
||||
disposerClass.members.first { c -> c.name == "ourTree" }.returnType.classifier as KClass<*>
|
||||
assertEquals("com.intellij.openapi.util.ObjectTree",
|
||||
ourTreeClass.qualifiedName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun objectTreeAssumptions() {
|
||||
val disposerClass = Disposer::class
|
||||
val objectTreeInstance =
|
||||
disposerClass.members.first { c -> c.name == "ourTree" }.apply { isAccessible = true }.call()!!
|
||||
val myObject2NodeMapField = objectTreeInstance.javaClass.getDeclaredField("myObject2ParentNode").apply { isAccessible = true }
|
||||
val map = myObject2NodeMapField.get(objectTreeInstance)
|
||||
|
||||
// ObjectTree.myObject2ParentNode validation
|
||||
assertEquals("it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap", map.javaClass.canonicalName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.diagnostic.hprof.classstore.HProfMetadata
|
||||
import com.intellij.diagnostic.hprof.histogram.Histogram
|
||||
import com.intellij.diagnostic.hprof.navigator.ObjectNavigator
|
||||
import com.intellij.diagnostic.hprof.parser.HProfEventBasedParser
|
||||
import com.intellij.diagnostic.hprof.visitors.RemapIDsVisitor
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.StandardOpenOption
|
||||
import java.util.function.LongUnaryOperator
|
||||
|
||||
class ObjectNavigatorTest {
|
||||
|
||||
private val tmpFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
tmpFolder.create()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
tmpFolder.delete()
|
||||
}
|
||||
|
||||
private fun openTempEmptyFileChannel(): FileChannel {
|
||||
return FileChannel.open(tmpFolder.newFile().toPath(),
|
||||
StandardOpenOption.READ,
|
||||
StandardOpenOption.WRITE,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING,
|
||||
StandardOpenOption.DELETE_ON_CLOSE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNameClash() {
|
||||
val hprofFile = tmpFolder.newFile()
|
||||
class MyTestClass1 {
|
||||
val field: String = ""
|
||||
}
|
||||
class MyTestClass2 {
|
||||
val field: String = ""
|
||||
}
|
||||
|
||||
var object1Id: Long = 0
|
||||
var object2Id: Long = 0
|
||||
|
||||
val scenario: HProfBuilder.() -> Unit = {
|
||||
object1Id = addObject(MyTestClass1())
|
||||
object2Id = addObject(MyTestClass2())
|
||||
}
|
||||
val classNameMapping: (Class<*>) -> String = { c ->
|
||||
if (c == MyTestClass1::class.java ||
|
||||
c == MyTestClass2::class.java) {
|
||||
"MyTestClass"
|
||||
} else {
|
||||
c.name
|
||||
}
|
||||
}
|
||||
HProfTestUtils.createHProfOnFile(hprofFile,
|
||||
scenario,
|
||||
classNameMapping)
|
||||
val (navigator, remap) = getObjectNavigatorAndRemappingFunction(hprofFile)
|
||||
val clashedClassNames = ArrayList<String>()
|
||||
|
||||
// Check that each class got assigned a unique name
|
||||
clashedClassNames.add(navigator.getClassForObjectId(remap.applyAsLong(object1Id)).name)
|
||||
clashedClassNames.add(navigator.getClassForObjectId(remap.applyAsLong(object2Id)).name)
|
||||
clashedClassNames.sort()
|
||||
|
||||
assertArrayEquals(arrayOf("MyTestClass!1", "MyTestClass!2"), clashedClassNames.toArray())
|
||||
|
||||
// Verify goToInstanceField works correctly for classes with name clash
|
||||
navigator.goTo(remap.applyAsLong(object1Id))
|
||||
navigator.goToInstanceField("MyTestClass", "field")
|
||||
|
||||
assertEquals("java.lang.String", navigator.getClass().undecoratedName)
|
||||
}
|
||||
|
||||
private fun getObjectNavigatorAndRemappingFunction(hprofFile: File): Pair<ObjectNavigator, LongUnaryOperator> {
|
||||
FileChannel.open(hprofFile.toPath(), StandardOpenOption.READ).use { hprofChannel ->
|
||||
val parser = HProfEventBasedParser(hprofChannel)
|
||||
val hprofMetadata = HProfMetadata.create(parser)
|
||||
val histogram = Histogram.create(parser, hprofMetadata.classStore)
|
||||
|
||||
val remapIDsVisitor = RemapIDsVisitor.createMemoryBased()
|
||||
parser.accept(remapIDsVisitor, "id mapping")
|
||||
parser.setIdRemappingFunction(remapIDsVisitor.getRemappingFunction())
|
||||
hprofMetadata.remapIds(remapIDsVisitor.getRemappingFunction())
|
||||
|
||||
return Pair(ObjectNavigator.createOnAuxiliaryFiles(
|
||||
parser,
|
||||
openTempEmptyFileChannel(),
|
||||
openTempEmptyFileChannel(),
|
||||
hprofMetadata,
|
||||
histogram.instanceCount
|
||||
), remapIDsVisitor.getRemappingFunction())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.intellij.diagnostic.hprof
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
|
||||
class ObjectTreeTestWrapper {
|
||||
|
||||
private val tree: Any = treeClazz.declaredConstructors.first { it.parameterCount == 0 }.apply { isAccessible = true }.newInstance()
|
||||
|
||||
fun getTree(): Any {
|
||||
return tree
|
||||
}
|
||||
|
||||
fun register(parent: Disposable, child: Disposable) {
|
||||
val method = treeClazz.getDeclaredMethod("register", Disposable::class.java, Disposable::class.java).apply { isAccessible = true }
|
||||
method.invoke(tree, parent, child)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val treeClazz = Class.forName("com.intellij.openapi.util.ObjectTree")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user