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