Integrate Android Studio updates and tests for memory snapshot analysis

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