/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ddmuilib; import com.android.ddmlib.Client; import com.android.ddmlib.IShellOutputReceiver; import com.android.ddmlib.Log; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.data.general.DefaultPieDataset; import org.jfree.experimental.chart.swt.ChartComposite; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Displays system information graphs obtained from a bugreport file or device. */ public class SysinfoPanel extends TablePanel implements IShellOutputReceiver { // UI components private Label mLabel; private Button mFetchButton; private Combo mDisplayMode; private DefaultPieDataset mDataset; // The bugreport file to process private File mDataFile; // To get output from adb commands private FileOutputStream mTempStream; // Selects the current display: MODE_CPU, etc. private int mMode = 0; private static final int MODE_CPU = 0; private static final int MODE_ALARM = 1; private static final int MODE_WAKELOCK = 2; private static final int MODE_MEMINFO = 3; private static final int MODE_SYNC = 4; // argument to dumpsys; section in the bugreport holding the data private static final String BUGREPORT_SECTION[] = {"cpuinfo", "alarm", "batteryinfo", "MEMORY INFO", "content"}; private static final String DUMP_COMMAND[] = {"dumpsys cpuinfo", "dumpsys alarm", "dumpsys batteryinfo", "cat /proc/meminfo ; procrank", "dumpsys content"}; private static final String CAPTIONS[] = {"CPU load", "Alarms", "Wakelocks", "Memory usage", "Sync"}; /** * Generates the dataset to display. * * @param file The bugreport file to process. */ public void generateDataset(File file) { mDataset.clear(); mLabel.setText(""); if (file == null) { return; } try { BufferedReader br = getBugreportReader(file); if (mMode == MODE_CPU) { readCpuDataset(br); } else if (mMode == MODE_ALARM) { readAlarmDataset(br); } else if (mMode == MODE_WAKELOCK) { readWakelockDataset(br); } else if (mMode == MODE_MEMINFO) { readMeminfoDataset(br); } else if (mMode == MODE_SYNC) { readSyncDataset(br); } } catch (IOException e) { Log.e("DDMS", e); } } /** * Sent when a new device is selected. The new device can be accessed with * {@link #getCurrentDevice()} */ @Override public void deviceSelected() { if (getCurrentDevice() != null) { mFetchButton.setEnabled(true); loadFromDevice(); } else { mFetchButton.setEnabled(false); } } /** * Sent when a new client is selected. The new client can be accessed with * {@link #getCurrentClient()}. */ @Override public void clientSelected() { } /** * Sets the focus to the proper control inside the panel. */ @Override public void setFocus() { mDisplayMode.setFocus(); } /** * Fetches a new bugreport from the device and updates the display. * Fetching is asynchronous. See also addOutput, flush, and isCancelled. */ private void loadFromDevice() { try { initShellOutputBuffer(); if (mMode == MODE_MEMINFO) { // Hack to add bugreport-style section header for meminfo mTempStream.write("------ MEMORY INFO ------\n".getBytes()); } getCurrentDevice().executeShellCommand( DUMP_COMMAND[mMode], this); } catch (IOException e) { Log.e("DDMS", e); } } /** * Initializes temporary output file for executeShellCommand(). * * @throws IOException on file error */ void initShellOutputBuffer() throws IOException { mDataFile = File.createTempFile("ddmsfile", ".txt"); mDataFile.deleteOnExit(); mTempStream = new FileOutputStream(mDataFile); } /** * Adds output to the temp file. IShellOutputReceiver method. Called by * executeShellCommand(). */ public void addOutput(byte[] data, int offset, int length) { try { mTempStream.write(data, offset, length); } catch (IOException e) { Log.e("DDMS", e); } } /** * Processes output from shell command. IShellOutputReceiver method. The * output is passed to generateDataset(). Called by executeShellCommand() on * completion. */ public void flush() { if (mTempStream != null) { try { mTempStream.close(); generateDataset(mDataFile); mTempStream = null; mDataFile = null; } catch (IOException e) { Log.e("DDMS", e); } } } /** * IShellOutputReceiver method. * * @return false - don't cancel */ public boolean isCancelled() { return false; } /** * Create our controls for the UI panel. */ @Override protected Control createControl(Composite parent) { Composite top = new Composite(parent, SWT.NONE); top.setLayout(new GridLayout(1, false)); top.setLayoutData(new GridData(GridData.FILL_BOTH)); Composite buttons = new Composite(top, SWT.NONE); buttons.setLayout(new RowLayout()); mDisplayMode = new Combo(buttons, SWT.PUSH); for (String mode : CAPTIONS) { mDisplayMode.add(mode); } mDisplayMode.select(mMode); mDisplayMode.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { mMode = mDisplayMode.getSelectionIndex(); if (mDataFile != null) { generateDataset(mDataFile); } else if (getCurrentDevice() != null) { loadFromDevice(); } } }); final Button loadButton = new Button(buttons, SWT.PUSH); loadButton.setText("Load from File"); loadButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog fileDialog = new FileDialog(loadButton.getShell(), SWT.OPEN); fileDialog.setText("Load bugreport"); String filename = fileDialog.open(); if (filename != null) { mDataFile = new File(filename); generateDataset(mDataFile); } } }); mFetchButton = new Button(buttons, SWT.PUSH); mFetchButton.setText("Update from Device"); mFetchButton.setEnabled(false); mFetchButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { loadFromDevice(); } }); mLabel = new Label(top, SWT.NONE); mLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mDataset = new DefaultPieDataset(); JFreeChart chart = ChartFactory.createPieChart("", mDataset, false /* legend */, true/* tooltips */, false /* urls */); ChartComposite chartComposite = new ChartComposite(top, SWT.BORDER, chart, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_HEIGHT, ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 3000, // max draw width. We don't want it to zoom, so we put a big number 3000, // max draw height. We don't want it to zoom, so we put a big number true, // off-screen buffer true, // properties true, // save true, // print false, // zoom true); chartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); return top; } public void clientChanged(final Client client, int changeMask) { // Don't care } /** * Helper to open a bugreport and skip to the specified section. * * @param file File to open * @return Reader to bugreport file * @throws java.io.IOException on file error */ private BufferedReader getBugreportReader(File file) throws IOException { BufferedReader br = new BufferedReader(new FileReader(file)); // Skip over the unwanted bugreport sections while (true) { String line = br.readLine(); if (line == null) { Log.d("DDMS", "Service not found " + line); break; } if ((line.startsWith("DUMP OF SERVICE ") || line.startsWith("-----")) && line.indexOf(BUGREPORT_SECTION[mMode]) > 0) { break; } } return br; } /** * Parse the time string generated by BatteryStats. * A typical new-format string is "11d 13h 45m 39s 999ms". * A typical old-format string is "12.3 sec". * @return time in ms */ private static long parseTimeMs(String s) { long total = 0; // Matches a single component e.g. "12.3 sec" or "45ms" Pattern p = Pattern.compile("([\\d\\.]+)\\s*([a-z]+)"); Matcher m = p.matcher(s); while (m.find()) { String label = m.group(2); if ("sec".equals(label)) { // Backwards compatibility with old time format total += (long) (Double.parseDouble(m.group(1)) * 1000); continue; } long value = Integer.parseInt(m.group(1)); if ("d".equals(label)) { total += value * 24 * 60 * 60 * 1000; } else if ("h".equals(label)) { total += value * 60 * 60 * 1000; } else if ("m".equals(label)) { total += value * 60 * 1000; } else if ("s".equals(label)) { total += value * 1000; } else if ("ms".equals(label)) { total += value; } } return total; } /** * Processes wakelock information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ void readWakelockDataset(BufferedReader br) throws IOException { Pattern lockPattern = Pattern.compile("Wake lock (\\S+): (.+) partial"); Pattern totalPattern = Pattern.compile("Total: (.+) uptime"); double total = 0; boolean inCurrent = false; while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } if (line.startsWith("Current Battery Usage Statistics")) { inCurrent = true; } else if (inCurrent) { Matcher m = lockPattern.matcher(line); if (m.find()) { double value = parseTimeMs(m.group(2)) / 1000.; mDataset.setValue(m.group(1), value); total -= value; } else { m = totalPattern.matcher(line); if (m.find()) { total += parseTimeMs(m.group(1)) / 1000.; } } } } if (total > 0) { mDataset.setValue("Unlocked", total); } } /** * Processes alarm information from bugreport. Updates mDataset with the new * data. * * @param br Reader providing the content * @throws IOException if error reading file */ void readAlarmDataset(BufferedReader br) throws IOException { Pattern pattern = Pattern .compile("(\\d+) alarms: Intent .*\\.([^. ]+) flags"); while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } Matcher m = pattern.matcher(line); if (m.find()) { long count = Long.parseLong(m.group(1)); String name = m.group(2); mDataset.setValue(name, count); } } } /** * Processes cpu load information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ void readCpuDataset(BufferedReader br) throws IOException { Pattern pattern = Pattern .compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } if (line.startsWith("Load:")) { mLabel.setText(line); continue; } Matcher m = pattern.matcher(line); if (m.find()) { String name = m.group(1); long both = Long.parseLong(m.group(2)); long user = Long.parseLong(m.group(3)); long kernel = Long.parseLong(m.group(4)); if ("TOTAL".equals(name)) { if (both < 100) { mDataset.setValue("Idle", (100 - both)); } } else { // Try to make graphs more useful even with rounding; // log often has 0% user + 0% kernel = 1% total // We arbitrarily give extra to kernel if (user > 0) { mDataset.setValue(name + " (user)", user); } if (kernel > 0) { mDataset.setValue(name + " (kernel)" , both - user); } if (user == 0 && kernel == 0 && both > 0) { mDataset.setValue(name, both); } } } } } /** * Processes meminfo information from bugreport. Updates mDataset with the * new data. * * @param br Reader providing the content * @throws IOException if error reading file */ void readMeminfoDataset(BufferedReader br) throws IOException { Pattern valuePattern = Pattern.compile("(\\d+) kB"); long total = 0; long other = 0; mLabel.setText("PSS in kB"); // Scan meminfo while (true) { String line = br.readLine(); if (line == null) { // End of file break; } Matcher m = valuePattern.matcher(line); if (m.find()) { long kb = Long.parseLong(m.group(1)); if (line.startsWith("MemTotal")) { total = kb; } else if (line.startsWith("MemFree")) { mDataset.setValue("Free", kb); total -= kb; } else if (line.startsWith("Slab")) { mDataset.setValue("Slab", kb); total -= kb; } else if (line.startsWith("PageTables")) { mDataset.setValue("PageTables", kb); total -= kb; } else if (line.startsWith("Buffers") && kb > 0) { mDataset.setValue("Buffers", kb); total -= kb; } else if (line.startsWith("Inactive")) { mDataset.setValue("Inactive", kb); total -= kb; } else if (line.startsWith("MemFree")) { mDataset.setValue("Free", kb); total -= kb; } } else { break; } } // Scan procrank while (true) { String line = br.readLine(); if (line == null) { break; } if (line.indexOf("PROCRANK") >= 0 || line.indexOf("PID") >= 0) { // procrank header continue; } if (line.indexOf("----") >= 0) { //end of procrank section break; } // Extract pss field from procrank output long pss = Long.parseLong(line.substring(23, 31).trim()); String cmdline = line.substring(43).trim().replace("/system/bin/", ""); // Arbitrary minimum size to display if (pss > 2000) { mDataset.setValue(cmdline, pss); } else { other += pss; } total -= pss; } mDataset.setValue("Other", other); mDataset.setValue("Unknown", total); } /** * Processes sync information from bugreport. Updates mDataset with the new * data. * * @param br Reader providing the content * @throws IOException if error reading file */ void readSyncDataset(BufferedReader br) throws IOException { while (true) { String line = br.readLine(); if (line == null || line.startsWith("DUMP OF SERVICE")) { // Done, or moved on to the next service break; } if (line.startsWith(" |") && line.length() > 70) { String authority = line.substring(3, 18).trim(); String duration = line.substring(61, 70).trim(); // Duration is MM:SS or HH:MM:SS (DateUtils.formatElapsedTime) String durParts[] = duration.split(":"); if (durParts.length == 2) { long dur = Long.parseLong(durParts[0]) * 60 + Long .parseLong(durParts[1]); mDataset.setValue(authority, dur); } else if (duration.length() == 3) { long dur = Long.parseLong(durParts[0]) * 3600 + Long.parseLong(durParts[1]) * 60 + Long .parseLong(durParts[2]); mDataset.setValue(authority, dur); } } } } }