diff options
author | Chih-Chung Chang <chihchung@google.com> | 2012-02-03 21:17:17 +0800 |
---|---|---|
committer | Chih-Chung Chang <chihchung@google.com> | 2012-02-14 08:21:37 +0800 |
commit | a9293180184b57a47110f1b3b9e3bc6ff0b2f9bf (patch) | |
tree | 8ab36832bc876006b61dc9964bbfb20ae31f7ec3 | |
parent | 82e43c4a00ccc2c660c8c5b130ef5bc6f4f9e84a (diff) | |
download | android_packages_apps_Snap-a9293180184b57a47110f1b3b9e3bc6ff0b2f9bf.tar.gz android_packages_apps_Snap-a9293180184b57a47110f1b3b9e3bc6ff0b2f9bf.tar.bz2 android_packages_apps_Snap-a9293180184b57a47110f1b3b9e3bc6ff0b2f9bf.zip |
Add profiling tools.
Change-Id: Ieab118ededaab5ef46408fac6fdb66b9fff4900e
-rw-r--r-- | src/com/android/gallery3d/ui/GLRootView.java | 49 | ||||
-rw-r--r-- | src/com/android/gallery3d/util/Profile.java | 227 | ||||
-rw-r--r-- | src/com/android/gallery3d/util/ProfileData.java | 167 | ||||
-rw-r--r-- | tests/src/com/android/gallery3d/util/ProfileTest.java | 179 |
4 files changed, 611 insertions, 11 deletions
diff --git a/src/com/android/gallery3d/ui/GLRootView.java b/src/com/android/gallery3d/ui/GLRootView.java index f2140bf07..3f2269f4f 100644 --- a/src/com/android/gallery3d/ui/GLRootView.java +++ b/src/com/android/gallery3d/ui/GLRootView.java @@ -19,6 +19,7 @@ package com.android.gallery3d.ui; import com.android.gallery3d.anim.CanvasAnimation; import com.android.gallery3d.common.Utils; import com.android.gallery3d.util.GalleryUtils; +import com.android.gallery3d.util.Profile; import android.app.Activity; import android.content.Context; @@ -59,6 +60,8 @@ public class GLRootView extends GLSurfaceView private static final boolean DEBUG_DRAWING_STAT = false; + private static final boolean DEBUG_PROFILE = false; + private static final int FLAG_INITIALIZED = 1; private static final int FLAG_NEED_LAYOUT = 2; @@ -87,7 +90,6 @@ public class GLRootView extends GLSurfaceView private final ReentrantLock mRenderLock = new ReentrantLock(); - private static final int TARGET_FRAME_TIME = 16; private long mLastDrawFinishTime; private boolean mInDownState = false; @@ -219,10 +221,10 @@ public class GLRootView extends GLSurfaceView } mGL = gl; mCanvas = new GLCanvasImpl(gl); - if (!DEBUG_FPS) { - setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); - } else { + if (DEBUG_FPS || DEBUG_PROFILE) { setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } else { + setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); } } @@ -237,6 +239,10 @@ public class GLRootView extends GLSurfaceView + ", gl10: " + gl1.toString()); Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); GalleryUtils.setRenderThread(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Start profiling"); + Profile.enable(20); // take a sample every 20ms + } GL11 gl = (GL11) gl1; Utils.assertTrue(mGL == gl); @@ -261,21 +267,31 @@ public class GLRootView extends GLSurfaceView @Override public void onDrawFrame(GL10 gl) { + long t0; + if (DEBUG_PROFILE) { + Profile.hold(); + t0 = System.nanoTime(); + } mRenderLock.lock(); try { onDrawFrameLocked(gl); } finally { mRenderLock.unlock(); } - long end = SystemClock.uptimeMillis(); - - if (mLastDrawFinishTime != 0) { - long wait = mLastDrawFinishTime + TARGET_FRAME_TIME - end; - if (wait > 0) { - SystemClock.sleep(wait); + if (DEBUG_PROFILE) { + long t = System.nanoTime(); + long durationInMs = (t - mLastDrawFinishTime) / 1000000; + long durationDrawInMs = (t - t0) / 1000000; + mLastDrawFinishTime = t; + + if (durationInMs > 34) { // 34ms -> we skipped at least 2 frames + Log.v(TAG, "----- SLOW (" + durationDrawInMs + "/" + + durationInMs + ") -----"); + Profile.commit(); + } else { + Profile.drop(); } } - mLastDrawFinishTime = SystemClock.uptimeMillis(); } private void onDrawFrameLocked(GL10 gl) { @@ -411,4 +427,15 @@ public class GLRootView extends GLSurfaceView public void unlockRenderThread() { mRenderLock.unlock(); } + + @Override + public void onPause() { + super.onPause(); + if (DEBUG_PROFILE) { + Log.d(TAG, "Stop profiling"); + Profile.disableAll(); + Profile.dumpToFile("/sdcard/gallery.prof"); + Profile.reset(); + } + } } diff --git a/src/com/android/gallery3d/util/Profile.java b/src/com/android/gallery3d/util/Profile.java new file mode 100644 index 000000000..96a815397 --- /dev/null +++ b/src/com/android/gallery3d/util/Profile.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2012 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.gallery3d.util; + +import com.android.gallery3d.common.Utils; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Random; + +// The Profile class is used to collect profiling information for a thread. It +// samples stack traces for a thread periodically. enable() and disable() is +// used to enable and disable profiling for the calling thread. The profiling +// information can then be dumped to a file using the dumpToFile() method. +// +// The disableAll() method can be used to disable profiling for all threads and +// can be called in onPause() to ensure all profiling is disabled when an +// activity is paused. +public class Profile { + private static final String TAG = "Profile"; + private static final int NS_PER_MS = 1000000; + + // This is a watchdog entry for one thread. + // For every cycleTime period, we dump the stack of the thread. + private static class WatchEntry { + Thread thread; + + // Both are in milliseconds + int cycleTime; + int wakeTime; + + boolean isHolding; + ArrayList<String[]> holdingStacks = new ArrayList<String[]>(); + } + + // This is a watchdog thread which dumps stacks of other threads periodically. + private static Watchdog sWatchdog = new Watchdog(); + + private static class Watchdog { + private ArrayList<WatchEntry> mList = new ArrayList<WatchEntry>(); + private HandlerThread mHandlerThread; + private Handler mHandler; + private Runnable mProcessRunnable = new Runnable() { + public void run() { + synchronized (Watchdog.this) { + processList(); + } + } + }; + private Random mRandom = new Random(); + private ProfileData mProfileData = new ProfileData(); + + public Watchdog() { + mHandlerThread = new HandlerThread("Watchdog Handler", + Process.THREAD_PRIORITY_FOREGROUND); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + + public synchronized void addWatchEntry(Thread thread, int cycleTime) { + WatchEntry e = new WatchEntry(); + e.thread = thread; + e.cycleTime = cycleTime; + int firstDelay = 1 + mRandom.nextInt(cycleTime); + e.wakeTime = (int) (System.nanoTime() / NS_PER_MS) + firstDelay; + mList.add(e); + processList(); + } + + public synchronized void removeWatchEntry(Thread thread) { + for (int i = 0; i < mList.size(); i++) { + if (mList.get(i).thread == thread) { + mList.remove(i); + break; + } + } + processList(); + } + + public synchronized void removeAllWatchEntries() { + mList.clear(); + processList(); + } + + private void processList() { + mHandler.removeCallbacks(mProcessRunnable); + if (mList.size() == 0) return; + + int currentTime = (int) (System.nanoTime() / NS_PER_MS); + int nextWakeTime = 0; + + for (WatchEntry entry : mList) { + if (currentTime > entry.wakeTime) { + entry.wakeTime += entry.cycleTime; + Thread thread = entry.thread; + sampleStack(entry); + } + + if (entry.wakeTime > nextWakeTime) { + nextWakeTime = entry.wakeTime; + } + } + + long delay = nextWakeTime - currentTime; + mHandler.postDelayed(mProcessRunnable, delay); + } + + private void sampleStack(WatchEntry entry) { + Thread thread = entry.thread; + StackTraceElement[] stack = thread.getStackTrace(); + String[] lines = new String[stack.length]; + for (int i = 0; i < stack.length; i++) { + lines[i] = stack[i].toString(); + } + if (entry.isHolding) { + entry.holdingStacks.add(lines); + } else { + mProfileData.addSample(lines); + } + } + + private WatchEntry findEntry(Thread thread) { + for (int i = 0; i < mList.size(); i++) { + WatchEntry entry = mList.get(i); + if (entry.thread == thread) return entry; + } + return null; + } + + public synchronized void dumpToFile(String filename) { + mProfileData.dumpToFile(filename); + } + + public synchronized void reset() { + mProfileData.reset(); + } + + public synchronized void hold(Thread t) { + WatchEntry entry = findEntry(t); + + // This can happen if the profiling is disabled (probably from + // another thread). Same check is applied in commit() and drop() + // below. + if (entry == null) return; + + entry.isHolding = true; + } + + public synchronized void commit(Thread t) { + WatchEntry entry = findEntry(t); + if (entry == null) return; + ArrayList<String[]> stacks = entry.holdingStacks; + for (int i = 0; i < stacks.size(); i++) { + mProfileData.addSample(stacks.get(i)); + } + entry.isHolding = false; + entry.holdingStacks.clear(); + } + + public synchronized void drop(Thread t) { + WatchEntry entry = findEntry(t); + if (entry == null) return; + entry.isHolding = false; + entry.holdingStacks.clear(); + } + } + + // Enable profiling for the calling thread. Periodically (every cycleTimeInMs + // milliseconds) sample the stack trace of the calling thread. + public static void enable(int cycleTimeInMs) { + Thread t = Thread.currentThread(); + sWatchdog.addWatchEntry(t, cycleTimeInMs); + } + + // Disable profiling for the calling thread. + public static void disable() { + sWatchdog.removeWatchEntry(Thread.currentThread()); + } + + // Disable profiling for all threads. + public static void disableAll() { + sWatchdog.removeAllWatchEntries(); + } + + // Dump the profiling data to a file. + public static void dumpToFile(String filename) { + sWatchdog.dumpToFile(filename); + } + + // Reset the collected profiling data. + public static void reset() { + sWatchdog.reset(); + } + + // Hold the future samples coming from current thread until commit() or + // drop() is called, and those samples are recorded or ignored as a result. + // This must called after enable() to be effective. + public static void hold() { + sWatchdog.hold(Thread.currentThread()); + } + + public static void commit() { + sWatchdog.commit(Thread.currentThread()); + } + + public static void drop() { + sWatchdog.drop(Thread.currentThread()); + } +} diff --git a/src/com/android/gallery3d/util/ProfileData.java b/src/com/android/gallery3d/util/ProfileData.java new file mode 100644 index 000000000..8de4c76ca --- /dev/null +++ b/src/com/android/gallery3d/util/ProfileData.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012 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.gallery3d.util; + +import com.android.gallery3d.common.Utils; + +import android.util.Log; + +import java.io.DataOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +// ProfileData keeps profiling samples in a tree structure. +// The addSample() method adds a sample. The dumpToFile() method saves the data +// to a file. The reset() method clears all samples. +public class ProfileData { + private static final String TAG = "ProfileData"; + + private static class Node { + public int id; // this is the name of this node, mapped from mNameToId + public Node parent; + public int sampleCount; + public ArrayList<Node> children; + public Node(Node parent, int id) { + this.parent = parent; + this.id = id; + } + } + + private Node mRoot; + private int mNextId; + private HashMap<String, Integer> mNameToId; + private DataOutputStream mOut; + private byte mScratch[] = new byte[4]; // scratch space for writeInt() + + public ProfileData() { + mRoot = new Node(null, -1); // The id of the root node is unused. + mNameToId = new HashMap<String, Integer>(); + } + + public void reset() { + mRoot = new Node(null, -1); + mNameToId.clear(); + mNextId = 0; + } + + private int nameToId(String name) { + Integer id = mNameToId.get(name); + if (id == null) { + id = ++mNextId; // The tool doesn't want id=0, so we start from 1. + mNameToId.put(name, id); + } + return id; + } + + public void addSample(String[] stack) { + int[] ids = new int[stack.length]; + for (int i = 0; i < stack.length; i++) { + ids[i] = nameToId(stack[i]); + } + + Node node = mRoot; + for (int i = stack.length - 1; i >= 0; i--) { + if (node.children == null) { + node.children = new ArrayList<Node>(); + } + + int id = ids[i]; + ArrayList<Node> children = node.children; + int j; + for (j = 0; j < children.size(); j++) { + if (children.get(j).id == id) break; + } + if (j == children.size()) { + children.add(new Node(node, id)); + } + + node = children.get(j); + } + + node.sampleCount++; + } + + public void dumpToFile(String filename) { + try { + mOut = new DataOutputStream(new FileOutputStream(filename)); + // Start record + writeInt(0); + writeInt(3); + writeInt(1); + writeInt(20000); // Sampling period: 20ms + writeInt(0); + + // Samples + writeAllStacks(mRoot, 0); + + // End record + writeInt(0); + writeInt(1); + writeInt(0); + writeAllSymbols(); + } catch (IOException ex) { + Log.w("Failed to dump to file", ex); + } finally { + Utils.closeSilently(mOut); + } + } + + // Writes out one stack, consisting of N+2 words: + // first word: sample count + // second word: depth of the stack (N) + // N words: each word is the id of one address in the stack + private void writeOneStack(Node node, int depth) throws IOException { + writeInt(node.sampleCount); + writeInt(depth); + while (depth-- > 0) { + writeInt(node.id); + node = node.parent; + } + } + + private void writeAllStacks(Node node, int depth) throws IOException { + if (node.sampleCount > 0) { + writeOneStack(node, depth); + } + + ArrayList<Node> children = node.children; + if (children != null) { + for (int i = 0; i < children.size(); i++) { + writeAllStacks(children.get(i), depth + 1); + } + } + } + + // Writes out the symbol table. Each line is like: + // 0x17e java.util.ArrayList.isEmpty(ArrayList.java:319) + private void writeAllSymbols() throws IOException { + for (Entry<String, Integer> entry : mNameToId.entrySet()) { + mOut.writeBytes(String.format("0x%x %s\n", entry.getValue(), entry.getKey())); + } + } + + private void writeInt(int v) throws IOException { + mScratch[0] = (byte) v; + mScratch[1] = (byte) (v >> 8); + mScratch[2] = (byte) (v >> 16); + mScratch[3] = (byte) (v >> 24); + mOut.write(mScratch); + } +} diff --git a/tests/src/com/android/gallery3d/util/ProfileTest.java b/tests/src/com/android/gallery3d/util/ProfileTest.java new file mode 100644 index 000000000..798b905f5 --- /dev/null +++ b/tests/src/com/android/gallery3d/util/ProfileTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2012 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.gallery3d.util; + +import com.android.gallery3d.util.Profile; + +import android.os.Environment; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import junit.framework.Assert; +import junit.framework.TestCase; + +@SmallTest +public class ProfileTest extends TestCase { + private static final String TAG = "ProfileTest"; + private static final String TEST_FILE = + Environment.getExternalStorageDirectory().getPath() + "/test.dat"; + + + public void testProfile() throws IOException { + ProfileData p = new ProfileData(); + ParsedProfile q; + String[] A = {"A"}; + String[] B = {"B"}; + String[] AC = {"A", "C"}; + String[] AD = {"A", "D"}; + + // Empty profile + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertTrue(q.mEntries.isEmpty()); + assertTrue(q.mSymbols.isEmpty()); + + // Only one sample + p.addSample(A); + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertEquals(1, q.mEntries.size()); + assertEquals(1, q.mSymbols.size()); + assertEquals(1, q.mEntries.get(0).sampleCount); + + // Two samples at the same place + p.addSample(A); + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertEquals(1, q.mEntries.size()); + assertEquals(1, q.mSymbols.size()); + assertEquals(2, q.mEntries.get(0).sampleCount); + + // Two samples at the different places + p.reset(); + p.addSample(A); + p.addSample(B); + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertEquals(2, q.mEntries.size()); + assertEquals(2, q.mSymbols.size()); + assertEquals(1, q.mEntries.get(0).sampleCount); + assertEquals(1, q.mEntries.get(1).sampleCount); + + // depth > 1 + p.reset(); + p.addSample(AC); + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertEquals(1, q.mEntries.size()); + assertEquals(2, q.mSymbols.size()); + assertEquals(1, q.mEntries.get(0).sampleCount); + + // two samples (AC and AD) + p.addSample(AD); + p.dumpToFile(TEST_FILE); + q = new ParsedProfile(TEST_FILE); + assertEquals(2, q.mEntries.size()); + assertEquals(3, q.mSymbols.size()); // three symbols: A, C, D + assertEquals(1, q.mEntries.get(0).sampleCount); + assertEquals(1, q.mEntries.get(0).sampleCount); + + // Remove the test file + new File(TEST_FILE).delete(); + } +} + +class ParsedProfile { + public class Entry { + int sampleCount; + int stackId[]; + } + + ArrayList<Entry> mEntries = new ArrayList<Entry>(); + HashMap<Integer, String> mSymbols = new HashMap<Integer, String>(); + private DataInputStream mIn; + private byte[] mScratch = new byte[4]; // scratch buffer for readInt + + public ParsedProfile(String filename) throws IOException { + mIn = new DataInputStream(new FileInputStream(filename)); + + Entry entry = parseOneEntry(); + checkIsFirstEntry(entry); + + while (true) { + entry = parseOneEntry(); + if (entry.sampleCount == 0) { + checkIsLastEntry(entry); + break; + } + mEntries.add(entry); + } + + // Read symbol table + while (true) { + String line = mIn.readLine(); + if (line == null) break; + String[] fields = line.split(" +"); + checkIsValidSymbolLine(fields); + mSymbols.put(Integer.decode(fields[0]), fields[1]); + } + } + + private void checkIsFirstEntry(Entry entry) { + Assert.assertEquals(0, entry.sampleCount); + Assert.assertEquals(3, entry.stackId.length); + Assert.assertEquals(1, entry.stackId[0]); + Assert.assertTrue(entry.stackId[1] > 0); // sampling period + Assert.assertEquals(0, entry.stackId[2]); // padding + } + + private void checkIsLastEntry(Entry entry) { + Assert.assertEquals(0, entry.sampleCount); + Assert.assertEquals(1, entry.stackId.length); + Assert.assertEquals(0, entry.stackId[0]); + } + + private void checkIsValidSymbolLine(String[] fields) { + Assert.assertEquals(2, fields.length); + Assert.assertTrue(fields[0].startsWith("0x")); + } + + private Entry parseOneEntry() throws IOException { + int sampleCount = readInt(); + int depth = readInt(); + Entry e = new Entry(); + e.sampleCount = sampleCount; + e.stackId = new int[depth]; + for (int i = 0; i < depth; i++) { + e.stackId[i] = readInt(); + } + return e; + } + + private int readInt() throws IOException { + mIn.read(mScratch, 0, 4); + return (mScratch[0] & 0xff) | + ((mScratch[1] & 0xff) << 8) | + ((mScratch[2] & 0xff) << 16) | + ((mScratch[3] & 0xff) << 24); + } +} |