/* * Copyright (C) 2009 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 dalvik.system; import java.util.logging.Logger; import java.io.DataInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; /** * A sampling profiler. * * @hide */ public class SamplingProfiler { private static final Logger logger = Logger.getLogger( SamplingProfiler.class.getName()); static final boolean DEBUG = false; enum State { /** The sampling thread hasn't started or is waiting to resume. */ PAUSED, /** The sampling thread is collecting samples. */ RUNNING, /** The sampling thread is shutting down. */ SHUTTING_DOWN } /** Pointer to native state. */ int pointer = 0; /** The thread that collects samples. */ Thread samplingThread; /** Time between samples. */ volatile int delay; // ms /** Number of samples taken (~samples per second * number of threads). */ int totalThreadsSampled = 0; /** Total time spent collecting samples. */ long totalSampleTime = 0; /** The state of the profiler. */ volatile State state = State.PAUSED; private SamplingProfiler() {} /** * Returns true if the profiler is running. */ public boolean isRunning() { return state == State.RUNNING; } /** * Starts collecting samples. * * @param samplesPerSecond number of times to sample each thread per second * @throws IllegalStateException if the profiler is * {@linkplain #shutDown()} shutting down} */ public synchronized void start(int samplesPerSecond) { if (samplesPerSecond < 1) { throw new IllegalArgumentException("samplesPerSecond < 1"); } ensureNotShuttingDown(); delay = 1000 / samplesPerSecond; if (!isRunning()) { if (DEBUG) logger.info("Starting profiler..."); state = State.RUNNING; if (samplingThread == null) { // TODO: Priority? samplingThread = new Thread(new Sampler(), "SamplingProfiler"); samplingThread.setDaemon(true); samplingThread.start(); } else { notifyAll(); } } } /** * Pauses sample collection. */ public synchronized void pause() { if (isRunning()) { if (DEBUG) logger.info("Pausing profiler..."); state = State.PAUSED; } } /** * Captures collected samples and clears the sample set. Returns null * if no data has been captured. * * <p>Note: The exact format is not documented because it's not set in * stone yet. * * @throws IllegalStateException if the profiler is * {@linkplain #shutDown()} shutting down} */ public synchronized byte[] snapshot() { ensureNotShuttingDown(); if (pointer == 0 || totalThreadsSampled == 0) { return null; } if (DEBUG) { int size = size(pointer); int collisions = collisions(pointer); long start = System.nanoTime(); byte[] bytes = snapshot(pointer); long elapsed = System.nanoTime() - start; /* * We shifted the times by 10 bits in the sampling thread to avoid * overflow. Undo the shift and then convert from ns to us. */ long averageSampleTime = ((totalSampleTime / totalThreadsSampled) << 10) / 1000; logger.info("Grabbed snapshot in " + (elapsed / 1000) + "us." + " Samples collected: " + totalThreadsSampled + ", Average sample time (per thread): " + averageSampleTime + "us" + ", Set size: " + size + ", Collisions: " + collisions); totalThreadsSampled = 0; totalSampleTime = 0; return bytes; } else { totalThreadsSampled = 0; return snapshot(pointer); } } /** * Identifies the "event thread". For a user-facing application, this * might be the UI thread. For a background process, this might be the * thread that processes incoming requests. * * @throws IllegalStateException if the profiler is * {@linkplain #shutDown()} shutting down} */ public synchronized void setEventThread(Thread eventThread) { ensureNotShuttingDown(); if (pointer == 0) { pointer = allocate(); } setEventThread(pointer, eventThread); } private void ensureNotShuttingDown() { if (state == State.SHUTTING_DOWN) { throw new IllegalStateException("Profiler is shutting down."); } } /** * Shuts down the profiler thread and frees native memory. The profiler * will recreate the thread the next time {@link #start(int)} is called. * * @throws IllegalStateException if the profiler is already shutting down * or if it hasn't started yet * */ public void shutDown() { Thread toStop; synchronized (this) { ensureNotShuttingDown(); toStop = samplingThread; if (toStop == null) { throw new IllegalStateException( "The profiler was never started."); } state = State.SHUTTING_DOWN; samplingThread = null; notifyAll(); } // Release lock to 'this' so background thread can grab it and stop. // Interrupt the thread in case it's sleeping. toStop.interrupt(); while (true) { try { toStop.join(); break; } catch (InterruptedException e) { /* ignore */ } } synchronized (this) { if (pointer != 0) { free(pointer); pointer = 0; } totalThreadsSampled = 0; totalSampleTime = 0; state = State.PAUSED; } } /** Collects some data. Returns number of threads sampled. */ private static native int sample(int pointer); /** Allocates native state. */ private static native int allocate(); /** Frees native state. */ private static native void free(int pointer); /** Gets the number of methods in the sample set. */ private static native int size(int pointer); /** Gets the number of collisions in the sample set. */ private static native int collisions(int pointer); /** Captures data. */ private static native byte[] snapshot(int pointer); /** Identifies the "event thread". */ private static native void setEventThread(int pointer, Thread thread); /** * Background thread that collects samples. */ class Sampler implements Runnable { public void run() { boolean firstSample = true; while (true) { synchronized (SamplingProfiler.this) { if (!isRunning()) { if (DEBUG) logger.info("Paused profiler."); while (!isRunning()) { if (state == State.SHUTTING_DOWN) { // Stop thread. return; } try { SamplingProfiler.this.wait(); } catch (InterruptedException e) { /* ignore */ } } firstSample = true; } if (pointer == 0) { pointer = allocate(); } if (firstSample) { if (DEBUG) logger.info("Started profiler."); firstSample = false; } if (DEBUG) { long start = System.nanoTime(); int threadsSampled = sample(pointer); long elapsed = System.nanoTime() - start; totalThreadsSampled += threadsSampled; totalSampleTime += elapsed >> 10; // avoids overflow. } else { totalThreadsSampled += sample(pointer); } } try { Thread.sleep(delay); } catch (InterruptedException e) { /* ignore */ } } } } /** * Dumps a snapshot to the log. Useful for debugging. */ public static void logSnapshot(byte[] snapshot) { DataInputStream in = new DataInputStream( new ByteArrayInputStream(snapshot)); try { int version = in.readUnsignedShort(); int classCount = in.readUnsignedShort(); StringBuilder sb = new StringBuilder(); sb.append("version=").append(version).append(' ') .append("classes=").append(classCount).append('\n'); logger.info(sb.toString()); for (int i = 0; i < classCount; i++) { sb = new StringBuilder(); sb.append("class ").append(in.readUTF()).append('\n'); int methodCount = in.readUnsignedShort(); for (int m = 0; m < methodCount; m++) { sb.append(" ").append(in.readUTF()).append(":\n"); sb.append(" event:\n"); appendCounts(in, sb); sb.append(" other:\n"); appendCounts(in, sb); } logger.info(sb.toString()); } } catch (IOException e) { logger.warning(e.toString()); } } private static void appendCounts(DataInputStream in, StringBuilder sb) throws IOException { sb.append(" running:\n"); sb.append(" caller: ").append(in.readShort()).append('\n'); sb.append(" leaf: ").append(in.readShort()).append('\n'); sb.append(" suspended:\n"); sb.append(" caller: ").append(in.readShort()).append('\n'); sb.append(" leaf: ").append(in.readShort()).append('\n'); } /** This will be allocated when the user calls getInstance(). */ private static final SamplingProfiler instance = new SamplingProfiler(); /** * Gets the profiler. The profiler is not running by default. Start it * with {@link #start(int)}. */ public static synchronized SamplingProfiler getInstance() { return instance; } }