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