diff --git a/docs/changelog.md b/docs/changelog.md index 8bf589bf2a..f22ecec4ec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 159f241306..07cd5556fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/shark/shark/api/shark.api b/shark/shark/api/shark.api index fac5cdd90e..24eb342547 100644 --- a/shark/shark/api/shark.api +++ b/shark/shark/api/shark.api @@ -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 (Ljava/util/List;Lshark/FlatteningPartitionedInstanceReferenceReader;Lshark/FieldInstanceReferenceReader;)V public fun read (Lshark/HeapObject$HeapInstance;)Lkotlin/sequences/Sequence; @@ -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 @@ -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 } diff --git a/shark/shark/build.gradle.kts b/shark/shark/build.gradle.kts index 9f939244b1..b1c138010d 100644 --- a/shark/shark/build.gradle.kts +++ b/shark/shark/build.gradle.kts @@ -5,6 +5,7 @@ plugins { dependencies { api(projects.shark.sharkGraph) + api(libs.bytesize) implementation(libs.coroutines.core) implementation(libs.kotlin.stdlib) diff --git a/shark/shark/src/main/java/shark/ByteSize.kt b/shark/shark/src/main/java/shark/ByteSize.kt deleted file mode 100644 index 57cd689a0a..0000000000 --- a/shark/shark/src/main/java/shark/ByteSize.kt +++ /dev/null @@ -1,60 +0,0 @@ -package shark - -/** - * Inspired by https://github.com/saket/file-size as well as Kotlin's Duration API. - */ -// DO NOT ADD A COMPANION OBJECT: a value class is supposed to be lightweight and its usage inlined -// into few instructions. After adding a companion object, call sites get a lot more instructions. -@JvmInline -value class ByteSize constructor( - val inWholeBytes: Long -) : Comparable { - - inline val inWholeKilobytes: Long - get() = inWholeBytes / BYTES_PER_KB - - inline val inWholeMegabytes: Long - get() = inWholeBytes / BYTES_PER_MB - - inline val inWholeGigabytes: Long - get() = inWholeBytes / BYTES_PER_GB - - override fun toString(): String { - return when { - inWholeBytes < BYTES_PER_KB -> "$inWholeBytes B" - inWholeBytes < BYTES_PER_MB -> "$inWholeKilobytes KB" - inWholeBytes < BYTES_PER_GB -> "$inWholeMegabytes MB" - else -> "$inWholeGigabytes GB" - } - } - - override operator fun compareTo(other: ByteSize) = inWholeBytes.compareTo(other.inWholeBytes) - - inline operator fun plus(other: ByteSize): ByteSize = - ByteSize(inWholeBytes = inWholeBytes + other.inWholeBytes) - - inline operator fun minus(other: ByteSize): ByteSize = - ByteSize(inWholeBytes = inWholeBytes - other.inWholeBytes) - - inline operator fun times(other: ByteSize): ByteSize = - ByteSize(inWholeBytes * other.inWholeBytes) - - inline operator fun div(other: ByteSize): ByteSize = - ByteSize(inWholeBytes / other.inWholeBytes) -} - -const val BYTES_PER_KB: Long = 1_000L -const val BYTES_PER_MB: Long = 1_000L * BYTES_PER_KB -const val BYTES_PER_GB: Long = 1_000L * BYTES_PER_MB - -val ZERO_BYTES: ByteSize = ByteSize(0L) - -inline val Long.bytes get() = ByteSize(this) -inline val Long.kilobytes get() = ByteSize(this * BYTES_PER_KB) -inline val Long.megabytes get() = ByteSize(this * BYTES_PER_MB) -inline val Long.gigabytes get() = ByteSize(this * BYTES_PER_GB) - -inline val Int.bytes get() = ByteSize(toLong()) -inline val Int.kilobytes get() = ByteSize(this * BYTES_PER_KB) -inline val Int.megabytes get() = ByteSize(this * BYTES_PER_MB) -inline val Int.gigabytes get() = ByteSize(this * BYTES_PER_GB) diff --git a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt index 532d694337..de30000097 100644 --- a/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt +++ b/shark/shark/src/main/java/shark/ObjectGrowthDetector.kt @@ -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 @@ -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) { @@ -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 } @@ -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( diff --git a/shark/shark/src/main/java/shark/Retained.kt b/shark/shark/src/main/java/shark/Retained.kt index 3dc12e4586..aee965ccb1 100644 --- a/shark/shark/src/main/java/shark/Retained.kt +++ b/shark/shark/src/main/java/shark/Retained.kt @@ -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 @@ -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 @@ -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 @@ -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) diff --git a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt index c4df0eea80..fc0929ed1a 100644 --- a/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt +++ b/shark/shark/src/test/java/shark/ObjectGrowthDetectorTest.kt @@ -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 @@ -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) } @@ -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) }