Overhaul GraphicsStatsService
* LRU cache of recently-used is dead, replaced
disk storage
* ASHMEM size is read from native by the system service,
no longer requires keeping a sizeof() in sync with a
constant in Java
* Supports dumping in proto format by passing --proto
* Rotates logs on a daily basis
* Keeps a history of the most recent 3 days
Bug: 33705836
Test: Manual. Verified log rotating works by setting it up to
rotate every minute instead of day. Confirmed /data/system/graphicsstats
only has the most recent 3 entries after several minutes
Change-Id: Ib84bafb26c58701cc86f123236de4fff01aaa4aa
diff --git a/services/core/java/com/android/server/GraphicsStatsService.java b/services/core/java/com/android/server/GraphicsStatsService.java
index ecbe1ca..bdd80e38 100644
--- a/services/core/java/com/android/server/GraphicsStatsService.java
+++ b/services/core/java/com/android/server/GraphicsStatsService.java
@@ -16,67 +16,145 @@
package com.android.server;
+import android.app.AlarmManager;
import android.app.AppOpsManager;
import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.MemoryFile;
+import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.Trace;
import android.util.Log;
import android.view.IGraphicsStats;
-import android.view.ThreadedRenderer;
+import android.view.IGraphicsStatsCallback;
+import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.TimeZone;
/**
* This service's job is to collect aggregate rendering profile data. It
* does this by allowing rendering processes to request an ashmem buffer
- * to place their stats into. This buffer will be pre-initialized with historical
- * data for that process if it exists (if the userId & packageName match a buffer
- * in the historical log)
+ * to place their stats into.
*
- * This service does not itself attempt to understand the data in the buffer,
- * its primary job is merely to manage distributing these buffers. However,
- * it is assumed that this buffer is for ThreadedRenderer and delegates
- * directly to ThreadedRenderer for dumping buffers.
+ * Buffers are rotated on a daily (in UTC) basis and only the 3 most-recent days
+ * are kept.
*
- * MEMORY USAGE:
+ * The primary consumer of this is incident reports and automated metric checking. It is not
+ * intended for end-developer consumption, for that we have gfxinfo.
*
- * This class consumes UP TO:
- * 1) [active rendering processes] * (ASHMEM_SIZE * 2)
- * 2) ASHMEM_SIZE (for scratch space used during dumping)
- * 3) ASHMEM_SIZE * HISTORY_SIZE
- *
- * This is currently under 20KiB total memory in the worst case of
- * 20 processes in history + 10 unique active processes.
+ * Buffer rotation process:
+ * 1) Alarm fires
+ * 2) onRotateGraphicsStatsBuffer() is sent to all active processes
+ * 3) Upon receiving the callback, the process will stop using the previous ashmem buffer and
+ * request a new one.
+ * 4) When that request is received we now know that the ashmem region is no longer in use so
+ * it gets queued up for saving to disk and a new ashmem region is created and returned
+ * for the process to use.
*
* @hide */
public class GraphicsStatsService extends IGraphicsStats.Stub {
public static final String GRAPHICS_STATS_SERVICE = "graphicsstats";
private static final String TAG = "GraphicsStatsService";
- private static final int ASHMEM_SIZE = 464;
- private static final int HISTORY_SIZE = 20;
+
+ private static final int SAVE_BUFFER = 1;
+ private static final int DELETE_OLD = 2;
+
+ // This isn't static because we need this to happen after registerNativeMethods, however
+ // the class is loaded (and thus static ctor happens) before that occurs.
+ private final int ASHMEM_SIZE = nGetAshmemSize();
+ private final byte[] ZERO_DATA = new byte[ASHMEM_SIZE];
private final Context mContext;
private final AppOpsManager mAppOps;
+ private final AlarmManager mAlarmManager;
private final Object mLock = new Object();
private ArrayList<ActiveBuffer> mActive = new ArrayList<>();
- private HistoricalData[] mHistoricalLog = new HistoricalData[HISTORY_SIZE];
- private int mNextHistoricalSlot = 0;
- private byte[] mTempBuffer = new byte[ASHMEM_SIZE];
+ private File mGraphicsStatsDir;
+ private final Object mFileAccessLock = new Object();
+ private Handler mWriteOutHandler;
+ private boolean mRotateIsScheduled = false;
public GraphicsStatsService(Context context) {
mContext = context;
mAppOps = context.getSystemService(AppOpsManager.class);
+ mAlarmManager = context.getSystemService(AlarmManager.class);
+ File systemDataDir = new File(Environment.getDataDirectory(), "system");
+ mGraphicsStatsDir = new File(systemDataDir, "graphicsstats");
+ mGraphicsStatsDir.mkdirs();
+ if (!mGraphicsStatsDir.exists()) {
+ throw new IllegalStateException("Graphics stats directory does not exist: "
+ + mGraphicsStatsDir.getAbsolutePath());
+ }
+ HandlerThread bgthread = new HandlerThread("GraphicsStats-disk", Process.THREAD_PRIORITY_BACKGROUND);
+ bgthread.start();
+
+ mWriteOutHandler = new Handler(bgthread.getLooper(), new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case SAVE_BUFFER:
+ saveBuffer((HistoricalBuffer) msg.obj);
+ break;
+ case DELETE_OLD:
+ deleteOldBuffers();
+ break;
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Current rotation policy is to rotate at midnight UTC. We don't specify RTC_WAKEUP because
+ * rotation can be delayed if there's otherwise no activity. However exact is used because
+ * we don't want the system to delay it by TOO much.
+ */
+ private void scheduleRotateLocked() {
+ if (mRotateIsScheduled) {
+ return;
+ }
+ mRotateIsScheduled = true;
+ Calendar calendar = normalizeDate(System.currentTimeMillis());
+ calendar.add(Calendar.DATE, 1);
+ mAlarmManager.setExact(AlarmManager.RTC, calendar.getTimeInMillis(), TAG, this::onAlarm,
+ mWriteOutHandler);
+ }
+
+ private void onAlarm() {
+ synchronized (mLock) {
+ mRotateIsScheduled = false;
+ scheduleRotateLocked();
+ for (ActiveBuffer active : mActive) {
+ try {
+ active.mCallback.onRotateGraphicsStatsBuffer();
+ } catch (RemoteException e) {
+ Log.w(TAG, String.format("Failed to notify '%s' (pid=%d) to rotate buffers",
+ active.mInfo.packageName, active.mPid), e);
+ }
+ }
+ }
+ // Give a few seconds for everyone to rotate before doing the cleanup
+ mWriteOutHandler.sendEmptyMessageDelayed(DELETE_OLD, 10000);
}
@Override
- public ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token)
+ public ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback token)
throws RemoteException {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
@@ -84,9 +162,12 @@
long callingIdentity = Binder.clearCallingIdentity();
try {
mAppOps.checkPackage(uid, packageName);
+ PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
synchronized (mLock) {
- pfd = requestBufferForProcessLocked(token, uid, pid, packageName);
+ pfd = requestBufferForProcessLocked(token, uid, pid, packageName, info.versionCode);
}
+ } catch (PackageManager.NameNotFoundException ex) {
+ throw new RemoteException("Unable to find package: '" + packageName + "'");
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
@@ -95,51 +176,130 @@
private ParcelFileDescriptor getPfd(MemoryFile file) {
try {
+ if (!file.getFileDescriptor().valid()) {
+ throw new IllegalStateException("Invalid file descriptor");
+ }
return new ParcelFileDescriptor(file.getFileDescriptor());
} catch (IOException ex) {
throw new IllegalStateException("Failed to get PFD from memory file", ex);
}
}
- private ParcelFileDescriptor requestBufferForProcessLocked(IBinder token,
- int uid, int pid, String packageName) throws RemoteException {
- ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName);
+ private ParcelFileDescriptor requestBufferForProcessLocked(IGraphicsStatsCallback token,
+ int uid, int pid, String packageName, int versionCode) throws RemoteException {
+ ActiveBuffer buffer = fetchActiveBuffersLocked(token, uid, pid, packageName, versionCode);
+ scheduleRotateLocked();
return getPfd(buffer.mProcessBuffer);
}
+ private Calendar normalizeDate(long timestamp) {
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ calendar.setTimeInMillis(timestamp);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ return calendar;
+ }
+
+ private File pathForApp(BufferInfo info) {
+ String subPath = String.format("%d/%s/%d/total",
+ normalizeDate(info.startTime).getTimeInMillis(), info.packageName, info.versionCode);
+ return new File(mGraphicsStatsDir, subPath);
+ }
+
+ private void saveBuffer(HistoricalBuffer buffer) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "saving graphicsstats for " + buffer.mInfo.packageName);
+ }
+ synchronized (mFileAccessLock) {
+ File path = pathForApp(buffer.mInfo);
+ File parent = path.getParentFile();
+ parent.mkdirs();
+ if (!parent.exists()) {
+ Log.w(TAG, "Unable to create path: '" + parent.getAbsolutePath() + "'");
+ return;
+ }
+ nSaveBuffer(path.getAbsolutePath(), buffer.mInfo.packageName, buffer.mInfo.versionCode,
+ buffer.mInfo.startTime, buffer.mInfo.endTime, buffer.mData);
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+ }
+
+ private void deleteRecursiveLocked(File file) {
+ if (file.isDirectory()) {
+ for (File child : file.listFiles()) {
+ deleteRecursiveLocked(child);
+ }
+ }
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete '" + file.getAbsolutePath() + "'!");
+ }
+ }
+
+ private void deleteOldBuffers() {
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "deleting old graphicsstats buffers");
+ synchronized (mFileAccessLock) {
+ File[] files = mGraphicsStatsDir.listFiles();
+ if (files == null || files.length <= 3) {
+ return;
+ }
+ long[] sortedDates = new long[files.length];
+ for (int i = 0; i < files.length; i++) {
+ try {
+ sortedDates[i] = Long.parseLong(files[i].getName());
+ } catch (NumberFormatException ex) {
+ // Skip unrecognized folders
+ }
+ }
+ if (sortedDates.length <= 3) {
+ return;
+ }
+ Arrays.sort(sortedDates);
+ for (int i = 0; i < sortedDates.length - 3; i++) {
+ deleteRecursiveLocked(new File(mGraphicsStatsDir, Long.toString(sortedDates[i])));
+ }
+ }
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+ }
+
+ private void addToSaveQueue(ActiveBuffer buffer) {
+ try {
+ HistoricalBuffer data = new HistoricalBuffer(buffer);
+ Message.obtain(mWriteOutHandler, SAVE_BUFFER, data).sendToTarget();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to copy graphicsstats from " + buffer.mInfo.packageName, e);
+ }
+ buffer.closeAllBuffers();
+ }
+
private void processDied(ActiveBuffer buffer) {
synchronized (mLock) {
mActive.remove(buffer);
- Log.d("GraphicsStats", "Buffer count: " + mActive.size());
}
- HistoricalData data = buffer.mPreviousData;
- buffer.mPreviousData = null;
- if (data == null) {
- data = mHistoricalLog[mNextHistoricalSlot];
- if (data == null) {
- data = new HistoricalData();
- }
- }
- data.update(buffer.mPackageName, buffer.mUid, buffer.mProcessBuffer);
- buffer.closeAllBuffers();
-
- mHistoricalLog[mNextHistoricalSlot] = data;
- mNextHistoricalSlot = (mNextHistoricalSlot + 1) % mHistoricalLog.length;
+ addToSaveQueue(buffer);
}
- private ActiveBuffer fetchActiveBuffersLocked(IBinder token, int uid, int pid,
- String packageName) throws RemoteException {
+ private ActiveBuffer fetchActiveBuffersLocked(IGraphicsStatsCallback token, int uid, int pid,
+ String packageName, int versionCode) throws RemoteException {
int size = mActive.size();
+ long today = normalizeDate(System.currentTimeMillis()).getTimeInMillis();
for (int i = 0; i < size; i++) {
- ActiveBuffer buffers = mActive.get(i);
- if (buffers.mPid == pid
- && buffers.mUid == uid) {
- return buffers;
+ ActiveBuffer buffer = mActive.get(i);
+ if (buffer.mPid == pid
+ && buffer.mUid == uid) {
+ // If the buffer is too old we remove it and return a new one
+ if (buffer.mInfo.startTime < today) {
+ buffer.binderDied();
+ break;
+ } else {
+ return buffer;
+ }
}
}
// Didn't find one, need to create it
try {
- ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName);
+ ActiveBuffer buffers = new ActiveBuffer(token, uid, pid, packageName, versionCode);
mActive.add(buffers);
return buffers;
} catch (IOException ex) {
@@ -147,71 +307,106 @@
}
}
- private HistoricalData removeHistoricalDataLocked(int uid, String packageName) {
- for (int i = 0; i < mHistoricalLog.length; i++) {
- final HistoricalData data = mHistoricalLog[i];
- if (data != null && data.mUid == uid
- && data.mPackageName.equals(packageName)) {
- if (i == mNextHistoricalSlot) {
- mHistoricalLog[i] = null;
- } else {
- mHistoricalLog[i] = mHistoricalLog[mNextHistoricalSlot];
- mHistoricalLog[mNextHistoricalSlot] = null;
+ private HashSet<File> dumpActiveLocked(long dump, ArrayList<HistoricalBuffer> buffers) {
+ HashSet<File> skipFiles = new HashSet<>(buffers.size());
+ for (int i = 0; i < buffers.size(); i++) {
+ HistoricalBuffer buffer = buffers.get(i);
+ File path = pathForApp(buffer.mInfo);
+ skipFiles.add(path);
+ nAddToDump(dump, path.getAbsolutePath(), buffer.mInfo.packageName,
+ buffer.mInfo.versionCode, buffer.mInfo.startTime, buffer.mInfo.endTime,
+ buffer.mData);
+ }
+ return skipFiles;
+ }
+
+ private void dumpHistoricalLocked(long dump, HashSet<File> skipFiles) {
+ for (File date : mGraphicsStatsDir.listFiles()) {
+ for (File pkg : date.listFiles()) {
+ for (File version : pkg.listFiles()) {
+ File data = new File(version, "total");
+ if (skipFiles.contains(data)) {
+ continue;
+ }
+ nAddToDump(dump, data.getAbsolutePath());
}
- return data;
}
}
- return null;
}
@Override
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
+ boolean dumpProto = false;
+ for (String str : args) {
+ if ("--proto".equals(str)) {
+ dumpProto = true;
+ break;
+ }
+ }
+ ArrayList<HistoricalBuffer> buffers;
synchronized (mLock) {
+ buffers = new ArrayList<>(mActive.size());
for (int i = 0; i < mActive.size(); i++) {
- final ActiveBuffer buffer = mActive.get(i);
- fout.print("Package: ");
- fout.print(buffer.mPackageName);
- fout.flush();
try {
- buffer.mProcessBuffer.readBytes(mTempBuffer, 0, 0, ASHMEM_SIZE);
- ThreadedRenderer.dumpProfileData(mTempBuffer, fd);
- } catch (IOException e) {
- fout.println("Failed to dump");
+ buffers.add(new HistoricalBuffer(mActive.get(i)));
+ } catch (IOException ex) {
+ // Ignore
}
- fout.println();
}
- for (HistoricalData buffer : mHistoricalLog) {
- if (buffer == null) continue;
- fout.print("Package: ");
- fout.print(buffer.mPackageName);
- fout.flush();
- ThreadedRenderer.dumpProfileData(buffer.mBuffer, fd);
- fout.println();
+ }
+ long dump = nCreateDump(fd.getInt$(), dumpProto);
+ try {
+ synchronized (mFileAccessLock) {
+ HashSet<File> skipList = dumpActiveLocked(dump, buffers);
+ buffers.clear();
+ dumpHistoricalLocked(dump, skipList);
}
+ } finally {
+ nFinishDump(dump);
+ }
+ }
+
+ private static native int nGetAshmemSize();
+ private static native long nCreateDump(int outFd, boolean isProto);
+ private static native void nAddToDump(long dump, String path, String packageName,
+ int versionCode, long startTime, long endTime, byte[] data);
+ private static native void nAddToDump(long dump, String path);
+ private static native void nFinishDump(long dump);
+ private static native void nSaveBuffer(String path, String packageName, int versionCode,
+ long startTime, long endTime, byte[] data);
+
+ private final class BufferInfo {
+ final String packageName;
+ final int versionCode;
+ long startTime;
+ long endTime;
+
+ BufferInfo(String packageName, int versionCode, long startTime) {
+ this.packageName = packageName;
+ this.versionCode = versionCode;
+ this.startTime = startTime;
}
}
private final class ActiveBuffer implements DeathRecipient {
+ final BufferInfo mInfo;
final int mUid;
final int mPid;
- final String mPackageName;
+ final IGraphicsStatsCallback mCallback;
final IBinder mToken;
MemoryFile mProcessBuffer;
- HistoricalData mPreviousData;
- ActiveBuffer(IBinder token, int uid, int pid, String packageName)
+ ActiveBuffer(IGraphicsStatsCallback token, int uid, int pid, String packageName, int versionCode)
throws RemoteException, IOException {
+ mInfo = new BufferInfo(packageName, versionCode, System.currentTimeMillis());
mUid = uid;
mPid = pid;
- mPackageName = packageName;
- mToken = token;
+ mCallback = token;
+ mToken = mCallback.asBinder();
mToken.linkToDeath(this, 0);
- mProcessBuffer = new MemoryFile("GFXStats-" + uid, ASHMEM_SIZE);
- mPreviousData = removeHistoricalDataLocked(mUid, mPackageName);
- if (mPreviousData != null) {
- mProcessBuffer.writeBytes(mPreviousData.mBuffer, 0, 0, ASHMEM_SIZE);
- }
+ mProcessBuffer = new MemoryFile("GFXStats-" + pid, ASHMEM_SIZE);
+ mProcessBuffer.writeBytes(ZERO_DATA, 0, 0, ASHMEM_SIZE);
}
@Override
@@ -228,17 +423,13 @@
}
}
- private final static class HistoricalData {
- final byte[] mBuffer = new byte[ASHMEM_SIZE];
- int mUid;
- String mPackageName;
-
- void update(String packageName, int uid, MemoryFile file) {
- mUid = uid;
- mPackageName = packageName;
- try {
- file.readBytes(mBuffer, 0, 0, ASHMEM_SIZE);
- } catch (IOException e) {}
+ private final class HistoricalBuffer {
+ final BufferInfo mInfo;
+ final byte[] mData = new byte[ASHMEM_SIZE];
+ HistoricalBuffer(ActiveBuffer active) throws IOException {
+ mInfo = active.mInfo;
+ mInfo.endTime = System.currentTimeMillis();
+ active.mProcessBuffer.readBytes(mData, 0, 0, ASHMEM_SIZE);
}
}
}