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, 179 info.getLongVersionCode()); 180 } 181 } catch (PackageManager.NameNotFoundException ex) { 182 throw new RemoteException("Unable to find package: '" + packageName + "'"); 183 } finally { 184 Binder.restoreCallingIdentity(callingIdentity); 185 } 186 return pfd; 187 } 188 getPfd(MemoryFile file)189 private ParcelFileDescriptor getPfd(MemoryFile file) { 190 try { 191 if (!file.getFileDescriptor().valid()) { 192 throw new IllegalStateException("Invalid file descriptor"); 193 } 194 return new ParcelFileDescriptor(file.getFileDescriptor()); 195 } catch (IOException ex) { 196 throw new IllegalStateException("Failed to get PFD from memory file", ex); 197 } 198 } 199 requestBufferForProcessLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)200 private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token, 201 int uid, int pid, String packageName, long versionCode) throws RemoteException { 202 ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode); 203 scheduleRotateLocked(); 204 return getPfd(buffer.mProcessBuffer); 205 } 206 normalizeDate(long timestamp)207 private Calendar normalizeDate(long timestamp) { 208 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 209 calendar.setTimeInMillis(timestamp); 210 calendar.set(Calendar.HOUR_OF_DAY, 0); 211 calendar.set(Calendar.MINUTE, 0); 212 calendar.set(Calendar.SECOND, 0); 213 calendar.set(Calendar.MILLISECOND, 0); 214 return calendar; 215 } 216 pathForApp(BufferInfo info)217 private File pathForApp(BufferInfo info) { 218 String subPath = String.format("%d/%s/%d/total", 219 normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode); 220 return new File(mGraphicsStatsDir, subPath); 221 } 222 saveBuffer(HistoricalBuffer buffer)223 private void saveBuffer(HistoricalBuffer buffer) { 224 if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { 225 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName); 226 } 227 synchronized (mFileAccessLock) { 228 File path = pathForApp(buffer.mInfo); 229 File parent = path.getParentFile(); 230 parent.mkdirs(); 231 if (!parent.exists()) { 232 Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'"); 233 return; 234 } 235 nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode, 236 buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData); 237 } 238 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 239 } 240 deleteRecursiveLocked(File file)241 private void deleteRecursiveLocked(File file) { 242 if (file.isDirectory()) { 243 for (File child : file.listFiles()) { 244 deleteRecursiveLocked(child); 245 } 246 } 247 if (!file.delete()) { 248 Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!"); 249 } 250 } 251 deleteOldBuffers()252 private void deleteOldBuffers() { 253 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers"); 254 synchronized (mFileAccessLock) { 255 File[] files = mGraphicsStatsDir.listFiles(); 256 if (files == null || files.length <= 3) { 257 return; 258 } 259 long[] sortedDates = new long[files.length]; 260 for (int i = 0; i < files.length; i++) { 261 try { 262 sortedDates[i] = Long.parseLong(files[i].getName()); 263 } catch (NumberFormatException ex) { 264 // Skip unrecognized folders 265 } 266 } 267 if (sortedDates.length <= 3) { 268 return; 269 } 270 Arrays.sort(sortedDates); 271 for (int i = 0; i < sortedDates.length - 3; i++) { 272 deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i]))); 273 } 274 } 275 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 276 } 277 addToSaveQueue(ActiveBuffer buffer)278 private void addToSaveQueue(ActiveBuffer buffer) { 279 try { 280 HistoricalBuffer data = new HistoricalBuffer(buffer); 281 Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget(); 282 } catch (IOException e) { 283 Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e); 284 } 285 buffer.closeAllBuffers(); 286 } 287 processDied(ActiveBuffer buffer)288 private void processDied(ActiveBuffer buffer) { 289 synchronized (mLock) { 290 mActive.remove(buffer); 291 } 292 addToSaveQueue(buffer); 293 } 294 fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)295 private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, 296 String packageName, long versionCode) throws RemoteException { 297 int size = mActive.size(); 298 long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); 299 for (int i = 0; i < size; i++) { 300 ActiveBuffer buffer = mActive.get(i); 301 if (buffer.mPid == pid 302 && buffer.mUid == uid) { 303 // If the buffer is too old we remove it and return a new one 304 if (buffer.mInfo.startTime < today) { 305 buffer.binderDied(); 306 break; 307 } else { 308 return buffer; 309 } 310 } 311 } 312 // Didn't find one, need to create it 313 try { 314 ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode); 315 mActive.add(buffers); 316 return buffers; 317 } catch (IOException ex) { 318 throw new RemoteException("Failed to allocate space"); 319 } 320 } 321 dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers)322 private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) { 323 HashSet<File> skipFiles = new HashSet<>(buffers.size()); 324 for (int i = 0; i < buffers.size(); i++) { 325 HistoricalBuffer buffer = buffers.get(i); 326 File path = pathForApp(buffer.mInfo); 327 skipFiles.add(path); 328 nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName, 329 buffer.mInfo.versionCode, buffer.mInfo.startTime, buffer.mInfo.endTime, 330 buffer.mData); 331 } 332 return skipFiles; 333 } 334 dumpHistoricalLocked(long dump, HashSet<File> skipFiles)335 private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) { 336 for (File date : mGraphicsStatsDir.listFiles()) { 337 for (File pkg : date.listFiles()) { 338 for (File version : pkg.listFiles()) { 339 File data = new File(version, "total"); 340 if (skipFiles.contains(data)) { 341 continue; 342 } 343 nAddToDump(dump, data.getAbsolutePath()); 344 } 345 } 346 } 347 } 348 349 @Override dump(FileDescriptor fd, PrintWriter fout, String[] args)350 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 351 if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, fout)) return; 352 boolean dumpProto = false; 353 for (String str : args) { 354 if ("--proto".equals(str)) { 355 dumpProto = true; 356 break; 357 } 358 } 359 ArrayList<HistoricalBuffer> buffers; 360 synchronized (mLock) { 361 buffers = new ArrayList<>(mActive.size()); 362 for (int i = 0; i < mActive.size(); i++) { 363 try { 364 buffers.add(new HistoricalBuffer(mActive.get(i))); 365 } catch (IOException ex) { 366 // Ignore 367 } 368 } 369 } 370 long dump = nCreateDump(fd.getInt$(), dumpProto); 371 try { 372 synchronized (mFileAccessLock) { 373 HashSet<File> skipList = dumpActiveLocked(dump, buffers); 374 buffers.clear(); 375 dumpHistoricalLocked(dump, skipList); 376 } 377 } finally { 378 nFinishDump(dump); 379 } 380 } 381 nGetAshmemSize()382 private static native int nGetAshmemSize(); nCreateDump(int outFd, boolean isProto)383 private static native long nCreateDump(int outFd, boolean isProto); nAddToDump(long dump, String path, String packageName, long versionCode, long startTime, long endTime, byte[] data)384 private static native void nAddToDump(long dump, String path, String packageName, 385 long versionCode, long startTime, long endTime, byte[] data); nAddToDump(long dump, String path)386 private static native void nAddToDump(long dump, String path); nFinishDump(long dump)387 private static native void nFinishDump(long dump); nSaveBuffer(String path, String packageName, long versionCode, long startTime, long endTime, byte[] data)388 private static native void nSaveBuffer(String path, String packageName, long versionCode, 389 long startTime, long endTime, byte[] data); 390 391 private final class BufferInfo { 392 final String packageName; 393 final long versionCode; 394 long startTime; 395 long endTime; 396 BufferInfo(String packageName, long versionCode, long startTime)397 BufferInfo(String packageName, long versionCode, long startTime) { 398 this.packageName = packageName; 399 this.versionCode = versionCode; 400 this.startTime = startTime; 401 } 402 } 403 404 private final class ActiveBuffer implements DeathRecipient { 405 final BufferInfo mInfo; 406 final int mUid; 407 final int mPid; 408 final IGraphicsStatsCallback mCallback; 409 final IBinder mToken; 410 MemoryFile mProcessBuffer; 411 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)412 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, 413 long versionCode) 414 throws RemoteException, IOException { 415 mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis()); 416 mUid = uid; 417 mPid = pid; 418 mCallback = token; 419 mToken = mCallback.asBinder(); 420 mToken.linkToDeath(this, 0); 421 mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE); 422 mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE); 423 } 424 425 @Override binderDied()426 public void binderDied() { 427 mToken.unlinkToDeath(this, 0); 428 processDied(this); 429 } 430 closeAllBuffers()431 void closeAllBuffers() { 432 if (mProcessBuffer != null) { 433 mProcessBuffer.close(); 434 mProcessBuffer = null; 435 } 436 } 437 } 438 439 private final class HistoricalBuffer { 440 final BufferInfo mInfo; 441 final byte[] mData = new byte[ASHMEM_SIZE]; HistoricalBuffer(ActiveBuffer active)442 HistoricalBuffer(ActiveBuffer active) throws IOException { 443 mInfo = active.mInfo; 444 mInfo.endTime = System.currentTimeMillis(); 445 active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE); 446 } 447 } 448 } 449