Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Please thank our [contributors](https://github.com/square/leakcanary/graphs/cont
* [#2841](https://github.com/square/leakcanary/pull/2841) `shark-cli`, `leakcanary-app-aidl` and `leakcanary-app-service` are no longer published to Maven Central. They were never meant to be depended on: the Shark CLI is distributed as a zip attached to the Github release and available on Homebrew, and the two app modules are the internal plumbing that lets an app talk to the LeakCanary UI app.
* [#2841](https://github.com/square/leakcanary/pull/2841) LeakCanary is now built with Kotlin 2.4, which raises the minimum Kotlin version for consumers to 2.3. The Kotlin compiler only reads metadata written by compilers up to one minor version ahead of itself, so building against LeakCanary with Kotlin 2.2 or older now fails with *"class ... was compiled with an incompatible version of Kotlin"*.
* 🔨 [#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.
* [#2711](https://github.com/square/leakcanary/pull/2711) Shark's `ByteSize` value class is replaced by [me.saket.bytesize](https://github.com/saket/byte-size), which Shark now exposes as an `api` dependency. `Retained.heapSize` is now a `me.saket.bytesize.DecimalByteSize`, so replace `shark.ByteSize` with `me.saket.bytesize.DecimalByteSize` and `shark.bytes` with `me.saket.bytesize.decimalBytes`. Retained sizes are also now rendered with up to two decimal places (`1.5 KB` instead of `1 KB`, `1.05 MB` instead of `1 MB`) and gain TB and PB units.

## Version 3.0 Alpha 9 (2026-06-25)

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ androidX-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator",
androidX-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workManager" }
androidX-work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workManager" }
androidX-collections = { module = "androidx.collection:collection-ktx", version = "1.4.0" }
bytesize = { module = "me.saket.bytesize:bytesize", version = "2.2.1" }

androidSupport = { module = "com.android.support:support-v4", version = "28.0.0" }
assertjCore = { module = "org.assertj:assertj-core", version = "3.9.1" }
Expand Down
43 changes: 2 additions & 41 deletions shark/shark/api/shark.api
Original file line number Diff line number Diff line change
Expand Up @@ -72,45 +72,6 @@ public final class shark/ApplicationLeak : shark/Leak {
public final class shark/ApplicationLeak$Companion {
}

public final class shark/ByteSize : java/lang/Comparable {
public static final synthetic fun box-impl (J)Lshark/ByteSize;
public synthetic fun compareTo (Ljava/lang/Object;)I
public fun compareTo-rK2stxE (J)I
public static fun compareTo-rK2stxE (JJ)I
public static fun constructor-impl (J)J
public static final fun div-BWD4q2E (JJ)J
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (JLjava/lang/Object;)Z
public static final fun equals-impl0 (JJ)Z
public final fun getInWholeBytes ()J
public static final fun getInWholeGigabytes-impl (J)J
public static final fun getInWholeKilobytes-impl (J)J
public static final fun getInWholeMegabytes-impl (J)J
public fun hashCode ()I
public static fun hashCode-impl (J)I
public static final fun minus-BWD4q2E (JJ)J
public static final fun plus-BWD4q2E (JJ)J
public static final fun times-BWD4q2E (JJ)J
public fun toString ()Ljava/lang/String;
public static fun toString-impl (J)Ljava/lang/String;
public final synthetic fun unbox-impl ()J
}

public final class shark/ByteSizeKt {
public static final field BYTES_PER_GB J
public static final field BYTES_PER_KB J
public static final field BYTES_PER_MB J
public static final fun getBytes (I)J
public static final fun getBytes (J)J
public static final fun getGigabytes (I)J
public static final fun getGigabytes (J)J
public static final fun getKilobytes (I)J
public static final fun getKilobytes (J)J
public static final fun getMegabytes (I)J
public static final fun getMegabytes (J)J
public static final fun getZERO_BYTES ()J
}

public final class shark/ChainingInstanceReferenceReader : shark/ReferenceReader {
public fun <init> (Ljava/util/List;Lshark/FlatteningPartitionedInstanceReferenceReader;Lshark/FieldInstanceReferenceReader;)V
public fun read (Lshark/HeapObject$HeapInstance;)Lkotlin/sequences/Sequence;
Expand Down Expand Up @@ -966,7 +927,7 @@ public final class shark/Retained {
public fun equals (Ljava/lang/Object;)Z
public static fun equals-impl (JLjava/lang/Object;)Z
public static final fun equals-impl0 (JJ)Z
public static final fun getHeapSize-UyN4wxk (J)J
public static final fun getHeapSize-mbjz_tw (J)J
public static final fun getObjectCount-impl (J)I
public fun hashCode ()I
public static fun hashCode-impl (J)I
Expand All @@ -978,7 +939,7 @@ public final class shark/Retained {
}

public final class shark/RetainedKt {
public static final fun Retained-5mcd9r4 (JI)J
public static final fun Retained-X-1A1GI (JI)J
public static final fun getUNKNOWN_RETAINED ()J
public static final fun getZERO_RETAINED ()J
}
Expand Down
1 change: 1 addition & 0 deletions shark/shark/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {

dependencies {
api(projects.shark.sharkGraph)
api(libs.bytesize)

implementation(libs.coroutines.core)
implementation(libs.kotlin.stdlib)
Expand Down
60 changes: 0 additions & 60 deletions shark/shark/src/main/java/shark/ByteSize.kt

This file was deleted.

16 changes: 12 additions & 4 deletions shark/shark/src/main/java/shark/ObjectGrowthDetector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.collection.MutableLongSet
import androidx.collection.mutableLongListOf
import java.util.ArrayDeque
import java.util.Deque
import me.saket.bytesize.DecimalByteSize
import shark.HeapObject.HeapClass
import shark.HeapObject.HeapInstance
import shark.HeapObject.HeapObjectArray
Expand Down Expand Up @@ -231,7 +232,7 @@ class ObjectGrowthDetector(
// A map that stores two ints, size and count, in a single long value with bit packing.
val retainedSizeAndCountMap = MutableLongLongMap(dequeuedNodes.size)
for (node in dequeuedNodes.asReversed()) {
var nodeRetainedSize = ZERO_BYTES
var nodeRetainedSize = DecimalByteSize(0L)
var nodeRetainedCount = 0

for (objectId in node.objectIds) {
Expand All @@ -248,7 +249,13 @@ class ObjectGrowthDetector(
if (dominatorObjectId != ValueHolder.NULL_REFERENCE) {
retainedSizeAndCountMap.increase(dominatorObjectId, retainedSize, retainedCount)
}
nodeRetainedSize += retainedSize.bytes
// Summing the raw longs rather than using ByteSize.plus(), whose parameter is the
// ByteSize interface and so boxes both operands. HotSpot scalar replaces those boxes but
// ART does not, and this loop runs once per object: measured over large-dump.hprof on an
// Android 14 arm64 device, the operator costs 32 bytes per iteration and 9MB of extra
// garbage per traversal. Can be reverted to nodeRetainedSize += retainedSize.decimalBytes
// once https://github.com/saket/byte-size/pull/24 ships.
nodeRetainedSize = DecimalByteSize(nodeRetainedSize.inWholeBytes + retainedSize)
nodeRetainedCount += retainedCount
}

Expand Down Expand Up @@ -353,13 +360,14 @@ class ObjectGrowthDetector(
return@reportedGrowingNodeRetainedSize
}

var heapSize = ZERO_BYTES
var heapSize = DecimalByteSize(0L)
var objectCount = 0
for (objectId in node.objectIds) {
val packed = retainedMap[objectId]
val additionalByteSize = packed.unpackAsFirstInt
val additionalObjectCount = packed.unpackAsSecondInt
heapSize += additionalByteSize.bytes
// See above: avoids boxing both operands on every iteration.
heapSize = DecimalByteSize(heapSize.inWholeBytes + additionalByteSize)
objectCount += additionalObjectCount
}
shortestPathNode.retained = Retained(
Expand Down
12 changes: 7 additions & 5 deletions shark/shark/src/main/java/shark/Retained.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package shark

import me.saket.bytesize.DecimalByteSize
import me.saket.bytesize.decimalBytes
import shark.internal.packedWith
import shark.internal.unpackAsFirstInt
import shark.internal.unpackAsSecondInt
Expand All @@ -12,7 +14,7 @@ inline fun Retained(
* The minimum number of bytes which would be freed if all references to this object were
* released. Should not exceed [Int.MAX_VALUE] bytes.
*/
heapSize: ByteSize,
heapSize: DecimalByteSize,

/**
* The minimum number of objects which would be unreachable if all references to this object were
Expand All @@ -28,8 +30,8 @@ value class Retained @PublishedApi internal constructor(
@PublishedApi @JvmField
internal val packedValue: Long
) {
inline val heapSize: ByteSize
get() = packedValue.unpackAsFirstInt.bytes
inline val heapSize: DecimalByteSize
get() = packedValue.unpackAsFirstInt.decimalBytes

inline val objectCount: Int
get() = packedValue.unpackAsSecondInt
Expand All @@ -41,5 +43,5 @@ value class Retained @PublishedApi internal constructor(
get() = this == ZERO_RETAINED
}

val ZERO_RETAINED = Retained(ZERO_BYTES, 0)
val UNKNOWN_RETAINED = Retained((-1).bytes, -1)
val ZERO_RETAINED = Retained(0.decimalBytes, 0)
val UNKNOWN_RETAINED = Retained((-1).decimalBytes, -1)
5 changes: 3 additions & 2 deletions shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package shark

import me.saket.bytesize.decimalBytes
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import shark.HprofHeapGraph.Companion.openHeapGraph
Expand Down Expand Up @@ -103,7 +104,7 @@ class ObjectGrowthDetectorTest {

val growingObject = heapTraversal.growingObjects.single()
assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
val expectedRetainedSizeIncrease = (12 + "World!".length * 2).bytes
val expectedRetainedSizeIncrease = (12 + "World!".length * 2).decimalBytes
assertThat(growingObject.retainedIncrease.heapSize).isEqualTo(expectedRetainedSizeIncrease)
}

Expand All @@ -126,7 +127,7 @@ class ObjectGrowthDetectorTest {

val growingObject = heapTraversal.growingObjects.single()
assertThat(growingObject.retainedIncrease.objectCount).isEqualTo(1)
val expectedRetainedSizeIncrease = (12 + "Turtles".length * 2).bytes
val expectedRetainedSizeIncrease = (12 + "Turtles".length * 2).decimalBytes
assertThat(growingObject.retainedIncrease.heapSize).isEqualTo(expectedRetainedSizeIncrease)
}

Expand Down
Loading