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 android.graphics; 18 19 import android.annotation.SystemApi; 20 import android.app.AlarmManager; 21 import android.app.AppOpsManager; 22 import android.content.Context; 23 import android.content.pm.PackageInfo; 24 import android.content.pm.PackageManager; 25 import android.os.Binder; 26 import android.os.Environment; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.IBinder; 30 import android.os.Message; 31 import android.os.ParcelFileDescriptor; 32 import android.os.Process; 33 import android.os.RemoteException; 34 import android.os.SharedMemory; 35 import android.os.Trace; 36 import android.os.UserHandle; 37 import android.system.ErrnoException; 38 import android.util.Log; 39 import android.view.IGraphicsStats; 40 import android.view.IGraphicsStatsCallback; 41 42 import com.android.internal.util.DumpUtils; 43 import com.android.internal.util.FastPrintWriter; 44 45 import java.io.File; 46 import java.io.FileDescriptor; 47 import java.io.IOException; 48 import java.io.PrintWriter; 49 import java.io.StringWriter; 50 import java.nio.ByteBuffer; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.Calendar; 54 import java.util.HashSet; 55 import java.util.TimeZone; 56 57 /** 58 * This service's job is to collect aggregate rendering profile data. It 59 * does this by allowing rendering processes to request an ashmem buffer 60 * to place their stats into. 61 * 62 * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days 63 * are kept. 64 * 65 * The primary consumer of this is incident reports and automated metric checking. It is not 66 * intended for end-developer consumption, for that we have gfxinfo. 67 * 68 * Buffer rotation process: 69 * 1) Alarm fires 70 * 2) onRotateGraphicsStatsBuffer() is sent to all active processes 71 * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and 72 * request a new one. 73 * 4) When that request is received we now know that the ashmem region is no longer in use so 74 * it gets queued up for saving to disk and a new ashmem region is created and returned 75 * for the process to use. 76 * 77 * @hide */ 78 public class GraphicsStatsService extends IGraphicsStats.Stub { 79 public static final String GRAPHICS_STATS_SERVICE = "graphicsstats"; 80 81 private static final String TAG = "GraphicsStatsService"; 82 83 private static final int SAVE_BUFFER = 1; 84 private static final int DELETE_OLD = 2; 85 86 private static final int AID_STATSD = 1066; // Statsd uid is set to 1066 forever. 87 88 // This isn't static because we need this to happen after registerNativeMethods, however 89 // the class is loaded (and thus static ctor happens) before that occurs. 90 private final int mAshmemSize = nGetAshmemSize(); 91 private final byte[] mZeroData = new byte[mAshmemSize]; 92 93 private final Context mContext; 94 private final AppOpsManager mAppOps; 95 private final AlarmManager mAlarmManager; 96 private final Object mLock = new Object(); 97 private ArrayList<ActiveBuffer> mActive = new ArrayList<>(); 98 private File mGraphicsStatsDir; 99 private final Object mFileAccessLock = new Object(); 100 private Handler mWriteOutHandler; 101 private boolean mRotateIsScheduled = false; 102 103 @SystemApi GraphicsStatsService(Context context)104 public GraphicsStatsService(Context context) { 105 mContext = context; 106 mAppOps = context.getSystemService(AppOpsManager.class); 107 mAlarmManager = context.getSystemService(AlarmManager.class); 108 File systemDataDir = new File(Environment.getDataDirectory(), "system"); 109 mGraphicsStatsDir = new File(systemDataDir, "graphicsstats"); 110 mGraphicsStatsDir.mkdirs(); 111 if (!mGraphicsStatsDir.exists()) { 112 throw new IllegalStateException("Graphics stats directory does not exist: " 113 + mGraphicsStatsDir.getAbsolutePath()); 114 } 115 HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", 116 Process.THREAD_PRIORITY_BACKGROUND); 117 bgthread.start(); 118 119 mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() { 120 @Override 121 public boolean handleMessage(Message msg) { 122 switch (msg.what) { 123 case SAVE_BUFFER: 124 saveBuffer((HistoricalBuffer) msg.obj); 125 break; 126 case DELETE_OLD: 127 deleteOldBuffers(); 128 break; 129 } 130 return true; 131 } 132 }); 133 nativeInit(); 134 } 135 136 /** 137 * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because 138 * rotation can be delayed if there's otherwise no activity. However exact is used because 139 * we don't want the system to delay it by TOO much. 140 */ scheduleRotateLocked()141 private void scheduleRotateLocked() { 142 if (mRotateIsScheduled) { 143 return; 144 } 145 mRotateIsScheduled = true; 146 Calendar calendar = normalizeDate(System.currentTimeMillis()); 147 calendar.add(Calendar.DATE, 1); 148 mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm, 149 mWriteOutHandler); 150 } 151 onAlarm()152 private void onAlarm() { 153 // We need to make a copy since some of the callbacks won't be proxy and thus 154 // can result in a re-entrant acquisition of mLock that would result in a modification 155 // of mActive during iteration. 156 ActiveBuffer[] activeCopy; 157 synchronized (mLock) { 158 mRotateIsScheduled = false; 159 scheduleRotateLocked(); 160 activeCopy = mActive.toArray(new ActiveBuffer[0]); 161 } 162 for (ActiveBuffer active : activeCopy) { 163 try { 164 active.mCallback.onRotateGraphicsStatsBuffer(); 165 } catch (RemoteException e) { 166 Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers", 167 active.mInfo.mPackageName, active.mPid), e); 168 } 169 } 170 // Give a few seconds for everyone to rotate before doing the cleanup 171 mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000); 172 } 173 174 @Override requestBufferForProcess(String packageName, IGraphicsStatsCallback token)175 public ParcelFileDescriptor requestBufferForProcess(String packageName, 176 IGraphicsStatsCallback token) throws RemoteException { 177 int uid = Binder.getCallingUid(); 178 int pid = Binder.getCallingPid(); 179 ParcelFileDescriptor pfd = null; 180 long callingIdentity = Binder.clearCallingIdentity(); 181 try { 182 mAppOps.checkPackage(uid, packageName); 183 PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser( 184 packageName, 185 0, 186 UserHandle.getUserId(uid)); 187 synchronized (mLock) { 188 pfd = requestBufferForProcessLocked(token, uid, pid, packageName, 189 info.getLongVersionCode()); 190 } 191 } catch (PackageManager.NameNotFoundException ex) { 192 throw new RemoteException("Unable to find package: '" + packageName + "'"); 193 } finally { 194 Binder.restoreCallingIdentity(callingIdentity); 195 } 196 return pfd; 197 } 198 199 // If lastFullDay is true, pullGraphicsStats returns stats for the last complete day/24h period 200 // that does not include today. If lastFullDay is false, pullGraphicsStats returns stats for the 201 // current day. 202 // This method is invoked from native code only. 203 @SuppressWarnings({"UnusedDeclaration"}) pullGraphicsStats(boolean lastFullDay, long pulledData)204 private void pullGraphicsStats(boolean lastFullDay, long pulledData) throws RemoteException { 205 int uid = Binder.getCallingUid(); 206 207 // DUMP and PACKAGE_USAGE_STATS permissions are required to invoke this method. 208 // TODO: remove exception for statsd daemon after required permissions are granted. statsd 209 // TODO: should have these permissions granted by data/etc/platform.xml, but it does not. 210 if (uid != AID_STATSD) { 211 StringWriter sw = new StringWriter(); 212 PrintWriter pw = new FastPrintWriter(sw); 213 if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) { 214 pw.flush(); 215 throw new RemoteException(sw.toString()); 216 } 217 } 218 219 long callingIdentity = Binder.clearCallingIdentity(); 220 try { 221 pullGraphicsStatsImpl(lastFullDay, pulledData); 222 } finally { 223 Binder.restoreCallingIdentity(callingIdentity); 224 } 225 } 226 pullGraphicsStatsImpl(boolean lastFullDay, long pulledData)227 private void pullGraphicsStatsImpl(boolean lastFullDay, long pulledData) { 228 long targetDay; 229 if (lastFullDay) { 230 // Get stats from yesterday. Stats stay constant, because the day is over. 231 targetDay = normalizeDate(System.currentTimeMillis() - 86400000).getTimeInMillis(); 232 } else { 233 // Get stats from today. Stats may change as more apps are run today. 234 targetDay = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); 235 } 236 237 // Find active buffers for targetDay. 238 ArrayList<HistoricalBuffer> buffers; 239 synchronized (mLock) { 240 buffers = new ArrayList<>(mActive.size()); 241 for (int i = 0; i < mActive.size(); i++) { 242 ActiveBuffer buffer = mActive.get(i); 243 if (buffer.mInfo.mStartTime == targetDay) { 244 try { 245 buffers.add(new HistoricalBuffer(buffer)); 246 } catch (IOException ex) { 247 // Ignore 248 } 249 } 250 } 251 } 252 253 // Dump active and historic buffers for targetDay in a serialized 254 // GraphicsStatsServiceDumpProto proto. 255 long dump = nCreateDump(-1, true); 256 try { 257 synchronized (mFileAccessLock) { 258 HashSet<File> skipList = dumpActiveLocked(dump, buffers); 259 buffers.clear(); 260 String subPath = String.format("%d", targetDay); 261 File dateDir = new File(mGraphicsStatsDir, subPath); 262 if (dateDir.exists()) { 263 for (File pkg : dateDir.listFiles()) { 264 for (File version : pkg.listFiles()) { 265 File data = new File(version, "total"); 266 if (skipList.contains(data)) { 267 continue; 268 } 269 nAddToDump(dump, data.getAbsolutePath()); 270 } 271 } 272 } 273 } 274 } finally { 275 nFinishDumpInMemory(dump, pulledData, lastFullDay); 276 } 277 } 278 requestBufferForProcessLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)279 private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token, 280 int uid, int pid, String packageName, long versionCode) throws RemoteException { 281 ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode); 282 scheduleRotateLocked(); 283 return buffer.getPfd(); 284 } 285 normalizeDate(long timestamp)286 private Calendar normalizeDate(long timestamp) { 287 Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); 288 calendar.setTimeInMillis(timestamp); 289 calendar.set(Calendar.HOUR_OF_DAY, 0); 290 calendar.set(Calendar.MINUTE, 0); 291 calendar.set(Calendar.SECOND, 0); 292 calendar.set(Calendar.MILLISECOND, 0); 293 return calendar; 294 } 295 pathForApp(BufferInfo info)296 private File pathForApp(BufferInfo info) { 297 String subPath = String.format("%d/%s/%d/total", 298 normalizeDate(info.mStartTime).getTimeInMillis(), info.mPackageName, 299 info.mVersionCode); 300 return new File(mGraphicsStatsDir, subPath); 301 } 302 saveBuffer(HistoricalBuffer buffer)303 private void saveBuffer(HistoricalBuffer buffer) { 304 if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { 305 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, 306 "saving graphicsstats for " + buffer.mInfo.mPackageName); 307 } 308 synchronized (mFileAccessLock) { 309 File path = pathForApp(buffer.mInfo); 310 File parent = path.getParentFile(); 311 parent.mkdirs(); 312 if (!parent.exists()) { 313 Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'"); 314 return; 315 } 316 nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.mPackageName, 317 buffer.mInfo.mVersionCode, buffer.mInfo.mStartTime, buffer.mInfo.mEndTime, 318 buffer.mData); 319 } 320 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 321 } 322 deleteRecursiveLocked(File file)323 private void deleteRecursiveLocked(File file) { 324 if (file.isDirectory()) { 325 for (File child : file.listFiles()) { 326 deleteRecursiveLocked(child); 327 } 328 } 329 if (!file.delete()) { 330 Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!"); 331 } 332 } 333 deleteOldBuffers()334 private void deleteOldBuffers() { 335 Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers"); 336 synchronized (mFileAccessLock) { 337 File[] files = mGraphicsStatsDir.listFiles(); 338 if (files == null || files.length <= 3) { 339 return; 340 } 341 long[] sortedDates = new long[files.length]; 342 for (int i = 0; i < files.length; i++) { 343 try { 344 sortedDates[i] = Long.parseLong(files[i].getName()); 345 } catch (NumberFormatException ex) { 346 // Skip unrecognized folders 347 } 348 } 349 if (sortedDates.length <= 3) { 350 return; 351 } 352 Arrays.sort(sortedDates); 353 for (int i = 0; i < sortedDates.length - 3; i++) { 354 deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i]))); 355 } 356 } 357 Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); 358 } 359 addToSaveQueue(ActiveBuffer buffer)360 private void addToSaveQueue(ActiveBuffer buffer) { 361 try { 362 HistoricalBuffer data = new HistoricalBuffer(buffer); 363 Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget(); 364 } catch (IOException e) { 365 Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.mPackageName, e); 366 } 367 buffer.closeAllBuffers(); 368 } 369 processDied(ActiveBuffer buffer)370 private void processDied(ActiveBuffer buffer) { 371 synchronized (mLock) { 372 mActive.remove(buffer); 373 } 374 addToSaveQueue(buffer); 375 } 376 fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)377 private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid, 378 String packageName, long versionCode) throws RemoteException { 379 int size = mActive.size(); 380 long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis(); 381 for (int i = 0; i < size; i++) { 382 ActiveBuffer buffer = mActive.get(i); 383 if (buffer.mPid == pid 384 && buffer.mUid == uid) { 385 // If the buffer is too old we remove it and return a new one 386 if (buffer.mInfo.mStartTime < today) { 387 buffer.binderDied(); 388 break; 389 } else { 390 return buffer; 391 } 392 } 393 } 394 // Didn't find one, need to create it 395 try { 396 ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode); 397 mActive.add(buffers); 398 return buffers; 399 } catch (IOException ex) { 400 throw new RemoteException("Failed to allocate space"); 401 } 402 } 403 dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers)404 private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) { 405 HashSet<File> skipFiles = new HashSet<>(buffers.size()); 406 for (int i = 0; i < buffers.size(); i++) { 407 HistoricalBuffer buffer = buffers.get(i); 408 File path = pathForApp(buffer.mInfo); 409 skipFiles.add(path); 410 nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.mPackageName, 411 buffer.mInfo.mVersionCode, buffer.mInfo.mStartTime, buffer.mInfo.mEndTime, 412 buffer.mData); 413 } 414 return skipFiles; 415 } 416 dumpHistoricalLocked(long dump, HashSet<File> skipFiles)417 private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) { 418 for (File date : mGraphicsStatsDir.listFiles()) { 419 for (File pkg : date.listFiles()) { 420 for (File version : pkg.listFiles()) { 421 File data = new File(version, "total"); 422 if (skipFiles.contains(data)) { 423 continue; 424 } 425 nAddToDump(dump, data.getAbsolutePath()); 426 } 427 } 428 } 429 } 430 431 @Override dump(FileDescriptor fd, PrintWriter fout, String[] args)432 protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { 433 if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, fout)) return; 434 boolean dumpProto = false; 435 for (String str : args) { 436 if ("--proto".equals(str)) { 437 dumpProto = true; 438 break; 439 } 440 } 441 ArrayList<HistoricalBuffer> buffers; 442 synchronized (mLock) { 443 buffers = new ArrayList<>(mActive.size()); 444 for (int i = 0; i < mActive.size(); i++) { 445 try { 446 buffers.add(new HistoricalBuffer(mActive.get(i))); 447 } catch (IOException ex) { 448 // Ignore 449 } 450 } 451 } 452 long dump = nCreateDump(fd.getInt$(), dumpProto); 453 try { 454 synchronized (mFileAccessLock) { 455 HashSet<File> skipList = dumpActiveLocked(dump, buffers); 456 buffers.clear(); 457 dumpHistoricalLocked(dump, skipList); 458 } 459 } finally { 460 nFinishDump(dump); 461 } 462 } 463 464 @Override finalize()465 protected void finalize() throws Throwable { 466 nativeDestructor(); 467 } 468 nativeInit()469 private native void nativeInit(); nativeDestructor()470 private static native void nativeDestructor(); 471 nGetAshmemSize()472 private static native int nGetAshmemSize(); nCreateDump(int outFd, boolean isProto)473 private static native long nCreateDump(int outFd, boolean isProto); nAddToDump(long dump, String path, String packageName, long versionCode, long startTime, long endTime, byte[] data)474 private static native void nAddToDump(long dump, String path, String packageName, 475 long versionCode, long startTime, long endTime, byte[] data); nAddToDump(long dump, String path)476 private static native void nAddToDump(long dump, String path); nFinishDump(long dump)477 private static native void nFinishDump(long dump); nFinishDumpInMemory(long dump, long pulledData, boolean lastFullDay)478 private static native void nFinishDumpInMemory(long dump, long pulledData, boolean lastFullDay); nSaveBuffer(String path, String packageName, long versionCode, long startTime, long endTime, byte[] data)479 private static native void nSaveBuffer(String path, String packageName, long versionCode, 480 long startTime, long endTime, byte[] data); 481 482 private final class BufferInfo { 483 final String mPackageName; 484 final long mVersionCode; 485 long mStartTime; 486 long mEndTime; 487 BufferInfo(String packageName, long versionCode, long startTime)488 BufferInfo(String packageName, long versionCode, long startTime) { 489 this.mPackageName = packageName; 490 this.mVersionCode = versionCode; 491 this.mStartTime = startTime; 492 } 493 } 494 495 private final class ActiveBuffer implements DeathRecipient { 496 final BufferInfo mInfo; 497 final int mUid; 498 final int mPid; 499 final IGraphicsStatsCallback mCallback; 500 final IBinder mToken; 501 SharedMemory mProcessBuffer; 502 ByteBuffer mMapping; 503 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, long versionCode)504 ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, 505 long versionCode) 506 throws RemoteException, IOException { 507 mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis()); 508 mUid = uid; 509 mPid = pid; 510 mCallback = token; 511 mToken = mCallback.asBinder(); 512 mToken.linkToDeath(this, 0); 513 try { 514 mProcessBuffer = SharedMemory.create("GFXStats-" + pid, mAshmemSize); 515 mMapping = mProcessBuffer.mapReadWrite(); 516 } catch (ErrnoException ex) { 517 ex.rethrowAsIOException(); 518 } 519 mMapping.position(0); 520 mMapping.put(mZeroData, 0, mAshmemSize); 521 } 522 523 @Override binderDied()524 public void binderDied() { 525 mToken.unlinkToDeath(this, 0); 526 processDied(this); 527 } 528 closeAllBuffers()529 void closeAllBuffers() { 530 if (mMapping != null) { 531 SharedMemory.unmap(mMapping); 532 mMapping = null; 533 } 534 if (mProcessBuffer != null) { 535 mProcessBuffer.close(); 536 mProcessBuffer = null; 537 } 538 } 539 getPfd()540 ParcelFileDescriptor getPfd() { 541 try { 542 return mProcessBuffer.getFdDup(); 543 } catch (IOException ex) { 544 throw new IllegalStateException("Failed to get PFD from memory file", ex); 545 } 546 } 547 readBytes(byte[] buffer, int count)548 void readBytes(byte[] buffer, int count) throws IOException { 549 if (mMapping == null) { 550 throw new IOException("SharedMemory has been deactivated"); 551 } 552 mMapping.position(0); 553 mMapping.get(buffer, 0, count); 554 } 555 } 556 557 private final class HistoricalBuffer { 558 final BufferInfo mInfo; 559 final byte[] mData = new byte[mAshmemSize]; HistoricalBuffer(ActiveBuffer active)560 HistoricalBuffer(ActiveBuffer active) throws IOException { 561 mInfo = active.mInfo; 562 mInfo.mEndTime = System.currentTimeMillis(); 563 active.readBytes(mData, mAshmemSize); 564 } 565 } 566 } 567