blob: 14a9a313ffeec6e73bc1e16995725547536766f6 [file] [log] [blame]
John Reckedc524c2015-03-18 15:24:33 -07001/*
2 * Copyright (C) 2015 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.server;
18
John Reckdf1742e2017-01-19 15:56:21 -080019import android.app.AlarmManager;
Jeff Sharkeycd654482016-01-08 17:42:11 -070020import android.app.AppOpsManager;
John Reckedc524c2015-03-18 15:24:33 -070021import android.content.Context;
John Reckdf1742e2017-01-19 15:56:21 -080022import android.content.pm.PackageInfo;
23import android.content.pm.PackageManager;
John Reckedc524c2015-03-18 15:24:33 -070024import android.os.Binder;
John Reckdf1742e2017-01-19 15:56:21 -080025import android.os.Environment;
26import android.os.Handler;
27import android.os.HandlerThread;
John Reckedc524c2015-03-18 15:24:33 -070028import android.os.IBinder;
29import android.os.MemoryFile;
John Reckdf1742e2017-01-19 15:56:21 -080030import android.os.Message;
John Reckedc524c2015-03-18 15:24:33 -070031import android.os.ParcelFileDescriptor;
John Reckdf1742e2017-01-19 15:56:21 -080032import android.os.Process;
John Reckedc524c2015-03-18 15:24:33 -070033import android.os.RemoteException;
John Reckdf1742e2017-01-19 15:56:21 -080034import android.os.Trace;
John Reckedc524c2015-03-18 15:24:33 -070035import android.util.Log;
36import android.view.IGraphicsStats;
John Reckdf1742e2017-01-19 15:56:21 -080037import android.view.IGraphicsStatsCallback;
John Reckedc524c2015-03-18 15:24:33 -070038
John Reckdf1742e2017-01-19 15:56:21 -080039import java.io.File;
John Reckedc524c2015-03-18 15:24:33 -070040import java.io.FileDescriptor;
41import java.io.IOException;
42import java.io.PrintWriter;
43import java.util.ArrayList;
John Reckdf1742e2017-01-19 15:56:21 -080044import java.util.Arrays;
45import java.util.Calendar;
46import java.util.HashSet;
47import java.util.TimeZone;
John Reckedc524c2015-03-18 15:24:33 -070048
49/**
50 * This service's job is to collect aggregate rendering profile data. It
51 * does this by allowing rendering processes to request an ashmem buffer
John Reckdf1742e2017-01-19 15:56:21 -080052 * to place their stats into.
John Reckedc524c2015-03-18 15:24:33 -070053 *
John Reckdf1742e2017-01-19 15:56:21 -080054 * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days
55 * are kept.
John Reckedc524c2015-03-18 15:24:33 -070056 *
John Reckdf1742e2017-01-19 15:56:21 -080057 * The primary consumer of this is incident reports and automated metric checking. It is not
58 * intended for end-developer consumption, for that we have gfxinfo.
John Reckedc524c2015-03-18 15:24:33 -070059 *
John Reckdf1742e2017-01-19 15:56:21 -080060 * Buffer rotation process:
61 * 1) Alarm fires
62 * 2) onRotateGraphicsStatsBuffer() is sent to all active processes
63 * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and
64 * request a new one.
65 * 4) When that request is received we now know that the ashmem region is no longer in use so
66 * it gets queued up for saving to disk and a new ashmem region is created and returned
67 * for the process to use.
John Reckedc524c2015-03-18 15:24:33 -070068 *
69 * @hide */
70public class GraphicsStatsService extends IGraphicsStats.Stub {
71 public static final String GRAPHICS_STATS_SERVICE = "graphicsstats";
72
73 private static final String TAG = "GraphicsStatsService";
John Reckdf1742e2017-01-19 15:56:21 -080074
75 private static final int SAVE_BUFFER = 1;
76 private static final int DELETE_OLD = 2;
77
78 // This isn't static because we need this to happen after registerNativeMethods, however
79 // the class is loaded (and thus static ctor happens) before that occurs.
80 private final int ASHMEM_SIZE = nGetAshmemSize();
81 private final byte[] ZERO_DATA = new byte[ASHMEM_SIZE];
John Reckedc524c2015-03-18 15:24:33 -070082
83 private final Context mContext;
Jeff Sharkeycd654482016-01-08 17:42:11 -070084 private final AppOpsManager mAppOps;
John Reckdf1742e2017-01-19 15:56:21 -080085 private final AlarmManager mAlarmManager;
John Reckedc524c2015-03-18 15:24:33 -070086 private final Object mLock = new Object();
87 private ArrayList<ActiveBuffer> mActive = new ArrayList<>();
John Reckdf1742e2017-01-19 15:56:21 -080088 private File mGraphicsStatsDir;
89 private final Object mFileAccessLock = new Object();
90 private Handler mWriteOutHandler;
91 private boolean mRotateIsScheduled = false;
John Reckedc524c2015-03-18 15:24:33 -070092
93 public GraphicsStatsService(Context context) {
94 mContext = context;
Jeff Sharkeycd654482016-01-08 17:42:11 -070095 mAppOps = context.getSystemService(AppOpsManager.class);
John Reckdf1742e2017-01-19 15:56:21 -080096 mAlarmManager = context.getSystemService(AlarmManager.class);
97 File systemDataDir = new File(Environment.getDataDirectory(), "system");
98 mGraphicsStatsDir = new File(systemDataDir, "graphicsstats");
99 mGraphicsStatsDir.mkdirs();
100 if (!mGraphicsStatsDir.exists()) {
101 throw new IllegalStateException("Graphics stats directory does not exist: "
102 + mGraphicsStatsDir.getAbsolutePath());
103 }
104 HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", Process.THREAD_PRIORITY_BACKGROUND);
105 bgthread.start();
106
107 mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() {
108 @Override
109 public boolean handleMessage(Message msg) {
110 switch (msg.what) {
111 case SAVE_BUFFER:
112 saveBuffer((HistoricalBuffer) msg.obj);
113 break;
114 case DELETE_OLD:
115 deleteOldBuffers();
116 break;
117 }
118 return true;
119 }
120 });
121 }
122
123 /**
124 * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because
125 * rotation can be delayed if there's otherwise no activity. However exact is used because
126 * we don't want the system to delay it by TOO much.
127 */
128 private void scheduleRotateLocked() {
129 if (mRotateIsScheduled) {
130 return;
131 }
132 mRotateIsScheduled = true;
133 Calendar calendar = normalizeDate(System.currentTimeMillis());
134 calendar.add(Calendar.DATE, 1);
135 mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm,
136 mWriteOutHandler);
137 }
138
139 private void onAlarm() {
John Reck37dfb422017-02-22 17:01:30 -0800140 // We need to make a copy since some of the callbacks won't be proxy and thus
141 // can result in a re-entrant acquisition of mLock that would result in a modification
142 // of mActive during iteration.
143 ActiveBuffer[] activeCopy;
John Reckdf1742e2017-01-19 15:56:21 -0800144 synchronized (mLock) {
145 mRotateIsScheduled = false;
146 scheduleRotateLocked();
John Reck37dfb422017-02-22 17:01:30 -0800147 activeCopy = mActive.toArray(new ActiveBuffer[0]);
148 }
149 for (ActiveBuffer active : activeCopy) {
150 try {
151 active.mCallback.onRotateGraphicsStatsBuffer();
152 } catch (RemoteException e) {
153 Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers",
154 active.mInfo.packageName, active.mPid), e);
John Reckdf1742e2017-01-19 15:56:21 -0800155 }
156 }
157 // Give a few seconds for everyone to rotate before doing the cleanup
158 mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000);
John Reckedc524c2015-03-18 15:24:33 -0700159 }
160
161 @Override
John Reckdf1742e2017-01-19 15:56:21 -0800162 public ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback token)
John Reckedc524c2015-03-18 15:24:33 -0700163 throws RemoteException {
164 int uid = Binder.getCallingUid();
165 int pid = Binder.getCallingPid();
166 ParcelFileDescriptor pfd = null;
167 long callingIdentity = Binder.clearCallingIdentity();
168 try {
Jeff Sharkeycd654482016-01-08 17:42:11 -0700169 mAppOps.checkPackage(uid, packageName);
John Reckdf1742e2017-01-19 15:56:21 -0800170 PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
John Reckedc524c2015-03-18 15:24:33 -0700171 synchronized (mLock) {
John Reckdf1742e2017-01-19 15:56:21 -0800172 pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode);
John Reckedc524c2015-03-18 15:24:33 -0700173 }
John Reckdf1742e2017-01-19 15:56:21 -0800174 } catch (PackageManager.NameNotFoundException ex) {
175 throw new RemoteException("Unable to find package: '" + packageName + "'");
John Reckedc524c2015-03-18 15:24:33 -0700176 } finally {
177 Binder.restoreCallingIdentity(callingIdentity);
178 }
179 return pfd;
180 }
181
182 private ParcelFileDescriptor getPfd(MemoryFile file) {
183 try {
John Reckdf1742e2017-01-19 15:56:21 -0800184 if (!file.getFileDescriptor().valid()) {
185 throw new IllegalStateException("Invalid file descriptor");
186 }
John Reckedc524c2015-03-18 15:24:33 -0700187 return new ParcelFileDescriptor(file.getFileDescriptor());
188 } catch (IOException ex) {
189 throw new IllegalStateException("Failed to get PFD from memory file", ex);
190 }
191 }
192
John Reckdf1742e2017-01-19 15:56:21 -0800193 private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
194 int uid, int pid, String packageName, int versionCode) throws RemoteException {
195 ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
196 scheduleRotateLocked();
John Reckedc524c2015-03-18 15:24:33 -0700197 return getPfd(buffer.mProcessBuffer);
198 }
199
John Reckdf1742e2017-01-19 15:56:21 -0800200 private Calendar normalizeDate(long timestamp) {
201 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
202 calendar.setTimeInMillis(timestamp);
203 calendar.set(Calendar.HOUR_OF_DAY, 0);
204 calendar.set(Calendar.MINUTE, 0);
205 calendar.set(Calendar.SECOND, 0);
206 calendar.set(Calendar.MILLISECOND, 0);
207 return calendar;
208 }
209
210 private File pathForApp(BufferInfo info) {
211 String subPath = String.format("%d/%s/%d/total",
212 normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode);
213 return new File(mGraphicsStatsDir, subPath);
214 }
215
216 private void saveBuffer(HistoricalBuffer buffer) {
217 if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
218 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName);
219 }
220 synchronized (mFileAccessLock) {
221 File path = pathForApp(buffer.mInfo);
222 File parent = path.getParentFile();
223 parent.mkdirs();
224 if (!parent.exists()) {
225 Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'");
226 return;
227 }
228 nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode,
229 buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData);
230 }
231 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
232 }
233
234 private void deleteRecursiveLocked(File file) {
235 if (file.isDirectory()) {
236 for (File child : file.listFiles()) {
237 deleteRecursiveLocked(child);
238 }
239 }
240 if (!file.delete()) {
241 Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!");
242 }
243 }
244
245 private void deleteOldBuffers() {
246 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers");
247 synchronized (mFileAccessLock) {
248 File[] files = mGraphicsStatsDir.listFiles();
249 if (files == null || files.length <= 3) {
250 return;
251 }
252 long[] sortedDates = new long[files.length];
253 for (int i = 0; i < files.length; i++) {
254 try {
255 sortedDates[i] = Long.parseLong(files[i].getName());
256 } catch (NumberFormatException ex) {
257 // Skip unrecognized folders
258 }
259 }
260 if (sortedDates.length <= 3) {
261 return;
262 }
263 Arrays.sort(sortedDates);
264 for (int i = 0; i < sortedDates.length - 3; i++) {
265 deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i])));
266 }
267 }
268 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
269 }
270
271 private void addToSaveQueue(ActiveBuffer buffer) {
272 try {
273 HistoricalBuffer data = new HistoricalBuffer(buffer);
274 Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget();
275 } catch (IOException e) {
276 Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e);
277 }
278 buffer.closeAllBuffers();
279 }
280
John Reckedc524c2015-03-18 15:24:33 -0700281 private void processDied(ActiveBuffer buffer) {
282 synchronized (mLock) {
283 mActive.remove(buffer);
John Reckedc524c2015-03-18 15:24:33 -0700284 }
John Reckdf1742e2017-01-19 15:56:21 -0800285 addToSaveQueue(buffer);
John Reckedc524c2015-03-18 15:24:33 -0700286 }
287
John Reckdf1742e2017-01-19 15:56:21 -0800288 private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
289 String packageName, int versionCode) throws RemoteException {
John Reckedc524c2015-03-18 15:24:33 -0700290 int size = mActive.size();
John Reckdf1742e2017-01-19 15:56:21 -0800291 long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
John Reckedc524c2015-03-18 15:24:33 -0700292 for (int i = 0; i < size; i++) {
John Reckdf1742e2017-01-19 15:56:21 -0800293 ActiveBuffer buffer = mActive.get(i);
294 if (buffer.mPid == pid
295 && buffer.mUid == uid) {
296 // If the buffer is too old we remove it and return a new one
297 if (buffer.mInfo.startTime < today) {
298 buffer.binderDied();
299 break;
300 } else {
301 return buffer;
302 }
John Reckedc524c2015-03-18 15:24:33 -0700303 }
304 }
305 // Didn't find one, need to create it
306 try {
John Reckdf1742e2017-01-19 15:56:21 -0800307 ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode);
John Reckedc524c2015-03-18 15:24:33 -0700308 mActive.add(buffers);
309 return buffers;
310 } catch (IOException ex) {
311 throw new RemoteException("Failed to allocate space");
312 }
313 }
314
John Reckdf1742e2017-01-19 15:56:21 -0800315 private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) {
316 HashSet<File> skipFiles = new HashSet<>(buffers.size());
317 for (int i = 0; i < buffers.size(); i++) {
318 HistoricalBuffer buffer = buffers.get(i);
319 File path = pathForApp(buffer.mInfo);
320 skipFiles.add(path);
321 nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName,
322 buffer.mInfo.versionCode, buffer.mInfo.startTime, buffer.mInfo.endTime,
323 buffer.mData);
324 }
325 return skipFiles;
326 }
327
328 private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) {
329 for (File date : mGraphicsStatsDir.listFiles()) {
330 for (File pkg : date.listFiles()) {
331 for (File version : pkg.listFiles()) {
332 File data = new File(version, "total");
333 if (skipFiles.contains(data)) {
334 continue;
335 }
336 nAddToDump(dump, data.getAbsolutePath());
John Reckedc524c2015-03-18 15:24:33 -0700337 }
John Reckedc524c2015-03-18 15:24:33 -0700338 }
339 }
John Reckedc524c2015-03-18 15:24:33 -0700340 }
341
342 @Override
343 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
344 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
John Reckdf1742e2017-01-19 15:56:21 -0800345 boolean dumpProto = false;
346 for (String str : args) {
347 if ("--proto".equals(str)) {
348 dumpProto = true;
349 break;
350 }
351 }
352 ArrayList<HistoricalBuffer> buffers;
John Reckedc524c2015-03-18 15:24:33 -0700353 synchronized (mLock) {
John Reckdf1742e2017-01-19 15:56:21 -0800354 buffers = new ArrayList<>(mActive.size());
John Reckedc524c2015-03-18 15:24:33 -0700355 for (int i = 0; i < mActive.size(); i++) {
John Reckedc524c2015-03-18 15:24:33 -0700356 try {
John Reckdf1742e2017-01-19 15:56:21 -0800357 buffers.add(new HistoricalBuffer(mActive.get(i)));
358 } catch (IOException ex) {
359 // Ignore
John Reckedc524c2015-03-18 15:24:33 -0700360 }
John Reckedc524c2015-03-18 15:24:33 -0700361 }
John Reckdf1742e2017-01-19 15:56:21 -0800362 }
363 long dump = nCreateDump(fd.getInt$(), dumpProto);
364 try {
365 synchronized (mFileAccessLock) {
366 HashSet<File> skipList = dumpActiveLocked(dump, buffers);
367 buffers.clear();
368 dumpHistoricalLocked(dump, skipList);
John Reckedc524c2015-03-18 15:24:33 -0700369 }
John Reckdf1742e2017-01-19 15:56:21 -0800370 } finally {
371 nFinishDump(dump);
372 }
373 }
374
375 private static native int nGetAshmemSize();
376 private static native long nCreateDump(int outFd, boolean isProto);
377 private static native void nAddToDump(long dump, String path, String packageName,
378 int versionCode, long startTime, long endTime, byte[] data);
379 private static native void nAddToDump(long dump, String path);
380 private static native void nFinishDump(long dump);
381 private static native void nSaveBuffer(String path, String packageName, int versionCode,
382 long startTime, long endTime, byte[] data);
383
384 private final class BufferInfo {
385 final String packageName;
386 final int versionCode;
387 long startTime;
388 long endTime;
389
390 BufferInfo(String packageName, int versionCode, long startTime) {
391 this.packageName = packageName;
392 this.versionCode = versionCode;
393 this.startTime = startTime;
John Reckedc524c2015-03-18 15:24:33 -0700394 }
395 }
396
397 private final class ActiveBuffer implements DeathRecipient {
John Reckdf1742e2017-01-19 15:56:21 -0800398 final BufferInfo mInfo;
John Reckedc524c2015-03-18 15:24:33 -0700399 final int mUid;
400 final int mPid;
John Reckdf1742e2017-01-19 15:56:21 -0800401 final IGraphicsStatsCallback mCallback;
John Reckedc524c2015-03-18 15:24:33 -0700402 final IBinder mToken;
403 MemoryFile mProcessBuffer;
John Reckedc524c2015-03-18 15:24:33 -0700404
John Reckdf1742e2017-01-19 15:56:21 -0800405 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
John Reckedc524c2015-03-18 15:24:33 -0700406 throws RemoteException, IOException {
John Reckdf1742e2017-01-19 15:56:21 -0800407 mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis());
John Reckedc524c2015-03-18 15:24:33 -0700408 mUid = uid;
409 mPid = pid;
John Reckdf1742e2017-01-19 15:56:21 -0800410 mCallback = token;
411 mToken = mCallback.asBinder();
John Reckedc524c2015-03-18 15:24:33 -0700412 mToken.linkToDeath(this, 0);
John Reckdf1742e2017-01-19 15:56:21 -0800413 mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE);
414 mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE);
John Reckedc524c2015-03-18 15:24:33 -0700415 }
416
417 @Override
418 public void binderDied() {
419 mToken.unlinkToDeath(this, 0);
420 processDied(this);
421 }
422
423 void closeAllBuffers() {
424 if (mProcessBuffer != null) {
425 mProcessBuffer.close();
426 mProcessBuffer = null;
427 }
428 }
429 }
430
John Reckdf1742e2017-01-19 15:56:21 -0800431 private final class HistoricalBuffer {
432 final BufferInfo mInfo;
433 final byte[] mData = new byte[ASHMEM_SIZE];
434 HistoricalBuffer(ActiveBuffer active) throws IOException {
435 mInfo = active.mInfo;
436 mInfo.endTime = System.currentTimeMillis();
437 active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE);
John Reckedc524c2015-03-18 15:24:33 -0700438 }
439 }
440}