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