2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.ddmuilib;
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;
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;
43 import java.io.BufferedReader;
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;
52 * Displays system information graphs obtained from a bugreport file or device.
54 public class SysinfoPanel extends TablePanel implements IShellOutputReceiver {
58 private Button mFetchButton;
59 private Combo mDisplayMode;
61 private DefaultPieDataset mDataset;
63 // The bugreport file to process
64 private File mDataFile;
66 // To get output from adb commands
67 private FileOutputStream mTempStream;
69 // Selects the current display: MODE_CPU, etc.
70 private int mMode = 0;
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;
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"};
82 private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo",
83 "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank",
86 private static final String CAPTIONS[] = {"CPU load", "Alarms",
87 "Wakelocks", "Memory usage", "Sync"};
90 * Generates the dataset to display.
92 * @param file The bugreport file to process.
94 public void generateDataset(File file) {
101 BufferedReader br = getBugreportReader(file);
102 if (mMode == MODE_CPU) {
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) {
113 } catch (IOException e) {
119 * Sent when a new device is selected. The new device can be accessed with
120 * {@link #getCurrentDevice()}
123 public void deviceSelected() {
124 if (getCurrentDevice() != null) {
125 mFetchButton.setEnabled(true);
128 mFetchButton.setEnabled(false);
133 * Sent when a new client is selected. The new client can be accessed with
134 * {@link #getCurrentClient()}.
137 public void clientSelected() {
141 * Sets the focus to the proper control inside the panel.
144 public void setFocus() {
145 mDisplayMode.setFocus();
149 * Fetches a new bugreport from the device and updates the display.
150 * Fetching is asynchronous. See also addOutput, flush, and isCancelled.
152 private void loadFromDevice() {
154 initShellOutputBuffer();
155 if (mMode == MODE_MEMINFO) {
156 // Hack to add bugreport-style section header for meminfo
157 mTempStream.write("------ MEMORY INFO ------\n".getBytes());
159 getCurrentDevice().executeShellCommand(
160 DUMP_COMMAND[mMode], this);
161 } catch (IOException e) {
163 } catch (TimeoutException e) {
165 } catch (AdbCommandRejectedException e) {
167 } catch (ShellCommandUnresponsiveException e) {
173 * Initializes temporary output file for executeShellCommand().
175 * @throws IOException on file error
177 void initShellOutputBuffer() throws IOException {
178 mDataFile = File.createTempFile("ddmsfile", ".txt");
179 mDataFile.deleteOnExit();
180 mTempStream = new FileOutputStream(mDataFile);
184 * Adds output to the temp file. IShellOutputReceiver method. Called by
185 * executeShellCommand().
187 public void addOutput(byte[] data, int offset, int length) {
189 mTempStream.write(data, offset, length);
191 catch (IOException e) {
197 * Processes output from shell command. IShellOutputReceiver method. The
198 * output is passed to generateDataset(). Called by executeShellCommand() on
201 public void flush() {
202 if (mTempStream != null) {
205 generateDataset(mDataFile);
208 } catch (IOException e) {
215 * IShellOutputReceiver method.
217 * @return false - don't cancel
219 public boolean isCancelled() {
224 * Create our controls for the UI panel.
227 protected Control createControl(Composite parent) {
228 Composite top = new Composite(parent, SWT.NONE);
229 top.setLayout(new GridLayout(1, false));
230 top.setLayoutData(new GridData(GridData.FILL_BOTH));
232 Composite buttons = new Composite(top, SWT.NONE);
233 buttons.setLayout(new RowLayout());
235 mDisplayMode = new Combo(buttons, SWT.PUSH);
236 for (String mode : CAPTIONS) {
237 mDisplayMode.add(mode);
239 mDisplayMode.select(mMode);
240 mDisplayMode.addSelectionListener(new SelectionAdapter() {
242 public void widgetSelected(SelectionEvent e) {
243 mMode = mDisplayMode.getSelectionIndex();
244 if (mDataFile != null) {
245 generateDataset(mDataFile);
246 } else if (getCurrentDevice() != null) {
252 final Button loadButton = new Button(buttons, SWT.PUSH);
253 loadButton.setText("Load from File");
254 loadButton.addSelectionListener(new SelectionAdapter() {
256 public void widgetSelected(SelectionEvent e) {
257 FileDialog fileDialog = new FileDialog(loadButton.getShell(),
259 fileDialog.setText("Load bugreport");
260 String filename = fileDialog.open();
261 if (filename != null) {
262 mDataFile = new File(filename);
263 generateDataset(mDataFile);
268 mFetchButton = new Button(buttons, SWT.PUSH);
269 mFetchButton.setText("Update from Device");
270 mFetchButton.setEnabled(false);
271 mFetchButton.addSelectionListener(new SelectionAdapter() {
273 public void widgetSelected(SelectionEvent e) {
278 mLabel = new Label(top, SWT.NONE);
279 mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
281 mDataset = new DefaultPieDataset();
282 JFreeChart chart = ChartFactory.createPieChart("", mDataset, false
283 /* legend */, true/* tooltips */, false /* urls */);
285 ChartComposite chartComposite = new ChartComposite(top,
287 ChartComposite.DEFAULT_HEIGHT,
288 ChartComposite.DEFAULT_HEIGHT,
289 ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
290 ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
292 // max draw width. We don't want it to zoom, so we put a big number
294 // max draw height. We don't want it to zoom, so we put a big number
295 true, // off-screen buffer
301 chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
305 public void clientChanged(final Client client, int changeMask) {
310 * Helper to open a bugreport and skip to the specified section.
312 * @param file File to open
313 * @return Reader to bugreport file
314 * @throws java.io.IOException on file error
316 private BufferedReader getBugreportReader(File file) throws
318 BufferedReader br = new BufferedReader(new FileReader(file));
319 // Skip over the unwanted bugreport sections
321 String line = br.readLine();
323 Log.d("DDMS", "Service not found " + line);
326 if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) &&
327 line.indexOf(BUGREPORT_SECTION[mMode]) > 0) {
335 * Parse the time string generated by BatteryStats.
336 * A typical new-format string is "11d 13h 45m 39s 999ms".
337 * A typical old-format string is "12.3 sec".
340 private static long parseTimeMs(String s) {
342 // Matches a single component e.g. "12.3 sec" or "45ms"
343 Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)");
344 Matcher m = p.matcher(s);
346 String label = m.group(2);
347 if ("sec".equals(label)) {
348 // Backwards compatibility with old time format
349 total += (long) (Double.parseDouble(m.group(1)) * 1000);
352 long value = Integer.parseInt(m.group(1));
353 if ("d".equals(label)) {
354 total += value * 24 * 60 * 60 * 1000;
355 } else if ("h".equals(label)) {
356 total += value * 60 * 60 * 1000;
357 } else if ("m".equals(label)) {
358 total += value * 60 * 1000;
359 } else if ("s".equals(label)) {
360 total += value * 1000;
361 } else if ("ms".equals(label)) {
368 * Processes wakelock information from bugreport. Updates mDataset with the
371 * @param br Reader providing the content
372 * @throws IOException if error reading file
374 void readWakelockDataset(BufferedReader br) throws IOException {
375 Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial");
376 Pattern totalPattern = Pattern.compile("Total: (.+) uptime");
378 boolean inCurrent = false;
381 String line = br.readLine();
382 if (line == null || line.startsWith("DUMP OF SERVICE")) {
383 // Done, or moved on to the next service
386 if (line.startsWith("Current Battery Usage Statistics")) {
388 } else if (inCurrent) {
389 Matcher m = lockPattern.matcher(line);
391 double value = parseTimeMs(m.group(2)) / 1000.;
392 mDataset.setValue(m.group(1), value);
395 m = totalPattern.matcher(line);
397 total += parseTimeMs(m.group(1)) / 1000.;
403 mDataset.setValue("Unlocked", total);
408 * Processes alarm information from bugreport. Updates mDataset with the new
411 * @param br Reader providing the content
412 * @throws IOException if error reading file
414 void readAlarmDataset(BufferedReader br) throws IOException {
415 Pattern pattern = Pattern
416 .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags");
419 String line = br.readLine();
420 if (line == null || line.startsWith("DUMP OF SERVICE")) {
421 // Done, or moved on to the next service
424 Matcher m = pattern.matcher(line);
426 long count = Long.parseLong(m.group(1));
427 String name = m.group(2);
428 mDataset.setValue(name, count);
434 * Processes cpu load information from bugreport. Updates mDataset with the
437 * @param br Reader providing the content
438 * @throws IOException if error reading file
440 void readCpuDataset(BufferedReader br) throws IOException {
441 Pattern pattern = Pattern
442 .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel");
445 String line = br.readLine();
446 if (line == null || line.startsWith("DUMP OF SERVICE")) {
447 // Done, or moved on to the next service
450 if (line.startsWith("Load:")) {
451 mLabel.setText(line);
454 Matcher m = pattern.matcher(line);
456 String name = m.group(1);
457 long both = Long.parseLong(m.group(2));
458 long user = Long.parseLong(m.group(3));
459 long kernel = Long.parseLong(m.group(4));
460 if ("TOTAL".equals(name)) {
462 mDataset.setValue("Idle", (100 - both));
465 // Try to make graphs more useful even with rounding;
466 // log often has 0% user + 0% kernel = 1% total
467 // We arbitrarily give extra to kernel
469 mDataset.setValue(name + " (user)", user);
472 mDataset.setValue(name + " (kernel)" , both - user);
474 if (user == 0 && kernel == 0 && both > 0) {
475 mDataset.setValue(name, both);
483 * Processes meminfo information from bugreport. Updates mDataset with the
486 * @param br Reader providing the content
487 * @throws IOException if error reading file
489 void readMeminfoDataset(BufferedReader br) throws IOException {
490 Pattern valuePattern = Pattern.compile("(\\d+) kB");
493 mLabel.setText("PSS in kB");
497 String line = br.readLine();
502 Matcher m = valuePattern.matcher(line);
504 long kb = Long.parseLong(m.group(1));
505 if (line.startsWith("MemTotal")) {
507 } else if (line.startsWith("MemFree")) {
508 mDataset.setValue("Free", kb);
510 } else if (line.startsWith("Slab")) {
511 mDataset.setValue("Slab", kb);
513 } else if (line.startsWith("PageTables")) {
514 mDataset.setValue("PageTables", kb);
516 } else if (line.startsWith("Buffers") && kb > 0) {
517 mDataset.setValue("Buffers", kb);
519 } else if (line.startsWith("Inactive")) {
520 mDataset.setValue("Inactive", kb);
522 } else if (line.startsWith("MemFree")) {
523 mDataset.setValue("Free", kb);
532 String line = br.readLine();
536 if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) {
540 if (line.indexOf("----") >= 0) {
541 //end of procrank section
544 // Extract pss field from procrank output
545 long pss = Long.parseLong(line.substring(23, 31).trim());
546 String cmdline = line.substring(43).trim().replace("/system/bin/", "");
547 // Arbitrary minimum size to display
549 mDataset.setValue(cmdline, pss);
555 mDataset.setValue("Other", other);
556 mDataset.setValue("Unknown", total);
560 * Processes sync information from bugreport. Updates mDataset with the new
563 * @param br Reader providing the content
564 * @throws IOException if error reading file
566 void readSyncDataset(BufferedReader br) throws IOException {
568 String line = br.readLine();
569 if (line == null || line.startsWith("DUMP OF SERVICE")) {
570 // Done, or moved on to the next service
573 if (line.startsWith(" |") && line.length() > 70) {
574 String authority = line.substring(3, 18).trim();
575 String duration = line.substring(61, 70).trim();
576 // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime)
577 String durParts[] = duration.split(":");
578 if (durParts.length == 2) {
579 long dur = Long.parseLong(durParts[0]) * 60 + Long
580 .parseLong(durParts[1]);
581 mDataset.setValue(authority, dur);
582 } else if (duration.length() == 3) {
583 long dur = Long.parseLong(durParts[0]) * 3600
584 + Long.parseLong(durParts[1]) * 60 + Long
585 .parseLong(durParts[2]);
586 mDataset.setValue(authority, dur);