Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Releases before 2.8.1 predate these markers.
* 🔀 `HeapDiff.growingObjects` is now sorted by decreasing retained size rather than by traversal order, so the growing objects worth looking at first come first.
* 🔀 `ShortestPathObjectNode.retainedIncrease` is now 0 until the third heap dump. Computing it needs a retained size for the previous heap dump, computed with the same metric while that heap dump was still open, and the first traversal has no growing objects to compute a retained size from. `ShortestPathObjectNode.retained` is still reported from the second heap dump on.
* 🔨 The traversal doesn't keep a dominator for every object it visits anymore. Instead, the objects that stay reachable without going through a growing object are walked once at the end of the traversal, and only when there are growing objects to report.
* ✨ Heap growth detection can now say which code grew the objects it reports as growing, the step described in section 4.2 of [BLeak](https://plasma-umass.org/bleak-paper.pdf). `ObjectGrowthAttributor.attributeGrowth()` takes the `HeapDiff` of a detector that ran in this process, runs the scenario once more with each growing collection swapped out for a proxy that records a stack trace on every call that can add to it, and returns one `GrowthAttribution` per growing object: `Attributed`, holding a stack trace and a call count for each distinct place that grew it, or `NotAttributed`, holding the reason that object couldn't be watched. Only a growing collection held by a writable field of interface type can be watched, and for an instance field the instance that declares the field has to be passed in as a `fieldOwners` entry, because there is no way to get from an object in a heap dump back to the live object it was dumped from.
* ✨ `ShortestPathObjectNode.reference` is the reference the node was reached through, as a `ReferenceLocation` holding the reference location type, the fully qualified name of the class that declares the reference and the reference name. `ShortestPathObjectNode.name` already spells that out, but as a display string that drops the package of the owning class and that can only be read back by parsing it.
* ⚠️ [#2841](https://github.com/square/leakcanary/pull/2841) The deobfuscation Gradle plugin moved to the Android Gradle Plugin Variant API and now requires AGP 8.0 or newer. `leakCanary.filterObfuscatedVariants` receives a `com.android.build.api.variant.Variant` instead of the removed `BaseVariant`, the task that copies the mapping file is renamed from `leakCanaryCopyObfuscationMappingFor${VariantName}` to `copy${VariantName}LeakCanaryObfuscationMapping`, and `CopyObfuscationMappingFileTask` no longer exposes `mergeAssetsDirectory` or `leakCanaryAssetsOutputFile`. Applying the plugin to a variant that doesn't have minification enabled now fails when the task runs rather than when the project is configured.
* ✨ A `HeapGraph` can now be read from several threads at the same time, so tools built on Shark can fan out the queries they run over a heap dump. Concurrent reads used to share a single record parsing buffer and an unsynchronized object record cache, and would fail with *"Buffer not fully consumed"* or read the wrong bytes. The `Sequence` instances a graph hands out are still meant to be read by a single thread: each thread should get its own.
* ✨ `HeapGraph.context` is thread safe, and the new `GraphContext.compute()` atomically replaces the value for a key with the result of a function of that value, which is how several threads share a value they each read and update. `GraphContext.getOrPut()` computes without holding a lock, so when threads race for the same key it can compute a value more than once and the value stored first wins.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package leakcanary

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.HeapDiff

class AndroidGrowthAttributionTest {

private val growingList = mutableListOf<String>()

@Test fun growth_is_attributed_to_the_code_that_grew_the_list() {
var iteration = 0
val scenario: () -> Unit = {
growingList += "growth ${iteration++}"
}
val heapDiff = HeapDiff.repeatingAndroidInProcessScenario()
.findRepeatedlyGrowingObjects(roundTripScenario = scenario)

val attributions = ObjectGrowthAttributor(fieldOwners = listOf(this))
.attributeGrowth(heapDiff, roundTripScenario = scenario)

val attributed = attributions.single() as GrowthAttribution.Attributed
val growthStack = attributed.growthStacks.single()
assertThat(growthStack.methodName).isEqualTo("add")
assertThat(growthStack.count).isEqualTo(1)
assertThat(growthStack.stackTrace[0].className)
.startsWith(AndroidGrowthAttributionTest::class.java.name)
}
}
34 changes: 34 additions & 0 deletions leakcanary/leakcanary-core/api/leakcanary-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ public final class leakcanary/DumpingRepeatingScenarioObjectGrowthDetector : sha
public fun findRepeatedlyGrowingObjects (IILkotlin/jvm/functions/Function0;)Lshark/HeapDiff;
}

public abstract interface class leakcanary/GrowthAttribution {
public abstract fun getGrowingObject ()Lshark/ShortestPathObjectNode;
}

public final class leakcanary/GrowthAttribution$Attributed : leakcanary/GrowthAttribution {
public fun <init> (Lshark/ShortestPathObjectNode;Ljava/util/List;)V
public fun getGrowingObject ()Lshark/ShortestPathObjectNode;
public final fun getGrowthStacks ()Ljava/util/List;
public fun toString ()Ljava/lang/String;
}

public final class leakcanary/GrowthAttribution$NotAttributed : leakcanary/GrowthAttribution {
public fun <init> (Lshark/ShortestPathObjectNode;Ljava/lang/String;)V
public fun getGrowingObject ()Lshark/ShortestPathObjectNode;
public final fun getReason ()Ljava/lang/String;
public fun toString ()Ljava/lang/String;
}

public final class leakcanary/GrowthStack {
public fun <init> (Ljava/lang/String;Ljava/util/List;I)V
public final fun getCount ()I
public final fun getMethodName ()Ljava/lang/String;
public final fun getStackTrace ()Ljava/util/List;
public fun toString ()Ljava/lang/String;
}

public abstract interface class leakcanary/HeapDumpDirectoryProvider {
public abstract fun heapDumpDirectory ()Ljava/io/File;
}
Expand Down Expand Up @@ -81,6 +107,14 @@ public final class leakcanary/HeapDumperKt {
public static synthetic fun withGc$default (Lleakcanary/HeapDumper;Lleakcanary/GcTrigger;ILjava/lang/Object;)Lleakcanary/HeapDumper;
}

public final class leakcanary/ObjectGrowthAttributor {
public fun <init> ()V
public fun <init> (Ljava/util/List;)V
public synthetic fun <init> (Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun attributeGrowth (Lshark/HeapDiff;ILkotlin/jvm/functions/Function0;)Ljava/util/List;
public static synthetic fun attributeGrowth$default (Lleakcanary/ObjectGrowthAttributor;Lshark/HeapDiff;ILkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/util/List;
}

public final class leakcanary/ObjectGrowthWarmupHeapDumper : leakcanary/HeapDumper {
public static final field Companion Lleakcanary/ObjectGrowthWarmupHeapDumper$Companion;
public fun <init> (Lshark/ObjectGrowthDetector;Lleakcanary/HeapDumper;Z)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package leakcanary

import shark.ShortestPathObjectNode

/**
* What [ObjectGrowthAttributor] found out about the code that grows one of the objects that
* `shark.ObjectGrowthDetector` reported as growing.
*/
sealed interface GrowthAttribution {

/** The growing object this is about, one of `shark.HeapDiff.growingObjects`. */
val growingObject: ShortestPathObjectNode

/** [ObjectGrowthAttributor] watched [growingObject] while the scenario ran again. */
class Attributed(
override val growingObject: ShortestPathObjectNode,
/**
* Every distinct stack trace that grew [growingObject] while the scenario ran, most frequent
* first.
*
* Empty when nothing grew [growingObject] through a method
* [ObjectGrowthAttributor] can see, which means the growth comes from somewhere else: a write
* straight to a field of the collection, a write to the array behind it, or another object
* entirely that the heap traversal grouped into the same node.
*/
val growthStacks: List<GrowthStack>,
) : GrowthAttribution {
override fun toString(): String {
val stacks = if (growthStacks.isEmpty()) {
" Nothing grew this object through a method that could be watched.\n"
} else {
growthStacks.joinToString(separator = "\n", postfix = "\n")
}
return "${growingObject.pathFromRootAsString()}\n$stacks"
}
}

/**
* [ObjectGrowthAttributor] could not watch [growingObject]. The growth reported by
* `shark.ObjectGrowthDetector` still stands, there just is no stack trace to go with it.
*/
class NotAttributed(
override val growingObject: ShortestPathObjectNode,
/** Human readable explanation of why [growingObject] could not be watched. */
val reason: String,
) : GrowthAttribution {
override fun toString(): String {
return "${growingObject.pathFromRootAsString()}\n Not attributed: $reason\n"
}
}
}

/**
* A stack trace that grew a [GrowthAttribution.growingObject], and how many times it did.
*/
class GrowthStack(
/** Name of the method that was called on the growing object, e.g. `add`. */
val methodName: String,

/** Where [methodName] was called from, innermost frame first. */
val stackTrace: List<StackTraceElement>,

/**
* How many times [stackTrace] called [methodName] while the scenario ran. A stack trace that
* grows the object once per scenario loop is a better suspect than one that only fires once.
*/
val count: Int,
) {
override fun toString(): String {
return " $count call(s) to $methodName()\n" +
stackTrace.joinToString(separator = "\n") { " at $it" }
}
}
Loading