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 */ addOutput(byte[] data, int offset, int length)187 public void addOutput(byte[] data, int offset, int length) { 188 try { 189 mTempStream.write(data, offset, length); 190 } 191 catch (IOException e) { 192 Log.e("DDMS", e); 193 } 194 } 195 196 /** 197 * Processes output from shell command. IShellOutputReceiver method. The 198 * output is passed to generateDataset(). Called by executeShellCommand() on 199 * completion. 200 */ flush()201 public void flush() { 202 if (mTempStream != null) { 203 try { 204 mTempStream.close(); 205 generateDataset(mDataFile); 206 mTempStream = null; 207 mDataFile = null; 208 } catch (IOException e) { 209 Log.e("DDMS", e); 210 } 211 } 212 } 213 214 /** 215 * IShellOutputReceiver method. 216 * 217 * @return false - don't cancel 218 */ isCancelled()219 public boolean isCancelled() { 220 return false; 221 } 222 223 /** 224 * Create our controls for the UI panel. 225 */ 226 @Override createControl(Composite parent)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)); 231 232 Composite buttons = new Composite(top, SWT.NONE); 233 buttons.setLayout(new RowLayout()); 234 235 mDisplayMode = new Combo(buttons, SWT.PUSH); 236 for (String mode : CAPTIONS) { 237 mDisplayMode.add(mode); 238 } 239 mDisplayMode.select(mMode); 240 mDisplayMode.addSelectionListener(new SelectionAdapter() { 241 @Override 242 public void widgetSelected(SelectionEvent e) { 243 mMode = mDisplayMode.getSelectionIndex(); 244 if (mDataFile != null) { 245 generateDataset(mDataFile); 246 } else if (getCurrentDevice() != null) { 247 loadFromDevice(); 248 } 249 } 250 }); 251 252 final Button loadButton = new Button(buttons, SWT.PUSH); 253 loadButton.setText("Load from File"); 254 loadButton.addSelectionListener(new SelectionAdapter() { 255 @Override 256 public void widgetSelected(SelectionEvent e) { 257 FileDialog fileDialog = new FileDialog(loadButton.getShell(), 258 SWT.OPEN); 259 fileDialog.setText("Load bugreport"); 260 String filename = fileDialog.open(); 261 if (filename != null) { 262 mDataFile = new File(filename); 263 generateDataset(mDataFile); 264 } 265 } 266 }); 267 268 mFetchButton = new Button(buttons, SWT.PUSH); 269 mFetchButton.setText("Update from Device"); 270 mFetchButton.setEnabled(false); 271 mFetchButton.addSelectionListener(new SelectionAdapter() { 272 @Override 273 public void widgetSelected(SelectionEvent e) { 274 loadFromDevice(); 275 } 276 }); 277 278 mLabel = new Label(top, SWT.NONE); 279 mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); 280 281 mDataset = new DefaultPieDataset(); 282 JFreeChart chart = ChartFactory.createPieChart("", mDataset, false 283 /* legend */, true/* tooltips */, false /* urls */); 284 285 ChartComposite chartComposite = new ChartComposite(top, 286 SWT.BORDER, chart, 287 ChartComposite.DEFAULT_HEIGHT, 288 ChartComposite.DEFAULT_HEIGHT, 289 ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, 290 ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 291 3000, 292 // max draw width. We don't want it to zoom, so we put a big number 293 3000, 294 // max draw height. We don't want it to zoom, so we put a big number 295 true, // off-screen buffer 296 true, // properties 297 true, // save 298 true, // print 299 false, // zoom 300 true); 301 chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); 302 return top; 303 } 304 clientChanged(final Client client, int changeMask)305 public void clientChanged(final Client client, int changeMask) { 306 // Don't care 307 } 308 309 /** 310 * Helper to open a bugreport and skip to the specified section. 311 * 312 * @param file File to open 313 * @return Reader to bugreport file 314 * @throws java.io.IOException on file error 315 */ getBugreportReader(File file)316 private BufferedReader getBugreportReader(File file) throws 317 IOException { 318 BufferedReader br = new BufferedReader(new FileReader(file)); 319 // Skip over the unwanted bugreport sections 320 while (true) { 321 String line = br.readLine(); 322 if (line == null) { 323 Log.d("DDMS", "Service not found " + line); 324 break; 325 } 326 if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) && 327 line.indexOf(BUGREPORT_SECTION[mMode]) > 0) { 328 break; 329 } 330 } 331 return br; 332 } 333 334 /** 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". 338 * @return time in ms 339 */ parseTimeMs(String s)340 private static long parseTimeMs(String s) { 341 long total = 0; 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); 345 while (m.find()) { 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); 350 continue; 351 } 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)) { 362 total += value; 363 } 364 } 365 return total; 366 } 367 /** 368 * Processes wakelock information from bugreport. Updates mDataset with the 369 * new data. 370 * 371 * @param br Reader providing the content 372 * @throws IOException if error reading file 373 */ readWakelockDataset(BufferedReader br)374 void readWakelockDataset(BufferedReader br) throws IOException { 375 Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); 376 Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); 377 double total = 0; 378 boolean inCurrent = false; 379 380 while (true) { 381 String line = br.readLine(); 382 if (line == null || line.startsWith("DUMP OF SERVICE")) { 383 // Done, or moved on to the next service 384 break; 385 } 386 if (line.startsWith("Current Battery Usage Statistics")) { 387 inCurrent = true; 388 } else if (inCurrent) { 389 Matcher m = lockPattern.matcher(line); 390 if (m.find()) { 391 double value = parseTimeMs(m.group(2)) / 1000.; 392 mDataset.setValue(m.group(1), value); 393 total -= value; 394 } else { 395 m = totalPattern.matcher(line); 396 if (m.find()) { 397 total += parseTimeMs(m.group(1)) / 1000.; 398 } 399 } 400 } 401 } 402 if (total > 0) { 403 mDataset.setValue("Unlocked", total); 404 } 405 } 406 407 /** 408 * Processes alarm information from bugreport. Updates mDataset with the new 409 * data. 410 * 411 * @param br Reader providing the content 412 * @throws IOException if error reading file 413 */ readAlarmDataset(BufferedReader br)414 void readAlarmDataset(BufferedReader br) throws IOException { 415 Pattern pattern = Pattern 416 .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); 417 418 while (true) { 419 String line = br.readLine(); 420 if (line == null || line.startsWith("DUMP OF SERVICE")) { 421 // Done, or moved on to the next service 422 break; 423 } 424 Matcher m = pattern.matcher(line); 425 if (m.find()) { 426 long count = Long.parseLong(m.group(1)); 427 String name = m.group(2); 428 mDataset.setValue(name, count); 429 } 430 } 431 } 432 433 /** 434 * Processes cpu load information from bugreport. Updates mDataset with the 435 * new data. 436 * 437 * @param br Reader providing the content 438 * @throws IOException if error reading file 439 */ readCpuDataset(BufferedReader br)440 void readCpuDataset(BufferedReader br) throws IOException { 441 Pattern pattern = Pattern 442 .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); 443 444 while (true) { 445 String line = br.readLine(); 446 if (line == null || line.startsWith("DUMP OF SERVICE")) { 447 // Done, or moved on to the next service 448 break; 449 } 450 if (line.startsWith("Load:")) { 451 mLabel.setText(line); 452 continue; 453 } 454 Matcher m = pattern.matcher(line); 455 if (m.find()) { 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)) { 461 if (both < 100) { 462 mDataset.setValue("Idle", (100 - both)); 463 } 464 } else { 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 468 if (user > 0) { 469 mDataset.setValue(name + " (user)", user); 470 } 471 if (kernel > 0) { 472 mDataset.setValue(name + " (kernel)" , both - user); 473 } 474 if (user == 0 && kernel == 0 && both > 0) { 475 mDataset.setValue(name, both); 476 } 477 } 478 } 479 } 480 } 481 482 /** 483 * Processes meminfo information from bugreport. Updates mDataset with the 484 * new data. 485 * 486 * @param br Reader providing the content 487 * @throws IOException if error reading file 488 */ readMeminfoDataset(BufferedReader br)489 void readMeminfoDataset(BufferedReader br) throws IOException { 490 Pattern valuePattern = Pattern.compile("(\\d+) kB"); 491 long total = 0; 492 long other = 0; 493 mLabel.setText("PSS in kB"); 494 495 // Scan meminfo 496 while (true) { 497 String line = br.readLine(); 498 if (line == null) { 499 // End of file 500 break; 501 } 502 Matcher m = valuePattern.matcher(line); 503 if (m.find()) { 504 long kb = Long.parseLong(m.group(1)); 505 if (line.startsWith("MemTotal")) { 506 total = kb; 507 } else if (line.startsWith("MemFree")) { 508 mDataset.setValue("Free", kb); 509 total -= kb; 510 } else if (line.startsWith("Slab")) { 511 mDataset.setValue("Slab", kb); 512 total -= kb; 513 } else if (line.startsWith("PageTables")) { 514 mDataset.setValue("PageTables", kb); 515 total -= kb; 516 } else if (line.startsWith("Buffers") && kb > 0) { 517 mDataset.setValue("Buffers", kb); 518 total -= kb; 519 } else if (line.startsWith("Inactive")) { 520 mDataset.setValue("Inactive", kb); 521 total -= kb; 522 } else if (line.startsWith("MemFree")) { 523 mDataset.setValue("Free", kb); 524 total -= kb; 525 } 526 } else { 527 break; 528 } 529 } 530 // Scan procrank 531 while (true) { 532 String line = br.readLine(); 533 if (line == null) { 534 break; 535 } 536 if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) { 537 // procrank header 538 continue; 539 } 540 if (line.indexOf("----") >= 0) { 541 //end of procrank section 542 break; 543 } 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 548 if (pss > 2000) { 549 mDataset.setValue(cmdline, pss); 550 } else { 551 other += pss; 552 } 553 total -= pss; 554 } 555 mDataset.setValue("Other", other); 556 mDataset.setValue("Unknown", total); 557 } 558 559 /** 560 * Processes sync information from bugreport. Updates mDataset with the new 561 * data. 562 * 563 * @param br Reader providing the content 564 * @throws IOException if error reading file 565 */ readSyncDataset(BufferedReader br)566 void readSyncDataset(BufferedReader br) throws IOException { 567 while (true) { 568 String line = br.readLine(); 569 if (line == null || line.startsWith("DUMP OF SERVICE")) { 570 // Done, or moved on to the next service 571 break; 572 } 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); 587 } 588 } 589 } 590 } 591 } 592