1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ahat.heapdump; 18 19 import java.awt.image.BufferedImage; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.HashMap; 23 import java.util.HashSet; 24 import java.util.List; 25 import java.util.Map; 26 import java.util.Objects; 27 import java.util.Set; 28 import java.util.TreeMap; 29 30 /** 31 * A java object that has `android.graphics.Bitmap` as its base class. 32 */ 33 public class AhatBitmapInstance extends AhatClassInstance implements Comparable<AhatBitmapInstance> { 34 35 private BitmapInfo mBitmapInfo = null; 36 AhatBitmapInstance(long id)37 AhatBitmapInstance(long id) { 38 super(id); 39 } 40 41 @Override isBitmapInstance()42 public boolean isBitmapInstance() { 43 return true; 44 } 45 46 @Override asBitmapInstance()47 public AhatBitmapInstance asBitmapInstance() { 48 return this; 49 } 50 51 /** 52 * simple order for all bitmap instances on TreeMultimap 53 */ compareTo(AhatBitmapInstance other)54 public int compareTo(AhatBitmapInstance other) { 55 return Long.compare(this.getId(), other.getId()); 56 } 57 58 /** 59 * Parsed information for bitmap contents dumped in the heapdump 60 */ 61 public static class BitmapDumpData { 62 private int count; 63 // See android.graphics.Bitmap.CompressFormat for format values. 64 // -1 means no compression for backward compatibility 65 private int format; 66 private Map<Long, byte[]> buffers; 67 private Set<Long> referenced; 68 private Map<BitmapInfo, List<AhatBitmapInstance>> instances; 69 BitmapDumpData(int count, int format)70 BitmapDumpData(int count, int format) { 71 this.count = count; 72 this.format = format; 73 this.buffers = new HashMap<Long, byte[]>(count); 74 this.referenced = new HashSet<Long>(count); 75 this.instances = new TreeMap<>(); 76 } 77 }; 78 79 /** 80 * find the BitmapDumpData that is included in the heap dump 81 * 82 * @param root root of the heap dump 83 * @param instances all the instances from where the bitmap dump data will be excluded 84 * @return true if valid bitmap dump data is found, false if not 85 */ findBitmapDumpData(SuperRoot root, Instances<AhatInstance> instances)86 public static BitmapDumpData findBitmapDumpData(SuperRoot root, Instances<AhatInstance> instances) { 87 final BitmapDumpData result; 88 AhatClassObj cls = null; 89 90 for (Reference ref : root.getReferences()) { 91 if (ref.ref.isClassObj()) { 92 cls = ref.ref.asClassObj(); 93 if (cls.getName().equals("android.graphics.Bitmap")) { 94 break; 95 } 96 } 97 } 98 99 if (cls == null) { 100 return null; 101 } 102 103 Value value = cls.getStaticField("dumpData"); 104 if (value == null || !value.isAhatInstance()) { 105 return null; 106 } 107 108 AhatClassInstance inst = value.asAhatInstance().asClassInstance(); 109 if (inst == null) { 110 return null; 111 } 112 113 result = toBitmapDumpData(inst); 114 if (result == null) { 115 return null; 116 } 117 118 /* Build the map for all the bitmap instances with its BitmapInfo as key, 119 * the map would be used to identify duplicated bitmaps later. This also 120 * initializes `mBitmapInfo` of each bitmap instance. 121 */ 122 for (AhatInstance obj : instances) { 123 AhatBitmapInstance bmp = obj.asBitmapInstance(); 124 if (bmp != null) { 125 BitmapInfo info = bmp.getBitmapInfo(result); 126 127 // Avoid adding instances referenced from BitmapDumpData. These 128 // instances shall *not* be counted. 129 if (info != null && !result.referenced.contains(bmp.getId())) { 130 result.instances.computeIfAbsent(info, k -> new ArrayList<>()).add(bmp); 131 } 132 } 133 } 134 135 return result; 136 } 137 toBitmapDumpData(AhatClassInstance inst)138 private static BitmapDumpData toBitmapDumpData(AhatClassInstance inst) { 139 if (!inst.isInstanceOfClass("android.graphics.Bitmap$DumpData")) { 140 return null; 141 } 142 143 int count = inst.getIntField("count", 0); 144 int format = inst.getIntField("format", -1); 145 146 if (count == 0 || format == -1) { 147 return null; 148 } 149 150 BitmapDumpData result = new BitmapDumpData(count, format); 151 152 AhatArrayInstance natives = inst.getArrayField("natives"); 153 AhatArrayInstance buffers = inst.getArrayField("buffers"); 154 if (natives == null || buffers == null) { 155 return null; 156 } 157 158 result.referenced.add(natives.getId()); 159 result.referenced.add(buffers.getId()); 160 161 result.buffers = new HashMap<>(result.count); 162 for (int i = 0; i < result.count; i++) { 163 Value nativePtr = natives.getValue(i); 164 Value bufferVal = buffers.getValue(i); 165 if (nativePtr == null || bufferVal == null) { 166 continue; 167 } 168 AhatInstance buffer = bufferVal.asAhatInstance(); 169 result.buffers.put(nativePtr.asLong(), buffer.asArrayInstance().asByteArray()); 170 result.referenced.add(buffer.getId()); 171 } 172 return result; 173 } 174 175 /** 176 * find duplicated bitmap instances 177 * 178 * @param bitmapDumpData parsed bitmap dump data 179 * @return A list of duplicated bitmaps (the same duplication stored in a sub-list) 180 */ findDuplicates(BitmapDumpData bitmapDumpData)181 public static List<List<AhatBitmapInstance>> findDuplicates(BitmapDumpData bitmapDumpData) { 182 if (bitmapDumpData != null) { 183 List<List<AhatBitmapInstance>> duplicates = new ArrayList<>(); 184 for (List<AhatBitmapInstance> values : bitmapDumpData.instances.values()) { 185 if (values.size() > 1) { 186 duplicates.add(new ArrayList<>(values)); 187 } 188 } 189 return duplicates; 190 } 191 return null; 192 } 193 194 private static class BitmapInfo implements Comparable<BitmapInfo> { 195 private final int width; 196 private final int height; 197 private final int format; 198 private final byte[] buffer; 199 private final int bufferHash; 200 BitmapInfo(int width, int height, int format, byte[] buffer)201 public BitmapInfo(int width, int height, int format, byte[] buffer) { 202 this.width = width; 203 this.height = height; 204 this.format = format; 205 this.buffer = buffer; 206 bufferHash = Arrays.hashCode(buffer); 207 } 208 209 @Override hashCode()210 public int hashCode() { 211 return Objects.hash(width, height, format, bufferHash); 212 } 213 214 /** 215 * order bitmaps by their size (dimension WxH), format, and buffer hash, 216 * by default in descending order so large bitmaps are more significant 217 */ 218 @Override compareTo(BitmapInfo other)219 public int compareTo(BitmapInfo other) { 220 if (other == this) { 221 return 0; 222 } 223 if (other.width * other.height != this.width * this.height) { 224 return other.width * other.height - this.width * this.height; 225 } 226 if (other.format != this.format) { 227 return other.format - this.format; 228 } 229 if (other.bufferHash != this.bufferHash) { 230 return other.bufferHash - this.bufferHash; 231 } 232 return 0; 233 } 234 235 @Override equals(Object o)236 public boolean equals(Object o) { 237 if (o == this) { 238 return true; 239 } 240 if (!(o instanceof BitmapInfo)) { 241 return false; 242 } 243 BitmapInfo other = (BitmapInfo)o; 244 return (this.width == other.width) 245 && (this.height == other.height) 246 && (this.format == other.format) 247 && (this.bufferHash == other.bufferHash); 248 } 249 } 250 251 /** 252 * Return bitmap info for this object, or null if no appropriate bitmap 253 * info is available. 254 */ getBitmapInfo(BitmapDumpData bitmapDumpData)255 private BitmapInfo getBitmapInfo(BitmapDumpData bitmapDumpData) { 256 if (mBitmapInfo != null) { 257 return mBitmapInfo; 258 } 259 260 if (!isInstanceOfClass("android.graphics.Bitmap")) { 261 return null; 262 } 263 264 Integer width = getIntField("mWidth", null); 265 if (width == null) { 266 return null; 267 } 268 269 Integer height = getIntField("mHeight", null); 270 if (height == null) { 271 return null; 272 } 273 274 byte[] buffer = getByteArrayField("mBuffer"); 275 if (buffer != null) { 276 if (buffer.length < 4 * height * width) { 277 return null; 278 } 279 mBitmapInfo = new BitmapInfo(width, height, -1, buffer); 280 return mBitmapInfo; 281 } 282 283 long nativePtr = getLongField("mNativePtr", -1l); 284 if (nativePtr == -1) { 285 return null; 286 } 287 288 if (bitmapDumpData == null || bitmapDumpData.count == 0) { 289 return null; 290 } 291 292 if (!bitmapDumpData.buffers.containsKey(nativePtr)) { 293 return null; 294 } 295 296 buffer = bitmapDumpData.buffers.get(nativePtr); 297 if (buffer == null) { 298 return null; 299 } 300 301 mBitmapInfo = new BitmapInfo(width, height, bitmapDumpData.format, buffer); 302 return mBitmapInfo; 303 } 304 305 /** 306 * Represents a bitmap with either 307 * - its format and content in `buffer` 308 * - or a BufferedImage with its raw pixels 309 */ 310 public static class Bitmap { 311 /** 312 * format of the bitmap content in buffer 313 */ 314 public String format; 315 /** 316 * byte buffer of the bitmap content 317 */ 318 public byte[] buffer; 319 /** 320 * BufferedImage with the bitmap's raw pixels 321 */ 322 public BufferedImage image; 323 324 /** 325 * Initialize a Bitmap instance 326 * @param format - format of the bitmap 327 * @param buffer - buffer of the bitmap content 328 * @param image - BufferedImage with the bitmap's raw pixel 329 */ Bitmap(String format, byte[] buffer, BufferedImage image)330 public Bitmap(String format, byte[] buffer, BufferedImage image) { 331 this.format = format; 332 this.buffer = buffer; 333 this.image = image; 334 } 335 } 336 asBufferedImage(BitmapInfo info)337 private BufferedImage asBufferedImage(BitmapInfo info) { 338 // Convert the raw data to an image 339 // Convert BGRA to ABGR 340 int[] abgr = new int[info.height * info.width]; 341 for (int i = 0; i < abgr.length; i++) { 342 abgr[i] = ( 343 (((int) info.buffer[i * 4 + 3] & 0xFF) << 24) 344 + (((int) info.buffer[i * 4 + 0] & 0xFF) << 16) 345 + (((int) info.buffer[i * 4 + 1] & 0xFF) << 8) 346 + ((int) info.buffer[i * 4 + 2] & 0xFF)); 347 } 348 349 BufferedImage bitmap = new BufferedImage( 350 info.width, info.height, BufferedImage.TYPE_4BYTE_ABGR); 351 bitmap.setRGB(0, 0, info.width, info.height, abgr, 0, info.width); 352 return bitmap; 353 } 354 355 /** 356 * Returns the bitmap associated with this instance. 357 * This is relevant for instances of android.graphics.Bitmap. 358 * Returns null if there is no bitmap pixel data associated 359 * with the given instance. 360 * 361 * @return the bitmap pixel data associated with this image 362 */ getBitmap()363 public Bitmap getBitmap() { 364 final BitmapInfo info = mBitmapInfo; 365 if (info == null) { 366 return null; 367 } 368 369 /** 370 * See android.graphics.Bitmap.CompressFormat for definitions 371 * -1 for legacy objects with content in `Bitmap.mBuffer` 372 */ 373 switch (info.format) { 374 case 0: /* JPEG */ 375 return new Bitmap("image/jpg", info.buffer, null); 376 case 1: /* PNG */ 377 return new Bitmap("image/png", info.buffer, null); 378 case 2: /* WEBP */ 379 case 3: /* WEBP_LOSSY */ 380 case 4: /* WEBP_LOSSLESS */ 381 return new Bitmap("image/webp", info.buffer, null); 382 case -1:/* Legacy */ 383 return new Bitmap(null, null, asBufferedImage(info)); 384 default: 385 return null; 386 } 387 } 388 389 } 390