Skip to content

Commit 70bf737

Browse files
pyricauclaude
andcommitted
Look a wrapper class up without boxing its class id
Whether an instance wraps a primitive is a lookup by class id, and that lookup went to a Map<Long, PrimitiveWrapperClass>, so hashing the key boxed the id of every instance dump, LOAD_CLASS and CLASS_DUMP record. That is 24 bytes per record: 431 MB of the 437 MB that stripping a 1.4 GB heap dump of 17961453 instances allocated. There are only ever 8 primitive wrapper classes, so the ids found so far fit in a LongArray that is cheaper to scan than to hash. What stripping allocates no longer grows with the heap dump, which matters on Android, where stripping runs inside the app whose heap was just dumped. Output is byte for byte identical on the Android heap dumps in our test resources and on heap dumps taken from a JVM with both identifier sizes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8e1f3bb commit 70bf737

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Releases before 2.8.1 predate these markers.
112112
* 💥 Reading or stripping a heap dump with 8 byte identifiers that holds a heap dump info record failed with *"Unknown tag 0x00"*. That record holds an `Int` heap id and then a string id, and both the reader skipping it and the stripper copying it over treated the heap id as an id too, so with 8 byte identifiers they moved 4 bytes too far and read the middle of whatever came next as a record tag. Heap dump info records only appear in heap dumps written by Android, which always uses 4 byte identifiers — where the two sizes are the same and the bug can't show — so this is about a heap dump written by `HprofWriter`.
113113
*`shark-cli strip-hprof` and `HprofPrimitiveArrayStripper.stripPrimitiveArrays(File)` now handle gzip on both ends: a heap dump whose content is gzipped is read gzipped whatever it's named, and the output is written gzipped when its name ends with ".gz", which is what the default output name of a ".hprof.gz" input already gave you — so "app.hprof.gz" strips to a gzipped "app-stripped.hprof.gz". The Android heap dumps in our test resources compress 3.9x to 4.4x, and 5.6x to 5.8x once stripped, so a heap dump that's been shared is usually gzipped by the time you get it, and stripping it meant gunzipping it first and gzipping the result again.
114114
*`StreamingSourceProvider.gunzipIfGzipped()` and `StreamingSinkProvider.gzip()` are what do that. They compose onto any source or sink, so a caller of the `stripPrimitiveArrays` overload that takes a source and a sink can opt into the same behavior, or into only one half of it.
115+
* 🔨 `HprofPrimitiveArrayStripper` allocated a boxed `Long` for every instance in the heap dump. Whether an instance wraps a primitive is a lookup by class id, and that lookup went to a `Map<Long, …>`, so hashing the key boxed the id of every instance dump, `LOAD_CLASS` and `CLASS_DUMP` record: 24 bytes each, which is 27.8 MB of the 33.9 MB stripping a 294 MB heap dump of 1154587 instances allocated, and 431 MB of the 437 MB on a 1.4 GB one of 17961453 instances. There are only ever 8 primitive wrapper classes, so the class ids found so far are now held in a `LongArray` and scanned rather than hashed, and what stripping allocates no longer grows with the heap dump: 5.8 MB whether the heap dump is 25 MB, 294 MB or 1.4 GB, where it used to be 9.8 MB, 33.9 MB and 437 MB. Wall clock is unchanged — the boxes were cheap to allocate and died immediately — so this is about the garbage collector, and it matters most on Android, where `HeapAnalysisConfig(stripHeapDump = true)` strips inside the app whose heap was just dumped.
115116
*`HprofPrimitiveArrayStripper` and `shark-cli strip-hprof` now say what stripping leaves behind. Everything that isn't a primitive array or a wrapped primitive is copied over unchanged, and that includes the string records holding the class, field and method names the rest of the heap dump refers to. Those hold no runtime data in a heap dump from Android, but a heap dump from a JVM also holds every string constant of every loaded class in them, so stripping a JVM heap dump leaves the constants written in the code behind.
116117

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

shark/shark-hprof/src/main/java/shark/HprofPrimitiveArrayStripper.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ class HprofPrimitiveArrayStripper {
171171
// Local ref optimizations
172172
val intByteSize = INT.byteSize
173173

174-
val primitiveWrapperClassesByNameStringId = mutableMapOf<Long, PrimitiveWrapperClass>()
175-
val primitiveWrapperClassesByClassId = mutableMapOf<Long, PrimitiveWrapperClass>()
174+
val primitiveWrapperClassesByNameStringId = PrimitiveWrapperClassesById()
175+
val primitiveWrapperClassesByClassId = PrimitiveWrapperClassesById()
176176
var startedReadingHeapDump = false
177177

178178
// Arrays are replaced by repeating one of these over their content, so that replacing an array
@@ -437,6 +437,36 @@ class HprofPrimitiveArrayStripper {
437437
val className: String,
438438
val valueType: PrimitiveType
439439
)
440+
441+
/**
442+
* The primitive wrapper classes found so far, looked up by an id read from the heap dump.
443+
*
444+
* Scanning an array of ids instead of hashing them matters here: looking a class id up in a [Map]
445+
* keyed by [Long] boxes the class id of every instance in the heap dump, which is hundreds of
446+
* megabytes of garbage on a large one. There are only ever a handful of entries to scan, one per
447+
* primitive wrapper class.
448+
*/
449+
private class PrimitiveWrapperClassesById {
450+
private var ids = LongArray(0)
451+
private var wrapperClasses = emptyArray<PrimitiveWrapperClass>()
452+
453+
operator fun set(
454+
id: Long,
455+
wrapperClass: PrimitiveWrapperClass
456+
) {
457+
ids += id
458+
wrapperClasses += wrapperClass
459+
}
460+
461+
operator fun get(id: Long): PrimitiveWrapperClass? {
462+
for (index in ids.indices) {
463+
if (ids[index] == id) {
464+
return wrapperClasses[index]
465+
}
466+
}
467+
return null
468+
}
469+
}
440470
}
441471

442472
/**

0 commit comments

Comments
 (0)