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