1 /* 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 17 package com.android.server; 18 19 import android.app.AlarmManager; 20 import android.app.AppOpsManager; 21 import android.content.Context; 22 import android.content.pm.PackageInfo; 23 import android.content.pm.PackageManager; 24 import android.os.Binder; 25 import android.os.Environment; 26 import android.os.Handler; 27 import android.os.HandlerThread; 28 import android.os.IBinder; 29 import android.os.MemoryFile; 30 import android.os.Message; 31 import android.os.ParcelFileDescriptor; 32 import android.os.Process; 33 import android.os.RemoteException; 34 import android.os.Trace; 35 import android.os.UserHandle; 36 import android.util.Log; 37 import android.view.IGraphicsStats; 38 import android.view.IGraphicsStatsCallback; 39 40 import com.android.internal.util.DumpUtils; 41 42 import java.io.File; 43 import java.io.FileDescriptor; 44 import java.io.IOException; 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Calendar; 49 import java.util.HashSet; 50 import java.util.TimeZone; 51 52 /** 53 * This service's job is to collect aggregate rendering profile data. It 54 * does this by allowing rendering processes to request an ashmem buffer 55 * to place their stats into. 56 * 57 * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days 58 * are kept. 59 * 60 * The primary consumer of this is incident reports and automated metric checking. It is not 61 * intended for end-developer consumption, for that we have gfxinfo. 62 * 63 * Buffer rotation process: 64 * 1) Alarm fires 65 * 2) onRotateGraphicsStatsBuffer() is sent to all active processes 66 * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and 67 * request a new one. 68 * 4) When that request is received we now know that the ashmem region is no longer in use so 69 * it gets queued up for saving to disk and a new ashmem region is created and returned 70 * for the process to use. 71 * 72 * @hide */ 73 public class GraphicsStatsService extends IGraphicsStats.Stub { 74 public static final String GRAPHICS_STATS_SERVICE = "graphicsstats"; 75 76 private static final String TAG = "GraphicsStatsService"; 77 78 private static final int SAVE_BUFFER = 1; 79 private static final int DELETE_OLD = 2; 80 81 // This isn't static because we need this to happen after registerNativeMethods, however 82 // the class is loaded (and thus static ctor happens) before that occurs. 83 private final int ASHMEM_SIZE = nGetAshmemSize(); 84 private final byte[] ZERO_DATA = new byte[ASHMEM_SIZE]; 85 86 private final Context mContext; 87 private final AppOpsManager mAppOps; 88 private final AlarmManager mAlarmManager; 89 private final Object mLock = new Object(); 90 private ArrayList<ActiveBuffer> mActive = new ArrayList<>(); 91 private File mGraphicsStatsDir; 92 private final Object mFileAccessLock = new Object(); 93 private Handler mWriteOutHandler; 94 private boolean mRotateIsScheduled = false; 95 GraphicsStatsService(Context context)96 public GraphicsStatsService(Context context) { 97 mContext = context; 98 mAppOps = context.getSystemService(AppOpsManager.class); 99 mAlarmManager = context.getSystemService(AlarmManager.class); 100 File systemDataDir = new File(Environment.getDataDirectory(), "system"); 101 mGraphicsStatsDir = new File(systemDataDir, "graphicsstats"); 102 mGraphicsStatsDir.mkdirs(); 103 if (!mGraphicsStatsDir.exists()) { 104 throw new IllegalStateException("Graphics stats directory does not exist: " 105 + mGraphicsStatsDir.getAbsolutePath()); 106 } 107 HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", Process.THREAD_PRIORITY_BACKGROUND); 108 bgthread.start(); 109 110 mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() { 111 @Override 112 public boolean handleMessage(Message msg) { 113 switch (msg.what) { 114 case SAVE_BUFFER: 115 saveBuffer((HistoricalBuffer) msg.obj); 116 break; 117 case DELETE_OLD: 118 deleteOldBuffers(); 119 break; 120 } 121 return true; 122 } 123 }); 124 } 125 126 /** 127 * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because 128 * rotation can be delayed if there's otherwise no activity. However exact is used because 129 * we don't want the system to delay it by TOO much. 130 */ scheduleRotateLocked()131 private void scheduleRotateLocked() { 132 if (mRotateIsScheduled) { 133 return; 134 } 135 mRotateIsScheduled = true; 136 Calendar calendar = normalizeDate(System.currentTimeMillis()); 137 calendar.add(Calendar.DATE, 1); 138 mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm, 139 mWriteOutHandler); 140 } 141 onAlarm()142 private void onAlarm() { 143 // We need to make a copy since some of the callbacks won't be proxy and thus 144 // can result in a re-entrant acquisition of mLock that would result in a modification 145 // of mActive during iteration. 146 ActiveBuffer[] activeCopy; 147 synchronized (mLock) { 148 mRotateIsScheduled = false; 149 scheduleRotateLocked(); 150 activeCopy = mActive.toArray(new ActiveBuffer[0]); 151 } 152 for (ActiveBuffer active : activeCopy) { 153 try { 154 active.mCallback.onRotateGraphicsStatsBuffer(); 155 } catch (RemoteException e) { 156 Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers", 157 active.mInfo.packageName, active.mPid), e); 158 } 159 } 160 // Give a few seconds for everyone to rotate before doing the cleanup 161 mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000); 162 } 163 164 @Override requestBufferForProcess(String packageName, IGraphicsStatsCallback token)165 public ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback token) 166 throws RemoteException { 167 int uid = Binder.getCallingUid(); 168 int pid = Binder.getCallingPid(); 169 ParcelFileDescriptor pfd = null; 170 long callingIdentity = Binder.clearCallingIdentity(); 171 try { 172 mAppOps.checkPackage(uid, packageName); 173 PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser( 174 packageName, 175 0, 176 UserHandle.getUserId(uid)); 177 synchronized (mLock) { 178 pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode); 179 } 180 } catch (PackageManager.NameNotFoundException ex) { 181 throw new RemoteException("Unable to find package: '" + packageName + "'"); 182 } finally { 183 Binder.restoreCallingIdentity(callingIdentity); 184 } 185 return pfd; 186 } 187 getPfd(MemoryFile file)188 private ParcelFileDescriptor getPfd(MemoryFile file) { 189 try { 190 if (!file.getFileDescriptor().valid()) { 191 throw new IllegalStateException("Invalid file descriptor"); 192 } 193 return new ParcelFileDescriptor(file.getFileDescriptor()); 194 } catch (IOException ex) { 195 throw new IllegalStateException("Failed to get PFD from memory file", ex); 196 } 197 } 198 requestBufferForProcessLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)199 private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token, 200 int uid, int pid, String packageName, int versionCode) throws RemoteException { 201 ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode); 202 scheduleRotateLocked(); 203 return getPfd(buffer.mProcessBuffer); 204 } 205 normalizeDate(long timestamp)206 private Calendar normalizeDate(long timestamp) { 207 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 208 calendar.setTimeInMillis(timestamp); 209 calendar.set(Calendar.HOUR_OF_DAY, 0); 210 calendar.set(Calendar.MINUTE, 0); 211 calendar.set(Calendar.SECOND, 0); 212 calendar.set(Calendar.MILLISECOND, 0); 213 return calendar; 214 } 215 pathForApp(BufferInfo info)216 private File pathForApp(BufferInfo info) { 217 String subPath = String.format("%d/%s/%d/total", 218 normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode); 219 return new File(mGraphicsStatsDir, subPath); 220 } 221 saveBuffer(HistoricalBuffer buffer)222 private void saveBuffer(HistoricalBuffer buffer) { 223 if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { 224 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName); 225 } 226 synchronized (mFileAccessLock) { 227 File path = pathForApp(buffer.mInfo); 228 File parent = path.getParentFile(); 229 parent.mkdirs(); 230 if (!parent.exists()) { 231 Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'"); 232 return; 233 } 234 nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode, 235 buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData); 236 } 237 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 238 } 239 deleteRecursiveLocked(File file)240 private void deleteRecursiveLocked(File file) { 241 if (file.isDirectory()) { 242 for (File child : file.listFiles()) { 243 deleteRecursiveLocked(child); 244 } 245 } 246 if (!file.delete()) { 247 Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!"); 248 } 249 } 250 deleteOldBuffers()251 private void deleteOldBuffers() { 252 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers"); 253 synchronized (mFileAccessLock) { 254 File[] files = mGraphicsStatsDir.listFiles(); 255 if (files == null || files.length <= 3) { 256 return; 257 } 258 long[] sortedDates = new long[files.length]; 259 for (int i = 0; i < files.length; i++) { 260 try { 261 sortedDates[i] = Long.parseLong(files[i].getName()); 262 } catch (NumberFormatException ex) { 263 // Skip unrecognized folders 264 } 265 } 266 if (sortedDates.length <= 3) { 267 return; 268 } 269 Arrays.sort(sortedDates); 270 for (int i = 0; i < sortedDates.length - 3; i++) { 271 deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i]))); 272 } 273 } 274 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 275 } 276 addToSaveQueue(ActiveBuffer buffer)277 private void addToSaveQueue(ActiveBuffer buffer) { 278 try { 279 HistoricalBuffer data = new HistoricalBuffer(buffer); 280 Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget(); 281 } catch (IOException e) { 282 Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e); 283 } 284 buffer.closeAllBuffers(); 285 } 286 processDied(ActiveBuffer buffer)287 private void processDied(ActiveBuffer buffer) { 288 synchronized (mLock) { 289 mActive.remove(buffer); 290 } 291 addToSaveQueue(buffer); 292 } 293 fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)294 private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, 295 String packageName, int versionCode) throws RemoteException { 296 int size = mActive.size(); 297 long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); 298 for (int i = 0; i < size; i++) { 299 ActiveBuffer buffer = mActive.get(i); 300 if (buffer.mPid == pid 301 && buffer.mUid == uid) { 302 // If the buffer is too old we remove it and return a new one 303 if (buffer.mInfo.startTime < today) { 304 buffer.binderDied(); 305 break; 306 } else { 307 return buffer; 308 } 309 } 310 } 311 // Didn't find one, need to create it 312 try { 313 ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode); 314 mActive.add(buffers); 315 return buffers; 316 } catch (IOException ex) { 317 throw new RemoteException("Failed to allocate space"); 318 } 319 } 320 dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers)321 private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) { 322 HashSet<File> skipFiles = new HashSet<>(buffers.size()); 323 for (int i = 0; i < buffers.size(); i++) { 324 HistoricalBuffer buffer = buffers.get(i); 325 File path = pathForApp(buffer.mInfo); 326 skipFiles.add(path); 327 nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName, 328 buffer.mInfo.versionCode, buffer.mInfo.startTime, buffer.mInfo.endTime, 329 buffer.mData); 330 } 331 return skipFiles; 332 } 333 dumpHistoricalLocked(long dump, HashSet<File> skipFiles)334 private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) { 335 for (File date : mGraphicsStatsDir.listFiles()) { 336 for (File pkg : date.listFiles()) { 337 for (File version : pkg.listFiles()) { 338 File data = new File(version, "total"); 339 if (skipFiles.contains(data)) { 340 continue; 341 } 342 nAddToDump(dump, data.getAbsolutePath()); 343 } 344 } 345 } 346 } 347 348 @Override dump(FileDescriptor fd, PrintWriter fout, String[] args)349 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 350 if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, fout)) return; 351 boolean dumpProto = false; 352 for (String str : args) { 353 if ("--proto".equals(str)) { 354 dumpProto = true; 355 break; 356 } 357 } 358 ArrayList<HistoricalBuffer> buffers; 359 synchronized (mLock) { 360 buffers = new ArrayList<>(mActive.size()); 361 for (int i = 0; i < mActive.size(); i++) { 362 try { 363 buffers.add(new HistoricalBuffer(mActive.get(i))); 364 } catch (IOException ex) { 365 // Ignore 366 } 367 } 368 } 369 long dump = nCreateDump(fd.getInt$(), dumpProto); 370 try { 371 synchronized (mFileAccessLock) { 372 HashSet<File> skipList = dumpActiveLocked(dump, buffers); 373 buffers.clear(); 374 dumpHistoricalLocked(dump, skipList); 375 } 376 } finally { 377 nFinishDump(dump); 378 } 379 } 380 nGetAshmemSize()381 private static native int nGetAshmemSize(); nCreateDump(int outFd, boolean isProto)382 private static native long nCreateDump(int outFd, boolean isProto); nAddToDump(long dump, String path, String packageName, int versionCode, long startTime, long endTime, byte[] data)383 private static native void nAddToDump(long dump, String path, String packageName, 384 int versionCode, long startTime, long endTime, byte[] data); nAddToDump(long dump, String path)385 private static native void nAddToDump(long dump, String path); nFinishDump(long dump)386 private static native void nFinishDump(long dump); nSaveBuffer(String path, String packageName, int versionCode, long startTime, long endTime, byte[] data)387 private static native void nSaveBuffer(String path, String packageName, int versionCode, 388 long startTime, long endTime, byte[] data); 389 390 private final class BufferInfo { 391 final String packageName; 392 final int versionCode; 393 long startTime; 394 long endTime; 395 BufferInfo(String packageName, int versionCode, long startTime)396 BufferInfo(String packageName, int versionCode, long startTime) { 397 this.packageName = packageName; 398 this.versionCode = versionCode; 399 this.startTime = startTime; 400 } 401 } 402 403 private final class ActiveBuffer implements DeathRecipient { 404 final BufferInfo mInfo; 405 final int mUid; 406 final int mPid; 407 final IGraphicsStatsCallback mCallback; 408 final IBinder mToken; 409 MemoryFile mProcessBuffer; 410 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)411 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode) 412 throws RemoteException, IOException { 413 mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis()); 414 mUid = uid; 415 mPid = pid; 416 mCallback = token; 417 mToken = mCallback.asBinder(); 418 mToken.linkToDeath(this, 0); 419 mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE); 420 mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE); 421 } 422 423 @Override binderDied()424 public void binderDied() { 425 mToken.unlinkToDeath(this, 0); 426 processDied(this); 427 } 428 closeAllBuffers()429 void closeAllBuffers() { 430 if (mProcessBuffer != null) { 431 mProcessBuffer.close(); 432 mProcessBuffer = null; 433 } 434 } 435 } 436 437 private final class HistoricalBuffer { 438 final BufferInfo mInfo; 439 final byte[] mData = new byte[ASHMEM_SIZE]; HistoricalBuffer(ActiveBuffer active)440 HistoricalBuffer(ActiveBuffer active) throws IOException { 441 mInfo = active.mInfo; 442 mInfo.endTime = System.currentTimeMillis(); 443 active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE); 444 } 445 } 446 } 447