blob: bdd80e382867d87ea70599b1ca64009f1ab11a86 [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() {
140 synchronized (mLock) {
141 mRotateIsScheduled = false;
142 scheduleRotateLocked();
143 for (ActiveBuffer active : mActive) {
144 try {
145 active.mCallback.onRotateGraphicsStatsBuffer();
146 } catch (RemoteException e) {
147 Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers",
148 active.mInfo.packageName, active.mPid), e);
149 }
150 }
151 }
152 // Give a few seconds for everyone to rotate before doing the cleanup
153 mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000);
John Reckedc524c2015-03-18 15:24:33 -0700154 }
155
156 @Override
John Reckdf1742e2017-01-19 15:56:21 -0800157 public ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback token)
John Reckedc524c2015-03-18 15:24:33 -0700158 throws RemoteException {
159 int uid = Binder.getCallingUid();
160 int pid = Binder.getCallingPid();
161 ParcelFileDescriptor pfd = null;
162 long callingIdentity = Binder.clearCallingIdentity();
163 try {
Jeff Sharkeycd654482016-01-08 17:42:11 -0700164 mAppOps.checkPackage(uid, packageName);
John Reckdf1742e2017-01-19 15:56:21 -0800165 PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
John Reckedc524c2015-03-18 15:24:33 -0700166 synchronized (mLock) {
John Reckdf1742e2017-01-19 15:56:21 -0800167 pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode);
John Reckedc524c2015-03-18 15:24:33 -0700168 }
John Reckdf1742e2017-01-19 15:56:21 -0800169 } catch (PackageManager.NameNotFoundException ex) {
170 throw new RemoteException("Unable to find package: '" + packageName + "'");
John Reckedc524c2015-03-18 15:24:33 -0700171 } finally {
172 Binder.restoreCallingIdentity(callingIdentity);
173 }
174 return pfd;
175 }
176
177 private ParcelFileDescriptor getPfd(MemoryFile file) {
178 try {
John Reckdf1742e2017-01-19 15:56:21 -0800179 if (!file.getFileDescriptor().valid()) {
180 throw new IllegalStateException("Invalid file descriptor");
181 }
John Reckedc524c2015-03-18 15:24:33 -0700182 return new ParcelFileDescriptor(file.getFileDescriptor());
183 } catch (IOException ex) {
184 throw new IllegalStateException("Failed to get PFD from memory file", ex);
185 }
186 }
187
John Reckdf1742e2017-01-19 15:56:21 -0800188 private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
189 int uid, int pid, String packageName, int versionCode) throws RemoteException {
190 ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
191 scheduleRotateLocked();
John Reckedc524c2015-03-18 15:24:33 -0700192 return getPfd(buffer.mProcessBuffer);
193 }
194
John Reckdf1742e2017-01-19 15:56:21 -0800195 private Calendar normalizeDate(long timestamp) {
196 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
197 calendar.setTimeInMillis(timestamp);
198 calendar.set(Calendar.HOUR_OF_DAY, 0);
199 calendar.set(Calendar.MINUTE, 0);
200 calendar.set(Calendar.SECOND, 0);
201 calendar.set(Calendar.MILLISECOND, 0);
202 return calendar;
203 }
204
205 private File pathForApp(BufferInfo info) {
206 String subPath = String.format("%d/%s/%d/total",
207 normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode);
208 return new File(mGraphicsStatsDir, subPath);
209 }
210
211 private void saveBuffer(HistoricalBuffer buffer) {
212 if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
213 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName);
214 }
215 synchronized (mFileAccessLock) {
216 File path = pathForApp(buffer.mInfo);
217 File parent = path.getParentFile();
218 parent.mkdirs();
219 if (!parent.exists()) {
220 Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'");
221 return;
222 }
223 nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode,
224 buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData);
225 }
226 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
227 }
228
229 private void deleteRecursiveLocked(File file) {
230 if (file.isDirectory()) {
231 for (File child : file.listFiles()) {
232 deleteRecursiveLocked(child);
233 }
234 }
235 if (!file.delete()) {
236 Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!");
237 }
238 }
239
240 private void deleteOldBuffers() {
241 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers");
242 synchronized (mFileAccessLock) {
243 File[] files = mGraphicsStatsDir.listFiles();
244 if (files == null || files.length <= 3) {
245 return;
246 }
247 long[] sortedDates = new long[files.length];
248 for (int i = 0; i < files.length; i++) {
249 try {
250 sortedDates[i] = Long.parseLong(files[i].getName());
251 } catch (NumberFormatException ex) {
252 // Skip unrecognized folders
253 }
254 }
255 if (sortedDates.length <= 3) {
256 return;
257 }
258 Arrays.sort(sortedDates);
259 for (int i = 0; i < sortedDates.length - 3; i++) {
260 deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i])));
261 }
262 }
263 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
264 }
265
266 private void addToSaveQueue(ActiveBuffer buffer) {
267 try {
268 HistoricalBuffer data = new HistoricalBuffer(buffer);
269 Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget();
270 } catch (IOException e) {
271 Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e);
272 }
273 buffer.closeAllBuffers();
274 }
275
John Reckedc524c2015-03-18 15:24:33 -0700276 private void processDied(ActiveBuffer buffer) {
277 synchronized (mLock) {
278 mActive.remove(buffer);
John Reckedc524c2015-03-18 15:24:33 -0700279 }
John Reckdf1742e2017-01-19 15:56:21 -0800280 addToSaveQueue(buffer);
John Reckedc524c2015-03-18 15:24:33 -0700281 }
282
John Reckdf1742e2017-01-19 15:56:21 -0800283 private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
284 String packageName, int versionCode) throws RemoteException {
John Reckedc524c2015-03-18 15:24:33 -0700285 int size = mActive.size();
John Reckdf1742e2017-01-19 15:56:21 -0800286 long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
John Reckedc524c2015-03-18 15:24:33 -0700287 for (int i = 0; i < size; i++) {
John Reckdf1742e2017-01-19 15:56:21 -0800288 ActiveBuffer buffer = mActive.get(i);
289 if (buffer.mPid == pid
290 && buffer.mUid == uid) {
291 // If the buffer is too old we remove it and return a new one
292 if (buffer.mInfo.startTime < today) {
293 buffer.binderDied();
294 break;
295 } else {
296 return buffer;
297 }
John Reckedc524c2015-03-18 15:24:33 -0700298 }
299 }
300 // Didn't find one, need to create it
301 try {
John Reckdf1742e2017-01-19 15:56:21 -0800302 ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode);
John Reckedc524c2015-03-18 15:24:33 -0700303 mActive.add(buffers);
304 return buffers;
305 } catch (IOException ex) {
306 throw new RemoteException("Failed to allocate space");
307 }
308 }
309
John Reckdf1742e2017-01-19 15:56:21 -0800310 private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) {
311 HashSet<File> skipFiles = new HashSet<>(buffers.size());
312 for (int i = 0; i < buffers.size(); i++) {
313 HistoricalBuffer buffer = buffers.get(i);
314 File path = pathForApp(buffer.mInfo);
315 skipFiles.add(path);
316 nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName,
317 buffer.mInfo.versionCode, buffer.mInfo.startTime, buffer.mInfo.endTime,
318 buffer.mData);
319 }
320 return skipFiles;
321 }
322
323 private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) {
324 for (File date : mGraphicsStatsDir.listFiles()) {
325 for (File pkg : date.listFiles()) {
326 for (File version : pkg.listFiles()) {
327 File data = new File(version, "total");
328 if (skipFiles.contains(data)) {
329 continue;
330 }
331 nAddToDump(dump, data.getAbsolutePath());
John Reckedc524c2015-03-18 15:24:33 -0700332 }
John Reckedc524c2015-03-18 15:24:33 -0700333 }
334 }
John Reckedc524c2015-03-18 15:24:33 -0700335 }
336
337 @Override
338 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
339 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
John Reckdf1742e2017-01-19 15:56:21 -0800340 boolean dumpProto = false;
341 for (String str : args) {
342 if ("--proto".equals(str)) {
343 dumpProto = true;
344 break;
345 }
346 }
347 ArrayList<HistoricalBuffer> buffers;
John Reckedc524c2015-03-18 15:24:33 -0700348 synchronized (mLock) {
John Reckdf1742e2017-01-19 15:56:21 -0800349 buffers = new ArrayList<>(mActive.size());
John Reckedc524c2015-03-18 15:24:33 -0700350 for (int i = 0; i < mActive.size(); i++) {
John Reckedc524c2015-03-18 15:24:33 -0700351 try {
John Reckdf1742e2017-01-19 15:56:21 -0800352 buffers.add(new HistoricalBuffer(mActive.get(i)));
353 } catch (IOException ex) {
354 // Ignore
John Reckedc524c2015-03-18 15:24:33 -0700355 }
John Reckedc524c2015-03-18 15:24:33 -0700356 }
John Reckdf1742e2017-01-19 15:56:21 -0800357 }
358 long dump = nCreateDump(fd.getInt$(), dumpProto);
359 try {
360 synchronized (mFileAccessLock) {
361 HashSet<File> skipList = dumpActiveLocked(dump, buffers);
362 buffers.clear();
363 dumpHistoricalLocked(dump, skipList);
John Reckedc524c2015-03-18 15:24:33 -0700364 }
John Reckdf1742e2017-01-19 15:56:21 -0800365 } finally {
366 nFinishDump(dump);
367 }
368 }
369
370 private static native int nGetAshmemSize();
371 private static native long nCreateDump(int outFd, boolean isProto);
372 private static native void nAddToDump(long dump, String path, String packageName,
373 int versionCode, long startTime, long endTime, byte[] data);
374 private static native void nAddToDump(long dump, String path);
375 private static native void nFinishDump(long dump);
376 private static native void nSaveBuffer(String path, String packageName, int versionCode,
377 long startTime, long endTime, byte[] data);
378
379 private final class BufferInfo {
380 final String packageName;
381 final int versionCode;
382 long startTime;
383 long endTime;
384
385 BufferInfo(String packageName, int versionCode, long startTime) {
386 this.packageName = packageName;
387 this.versionCode = versionCode;
388 this.startTime = startTime;
John Reckedc524c2015-03-18 15:24:33 -0700389 }
390 }
391
392 private final class ActiveBuffer implements DeathRecipient {
John Reckdf1742e2017-01-19 15:56:21 -0800393 final BufferInfo mInfo;
John Reckedc524c2015-03-18 15:24:33 -0700394 final int mUid;
395 final int mPid;
John Reckdf1742e2017-01-19 15:56:21 -0800396 final IGraphicsStatsCallback mCallback;
John Reckedc524c2015-03-18 15:24:33 -0700397 final IBinder mToken;
398 MemoryFile mProcessBuffer;
John Reckedc524c2015-03-18 15:24:33 -0700399
John Reckdf1742e2017-01-19 15:56:21 -0800400 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
John Reckedc524c2015-03-18 15:24:33 -0700401 throws RemoteException, IOException {
John Reckdf1742e2017-01-19 15:56:21 -0800402 mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis());
John Reckedc524c2015-03-18 15:24:33 -0700403 mUid = uid;
404 mPid = pid;
John Reckdf1742e2017-01-19 15:56:21 -0800405 mCallback = token;
406 mToken = mCallback.asBinder();
John Reckedc524c2015-03-18 15:24:33 -0700407 mToken.linkToDeath(this, 0);
John Reckdf1742e2017-01-19 15:56:21 -0800408 mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE);
409 mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE);
John Reckedc524c2015-03-18 15:24:33 -0700410 }
411
412 @Override
413 public void binderDied() {
414 mToken.unlinkToDeath(this, 0);
415 processDied(this);
416 }
417
418 void closeAllBuffers() {
419 if (mProcessBuffer != null) {
420 mProcessBuffer.close();
421 mProcessBuffer = null;
422 }
423 }
424 }
425
John Reckdf1742e2017-01-19 15:56:21 -0800426 private final class HistoricalBuffer {
427 final BufferInfo mInfo;
428 final byte[] mData = new byte[ASHMEM_SIZE];
429 HistoricalBuffer(ActiveBuffer active) throws IOException {
430 mInfo = active.mInfo;
431 mInfo.endTime = System.currentTimeMillis();
432 active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE);
John Reckedc524c2015-03-18 15:24:33 -0700433 }
434 }
435}