Skip to content

Commit

Permalink
Merge pull request #2365 from square/py/ids
Browse files Browse the repository at this point in the history
Introduce additional ways to represent object ids
  • Loading branch information
pyricau committed Apr 19, 2022
2 parents c04c407 + 27bcd90 commit f367992
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package leakcanary

import android.os.Build.VERSION.SDK_INT
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import shark.HprofHeapGraph.Companion.openHeapGraph
import shark.hexIdentityHashCode

class AndroidExtensionsTest {

@get:Rule
var testFolder = TemporaryFolder()

@Test fun identityHashCode() {
assumeTrue("SDK_INT is $SDK_INT, shadow\$_monitor_ was introduced in 21", SDK_INT >= 24)

// leakcanary.AndroidExtensionsTest@c559955
val thisToString = toString()

val heapDumpFile = testFolder.newFile()
AndroidDebugHeapDumper.dumpHeap(heapDumpFile)

val testClassName = this::class.java.name

val identityHashCodeFromDump = heapDumpFile.openHeapGraph().use { graph ->
val testClass = graph.findClassByName(testClassName)!!
val testInstance = testClass.instances.single()
testInstance.hexIdentityHashCode
}

assertThat("$testClassName@$identityHashCodeFromDump").isEqualTo(thisToString)
}
}
5 changes: 5 additions & 0 deletions shark-android/api/shark-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public final class shark/AndroidBuildMirror$Companion {
public final fun fromHeapGraph (Lshark/HeapGraph;)Lshark/AndroidBuildMirror;
}

public final class shark/AndroidExtensionsKt {
public static final fun getHexIdentityHashCode (Lshark/HeapObject$HeapInstance;)Ljava/lang/String;
public static final fun getIdentityHashCode (Lshark/HeapObject$HeapInstance;)Ljava/lang/Integer;
}

