1/*
2 * Copyright (C) 2009 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
17package com.android.internal.os;
18
19import android.content.pm.PackageInfo;
20import android.os.Build;
21import android.os.SystemProperties;
22import android.util.Log;
23import dalvik.system.profiler.BinaryHprofWriter;
24import dalvik.system.profiler.SamplingProfiler;
25import java.io.BufferedOutputStream;
26import java.io.File;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import java.io.OutputStream;
30import java.io.PrintStream;
31import java.util.Date;
32import java.util.concurrent.Executor;
33import java.util.concurrent.Executors;
34import java.util.concurrent.ThreadFactory;
35import java.util.concurrent.atomic.AtomicBoolean;
36import libcore.io.IoUtils;
37
38/**
39 * Integrates the framework with Dalvik's sampling profiler.
40 */
41public class SamplingProfilerIntegration {
42
43    private static final String TAG = "SamplingProfilerIntegration";
44
45    public static final String SNAPSHOT_DIR = "/data/snapshots";
46
47    private static final boolean enabled;
48    private static final Executor snapshotWriter;
49    private static final int samplingProfilerMilliseconds;
50    private static final int samplingProfilerDepth;
51
52    /** Whether or not a snapshot is being persisted. */
53    private static final AtomicBoolean pending = new AtomicBoolean(false);
54
55    static {
56        samplingProfilerMilliseconds = SystemProperties.getInt("persist.sys.profiler_ms", 0);
57        samplingProfilerDepth = SystemProperties.getInt("persist.sys.profiler_depth", 4);
58        if (samplingProfilerMilliseconds > 0) {
59            File dir = new File(SNAPSHOT_DIR);
60            dir.mkdirs();
61            // the directory needs to be writable to anybody to allow file writing
62            dir.setWritable(true, false);
63            // the directory needs to be executable to anybody to allow file creation
64            dir.setExecutable(true, false);
65            if (dir.isDirectory()) {
66                snapshotWriter = Executors.newSingleThreadExecutor(new ThreadFactory() {
67                        public Thread newThread(Runnable r) {
68                            return new Thread(r, TAG);
69                        }
70                    });
71                enabled = true;
72                Log.i(TAG, "Profiling enabled. Sampling interval ms: "
73                      + samplingProfilerMilliseconds);
74            } else {
75                snapshotWriter = null;
76                enabled = true;
77                Log.w(TAG, "Profiling setup failed. Could not create " + SNAPSHOT_DIR);
78            }
79        } else {
80            snapshotWriter = null;
81            enabled = false;
82            Log.i(TAG, "Profiling disabled.");
83        }
84    }
85
86    private static SamplingProfiler samplingProfiler;
87    private static long startMillis;
88
89    /**
90     * Is profiling enabled?
91     */
92    public static boolean isEnabled() {
93        return enabled;
94    }
95
96    /**
97     * Starts the profiler if profiling is enabled.
98     */
99    public static void start() {
100        if (!enabled) {
101            return;
102        }
103        if (samplingProfiler != null) {
104            Log.e(TAG, "SamplingProfilerIntegration already started at " + new Date(startMillis));
105            return;
106        }
107
108        ThreadGroup group = Thread.currentThread().getThreadGroup();
109        SamplingProfiler.ThreadSet threadSet = SamplingProfiler.newThreadGroupTheadSet(group);
110        samplingProfiler = new SamplingProfiler(samplingProfilerDepth, threadSet);
111        samplingProfiler.start(samplingProfilerMilliseconds);
112        startMillis = System.currentTimeMillis();
113    }
114
115    /**
116     * Writes a snapshot if profiling is enabled.
117     */
118    public static void writeSnapshot(final String processName, final PackageInfo packageInfo) {
119        if (!enabled) {
120            return;
121        }
122        if (samplingProfiler == null) {
123            Log.e(TAG, "SamplingProfilerIntegration is not started");
124            return;
125        }
126
127        /*
128         * If we're already writing a snapshot, don't bother enqueueing another
129         * request right now. This will reduce the number of individual
130         * snapshots and in turn the total amount of memory consumed (one big
131         * snapshot is smaller than N subset snapshots).
132         */
133        if (pending.compareAndSet(false, true)) {
134            snapshotWriter.execute(new Runnable() {
135                public void run() {
136                    try {
137                        writeSnapshotFile(processName, packageInfo);
138                    } finally {
139                        pending.set(false);
140                    }
141                }
142            });
143        }
144    }
145
146    /**
147     * Writes the zygote's snapshot to internal storage if profiling is enabled.
148     */
149    public static void writeZygoteSnapshot() {
150        if (!enabled) {
151            return;
152        }
153        writeSnapshotFile("zygote", null);
154        samplingProfiler.shutdown();
155        samplingProfiler = null;
156        startMillis = 0;
157    }
158
159    /**
160     * pass in PackageInfo to retrieve various values for snapshot header
161     */
162    private static void writeSnapshotFile(String processName, PackageInfo packageInfo) {
163        if (!enabled) {
164            return;
165        }
166        samplingProfiler.stop();
167
168        /*
169         * We use the global start time combined with the process name
170         * as a unique ID. We can't use a counter because processes
171         * restart. This could result in some overlap if we capture
172         * two snapshots in rapid succession.
173         */
174        String name = processName.replaceAll(":", ".");
175        String path = SNAPSHOT_DIR + "/" + name + "-" + startMillis + ".snapshot";
176        long start = System.currentTimeMillis();
177        OutputStream outputStream = null;
178        try {
179            outputStream = new BufferedOutputStream(new FileOutputStream(path));
180            PrintStream out = new PrintStream(outputStream);
181            generateSnapshotHeader(name, packageInfo, out);
182            if (out.checkError()) {
183                throw new IOException();
184            }
185            BinaryHprofWriter.write(samplingProfiler.getHprofData(), outputStream);
186        } catch (IOException e) {
187            Log.e(TAG, "Error writing snapshot to " + path, e);
188            return;
189        } finally {
190            IoUtils.closeQuietly(outputStream);
191        }
192        // set file readable to the world so that SamplingProfilerService
193        // can put it to dropbox
194        new File(path).setReadable(true, false);
195
196        long elapsed = System.currentTimeMillis() - start;
197        Log.i(TAG, "Wrote snapshot " + path + " in " + elapsed + "ms.");
198        samplingProfiler.start(samplingProfilerMilliseconds);
199    }
200
201    /**
202     * generate header for snapshots, with the following format
203     * (like an HTTP header but without the \r):
204     *
205     * Version: <version number of profiler>\n
206     * Process: <process name>\n
207     * Package: <package name, if exists>\n
208     * Package-Version: <version number of the package, if exists>\n
209     * Build: <fingerprint>\n
210     * \n
211     * <the actual snapshot content begins here...>
212     */
213    private static void generateSnapshotHeader(String processName, PackageInfo packageInfo,
214            PrintStream out) {
215        // profiler version
216        out.println("Version: 3");
217        out.println("Process: " + processName);
218        if (packageInfo != null) {
219            out.println("Package: " + packageInfo.packageName);
220            out.println("Package-Version: " + packageInfo.versionCode);
221        }
222        out.println("Build: " + Build.FINGERPRINT);
223        // single blank line means the end of snapshot header.
224        out.println();
225    }
226}
227