• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.ddmuilib;
18 
19 import com.android.ddmlib.Client;
20 import com.android.ddmlib.IShellOutputReceiver;
21 import com.android.ddmlib.Log;
22 
23 import org.eclipse.swt.SWT;
24 import org.eclipse.swt.events.SelectionAdapter;
25 import org.eclipse.swt.events.SelectionEvent;
26 import org.eclipse.swt.layout.GridData;
27 import org.eclipse.swt.layout.GridLayout;
28 import org.eclipse.swt.layout.RowLayout;
29 import org.eclipse.swt.widgets.Button;
30 import org.eclipse.swt.widgets.Combo;
31 import org.eclipse.swt.widgets.Composite;
32 import org.eclipse.swt.widgets.Control;
33 import org.eclipse.swt.widgets.FileDialog;
34 import org.eclipse.swt.widgets.Label;
35 import org.jfree.chart.ChartFactory;
36 import org.jfree.chart.JFreeChart;
37 import org.jfree.data.general.DefaultPieDataset;
38 import org.jfree.experimental.chart.swt.ChartComposite;
39 
40 import java.io.BufferedReader;
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.FileReader;
44 import java.io.IOException;
45 import java.util.regex.Matcher;
46 import java.util.regex.Pattern;
47 
48 /**
49  * Displays system information graphs obtained from a bugreport file or device.
50  */
51 public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {
52 
53     // UI components
54     private Label mLabel;
55     private Button mFetchButton;
56     private Combo mDisplayMode;
57 
58     private DefaultPieDataset mDataset;
59 
60     // The bugreport file to process
61     private File mDataFile;
62 
63     // To get output from adb commands
64     private FileOutputStream mTempStream;
65 
66     // Selects the current display: MODE_CPU, etc.
67     private int mMode = 0;
68 
69     private static final int MODE_CPU = 0;
70     private static final int MODE_ALARM = 1;
71     private static final int MODE_WAKELOCK = 2;
72     private static final int MODE_MEMINFO = 3;
73     private static final int MODE_SYNC = 4;
74 
75     // argument to dumpsys; section in the bugreport holding the data
76     private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm",
77             "batteryinfo", "MEMORY INFO", "content"};
78 
79     private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
80             "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
81             "dumpsys content"};
82 
83     private static final String CAPTIONS[] = {"CPU load", "Alarms",
84             "Wakelocks", "Memory usage", "Sync"};
85 
86     /**
87      * Generates the dataset to display.
88      *
89      * @param file The bugreport file to process.
90      */
generateDataset(File file)91     public void generateDataset(File file) {
92         mDataset.clear();
93         mLabel.setText("");
94         if (file == null) {
95             return;
96         }
97         try {
98             BufferedReader br = getBugreportReader(file);
99             if (mMode == MODE_CPU) {
100                 readCpuDataset(br);
101             } else if (mMode == MODE_ALARM) {
102                 readAlarmDataset(br);
103             } else if (mMode == MODE_WAKELOCK) {
104                 readWakelockDataset(br);
105             } else if (mMode == MODE_MEMINFO) {
106                 readMeminfoDataset(br);
107             } else if (mMode == MODE_SYNC) {
108                 readSyncDataset(br);
109             }
110         } catch (IOException e) {
111             Log.e("DDMS", e);
112         }
113     }
114 
115     /**
116      * Sent when a new device is selected. The new device can be accessed with
117      * {@link #getCurrentDevice()}
118      */
119     @Override
deviceSelected()120     public void deviceSelected() {
121         if (getCurrentDevice() != null) {
122             mFetchButton.setEnabled(true);
123             loadFromDevice();
124         } else {
125             mFetchButton.setEnabled(false);
126         }
127     }
128 
129     /**
130      * Sent when a new client is selected. The new client can be accessed with
131      * {@link #getCurrentClient()}.
132      */
133     @Override
clientSelected()134     public void clientSelected() {
135     }
136 
137     /**
138      * Sets the focus to the proper control inside the panel.
139      */
140     @Override
setFocus()141     public void setFocus() {
142         mDisplayMode.setFocus();
143     }
144 
145     /**
146      * Fetches a new bugreport from the device and updates the display.
147      * Fetching is asynchronous.  See also addOutput, flush, and isCancelled.
148      */
loadFromDevice()149     private void loadFromDevice() {
150         try {
151             initShellOutputBuffer();
152             if (mMode == MODE_MEMINFO) {
153                 // Hack to add bugreport-style section header for meminfo
154                 mTempStream.write("------ MEMORY INFO ------\n".getBytes());
155             }
156             getCurrentDevice().executeShellCommand(
157                     DUMP_COMMAND[mMode], this);
158         } catch (IOException e) {
159             Log.e("DDMS", e);
160         }
161     }
162 
163     /**
164      * Initializes temporary output file for executeShellCommand().
165      *
166      * @throws IOException on file error
167      */
initShellOutputBuffer()168     void initShellOutputBuffer() throws IOException {
169         mDataFile = File.createTempFile("ddmsfile", ".txt");
170         mDataFile.deleteOnExit();
171         mTempStream = new FileOutputStream(mDataFile);
172     }
173 
174     /**
175      * Adds output to the temp file. IShellOutputReceiver method. Called by
176      * executeShellCommand().
177      */
addOutput(byte[] data, int offset, int length)178     public void addOutput(byte[] data, int offset, int length) {
179         try {
180             mTempStream.write(data, offset, length);
181         }
182         catch (IOException e) {
183             Log.e("DDMS", e);
184         }
185     }
186 
187     /**
188      * Processes output from shell command. IShellOutputReceiver method. The
189      * output is passed to generateDataset(). Called by executeShellCommand() on
190      * completion.
191      */
flush()192     public void flush() {
193         if (mTempStream != null) {
194             try {
195                 mTempStream.close();
196                 generateDataset(mDataFile);
197                 mTempStream = null;
198                 mDataFile = null;
199             } catch (IOException e) {
200                 Log.e("DDMS", e);
201             }
202         }
203     }
204 
205     /**
206      * IShellOutputReceiver method.
207      *
208      * @return false - don't cancel
209      */
isCancelled()210     public boolean isCancelled() {
211         return false;
212     }
213 
214     /**
215      * Create our controls for the UI panel.
216      */
217     @Override
createControl(Composite parent)218     protected Control createControl(Composite parent) {
219         Composite top = new Composite(parent, SWT.NONE);
220         top.setLayout(new GridLayout(1, false));
221         top.setLayoutData(new GridData(GridData.FILL_BOTH));
222 
223         Composite buttons = new Composite(top, SWT.NONE);
224         buttons.setLayout(new RowLayout());
225 
226         mDisplayMode = new Combo(buttons, SWT.PUSH);
227         for (String mode : CAPTIONS) {
228             mDisplayMode.add(mode);
229         }
230         mDisplayMode.select(mMode);
231         mDisplayMode.addSelectionListener(new SelectionAdapter() {
232             @Override
233             public void widgetSelected(SelectionEvent e) {
234                 mMode = mDisplayMode.getSelectionIndex();
235                 if (mDataFile != null) {
236                     generateDataset(mDataFile);
237                 } else if (getCurrentDevice() != null) {
238                     loadFromDevice();
239                 }
240             }
241         });
242 
243         final Button loadButton = new Button(buttons, SWT.PUSH);
244         loadButton.setText("Load from File");
245         loadButton.addSelectionListener(new SelectionAdapter() {
246             @Override
247             public void widgetSelected(SelectionEvent e) {
248                 FileDialog fileDialog = new FileDialog(loadButton.getShell(),
249                         SWT.OPEN);
250                 fileDialog.setText("Load bugreport");
251                 String filename = fileDialog.open();
252                 if (filename != null) {
253                     mDataFile = new File(filename);
254                     generateDataset(mDataFile);
255                 }
256             }
257         });
258 
259         mFetchButton = new Button(buttons, SWT.PUSH);
260         mFetchButton.setText("Update from Device");
261         mFetchButton.setEnabled(false);
262         mFetchButton.addSelectionListener(new SelectionAdapter() {
263             @Override
264             public void widgetSelected(SelectionEvent e) {
265                 loadFromDevice();
266             }
267         });
268 
269         mLabel = new Label(top, SWT.NONE);
270         mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
271 
272         mDataset = new DefaultPieDataset();
273         JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
274                 /* legend */, true/* tooltips */, false /* urls */);
275 
276         ChartComposite chartComposite = new ChartComposite(top,
277                 SWT.BORDER, chart,
278                 ChartComposite.DEFAULT_HEIGHT,
279                 ChartComposite.DEFAULT_HEIGHT,
280                 ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
281                 ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
282                 3000,
283                 // max draw width. We don't want it to zoom, so we put a big number
284                 3000,
285                 // max draw height. We don't want it to zoom, so we put a big number
286                 true,  // off-screen buffer
287                 true,  // properties
288                 true,  // save
289                 true,  // print
290                 false,  // zoom
291                 true);
292         chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
293         return top;
294     }
295 
clientChanged(final Client client, int changeMask)296     public void clientChanged(final Client client, int changeMask) {
297         // Don't care
298     }
299 
300     /**
301      * Helper to open a bugreport and skip to the specified section.
302      *
303      * @param file File to open
304      * @return Reader to bugreport file
305      * @throws java.io.IOException on file error
306      */
getBugreportReader(File file)307     private BufferedReader getBugreportReader(File file) throws
308             IOException {
309         BufferedReader br = new BufferedReader(new FileReader(file));
310         // Skip over the unwanted bugreport sections
311         while (true) {
312             String line = br.readLine();
313             if (line == null) {
314                 Log.d("DDMS", "Service not found " + line);
315                 break;
316             }
317             if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
318                     line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
319                 break;
320             }
321         }
322         return br;
323     }
324 
325     /**
326      * Parse the time string generated by BatteryStats.
327      * A typical new-format string is "11d 13h 45m 39s 999ms".
328      * A typical old-format string is "12.3 sec".
329      * @return time in ms
330      */
parseTimeMs(String s)331     private static long parseTimeMs(String s) {
332         long total = 0;
333         // Matches a single component e.g. "12.3 sec" or "45ms"
334         Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
335         Matcher m = p.matcher(s);
336         while (m.find()) {
337             String label = m.group(2);
338             if ("sec".equals(label)) {
339                 // Backwards compatibility with old time format
340                 total += (long) (Double.parseDouble(m.group(1)) * 1000);
341                 continue;
342             }
343             long value = Integer.parseInt(m.group(1));
344             if ("d".equals(label)) {
345                 total += value * 24 * 60 * 60 * 1000;
346             } else if ("h".equals(label)) {
347                 total += value * 60 * 60 * 1000;
348             } else if ("m".equals(label)) {
349                 total += value * 60 * 1000;
350             } else if ("s".equals(label)) {
351                 total += value * 1000;
352             } else if ("ms".equals(label)) {
353                 total += value;
354             }
355         }
356         return total;
357     }
358     /**
359      * Processes wakelock information from bugreport. Updates mDataset with the
360      * new data.
361      *
362      * @param br Reader providing the content
363      * @throws IOException if error reading file
364      */
readWakelockDataset(BufferedReader br)365     void readWakelockDataset(BufferedReader br) throws IOException {
366         Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
367         Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
368         double total = 0;
369         boolean inCurrent = false;
370 
371         while (true) {
372             String line = br.readLine();
373             if (line == null || line.startsWith("DUMP OF SERVICE")) {
374                 // Done, or moved on to the next service
375                 break;
376             }
377             if (line.startsWith("Current Battery Usage Statistics")) {
378                 inCurrent = true;
379             } else if (inCurrent) {
380                 Matcher m = lockPattern.matcher(line);
381                 if (m.find()) {
382                     double value = parseTimeMs(m.group(2)) / 1000.;
383                     mDataset.setValue(m.group(1), value);
384                     total -= value;
385                 } else {
386                     m = totalPattern.matcher(line);
387                     if (m.find()) {
388                         total += parseTimeMs(m.group(1)) / 1000.;
389                     }
390                 }
391             }
392         }
393         if (total > 0) {
394             mDataset.setValue("Unlocked", total);
395         }
396     }
397 
398     /**
399      * Processes alarm information from bugreport. Updates mDataset with the new
400      * data.
401      *
402      * @param br Reader providing the content
403      * @throws IOException if error reading file
404      */
readAlarmDataset(BufferedReader br)405     void readAlarmDataset(BufferedReader br) throws IOException {
406         Pattern pattern = Pattern
407                 .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
408 
409         while (true) {
410             String line = br.readLine();
411             if (line == null || line.startsWith("DUMP OF SERVICE")) {
412                 // Done, or moved on to the next service
413                 break;
414             }
415             Matcher m = pattern.matcher(line);
416             if (m.find()) {
417                 long count = Long.parseLong(m.group(1));
418                 String name = m.group(2);
419                 mDataset.setValue(name, count);
420             }
421         }
422     }
423 
424     /**
425      * Processes cpu load information from bugreport. Updates mDataset with the
426      * new data.
427      *
428      * @param br Reader providing the content
429      * @throws IOException if error reading file
430      */
readCpuDataset(BufferedReader br)431     void readCpuDataset(BufferedReader br) throws IOException {
432         Pattern pattern = Pattern
433                 .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
434 
435         while (true) {
436             String line = br.readLine();
437             if (line == null || line.startsWith("DUMP OF SERVICE")) {
438                 // Done, or moved on to the next service
439                 break;
440             }
441             if (line.startsWith("Load:")) {
442                 mLabel.setText(line);
443                 continue;
444             }
445             Matcher m = pattern.matcher(line);
446             if (m.find()) {
447                 String name = m.group(1);
448                 long both = Long.parseLong(m.group(2));
449                 long user = Long.parseLong(m.group(3));
450                 long kernel = Long.parseLong(m.group(4));
451                 if ("TOTAL".equals(name)) {
452                     if (both < 100) {
453                         mDataset.setValue("Idle", (100 - both));
454                     }
455                 } else {
456                     // Try to make graphs more useful even with rounding;
457                     // log often has 0% user + 0% kernel = 1% total
458                     // We arbitrarily give extra to kernel
459                     if (user > 0) {
460                         mDataset.setValue(name + " (user)", user);
461                     }
462                     if (kernel > 0) {
463                         mDataset.setValue(name + " (kernel)" , both - user);
464                     }
465                     if (user == 0 && kernel == 0 && both > 0) {
466                         mDataset.setValue(name, both);
467                     }
468                 }
469             }
470         }
471     }
472 
473     /**
474      * Processes meminfo information from bugreport. Updates mDataset with the
475      * new data.
476      *
477      * @param br Reader providing the content
478      * @throws IOException if error reading file
479      */
readMeminfoDataset(BufferedReader br)480     void readMeminfoDataset(BufferedReader br) throws IOException {
481         Pattern valuePattern = Pattern.compile("(\\d+) kB");
482         long total = 0;
483         long other = 0;
484         mLabel.setText("PSS in kB");
485 
486         // Scan meminfo
487         while (true) {
488             String line = br.readLine();
489             if (line == null) {
490                 // End of file
491                 break;
492             }
493             Matcher m = valuePattern.matcher(line);
494             if (m.find()) {
495                 long kb = Long.parseLong(m.group(1));
496                 if (line.startsWith("MemTotal")) {
497                     total = kb;
498                 } else if (line.startsWith("MemFree")) {
499                     mDataset.setValue("Free", kb);
500                     total -= kb;
501                 } else if (line.startsWith("Slab")) {
502                     mDataset.setValue("Slab", kb);
503                     total -= kb;
504                 } else if (line.startsWith("PageTables")) {
505                     mDataset.setValue("PageTables", kb);
506                     total -= kb;
507                 } else if (line.startsWith("Buffers") && kb > 0) {
508                     mDataset.setValue("Buffers", kb);
509                     total -= kb;
510                 } else if (line.startsWith("Inactive")) {
511                     mDataset.setValue("Inactive", kb);
512                     total -= kb;
513                 } else if (line.startsWith("MemFree")) {
514                     mDataset.setValue("Free", kb);
515                     total -= kb;
516                 }
517             } else {
518                 break;
519             }
520         }
521         // Scan procrank
522         while (true) {
523             String line = br.readLine();
524             if (line == null) {
525                 break;
526             }
527             if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
528                 // procrank header
529                 continue;
530             }
531             if  (line.indexOf("----") >= 0) {
532                 //end of procrank section
533                 break;
534             }
535             // Extract pss field from procrank output
536             long pss = Long.parseLong(line.substring(23, 31).trim());
537             String cmdline = line.substring(43).trim().replace("/system/bin/", "");
538             // Arbitrary minimum size to display
539             if (pss > 2000) {
540                 mDataset.setValue(cmdline, pss);
541             } else {
542                 other += pss;
543             }
544             total -= pss;
545         }
546         mDataset.setValue("Other", other);
547         mDataset.setValue("Unknown", total);
548     }
549 
550     /**
551      * Processes sync information from bugreport. Updates mDataset with the new
552      * data.
553      *
554      * @param br Reader providing the content
555      * @throws IOException if error reading file
556      */
readSyncDataset(BufferedReader br)557     void readSyncDataset(BufferedReader br) throws IOException {
558         while (true) {
559             String line = br.readLine();
560             if (line == null || line.startsWith("DUMP OF SERVICE")) {
561                 // Done, or moved on to the next service
562                 break;
563             }
564             if (line.startsWith(" |") && line.length() > 70) {
565                 String authority = line.substring(3, 18).trim();
566                 String duration = line.substring(61, 70).trim();
567                 // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
568                 String durParts[] = duration.split(":");
569                 if (durParts.length == 2) {
570                     long dur = Long.parseLong(durParts[0]) * 60 + Long
571                             .parseLong(durParts[1]);
572                     mDataset.setValue(authority, dur);
573                 } else if (duration.length() == 3) {
574                     long dur = Long.parseLong(durParts[0]) * 3600
575                             + Long.parseLong(durParts[1]) * 60 + Long
576                             .parseLong(durParts[2]);
577                     mDataset.setValue(authority, dur);
578                 }
579             }
580         }
581     }
582 }
583