public final class shark/AndroidMetadataExtractor : shark/MetadataExtractor {
public static final field INSTANCE Lshark/AndroidMetadataExtractor;
public fun extractMetadata (Lshark/HeapGraph;)Ljava/util/Map;
Expand Down
38 changes: 38 additions & 0 deletions shark-android/src/main/java/shark/AndroidExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package shark

import shark.HeapObject.HeapInstance

/**
* The system identity hash code, or null if it couldn't be found.
*
* Based on the Object.identityHashCode implementation in AOSP.
*
* Backing field shadow$_monitor_ was added in API 24.
* https://cs.android.com/android/_/android/platform/libcore/+
* /de626ec8a109ea18283d96c720cc57e2f32f67fa:ojluni/src/main/java/java/lang/Object.java;
* dlc=ba7cc9f5357c323a1006119a20ce025fd4c57fd2
*/
val HeapInstance.identityHashCode: Int?
get() {
// Top 2 bits.
val lockWordStateMask = -0x40000000
// Top 2 bits are value 2 (kStateHash).
val lockWordStateHash = -0x80000000
// Low 28 bits.
val lockWordHashMask = 0x0FFFFFFF
val lockWord = this["java.lang.Object", "shadow\$_monitor_"]?.value?.asInt
return if (lockWord != null && lockWord and lockWordStateMask == lockWordStateHash) {
lockWord and lockWordHashMask
} else null
}

/**
* The system identity hashCode represented as hex, or null if it couldn't be found.
* This is the string identifier you see when calling Object.toString() at runtime on a class that
* does not override its hashCode() method, e.g. com.example.MyThing@6bd57cf
*/
val HeapInstance.hexIdentityHashCode: String?
get() {
val hashCode = identityHashCode ?: return null
return Integer.toHexString(hashCode)
}
5 changes: 5 additions & 0 deletions shark-graph/api/shark-graph.api
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public final class shark/HeapField {

public abstract interface class shark/HeapGraph {
public abstract fun findClassByName (Ljava/lang/String;)Lshark/HeapObject$HeapClass;
public abstract fun findHeapDumpIndex (J)I
public abstract fun findObjectByHeapDumpIndex (I)Lshark/HeapObject;
public abstract fun findObjectById (J)Lshark/HeapObject;
public abstract fun findObjectByIdOrNull (J)Lshark/HeapObject;
public abstract fun findObjectByIndex (I)Lshark/HeapObject;
Expand Down Expand Up @@ -51,6 +53,7 @@ public abstract class shark/HeapObject {
public abstract fun getGraph ()Lshark/HeapGraph;
public abstract fun getObjectId ()J
public abstract fun getObjectIndex ()I
public final fun getPositiveObjectId ()J
public abstract fun getRecordSize ()I
public abstract fun readRecord ()Lshark/HprofRecord$HeapDumpRecord$ObjectRecord;
}
Expand Down Expand Up @@ -171,6 +174,8 @@ public final class shark/HprofHeapGraph : shark/CloseableHeapGraph {
public static final field Companion Lshark/HprofHeapGraph$Companion;
public fun close ()V
public fun findClassByName (Ljava/lang/String;)Lshark/HeapObject$HeapClass;
public fun findHeapDumpIndex (J)I
public fun findObjectByHeapDumpIndex (I)Lshark/HeapObject;
public fun findObjectById (J)Lshark/HeapObject;
public fun findObjectByIdOrNull (J)Lshark/HeapObject;
public fun findObjectByIndex (I)Lshark/HeapObject;
Expand Down
18 changes: 17 additions & 1 deletion shark-graph/src/main/java/shark/HeapGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,20 @@ interface HeapGraph {
* Returns true if the provided [objectId] exists in the heap dump.
*/
fun objectExists(objectId: Long): Boolean
}

/**
* Returns the 1-based index in the heap dump of the object corresponding to the provided
* [objectId], and throws [IllegalArgumentException] otherwise.
*
* This is the object index that YourKit provides in its Java profiler.
*/
fun findHeapDumpIndex(objectId: Long): Int

/**
* Returns the [HeapObject] corresponding to the provided [heapDumpIndex], and throws
* [IllegalArgumentException] if [heapDumpIndex] is less than 1 or more than [objectCount].
*
* This is the object index that YourKit provides in its Java profiler.
*/
fun findObjectByHeapDumpIndex(heapDumpIndex: Int): HeapObject
}
6 changes: 6 additions & 0 deletions shark-graph/src/main/java/shark/HeapObject.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ sealed class HeapObject {
*/
abstract val objectId: Long

/**
* [objectId] masked to be a positive unique identifier, as reported in Android Studio.
*/
val positiveObjectId: Long
get() = objectId and (-0x1L ushr (8 - graph.identifierByteSize) * 8)

/**
* An positive object index that's specific to how Shark stores objects in memory.
* The index starts at 0 and ends at [HeapGraph.objectCount] - 1. There are no gaps, every index
Expand Down
24 changes: 24 additions & 0 deletions shark-graph/src/main/java/shark/HprofHeapGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,30 @@ class HprofHeapGraph internal constructor(
return index.objectIdIsIndexed(objectId)
}

override fun findHeapDumpIndex(objectId: Long): Int {
val (_, indexedObject) = index.indexedObjectOrNull(objectId)?: throw IllegalArgumentException(
"Object id $objectId not found in heap dump."
)
val position = indexedObject.position

var countObjectsBefore = 1
index.indexedObjectSequence()
.forEach {
if (position > it.second.position) {
countObjectsBefore++
}
}
return countObjectsBefore
}

override fun findObjectByHeapDumpIndex(heapDumpIndex: Int): HeapObject {
require(heapDumpIndex in 1..objectCount) {
"$heapDumpIndex should be in range [1, $objectCount]"
}
val (objectId, _) = index.indexedObjectSequence().toList().sortedBy { it.second.position }[heapDumpIndex]
return findObjectById(objectId)
}

override fun close() {
reader.close()
}
Expand Down
23 changes: 23 additions & 0 deletions shark-graph/src/test/java/shark/HprofIndexParsingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,27 @@ class HprofIndexParsingTest {
assertThat(stickyClassRoots).hasSize(1)
assertThat(stickyClassRoots.first().id).isEqualTo(loadClassRecord.id)
}

@Test fun `heap dump index is computed based on position in heap dump`() {
val bytes = dump {
instance(clazz("com.example.MyClass1"))
instance(clazz("com.example.MyClass2"))
}

bytes.openHeapGraph().use { graph ->
val class1 = graph.findClassByName("com.example.MyClass1")!!
val class1Index = graph.findHeapDumpIndex(class1.objectId)
val instance1 = class1.instances.single()
val instance1Index = graph.findHeapDumpIndex(instance1.objectId)

val class2 = graph.findClassByName("com.example.MyClass2")!!
val class2Index = graph.findHeapDumpIndex(class2.objectId)
val instance2 = class2.instances.single()
val instance2Index = graph.findHeapDumpIndex(instance2.objectId)

assertThat(instance1Index).isEqualTo(class1Index + 1)
assertThat(class2Index).isEqualTo(instance1Index + 1)
assertThat(instance2Index).isEqualTo(class2Index + 1)
}
}
}

0 comments on commit f367992

Please sign in to comment.