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