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