diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/Analyzer.java b/platform/platform-impl/src/com/intellij/diagnostic/hprof/Analyzer.java index d4d13e5d008f..9eb07f9183b2 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/Analyzer.java +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/Analyzer.java @@ -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 analysisCallback; + Function3 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]); diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisConfig.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisConfig.kt index 3c3498714c75..9ecf0cbb86b2 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisConfig.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisConfig.kt @@ -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) = AnalysisConfig(PerClassOptions(nominatedClasses)) } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisContext.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisContext.kt index 735639df29cb..28fd32a6ff08 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisContext.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalysisContext.kt @@ -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() + var disposerTreeObjectId = 0 } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeClassloaderReferences.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeClassloaderReferences.kt index 82b80b802642..cfcc1fe7c0ea 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeClassloaderReferences.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeClassloaderReferences.kt @@ -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 "" } } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeDisposer.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeDisposer.kt index d4c714e54b54..567f094dcb71 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeDisposer.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeDisposer.kt @@ -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() - val maxTreeDepth = 200 - val tooDeepObjectClasses = HashSet() - 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().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, - maxTreeDepth: Int, - currentTreeDepth: Int, - groupingToObjectStats: HashMap) { - 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() + + 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 = 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() + 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("") + + data class StackObject(val node: DisposerNode, val childrenIds: LongCollection) + + val stack = Stack() + stack.push(StackObject(rootNode, topLevelObjectIds)) + + while (!stack.empty()) { + val (currentNode, childrenIds) = stack.pop() + + val nodeToChildren = HashMap() + 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() + 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() + val maxTreeDepth = 200 + val tooDeepObjectClasses = HashSet() + 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() val countByClass = Object2IntOpenHashMap() 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( + 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( + val classOrderByByteCount = ArrayDeque( disposedDominatorReportEntries .sortedByDescending { it.size } .map { it.classDefinition } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeGraph.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeGraph.kt index 8513a7205400..55a00f718eaa 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeGraph.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/AnalyzeGraph.kt @@ -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() - 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 - 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>() + 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 + * : 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() + val indexToString = mutableListOf() + + 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) + } } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/ClassNomination.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/ClassNomination.kt index 2ccfd050baee..66aa32a1519b 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/ClassNomination.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/ClassNomination.kt @@ -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 { val resultClasses = HashSet() diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/GCRootPathsTree.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/GCRootPathsTree.kt index 71de7dbd54f3..7c2a0e2127d3 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/GCRootPathsTree.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/GCRootPathsTree.kt @@ -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() - val weakClasses = mutableSetOf() - val nonSoftWeakClasses = mutableSetOf() - - 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>() 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() - 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") } } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/HProfAnalysis.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/HProfAnalysis.kt index 2bde0d8add24..039a853af7ba 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/HProfAnalysis.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/analysis/HProfAnalysis.kt @@ -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) } diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/classstore/ClassDefinition.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/classstore/ClassDefinition.kt index 809cf0b1aa75..1b8e8227085f 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/classstore/ClassDefinition.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/classstore/ClassDefinition.kt @@ -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 diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/navigator/ObjectNavigator.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/navigator/ObjectNavigator.kt index 15396565fdda..646de8db1c3e 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/navigator/ObjectNavigator.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/navigator/ObjectNavigator.kt @@ -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 @@ -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, diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/AnalysisReport.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/AnalysisReport.kt new file mode 100644 index 000000000000..f76f84c831e6 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/AnalysisReport.kt @@ -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()) diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/FileBackedUShortList.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/FileBackedUShortList.kt new file mode 100644 index 000000000000..8d406feb50a4 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/FileBackedUShortList.kt @@ -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())) + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/HprofWriter.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/HprofWriter.kt new file mode 100644 index 000000000000..bd67c53840b7 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/HprofWriter.kt @@ -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() + private val idToStringMap = Long2ObjectOpenHashMap() + 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, + staticFields: Array, + instanceFields: Array + ) { + 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) + } + } +} + diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/ListProvider.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/ListProvider.kt new file mode 100644 index 000000000000..b46c69ab62b8 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/ListProvider.kt @@ -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 +} diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/RefIndexUtil.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/RefIndexUtil.kt new file mode 100644 index 000000000000..db1c66f90274 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/RefIndexUtil.kt @@ -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 +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeNode.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeNode.kt new file mode 100644 index 000000000000..15d1dbc17cc2 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeNode.kt @@ -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 +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeVisualizer.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeVisualizer.kt new file mode 100644 index 000000000000..043814cb227f --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/TreeVisualizer.kt @@ -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() + + 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().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)) } + } + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/UShortList.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/UShortList.kt new file mode 100644 index 000000000000..89218c8aa3ec --- /dev/null +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/util/UShortList.kt @@ -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) +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/diagnostic/hprof/visitors/CollectRootReasonsVisitor.kt b/platform/platform-impl/src/com/intellij/diagnostic/hprof/visitors/CollectRootReasonsVisitor.kt index bae427990d9a..f8a9086df31d 100644 --- a/platform/platform-impl/src/com/intellij/diagnostic/hprof/visitors/CollectRootReasonsVisitor.kt +++ b/platform/platform-impl/src/com/intellij/diagnostic/hprof/visitors/CollectRootReasonsVisitor.kt @@ -58,7 +58,10 @@ class CollectRootReasonsVisitor(private val threadsMap: Long2ObjectMap - 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 diff --git a/platform/platform-tests/intellij.platform.tests.iml b/platform/platform-tests/intellij.platform.tests.iml index 7bfa02545503..a1d048c5dad5 100644 --- a/platform/platform-tests/intellij.platform.tests.iml +++ b/platform/platform-tests/intellij.platform.tests.iml @@ -71,5 +71,6 @@ + \ No newline at end of file diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.1.8.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.1.8.txt new file mode 100644 index 000000000000..f2584c7c894b --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.1.8.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.17.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.17.txt new file mode 100644 index 000000000000..64c247fbcce8 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.17.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.txt new file mode 100644 index 000000000000..459202cee368 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testClassNameClash.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testDisposerTreeSummarySection.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDisposerTreeSummarySection.txt new file mode 100644 index 000000000000..eb220216f19f --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDisposerTreeSummarySection.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.17.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.17.txt new file mode 100644 index 000000000000..9a24baae386d --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.17.txt @@ -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 diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.txt new file mode 100644 index 000000000000..b92a6a883921 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testDominatorTreeFlameGraph.txt @@ -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 diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.17.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.17.txt new file mode 100644 index 000000000000..cf7694bec53a --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.17.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.txt new file mode 100644 index 000000000000..12bd9209f780 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testIgnoreRootWithNoMatchingObject.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.1.8.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.1.8.txt new file mode 100644 index 000000000000..d1053da6b0cd --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.1.8.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.17.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.17.txt new file mode 100644 index 000000000000..d9d62da43a42 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.17.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.txt new file mode 100644 index 000000000000..f1b92768cafc --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testJavaFrameGCRootPriority.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.1.8.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.1.8.txt new file mode 100644 index 000000000000..80e0c129751c --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.1.8.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.17.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.17.txt new file mode 100644 index 000000000000..d9cca0ba595f --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.17.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.txt b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.txt new file mode 100644 index 000000000000..21ed54d594c3 --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/analysis-baseline/testPathsThroughDifferentFields.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/sample-report.txt b/platform/platform-tests/testData/diagnostic/sample-report.txt new file mode 100644 index 000000000000..e4871b6d109e --- /dev/null +++ b/platform/platform-tests/testData/diagnostic/sample-report.txt @@ -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 + diff --git a/platform/platform-tests/testData/diagnostic/sample.hprof b/platform/platform-tests/testData/diagnostic/sample.hprof new file mode 100644 index 000000000000..56832d2813a4 Binary files /dev/null and b/platform/platform-tests/testData/diagnostic/sample.hprof differ diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedHashMapTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedHashMapTest.kt new file mode 100644 index 000000000000..7605e7a0a5bb --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedHashMapTest.kt @@ -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)) + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedIntListTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedIntListTest.kt new file mode 100644 index 000000000000..f1f43a6616ef --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/FileBackedIntListTest.kt @@ -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]) + } +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfBuilder.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfBuilder.kt new file mode 100644 index 000000000000..0a531682c68d --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfBuilder.kt @@ -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() + private val stringToIdMap = Object2LongOpenHashMap() + private val classObjectIdToClassSerialNumber = Long2IntOpenHashMap() + + private val classToObjectWithStatics = HashMap, 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() + 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() + val staticFields = mutableListOf() + + 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() + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfEventBasedParserTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfEventBasedParserTest.kt new file mode 100644 index 000000000000..a719c6b62435 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfEventBasedParserTest.kt @@ -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() + 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") + +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfScenarioRunner.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfScenarioRunner.kt new file mode 100644 index 000000000000..c7366bc63b02 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfScenarioRunner.kt @@ -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?) { + val hprofFile = tmpFolder.newFile() + HProfTestUtils.createHProfOnFile(hprofFile, + scenario, + { c -> mapClassName(c) }) + compareReportToBaseline(hprofFile, baselineFileName, nominatedClassNames) + } + + private fun compareReportToBaseline(hprofFile: File, baselineFileName: String, nominatedClassNames: List? = 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) + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfTestUtils.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfTestUtils.kt new file mode 100644 index 000000000000..836be8a972a0 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HProfTestUtils.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HeapAnalysisTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HeapAnalysisTest.kt new file mode 100644 index 000000000000..ac4a03f08527 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/HeapAnalysisTest.kt @@ -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? = 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() + + fun createWeakReference(obj: T): WeakReference { + set.add(obj) + return WeakReference(obj) + } + + override fun close() { + set.clear() + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectLayoutAssumptionTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectLayoutAssumptionTest.kt new file mode 100644 index 000000000000..151f6bbca5d6 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectLayoutAssumptionTest.kt @@ -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) + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectNavigatorTest.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectNavigatorTest.kt new file mode 100644 index 000000000000..0f33180c8df4 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectNavigatorTest.kt @@ -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() + + // 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 { + 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()) + } + } + +} diff --git a/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectTreeTestWrapper.kt b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectTreeTestWrapper.kt new file mode 100644 index 000000000000..09ea8972a75b --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/diagnostic/hprof/ObjectTreeTestWrapper.kt @@ -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") + } +} \ No newline at end of file