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