1 /*
2  * Copyright (C) 2016 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.bugreport.html;
18 
19 import com.android.bugreport.anr.Anr;
20 import com.android.bugreport.bugreport.Bugreport;
21 import com.android.bugreport.cpuinfo.CpuUsage;
22 import com.android.bugreport.cpuinfo.CpuUsageSnapshot;
23 import com.android.bugreport.logcat.Logcat;
24 import com.android.bugreport.logcat.LogLine;
25 import com.android.bugreport.stacks.ProcessSnapshot;
26 import com.android.bugreport.stacks.JavaStackFrameSnapshot;
27 import com.android.bugreport.stacks.KernelStackFrameSnapshot;
28 import com.android.bugreport.stacks.LockSnapshot;
29 import com.android.bugreport.stacks.NativeStackFrameSnapshot;
30 import com.android.bugreport.stacks.StackFrameSnapshot;
31 import com.android.bugreport.stacks.ThreadSnapshot;
32 import com.android.bugreport.stacks.VmTraces;
33 
34 import com.google.clearsilver.jsilver.JSilver;
35 import com.google.clearsilver.jsilver.JSilverOptions;
36 import com.google.clearsilver.jsilver.autoescape.EscapeMode;
37 import com.google.clearsilver.jsilver.data.Data;
38 import com.google.clearsilver.jsilver.resourceloader.ClassResourceLoader;
39 
40 import java.io.File;
41 import java.io.FileWriter;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.Collection;
45 import java.util.HashMap;
46 import java.util.HashSet;
47 import java.util.List;
48 
49 /**
50  * Formats a bugreport as html and writes the file.
51  */
52 public class Renderer {
53     /**
54      * The next id of the panel to use.
55      */
56     private int mNextPanelId;
57 
Renderer()58     public Renderer() {
59     }
60 
61     /**
62      * Render the Bugreport into the html file.
63      */
render(File outFile, Bugreport bugreport)64     public void render(File outFile, Bugreport bugreport) throws IOException {
65         // Load the template and renderer
66         final JSilverOptions options = new JSilverOptions();
67         options.setEscapeMode(EscapeMode.ESCAPE_HTML);
68         final JSilver jsilver = new JSilver(new ClassResourceLoader(getClass()), options);
69         final Data hdf = jsilver.createData();
70 
71         // Build the hierarchical data format data structure
72         makeHdf(hdf, bugreport);
73 
74         if (false) {
75             System.out.println(hdf);
76         }
77 
78         // Render it
79         final FileWriter writer = new FileWriter(outFile);
80         try {
81             jsilver.render("anr-template.html", hdf, writer);
82             writer.close();
83         } catch (IOException ex) {
84             // Delete the file so we don't leave half-written files laying around.
85             try {
86                 writer.close();
87             } catch (IOException e) {
88             }
89             outFile.delete();
90             // And rethrow the exception.
91             throw ex;
92         }
93     }
94 
95     /**
96      * Build the hdf for a Bugreport.
97      */
makeHdf(Data hdf, Bugreport bugreport)98     private void makeHdf(Data hdf, Bugreport bugreport) {
99         // Triage
100         makeTriageHdf(hdf, bugreport);
101 
102         // Logcat
103         makeLogcatHdf(hdf.createChild("logcat"), bugreport);
104 
105         // Monkey Anr
106         if (bugreport.monkeyAnr != null) {
107             makeAnrHdf(hdf, bugreport.monkeyAnr);
108         }
109 
110         // VM Traces Last ANR
111         makeVmTracesHdf(hdf.createChild("vmTracesLastAnr"), bugreport.anr,
112                 bugreport.vmTracesLastAnr);
113 
114         // VM Traces Just Now
115         makeVmTracesHdf(hdf.createChild("vmTracesJustNow"), bugreport.anr,
116                 bugreport.vmTracesJustNow);
117     }
118 
119     /**
120      * Build the hdf for an Anr.
121      */
makeAnrHdf(Data hdf, Anr anr)122     private void makeAnrHdf(Data hdf, Anr anr) {
123         // CPU Usage
124         final int N = anr.cpuUsages.size();
125         for (int i=0; i<N; i++) {
126             makeCpuUsageSnapshotHdf(hdf.createChild("monkey.cpuUsage." + i), anr.cpuUsages.get(i));
127         }
128 
129         // Processes
130         makeVmTracesHdf(hdf.createChild("monkey"), anr, anr.vmTraces);
131     }
132 
133     /**
134      * Build the hdf for a set of vm traces.  Sorts them by likelihood based on the anr.
135      */
makeVmTracesHdf(Data hdf, Anr anr, VmTraces vmTraces)136     private void makeVmTracesHdf(Data hdf, Anr anr, VmTraces vmTraces) {
137         // Process List
138         final Data processesHdf = hdf.createChild("processes");
139         sortProcesses(anr, vmTraces.processes);
140         final int N = vmTraces.processes.size();
141         for (int i=0; i<N; i++) {
142             makeProcessSnapshotHdf(processesHdf.createChild(Integer.toString(i)),
143                     vmTraces.processes.get(i));
144         }
145     }
146 
147 
148     /**
149      * Make the HDF for the triaged panel.
150      *
151      * Any single thread will only appear once in the triage panel, at the topmost
152      * position.
153      */
makeTriageHdf(Data hdf, Bugreport bugreport)154     private void makeTriageHdf(Data hdf, Bugreport bugreport) {
155         final Anr anr = bugreport.anr;
156 
157         int N;
158         final HashMap<Integer,HashSet<Integer>> visited = new HashMap<Integer,HashSet<Integer>>();
159 
160         // General information
161         hdf.setValue("triage.processName", anr.processName);
162         hdf.setValue("triage.componentPackage", anr.componentPackage);
163         hdf.setValue("triage.componentClass", anr.componentClass);
164         hdf.setValue("triage.pid", Integer.toString(anr.pid));
165         hdf.setValue("triage.reason", anr.reason);
166 
167         final ProcessSnapshot offendingProcess = anr.vmTraces.getProcess(anr.pid);
168         final ThreadSnapshot offendingThread = anr.vmTraces.getThread(anr.pid, "main");
169         if (offendingThread != null) {
170             makeThreadSnapshotHdf(hdf.createChild("triage.mainThread"), offendingProcess,
171                     offendingThread);
172 
173             HashSet<Integer> visitedThreads = new HashSet<Integer>();
174             visitedThreads.add(offendingThread.tid);
175             visited.put(offendingProcess.pid, visitedThreads);
176         }
177 
178         // Deadlocked Processes
179         final ArrayList<ProcessSnapshot> deadlockedProcesses = cloneAndFilter(visited,
180                 anr.vmTraces.deadlockedProcesses);
181         sortProcesses(anr, deadlockedProcesses);
182         N = deadlockedProcesses.size();
183         for (int i=0; i<N; i++) {
184             makeProcessSnapshotHdf(hdf.createChild("triage.deadlockedProcesses." + i),
185                     deadlockedProcesses.get(i));
186         }
187 
188         // Interesting Processes
189         final ArrayList<ProcessSnapshot> interestingProcesses = cloneAndFilter(visited,
190                 anr.vmTraces.interestingProcesses);
191         sortProcesses(anr, interestingProcesses);
192         N = interestingProcesses.size();
193         for (int i=0; i<N; i++) {
194             makeProcessSnapshotHdf(hdf.createChild("triage.interestingProcesses." + i),
195                     interestingProcesses.get(i));
196         }
197     }
198 
199     /**
200      * Makes a copy of the process and threads, removing ones that have accumulated in the
201      * visited list (probably from previous sections on the current page).
202      *
203      * @see #makeTriageHdf
204      */
cloneAndFilter(HashMap<Integer,HashSet<Integer>> visited, Collection<ProcessSnapshot> list)205     private ArrayList<ProcessSnapshot> cloneAndFilter(HashMap<Integer,HashSet<Integer>> visited,
206             Collection<ProcessSnapshot> list) {
207         final ArrayList<ProcessSnapshot> result = new ArrayList<ProcessSnapshot>();
208         for (ProcessSnapshot process: list) {
209             final ProcessSnapshot cloneProcess = process.clone();
210             HashSet<Integer> visitedThreads = visited.get(process.pid);
211             if (visitedThreads == null) {
212                 visitedThreads = new HashSet<Integer>();
213                 visited.put(process.pid, visitedThreads);
214             }
215             final int N = cloneProcess.threads.size();
216             for (int i=N-1; i>=0; i--) {
217                 final ThreadSnapshot cloneThread = cloneProcess.threads.get(i);
218                 if (visitedThreads.contains(cloneThread.tid)) {
219                     cloneProcess.threads.remove(i);
220                 }
221                 visitedThreads.add(cloneThread.tid);
222             }
223             if (cloneProcess.threads.size() > 0) {
224                 result.add(cloneProcess);
225             }
226         }
227         return result;
228     }
229 
230     /**
231      * Build the hdf for a CpuUsageSnapshot.
232      */
makeCpuUsageSnapshotHdf(Data hdf, CpuUsageSnapshot snapshot)233     private void makeCpuUsageSnapshotHdf(Data hdf, CpuUsageSnapshot snapshot) {
234         int N;
235 
236         N = snapshot.cpuUsage.size();
237         for (int i=0; i<N; i++) {
238             makeCpuUsageHdf(hdf.createChild(Integer.toString(i)), snapshot.cpuUsage.get(i));
239         }
240     }
241 
242     /**
243      * Build the hdf for a CpuUsage.
244      */
makeCpuUsageHdf(Data hdf, CpuUsage cpuUsage)245     private void makeCpuUsageHdf(Data hdf, CpuUsage cpuUsage) {
246     }
247 
248     /**
249      * Build the hdf for a ProcessSnapshot.
250      */
makeProcessSnapshotHdf(Data hdf, ProcessSnapshot process)251     private void makeProcessSnapshotHdf(Data hdf, ProcessSnapshot process) {
252         int N;
253 
254         hdf.setValue("panelId", Integer.toString(mNextPanelId++));
255 
256         hdf.setValue("pid", Integer.toString(process.pid));
257         hdf.setValue("cmdLine", process.cmdLine);
258         hdf.setValue("date", process.date);
259 
260         N = process.threads.size();
261         for (int i=0; i<N; i++) {
262             makeThreadSnapshotHdf(hdf.createChild("threads." + i), process, process.threads.get(i));
263         }
264     }
265 
266     /**
267      * Build the hdf for a ThreadSnapshot.
268      */
makeThreadSnapshotHdf(Data hdf, ProcessSnapshot process, ThreadSnapshot thread)269     private void makeThreadSnapshotHdf(Data hdf, ProcessSnapshot process, ThreadSnapshot thread) {
270         int N, M;
271 
272         hdf.setValue("name", thread.name);
273         hdf.setValue("daemon", thread.daemon);
274         hdf.setValue("priority", Integer.toString(thread.priority));
275         hdf.setValue("tid", Integer.toString(thread.tid));
276         hdf.setValue("sysTid", Integer.toString(thread.sysTid));
277         hdf.setValue("vmState", thread.vmState);
278         hdf.setValue("runnable", thread.runnable ? "1" : "0");
279         hdf.setValue("blocked", thread.blocked ? "1" : "0");
280         hdf.setValue("interesting", thread.interesting ? "1" : "0");
281         hdf.setValue("binder", thread.isBinder() ? "1" : "0");
282         hdf.setValue("outboundBinderCall", buildFunctionName(thread.outboundBinderPackage,
283                     thread.outboundBinderClass, thread.outboundBinderMethod));
284         hdf.setValue("inboundBinderCall", buildFunctionName(thread.inboundBinderPackage,
285                     thread.inboundBinderClass, thread.inboundBinderMethod));
286 
287         N = thread.attributeText.size();
288         for (int i=0; i<N; i++) {
289             hdf.setValue("attributes." + i, thread.attributeText.get(i));
290         }
291 
292         hdf.setValue("heldMutexes", thread.heldMutexes);
293 
294         N = thread.frames.size();
295         for (int i=0; i<N; i++) {
296             makeStackFrameSnapshotHdf(hdf.createChild("frames." + i), process,
297                     thread.frames.get(i));
298         }
299     }
300 
301     /**
302      * Combine package, class and method into fully qualified name.
303      */
buildFunctionName(String pkg, String cls, String meth)304     private String buildFunctionName(String pkg, String cls, String meth) {
305         final StringBuilder result = new StringBuilder();
306         if (pkg != null && pkg.length() > 0) {
307             result.append(pkg);
308             result.append('.');
309         }
310         if (cls != null && cls.length() > 0) {
311             result.append(cls);
312             result.append('.');
313         }
314         if (meth != null && meth.length() > 0) {
315             result.append(meth);
316         }
317         return result.toString();
318     }
319 
320     /**
321      * Build the hdf for a StackFrameSnapshot.
322      */
makeStackFrameSnapshotHdf(Data hdf, ProcessSnapshot process, StackFrameSnapshot frame)323     private void makeStackFrameSnapshotHdf(Data hdf, ProcessSnapshot process,
324             StackFrameSnapshot frame) {
325         hdf.setValue("text", frame.text);
326 
327         if (frame.frameType == StackFrameSnapshot.FRAME_TYPE_NATIVE) {
328             final NativeStackFrameSnapshot f = (NativeStackFrameSnapshot)frame;
329             hdf.setValue("frameType", "native");
330             hdf.setValue("symbol", f.symbol);
331             hdf.setValue("library", f.library);
332             hdf.setValue("offset", Integer.toString(f.offset));
333 
334         } else if (frame.frameType == StackFrameSnapshot.FRAME_TYPE_KERNEL) {
335             final KernelStackFrameSnapshot f = (KernelStackFrameSnapshot)frame;
336             hdf.setValue("frameType", "kernel");
337             hdf.setValue("syscall", f.syscall);
338             hdf.setValue("offset0", Integer.toString(f.offset0));
339             hdf.setValue("offset1", Integer.toString(f.offset1));
340 
341         } else if (frame.frameType == StackFrameSnapshot.FRAME_TYPE_JAVA) {
342             final JavaStackFrameSnapshot f = (JavaStackFrameSnapshot)frame;
343             hdf.setValue("frameType", "java");
344             hdf.setValue("packageName", f.packageName);
345             hdf.setValue("className", f.className);
346             hdf.setValue("methodName", f.methodName);
347             hdf.setValue("sourceFile", f.sourceFile);
348             hdf.setValue("sourceLine", Integer.toString(f.sourceLine));
349             hdf.setValue("language",
350                     (f.language == JavaStackFrameSnapshot.LANGUAGE_JAVA ? "java" : "jni"));
351             final int N = f.locks.size();
352             for (int i=0; i<N; i++) {
353                 final LockSnapshot lock = f.locks.get(i);
354                 final Data lockHdf = hdf.createChild("locks." + i);
355                 if (lock.type == LockSnapshot.LOCKED) {
356                     lockHdf.setValue("type", "locked");
357                 } else if (lock.type == LockSnapshot.WAITING) {
358                     lockHdf.setValue("type", "waiting");
359                 } else if (lock.type == LockSnapshot.BLOCKED) {
360                     lockHdf.setValue("type", "blocked");
361                 }
362                 lockHdf.setValue("address", lock.address);
363                 lockHdf.setValue("packageName", lock.packageName);
364                 lockHdf.setValue("className", lock.className);
365                 lockHdf.setValue("threadId", Integer.toString(lock.threadId));
366                 if (lock.threadId >= 0) {
367                     final ThreadSnapshot referenced = process.getThread(lock.threadId);
368                     if (referenced != null) {
369                         lockHdf.setValue("threadName", referenced.name);
370                     }
371                 }
372             }
373         } else {
374             hdf.setValue("frameType", "other");
375         }
376     }
377 
378     /**
379      * Sort processes so the more interesting ones are at the top.
380      */
sortProcesses(Anr anr, List<ProcessSnapshot> processes)381     private void sortProcesses(Anr anr, List<ProcessSnapshot> processes) {
382         final int N = processes.size();
383 
384         // Last is alphabetical
385         processes.sort(new java.util.Comparator<ProcessSnapshot>() {
386                 @Override
387                 public int compare(ProcessSnapshot a, ProcessSnapshot b) {
388                     return a.cmdLine.compareTo(b.cmdLine);
389                 }
390 
391                 @Override
392                 public boolean equals(Object that) {
393                     return this == that;
394                 }
395             });
396 
397         // Move the ones that start with / to the end. They're typically not interesting
398         for (int i=0, j=0; i<N; i++) {
399             final ProcessSnapshot process = processes.get(j);
400             if (process.cmdLine.length() > 0 && process.cmdLine.charAt(0) == '/') {
401                 processes.remove(j);
402                 processes.add(process);
403             } else {
404                 j++;
405             }
406         }
407 
408         // TODO: Next is by CPU %
409 
410         // The system process always goes second
411         for (int i=0; i<N; i++) {
412             final ProcessSnapshot process = processes.get(i);
413             if ("system_server".equals(process.cmdLine)) {
414                 processes.remove(i);
415                 processes.add(0, process);
416                 break;
417             }
418         }
419 
420         // The blamed process always goes first
421         for (int i=0; i<N; i++) {
422             final ProcessSnapshot process = processes.get(i);
423             if (process.pid == anr.pid) {
424                 processes.remove(i);
425                 processes.add(0, process);
426                 break;
427             }
428         }
429 
430         // And do the threads too.
431         sortThreads(processes);
432     }
433 
434     /**
435      * Sort threads so the more interesting ones are at the top.
436      */
sortThreads(List<ProcessSnapshot> processes)437     private void sortThreads(List<ProcessSnapshot> processes) {
438         for (ProcessSnapshot process: processes) {
439             final int N = process.threads.size();
440 
441             final ArrayList<ThreadSnapshot> mainThreads = new ArrayList<ThreadSnapshot>();
442             final ArrayList<ThreadSnapshot> blockedThreads = new ArrayList<ThreadSnapshot>();
443             final ArrayList<ThreadSnapshot> binderThreads = new ArrayList<ThreadSnapshot>();
444             final ArrayList<ThreadSnapshot> interestingThreads = new ArrayList<ThreadSnapshot>();
445             final ArrayList<ThreadSnapshot> otherThreads = new ArrayList<ThreadSnapshot>();
446 
447             int insertAt = 0; // in case there are more than one called "main"
448             for (int i=0; i<N; i++) {
449                 final ThreadSnapshot thread = process.threads.get(i);
450                 if ("main".equals(thread.name)) {
451                     mainThreads.add(thread);
452                 } else if (thread.blocked) {
453                     blockedThreads.add(thread);
454                 } else if (thread.isBinder()) {
455                     binderThreads.add(thread);
456                 } else if (thread.interesting) {
457                     interestingThreads.add(thread);
458                 } else {
459                     otherThreads.add(thread);
460                 }
461             }
462 
463             // Within those groups, sort by name.
464             final java.util.Comparator<ThreadSnapshot> cmp
465                     = new java.util.Comparator<ThreadSnapshot>() {
466                 @Override
467                 public int compare(ThreadSnapshot a, ThreadSnapshot b) {
468                     return a.name.compareTo(b.name);
469                 }
470 
471                 @Override
472                 public boolean equals(Object that) {
473                     return this == that;
474                 }
475             };
476             mainThreads.sort(cmp);
477             blockedThreads.sort(cmp);
478             binderThreads.sort(cmp);
479             interestingThreads.sort(cmp);
480             otherThreads.sort(cmp);
481 
482             process.threads = mainThreads;
483             process.threads.addAll(blockedThreads);
484             process.threads.addAll(binderThreads);
485             process.threads.addAll(interestingThreads);
486             process.threads.addAll(otherThreads);
487         }
488     }
489 
490     /**
491      * Make the hdf for the logcat panel.
492      */
makeLogcatHdf(Data hdf, Bugreport bugreport)493     private void makeLogcatHdf(Data hdf, Bugreport bugreport) {
494         int N;
495 
496         final Data interestingHdf = hdf.createChild("interesting");
497         N = bugreport.interestingLogLines.size();
498         for (int i=0; i<N; i++) {
499             final LogLine line = bugreport.interestingLogLines.get(i);
500             makeLogcatLineHdf(interestingHdf.createChild(Integer.toString(i)), line);
501         }
502 
503         final Logcat logcat = bugreport.logcat;
504         final Data linesHdf = hdf.createChild("lines");
505         N = logcat.lines.size();
506         for (int i=0; i<N; i++) {
507             final LogLine line = logcat.lines.get(i);
508             makeLogcatLineHdf(linesHdf.createChild(Integer.toString(i)), line);
509         }
510     }
511 
512     /**
513      * Make hdf for a line of logcat.
514      */
makeLogcatLineHdf(Data hdf, LogLine line)515     private void makeLogcatLineHdf(Data hdf, LogLine line) {
516         hdf.setValue("lineno", Integer.toString(line.lineno));
517         if (line.bufferBegin != null) {
518             hdf.setValue("bufferBegin", line.bufferBegin);
519             hdf.setValue("rawText", line.rawText);
520         } else {
521             hdf.setValue("header", line.header);
522             hdf.setValue("level", Character.toString(line.level));
523             hdf.setValue("tag", line.tag);
524             hdf.setValue("text", line.text);
525             if (line.regionAnr) {
526                 hdf.setValue("regionAnr", "1");
527             }
528             if (line.regionBugreport) {
529                 hdf.setValue("regionBugreport", "1");
530             }
531 
532             String title = "Process: ??";
533             if (line.process != null) {
534                 title = "Process: " + line.process.cmdLine;
535                 if (line.thread != null) {
536                     title += "\nThread: " + line.thread.name;
537                 }
538             }
539             hdf.setValue("title", title);
540         }
541     }
542 }
543