• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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