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/Android.mk b/Android.mk
index b26543d..e645ac1 100644
--- a/Android.mk
+++ b/Android.mk
@@ -322,6 +322,7 @@
core/java/android/view/IAppTransitionAnimationSpecsFuture.aidl \
core/java/android/view/IDockedStackListener.aidl \
core/java/android/view/IGraphicsStats.aidl \
+ core/java/android/view/IGraphicsStatsCallback.aidl \
core/java/android/view/IInputFilter.aidl \
core/java/android/view/IInputFilterHost.aidl \
core/java/android/view/IOnKeyguardExitResult.aidl \
@@ -1421,6 +1422,24 @@
include $(BUILD_JAVA_LIBRARY)
+# ==== c++ proto device library ==============================
+include $(CLEAR_VARS)
+LOCAL_MODULE := libplatformprotos
+# b/34740546, work around clang-tidy segmentation fault.
+LOCAL_TIDY_CHECKS := -modernize*
+LOCAL_PROTOC_OPTIMIZE_TYPE := lite
+LOCAL_PROTOC_FLAGS := \
+ --include_source_info \
+ -Iexternal/protobuf/src
+LOCAL_SRC_FILES := \
+ $(call all-proto-files-under, core/proto) \
+ $(call all-proto-files-under, libs/incident/proto)
+LOCAL_C_INCLUDES := \
+ $(call generated-sources-dir-for,STATIC_LIBRARIES,libplatformprotos,)/proto
+LOCAL_EXPORT_C_INCLUDES := \
+ $(call generated-sources-dir-for,STATIC_LIBRARIES,libplatformprotos,)/proto
+include $(BUILD_STATIC_LIBRARY)
+
# ==== c++ proto host library ==============================
include $(CLEAR_VARS)
LOCAL_MODULE := libplatformprotos
diff --git a/core/java/android/view/IGraphicsStats.aidl b/core/java/android/view/IGraphicsStats.aidl
index c235eb2..e6b750b 100644
--- a/core/java/android/view/IGraphicsStats.aidl
+++ b/core/java/android/view/IGraphicsStats.aidl
@@ -17,10 +17,11 @@
package android.view;
import android.os.ParcelFileDescriptor;
+import android.view.IGraphicsStatsCallback;
/**
* @hide
*/
interface IGraphicsStats {
- ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token);
+ ParcelFileDescriptor requestBufferForProcess(String packageName, IGraphicsStatsCallback callback);
}
diff --git a/core/java/android/view/IGraphicsStatsCallback.aidl b/core/java/android/view/IGraphicsStatsCallback.aidl
new file mode 100644
index 0000000..f70e141
--- /dev/null
+++ b/core/java/android/view/IGraphicsStatsCallback.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+/**
+ * @hide
+ */
+oneway interface IGraphicsStatsCallback {
+ void onRotateGraphicsStatsBuffer();
+}
diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java
index 4ceb236..4bb7968 100644
--- a/core/java/android/view/ThreadedRenderer.java
+++ b/core/java/android/view/ThreadedRenderer.java
@@ -25,9 +25,9 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.AnimatedVectorDrawable;
-import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.Trace;
import android.util.Log;
@@ -248,10 +248,10 @@
*
* @return A threaded renderer backed by OpenGL.
*/
- public static ThreadedRenderer create(Context context, boolean translucent) {
+ public static ThreadedRenderer create(Context context, boolean translucent, String name) {
ThreadedRenderer renderer = null;
if (isAvailable()) {
- renderer = new ThreadedRenderer(context, translucent);
+ renderer = new ThreadedRenderer(context, translucent, name);
}
return renderer;
}
@@ -275,10 +275,6 @@
nOverrideProperty(name, value);
}
- public static void dumpProfileData(byte[] data, FileDescriptor fd) {
- nDumpProfileData(data, fd);
- }
-
// Keep in sync with DrawFrameTask.h SYNC_* flags
// Nothing interesting to report
private static final int SYNC_OK = 0;
@@ -334,7 +330,7 @@
private boolean mEnabled;
private boolean mRequested = true;
- ThreadedRenderer(Context context, boolean translucent) {
+ ThreadedRenderer(Context context, boolean translucent, String name) {
final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
mLightY = a.getDimension(R.styleable.Lighting_lightY, 0);
mLightZ = a.getDimension(R.styleable.Lighting_lightZ, 0);
@@ -348,6 +344,7 @@
mRootNode = RenderNode.adopt(rootNodePtr);
mRootNode.setClipToBounds(false);
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
+ nSetName(mNativeProxy, name);
ProcessInitializer.sInstance.init(context, mNativeProxy);
@@ -815,15 +812,6 @@
}
/**
- * Optional, sets the name of the renderer. Useful for debugging purposes.
- *
- * @param name The name of this renderer, can be null
- */
- void setName(String name) {
- nSetName(mNativeProxy, name);
- }
-
- /**
* Blocks until all previously queued work has completed.
*/
void fence() {
@@ -884,20 +872,29 @@
private static class ProcessInitializer {
static ProcessInitializer sInstance = new ProcessInitializer();
- private static IBinder sProcToken;
private boolean mInitialized = false;
+ private Context mAppContext;
+ private IGraphicsStats mGraphicsStatsService;
+ private IGraphicsStatsCallback mGraphicsStatsCallback = new IGraphicsStatsCallback.Stub() {
+ @Override
+ public void onRotateGraphicsStatsBuffer() throws RemoteException {
+ rotateBuffer();
+ }
+ };
+
private ProcessInitializer() {}
synchronized void init(Context context, long renderProxy) {
if (mInitialized) return;
mInitialized = true;
+ mAppContext = context.getApplicationContext();
initSched(context, renderProxy);
- initGraphicsStats(context, renderProxy);
+ initGraphicsStats();
}
- private static void initSched(Context context, long renderProxy) {
+ private void initSched(Context context, long renderProxy) {
try {
int tid = nGetRenderThreadTid(renderProxy);
ActivityManager.getService().setRenderThread(tid);
@@ -906,17 +903,28 @@
}
}
- private static void initGraphicsStats(Context context, long renderProxy) {
+ private void initGraphicsStats() {
try {
IBinder binder = ServiceManager.getService("graphicsstats");
if (binder == null) return;
- IGraphicsStats graphicsStatsService = IGraphicsStats.Stub
- .asInterface(binder);
- sProcToken = new Binder();
- final String pkg = context.getApplicationInfo().packageName;
- ParcelFileDescriptor pfd = graphicsStatsService.
- requestBufferForProcess(pkg, sProcToken);
- nSetProcessStatsBuffer(renderProxy, pfd.getFd());
+ mGraphicsStatsService = IGraphicsStats.Stub.asInterface(binder);
+ requestBuffer();
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
+ }
+ }
+
+ private void rotateBuffer() {
+ nRotateProcessStatsBuffer();
+ requestBuffer();
+ }
+
+ private void requestBuffer() {
+ try {
+ final String pkg = mAppContext.getApplicationInfo().packageName;
+ ParcelFileDescriptor pfd = mGraphicsStatsService
+ .requestBufferForProcess(pkg, mGraphicsStatsCallback);
+ nSetProcessStatsBuffer(pfd.getFd());
pfd.close();
} catch (Throwable t) {
Log.w(LOG_TAG, "Could not acquire gfx stats buffer", t);
@@ -936,7 +944,8 @@
static native void setupShadersDiskCache(String cacheFile);
- private static native void nSetProcessStatsBuffer(long nativeProxy, int fd);
+ private static native void nRotateProcessStatsBuffer();
+ private static native void nSetProcessStatsBuffer(int fd);
private static native int nGetRenderThreadTid(long nativeProxy);
private static native long nCreateRootRenderNode();
@@ -981,7 +990,6 @@
private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd,
@DumpFlags int dumpFlags);
- private static native void nDumpProfileData(byte[] data, FileDescriptor fd);
private static native void nAddRenderNode(long nativeProxy, long rootRenderNode,
boolean placeFront);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 6cdd483..c81e938 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -887,9 +887,9 @@
final boolean hasSurfaceInsets = insets.left != 0 || insets.right != 0
|| insets.top != 0 || insets.bottom != 0;
final boolean translucent = attrs.format != PixelFormat.OPAQUE || hasSurfaceInsets;
- mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent);
+ mAttachInfo.mThreadedRenderer = ThreadedRenderer.create(mContext, translucent,
+ attrs.getTitle().toString());
if (mAttachInfo.mThreadedRenderer != null) {
- mAttachInfo.mThreadedRenderer.setName(attrs.getTitle().toString());
mAttachInfo.mHardwareAccelerated =
mAttachInfo.mHardwareAccelerationRequested = true;
}
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index d37f96a..37eae48a 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -43,7 +43,6 @@
#include <FrameInfo.h>
#include <FrameMetricsObserver.h>
#include <IContextFactory.h>
-#include <JankTracker.h>
#include <PropertyValuesAnimatorSet.h>
#include <RenderNode.h>
#include <renderthread/CanvasContext.h>
@@ -587,10 +586,13 @@
return atoi(prop) > 0 ? JNI_TRUE : JNI_FALSE;
}
+static void android_view_ThreadedRenderer_rotateProcessStatsBuffer(JNIEnv* env, jobject clazz) {
+ RenderProxy::rotateProcessStatsBuffer();
+}
+
static void android_view_ThreadedRenderer_setProcessStatsBuffer(JNIEnv* env, jobject clazz,
- jlong proxyPtr, jint fd) {
- RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
- proxy->setProcessStatsBuffer(fd);
+ jint fd) {
+ RenderProxy::setProcessStatsBuffer(fd);
}
static jint android_view_ThreadedRenderer_getRenderThreadTid(JNIEnv* env, jobject clazz,
@@ -817,15 +819,6 @@
proxy->dumpProfileInfo(fd, dumpFlags);
}
-static void android_view_ThreadedRenderer_dumpProfileData(JNIEnv* env, jobject clazz,
- jbyteArray jdata, jobject javaFileDescriptor) {
- int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor);
- ScopedByteArrayRO buffer(env, jdata);
- if (buffer.get()) {
- JankTracker::dumpBuffer(buffer.get(), buffer.size(), fd);
- }
-}
-
static void android_view_ThreadedRenderer_addRenderNode(JNIEnv* env, jobject clazz,
jlong proxyPtr, jlong renderNodePtr, jboolean placeFront) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
@@ -910,7 +903,8 @@
static const JNINativeMethod gMethods[] = {
{ "nSupportsOpenGL", "()Z", (void*) android_view_ThreadedRenderer_supportsOpenGL },
- { "nSetProcessStatsBuffer", "(JI)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer },
+ { "nRotateProcessStatsBuffer", "()V", (void*) android_view_ThreadedRenderer_rotateProcessStatsBuffer },
+ { "nSetProcessStatsBuffer", "(I)V", (void*) android_view_ThreadedRenderer_setProcessStatsBuffer },
{ "nGetRenderThreadTid", "(J)I", (void*) android_view_ThreadedRenderer_getRenderThreadTid },
{ "nCreateRootRenderNode", "()J", (void*) android_view_ThreadedRenderer_createRootRenderNode },
{ "nCreateProxy", "(ZJ)J", (void*) android_view_ThreadedRenderer_createProxy },
@@ -943,7 +937,6 @@
{ "nNotifyFramePending", "(J)V", (void*) android_view_ThreadedRenderer_notifyFramePending },
{ "nSerializeDisplayListTree", "(J)V", (void*) android_view_ThreadedRenderer_serializeDisplayListTree },
{ "nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo },
- { "nDumpProfileData", "([BLjava/io/FileDescriptor;)V", (void*) android_view_ThreadedRenderer_dumpProfileData },
{ "setupShadersDiskCache", "(Ljava/lang/String;)V",
(void*) android_view_ThreadedRenderer_setupShadersDiskCache },
{ "nAddRenderNode", "(JJZ)V", (void*) android_view_ThreadedRenderer_addRenderNode},
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index ba1d664..a2f07d9 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -21,6 +21,7 @@
import "frameworks/base/libs/incident/proto/android/privacy.proto";
import "frameworks/base/core/proto/android/service/appwidget.proto";
+import "frameworks/base/core/proto/android/service/graphicsstats.proto";
import "frameworks/base/core/proto/android/service/fingerprint.proto";
import "frameworks/base/core/proto/android/service/netstats.proto";
import "frameworks/base/core/proto/android/service/notification.proto";
@@ -57,4 +58,5 @@
android.providers.settings.SettingsServiceDumpProto settings = 3002;
android.service.appwidget.AppWidgetServiceDumpProto appwidget = 3003;
android.service.notification.NotificationServiceDumpProto notification = 3004;
+ android.service.GraphicsStatsServiceDumpProto graphicsstats = 3005;
}
diff --git a/core/proto/android/service/graphicsstats.proto b/core/proto/android/service/graphicsstats.proto
new file mode 100644
index 0000000..6dbfe48
--- /dev/null
+++ b/core/proto/android/service/graphicsstats.proto
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package android.service;
+
+option java_multiple_files = true;
+option java_outer_classname = "GraphicsStatsServiceProto";
+
+message GraphicsStatsServiceDumpProto {
+ repeated GraphicsStatsProto stats = 1;
+}
+
+message GraphicsStatsProto {
+
+ // The package name of the app
+ string package_name = 1;
+
+ // The version code of the app
+ int32 version_code = 2;
+
+ // The start & end timestamps in UTC as
+ // milliseconds since January 1, 1970
+ // Compatible with java.util.Date#setTime()
+ int64 stats_start = 3;
+ int64 stats_end = 4;
+
+ // The aggregated statistics for the package
+ GraphicsStatsJankSummaryProto summary = 5;
+
+ // The frame time histogram for the package
+ repeated GraphicsStatsHistogramBucketProto histogram = 6;
+}
+
+message GraphicsStatsJankSummaryProto {
+ // Distinct frame count.
+ int32 total_frames = 1;
+
+ // Number of frames with slow render time. Frames are considered janky if
+ // they took more than a vsync interval (typically 16.667ms) to be rendered.
+ int32 janky_frames = 2;
+
+ // Number of "missed vsync" events.
+ int32 missed_vsync_count = 3;
+
+ // Number of "high input latency" events.
+ int32 high_input_latency_count = 4;
+
+ // Number of "slow UI thread" events.
+ int32 slow_ui_thread_count = 5;
+
+ // Number of "slow bitmap upload" events.
+ int32 slow_bitmap_upload_count = 6;
+
+ // Number of "slow draw" events.
+ int32 slow_draw_count = 7;
+}
+
+message GraphicsStatsHistogramBucketProto {
+ // Lower bound of render time in milliseconds.
+ int32 render_millis = 1;
+ // Number of frames in the bucket.
+ int32 frame_count = 2;
+}
diff --git a/libs/hwui/Android.mk b/libs/hwui/Android.mk
index ff40c8a..9515b82 100644
--- a/libs/hwui/Android.mk
+++ b/libs/hwui/Android.mk
@@ -47,6 +47,7 @@
renderthread/RenderThread.cpp \
renderthread/TimeLord.cpp \
renderthread/Frame.cpp \
+ service/GraphicsStatsService.cpp \
thread/TaskManager.cpp \
utils/Blur.cpp \
utils/GLUtils.cpp \
@@ -293,6 +294,7 @@
tests/unit/GlopBuilderTests.cpp \
tests/unit/GpuMemoryTrackerTests.cpp \
tests/unit/GradientCacheTests.cpp \
+ tests/unit/GraphicsStatsServiceTests.cpp \
tests/unit/LayerUpdateQueueTests.cpp \
tests/unit/LeakCheckTests.cpp \
tests/unit/LinearAllocatorTests.cpp \
diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp
index 2132c2b..7be71ee 100644
--- a/libs/hwui/JankTracker.cpp
+++ b/libs/hwui/JankTracker.cpp
@@ -110,7 +110,7 @@
}
// Only called when dumping stats, less performance sensitive
-static uint32_t frameTimeForFrameCountIndex(uint32_t index) {
+int32_t JankTracker::frameTimeForFrameCountIndex(uint32_t index) {
index = index + kBucketMinThreshold;
if (index > kBucket2msIntervals) {
index += (index - kBucket2msIntervals);
@@ -123,6 +123,10 @@
return index;
}
+int32_t JankTracker::frameTimeForSlowFrameCountIndex(uint32_t index) {
+ return (index * kSlowFrameBucketIntervalMs) + kSlowFrameBucketStartMs;
+}
+
JankTracker::JankTracker(const DisplayInfo& displayInfo) {
// By default this will use malloc memory. It may be moved later to ashmem
// if there is shared space for it and a request comes in to do that.
@@ -161,8 +165,25 @@
mData = nullptr;
}
+void JankTracker::rotateStorage() {
+ // If we are mapped we want to stop using the ashmem backend and switch to malloc
+ // We are expecting a switchStorageToAshmem call to follow this, but it's not guaranteed
+ // If we aren't sitting on top of ashmem then just do a reset() as it's functionally
+ // equivalent do a free, malloc, reset.
+ if (mIsMapped) {
+ freeData();
+ mData = new ProfileData;
+ }
+ reset();
+}
+
void JankTracker::switchStorageToAshmem(int ashmemfd) {
int regionSize = ashmem_get_size_region(ashmemfd);
+ if (regionSize < 0) {
+ int err = errno;
+ ALOGW("Failed to get ashmem region size from fd %d, err %d %s", ashmemfd, err, strerror(err));
+ return;
+ }
if (regionSize < static_cast<int>(sizeof(ProfileData))) {
ALOGW("Ashmem region is too small! Received %d, required %u",
regionSize, static_cast<unsigned int>(sizeof(ProfileData)));
@@ -279,15 +300,19 @@
}
}
-void JankTracker::dumpBuffer(const void* buffer, size_t bufsize, int fd) {
- if (bufsize < sizeof(ProfileData)) {
- return;
+void JankTracker::dumpData(int fd, const ProfileDataDescription* description, const ProfileData* data) {
+ if (description) {
+ switch (description->type) {
+ case JankTrackerType::Generic:
+ break;
+ case JankTrackerType::Package:
+ dprintf(fd, "\nPackage: %s", description->name.c_str());
+ break;
+ case JankTrackerType::Window:
+ dprintf(fd, "\nWindow: %s", description->name.c_str());
+ break;
+ }
}
- const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer);
- dumpData(data, fd);
-}
-
-void JankTracker::dumpData(const ProfileData* data, int fd) {
if (sFrameStart != FrameInfoIndex::IntendedVsync) {
dprintf(fd, "\nNote: Data has been filtered!");
}
@@ -308,7 +333,7 @@
data->frameCounts[i]);
}
for (size_t i = 0; i < data->slowFrameCounts.size(); i++) {
- dprintf(fd, " %zums=%u", (i * kSlowFrameBucketIntervalMs) + kSlowFrameBucketStartMs,
+ dprintf(fd, " %ums=%u", frameTimeForSlowFrameCountIndex(i),
data->slowFrameCounts[i]);
}
dprintf(fd, "\n");
diff --git a/libs/hwui/JankTracker.h b/libs/hwui/JankTracker.h
index 8b482d5..6ff5d89 100644
--- a/libs/hwui/JankTracker.h
+++ b/libs/hwui/JankTracker.h
@@ -54,29 +54,50 @@
nsecs_t statStartTime;
};
+enum class JankTrackerType {
+ // The default, means there's no description set
+ Generic,
+ // The profile data represents a package
+ Package,
+ // The profile data is for a specific window
+ Window,
+};
+
+// Metadata about the ProfileData being collected
+struct ProfileDataDescription {
+ JankTrackerType type;
+ std::string name;
+};
+
// TODO: Replace DrawProfiler with this
class JankTracker {
public:
explicit JankTracker(const DisplayInfo& displayInfo);
~JankTracker();
+ void setDescription(JankTrackerType type, const std::string&& name) {
+ mDescription.type = type;
+ mDescription.name = name;
+ }
+
void addFrame(const FrameInfo& frame);
- void dump(int fd) { dumpData(mData, fd); }
+ void dump(int fd) { dumpData(fd, &mDescription, mData); }
void reset();
+ void rotateStorage();
void switchStorageToAshmem(int ashmemfd);
uint32_t findPercentile(int p) { return findPercentile(mData, p); }
-
- ANDROID_API static void dumpBuffer(const void* buffer, size_t bufsize, int fd);
+ static int32_t frameTimeForFrameCountIndex(uint32_t index);
+ static int32_t frameTimeForSlowFrameCountIndex(uint32_t index);
private:
void freeData();
void setFrameInterval(nsecs_t frameIntervalNanos);
static uint32_t findPercentile(const ProfileData* data, int p);
- static void dumpData(const ProfileData* data, int fd);
+ static void dumpData(int fd, const ProfileDataDescription* description, const ProfileData* data);
std::array<int64_t, NUM_BUCKETS> mThresholds;
int64_t mFrameInterval;
@@ -90,6 +111,7 @@
nsecs_t mDequeueTimeForgiveness = 0;
ProfileData* mData;
bool mIsMapped = false;
+ ProfileDataDescription mDescription;
};
} /* namespace uirenderer */
diff --git a/libs/hwui/hwui_static_deps.mk b/libs/hwui/hwui_static_deps.mk
index a75fd6a..8826cfc 100644
--- a/libs/hwui/hwui_static_deps.mk
+++ b/libs/hwui/hwui_static_deps.mk
@@ -22,11 +22,12 @@
libskia \
libui \
libgui \
- libprotobuf-cpp-lite \
+ libprotobuf-cpp-full \
libharfbuzz_ng \
libft2 \
libminikin \
- libandroidfw
+ libandroidfw \
+ libRScpp
-# enable RENDERSCRIPT
-LOCAL_SHARED_LIBRARIES += libRScpp
+LOCAL_STATIC_LIBRARIES += \
+ libplatformprotos
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index a53e5e0..02a9ffa 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -590,6 +590,7 @@
}
void CanvasContext::dumpFrames(int fd) {
+ mJankTracker.dump(fd);
FILE* file = fdopen(fd, "a");
fprintf(file, "\n\n---PROFILEDATA---\n");
for (size_t i = 0; i < static_cast<size_t>(FrameInfoIndex::NumIndexes); i++) {
@@ -615,6 +616,10 @@
mRenderThread.jankTracker().reset();
}
+void CanvasContext::setName(const std::string&& name) {
+ mJankTracker.setDescription(JankTrackerType::Window, std::move(name));
+}
+
void CanvasContext::serializeDisplayListTree() {
#if ENABLE_RENDERNODE_SERIALIZATION
using namespace google::protobuf::io;
diff --git a/libs/hwui/renderthread/CanvasContext.h b/libs/hwui/renderthread/CanvasContext.h
index aa01caa..738c091 100644
--- a/libs/hwui/renderthread/CanvasContext.h
+++ b/libs/hwui/renderthread/CanvasContext.h
@@ -155,8 +155,7 @@
void dumpFrames(int fd);
void resetFrameStats();
- void setName(const std::string&& name) { mName = name; }
- const std::string& name() { return mName; }
+ void setName(const std::string&& name);
void serializeDisplayListTree();
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index 11614fa..f4a4773 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -487,9 +487,22 @@
void RenderProxy::setProcessStatsBuffer(int fd) {
SETUP_TASK(setProcessStatsBuffer);
- args->thread = &mRenderThread;
+ auto& rt = RenderThread::getInstance();
+ args->thread = &rt;
args->fd = dup(fd);
- post(task);
+ rt.queue(task);
+}
+
+CREATE_BRIDGE1(rotateProcessStatsBuffer, RenderThread* thread) {
+ args->thread->jankTracker().rotateStorage();
+ return nullptr;
+}
+
+void RenderProxy::rotateProcessStatsBuffer() {
+ SETUP_TASK(rotateProcessStatsBuffer);
+ auto& rt = RenderThread::getInstance();
+ args->thread = &rt;
+ rt.queue(task);
}
int RenderProxy::getRenderThreadTid() {
diff --git a/libs/hwui/renderthread/RenderProxy.h b/libs/hwui/renderthread/RenderProxy.h
index 1629090..a60ed55 100644
--- a/libs/hwui/renderthread/RenderProxy.h
+++ b/libs/hwui/renderthread/RenderProxy.h
@@ -113,7 +113,8 @@
uint32_t frameTimePercentile(int p);
ANDROID_API static void dumpGraphicsMemory(int fd);
- ANDROID_API void setProcessStatsBuffer(int fd);
+ ANDROID_API static void rotateProcessStatsBuffer();
+ ANDROID_API static void setProcessStatsBuffer(int fd);
ANDROID_API int getRenderThreadTid();
ANDROID_API void serializeDisplayListTree();
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index d121bcf..9bc5985 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -74,6 +74,7 @@
};
class ANDROID_API RenderThread : public Thread {
+ PREVENT_COPY_AND_ASSIGN(RenderThread);
public:
// RenderThread takes complete ownership of tasks that are queued
// and will delete them after they are run
diff --git a/libs/hwui/service/GraphicsStatsService.cpp b/libs/hwui/service/GraphicsStatsService.cpp
new file mode 100644
index 0000000..ab6420e
--- /dev/null
+++ b/libs/hwui/service/GraphicsStatsService.cpp
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "GraphicsStatsService.h"
+
+#include "JankTracker.h"
+
+#include <frameworks/base/core/proto/android/service/graphicsstats.pb.h>
+#include <google/protobuf/io/zero_copy_stream_impl.h>
+#include <log/log.h>
+
+#include <inttypes.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+namespace android {
+namespace uirenderer {
+
+using namespace google::protobuf;
+
+constexpr int32_t sCurrentFileVersion = 1;
+constexpr int32_t sHeaderSize = 4;
+static_assert(sizeof(sCurrentFileVersion) == sHeaderSize, "Header size is wrong");
+
+constexpr int sHistogramSize =
+ std::tuple_size<decltype(ProfileData::frameCounts)>::value +
+ std::tuple_size<decltype(ProfileData::slowFrameCounts)>::value;
+
+static void mergeProfileDataIntoProto(service::GraphicsStatsProto* proto,
+ const std::string& package, int versionCode, int64_t startTime, int64_t endTime,
+ const ProfileData* data);
+static void dumpAsTextToFd(service::GraphicsStatsProto* proto, int outFd);
+
+bool GraphicsStatsService::parseFromFile(const std::string& path, service::GraphicsStatsProto* output) {
+
+ int fd = open(path.c_str(), O_RDONLY);
+ if (fd == -1) {
+ int err = errno;
+ // The file not existing is normal for addToDump(), so only log if
+ // we get an unexpected error
+ if (err != ENOENT) {
+ ALOGW("Failed to open '%s', errno=%d (%s)", path.c_str(), err, strerror(err));
+ }
+ return false;
+ }
+ uint32_t file_version;
+ ssize_t bytesRead = read(fd, &file_version, sHeaderSize);
+ if (bytesRead != sHeaderSize || file_version != sCurrentFileVersion) {
+ ALOGW("Failed to read '%s', bytesRead=%zd file_version=%d", path.c_str(), bytesRead,
+ file_version);
+ close(fd);
+ return false;
+ }
+
+ io::FileInputStream input(fd);
+ bool success = output->ParseFromZeroCopyStream(&input);
+ if (input.GetErrno() != 0) {
+ ALOGW("Error reading from fd=%d, path='%s' err=%d (%s)",
+ fd, path.c_str(), input.GetErrno(), strerror(input.GetErrno()));
+ success = false;
+ } else if (!success) {
+ ALOGW("Parse failed on '%s' error='%s'",
+ path.c_str(), output->InitializationErrorString().c_str());
+ }
+ close(fd);
+ return success;
+}
+
+void mergeProfileDataIntoProto(service::GraphicsStatsProto* proto, const std::string& package,
+ int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
+ if (proto->stats_start() == 0 || proto->stats_start() > startTime) {
+ proto->set_stats_start(startTime);
+ }
+ if (proto->stats_end() == 0 || proto->stats_end() < endTime) {
+ proto->set_stats_end(endTime);
+ }
+ proto->set_package_name(package);
+ proto->set_version_code(versionCode);
+ auto summary = proto->mutable_summary();
+ summary->set_total_frames(summary->total_frames() + data->totalFrameCount);
+ summary->set_janky_frames(summary->janky_frames() + data->jankFrameCount);
+ summary->set_missed_vsync_count(
+ summary->missed_vsync_count() + data->jankTypeCounts[kMissedVsync]);
+ summary->set_high_input_latency_count(
+ summary->high_input_latency_count() + data->jankTypeCounts[kHighInputLatency]);
+ summary->set_slow_ui_thread_count(
+ summary->slow_ui_thread_count() + data->jankTypeCounts[kSlowUI]);
+ summary->set_slow_bitmap_upload_count(
+ summary->slow_bitmap_upload_count() + data->jankTypeCounts[kSlowSync]);
+ summary->set_slow_draw_count(
+ summary->slow_draw_count() + data->jankTypeCounts[kSlowRT]);
+
+ bool creatingHistogram = false;
+ if (proto->histogram_size() == 0) {
+ proto->mutable_histogram()->Reserve(sHistogramSize);
+ creatingHistogram = true;
+ } else if (proto->histogram_size() != sHistogramSize) {
+ LOG_ALWAYS_FATAL("Histogram size mismatch, proto is %d expected %d",
+ proto->histogram_size(), sHistogramSize);
+ }
+ for (size_t i = 0; i < data->frameCounts.size(); i++) {
+ service::GraphicsStatsHistogramBucketProto* bucket;
+ int32_t renderTime = JankTracker::frameTimeForFrameCountIndex(i);
+ if (creatingHistogram) {
+ bucket = proto->add_histogram();
+ bucket->set_render_millis(renderTime);
+ } else {
+ bucket = proto->mutable_histogram(i);
+ LOG_ALWAYS_FATAL_IF(bucket->render_millis() != renderTime,
+ "Frame time mistmatch %d vs. %d", bucket->render_millis(), renderTime);
+ }
+ bucket->set_frame_count(bucket->frame_count() + data->frameCounts[i]);
+ }
+ for (size_t i = 0; i < data->slowFrameCounts.size(); i++) {
+ service::GraphicsStatsHistogramBucketProto* bucket;
+ int32_t renderTime = JankTracker::frameTimeForSlowFrameCountIndex(i);
+ if (creatingHistogram) {
+ bucket = proto->add_histogram();
+ bucket->set_render_millis(renderTime);
+ } else {
+ constexpr int offset = std::tuple_size<decltype(ProfileData::frameCounts)>::value;
+ bucket = proto->mutable_histogram(offset + i);
+ LOG_ALWAYS_FATAL_IF(bucket->render_millis() != renderTime,
+ "Frame time mistmatch %d vs. %d", bucket->render_millis(), renderTime);
+ }
+ bucket->set_frame_count(bucket->frame_count() + data->slowFrameCounts[i]);
+ }
+}
+
+static int32_t findPercentile(service::GraphicsStatsProto* proto, int percentile) {
+ int32_t pos = percentile * proto->summary().total_frames() / 100;
+ int32_t remaining = proto->summary().total_frames() - pos;
+ for (auto it = proto->histogram().rbegin(); it != proto->histogram().rend(); ++it) {
+ remaining -= it->frame_count();
+ if (remaining <= 0) {
+ return it->render_millis();
+ }
+ }
+ return 0;
+}
+
+void dumpAsTextToFd(service::GraphicsStatsProto* proto, int fd) {
+ // This isn't a full validation, just enough that we can deref at will
+ LOG_ALWAYS_FATAL_IF(proto->package_name().empty()
+ || !proto->has_summary(), "package_name() '%s' summary %d",
+ proto->package_name().c_str(), proto->has_summary());
+ dprintf(fd, "\nPackage: %s", proto->package_name().c_str());
+ dprintf(fd, "\nVersion: %d", proto->version_code());
+ dprintf(fd, "\nStats since: %lldns", proto->stats_start());
+ dprintf(fd, "\nStats end: %lldns", proto->stats_end());
+ auto summary = proto->summary();
+ dprintf(fd, "\nTotal frames rendered: %d", summary.total_frames());
+ dprintf(fd, "\nJanky frames: %d (%.2f%%)", summary.janky_frames(),
+ (float) summary.janky_frames() / (float) summary.total_frames() * 100.0f);
+ dprintf(fd, "\n50th percentile: %dms", findPercentile(proto, 50));
+ dprintf(fd, "\n90th percentile: %dms", findPercentile(proto, 90));
+ dprintf(fd, "\n95th percentile: %dms", findPercentile(proto, 95));
+ dprintf(fd, "\n99th percentile: %dms", findPercentile(proto, 99));
+ dprintf(fd, "\nNumber Missed Vsync: %d", summary.missed_vsync_count());
+ dprintf(fd, "\nNumber High input latency: %d", summary.high_input_latency_count());
+ dprintf(fd, "\nNumber Slow UI thread: %d", summary.slow_ui_thread_count());
+ dprintf(fd, "\nNumber Slow bitmap uploads: %d", summary.slow_bitmap_upload_count());
+ dprintf(fd, "\nNumber Slow issue draw commands: %d", summary.slow_draw_count());
+ dprintf(fd, "\nHISTOGRAM:");
+ for (const auto& it : proto->histogram()) {
+ dprintf(fd, " %dms=%d", it.render_millis(), it.frame_count());
+ }
+ dprintf(fd, "\n");
+}
+
+void GraphicsStatsService::saveBuffer(const std::string& path, const std::string& package,
+ int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
+ service::GraphicsStatsProto statsProto;
+ if (!parseFromFile(path, &statsProto)) {
+ statsProto.Clear();
+ }
+ mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data);
+ // Although we might not have read any data from the file, merging the existing data
+ // should always fully-initialize the proto
+ LOG_ALWAYS_FATAL_IF(!statsProto.IsInitialized(), "%s",
+ statsProto.InitializationErrorString().c_str());
+ LOG_ALWAYS_FATAL_IF(statsProto.package_name().empty()
+ || !statsProto.has_summary(), "package_name() '%s' summary %d",
+ statsProto.package_name().c_str(), statsProto.has_summary());
+ int outFd = open(path.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0660);
+ if (outFd <= 0) {
+ int err = errno;
+ ALOGW("Failed to open '%s', error=%d (%s)", path.c_str(), err, strerror(err));
+ return;
+ }
+ int wrote = write(outFd, &sCurrentFileVersion, sHeaderSize);
+ if (wrote != sHeaderSize) {
+ int err = errno;
+ ALOGW("Failed to write header to '%s', returned=%d errno=%d (%s)",
+ path.c_str(), wrote, err, strerror(err));
+ close(outFd);
+ return;
+ }
+ {
+ io::FileOutputStream output(outFd);
+ bool success = statsProto.SerializeToZeroCopyStream(&output) && output.Flush();
+ if (output.GetErrno() != 0) {
+ ALOGW("Error writing to fd=%d, path='%s' err=%d (%s)",
+ outFd, path.c_str(), output.GetErrno(), strerror(output.GetErrno()));
+ success = false;
+ } else if (!success) {
+ ALOGW("Serialize failed on '%s' unknown error", path.c_str());
+ }
+ }
+ close(outFd);
+}
+
+class GraphicsStatsService::Dump {
+public:
+ Dump(int outFd, DumpType type) : mFd(outFd), mType(type) {}
+ int fd() { return mFd; }
+ DumpType type() { return mType; }
+ service::GraphicsStatsServiceDumpProto& proto() { return mProto; }
+private:
+ int mFd;
+ DumpType mType;
+ service::GraphicsStatsServiceDumpProto mProto;
+};
+
+GraphicsStatsService::Dump* GraphicsStatsService::createDump(int outFd, DumpType type) {
+ return new Dump(outFd, type);
+}
+
+void GraphicsStatsService::addToDump(Dump* dump, const std::string& path, const std::string& package,
+ int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data) {
+ service::GraphicsStatsProto statsProto;
+ if (!path.empty() && !parseFromFile(path, &statsProto)) {
+ statsProto.Clear();
+ }
+ if (data) {
+ mergeProfileDataIntoProto(&statsProto, package, versionCode, startTime, endTime, data);
+ }
+ if (!statsProto.IsInitialized()) {
+ ALOGW("Failed to load profile data from path '%s' and data %p",
+ path.empty() ? "<empty>" : path.c_str(), data);
+ return;
+ }
+
+ if (dump->type() == DumpType::Protobuf) {
+ dump->proto().add_stats()->CopyFrom(statsProto);
+ } else {
+ dumpAsTextToFd(&statsProto, dump->fd());
+ }
+}
+
+void GraphicsStatsService::addToDump(Dump* dump, const std::string& path) {
+ service::GraphicsStatsProto statsProto;
+ if (!parseFromFile(path, &statsProto)) {
+ return;
+ }
+ if (dump->type() == DumpType::Protobuf) {
+ dump->proto().add_stats()->CopyFrom(statsProto);
+ } else {
+ dumpAsTextToFd(&statsProto, dump->fd());
+ }
+}
+
+void GraphicsStatsService::finishDump(Dump* dump) {
+ if (dump->type() == DumpType::Protobuf) {
+ io::FileOutputStream stream(dump->fd());
+ dump->proto().SerializeToZeroCopyStream(&stream);
+ }
+ delete dump;
+}
+
+} /* namespace uirenderer */
+} /* namespace android */
\ No newline at end of file
diff --git a/libs/hwui/service/GraphicsStatsService.h b/libs/hwui/service/GraphicsStatsService.h
new file mode 100644
index 0000000..d0fd60e
--- /dev/null
+++ b/libs/hwui/service/GraphicsStatsService.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <string>
+
+#include "JankTracker.h"
+#include "utils/Macros.h"
+
+namespace android {
+namespace service {
+class GraphicsStatsProto;
+}
+
+namespace uirenderer {
+
+/*
+ * The exported entry points used by GraphicsStatsService.java in f/b/services/core
+ *
+ * NOTE: Avoid exporting a requirement on the protobuf itself. Keep the usage
+ * of the generated protobuf classes internal to libhwui.so to minimize library
+ * bloat.
+ */
+class GraphicsStatsService {
+public:
+ class Dump;
+ enum class DumpType {
+ Text,
+ Protobuf,
+ };
+
+ ANDROID_API static void saveBuffer(const std::string& path, const std::string& package,
+ int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data);
+
+ ANDROID_API static Dump* createDump(int outFd, DumpType type);
+ ANDROID_API static void addToDump(Dump* dump, const std::string& path, const std::string& package,
+ int versionCode, int64_t startTime, int64_t endTime, const ProfileData* data);
+ ANDROID_API static void addToDump(Dump* dump, const std::string& path);
+ ANDROID_API static void finishDump(Dump* dump);
+
+ // Visible for testing
+ static bool parseFromFile(const std::string& path, service::GraphicsStatsProto* output);
+};
+
+} /* namespace uirenderer */
+} /* namespace android */
\ No newline at end of file
diff --git a/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
new file mode 100644
index 0000000..cfe1134
--- /dev/null
+++ b/libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "service/GraphicsStatsService.h"
+
+#include <frameworks/base/core/proto/android/service/graphicsstats.pb.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+using namespace android;
+using namespace android::uirenderer;
+
+std::string findRootPath() {
+ char path[1024];
+ ssize_t r = readlink("/proc/self/exe", path, 1024);
+ // < 1023 because we need room for the null terminator
+ if (r <= 0 || r > 1023) {
+ int err = errno;
+ fprintf(stderr, "Failed to read from /proc/self/exe; r=%zd, err=%d (%s)\n",
+ r, err, strerror(err));
+ exit(EXIT_FAILURE);
+ }
+ while (--r > 0) {
+ if (path[r] == '/') {
+ path[r] = '\0';
+ return std::string(path);
+ }
+ }
+ return std::string();
+}
+
+// No code left untested
+TEST(GraphicsStats, findRootPath) {
+ std::string expected = "/data/nativetest/hwui_unit_tests";
+ EXPECT_EQ(expected, findRootPath());
+}
+
+TEST(GraphicsStats, saveLoad) {
+ std::string path = findRootPath() + "/test_saveLoad";
+ std::string packageName = "com.test.saveLoad";
+ ProfileData mockData;
+ mockData.jankFrameCount = 20;
+ mockData.totalFrameCount = 100;
+ mockData.statStartTime = 10000;
+ // Fill with patterned data we can recognize but which won't map to a
+ // memset or basic for iteration count
+ for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+ mockData.frameCounts[i] = ((i % 10) + 1) * 2;
+ }
+ for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+ mockData.slowFrameCounts[i] = (i % 5) + 1;
+ }
+ GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData);
+ service::GraphicsStatsProto loadedProto;
+ EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto));
+ // Clean up the file
+ unlink(path.c_str());
+
+ EXPECT_EQ(packageName, loadedProto.package_name());
+ EXPECT_EQ(5, loadedProto.version_code());
+ EXPECT_EQ(3000, loadedProto.stats_start());
+ EXPECT_EQ(7000, loadedProto.stats_end());
+ // ASSERT here so we don't continue with a nullptr deref crash if this is false
+ ASSERT_TRUE(loadedProto.has_summary());
+ EXPECT_EQ(20, loadedProto.summary().janky_frames());
+ EXPECT_EQ(100, loadedProto.summary().total_frames());
+ EXPECT_EQ(mockData.frameCounts.size() + mockData.slowFrameCounts.size(),
+ (size_t) loadedProto.histogram_size());
+ for (size_t i = 0; i < (size_t) loadedProto.histogram_size(); i++) {
+ int expectedCount, expectedBucket;
+ if (i < mockData.frameCounts.size()) {
+ expectedCount = ((i % 10) + 1) * 2;
+ expectedBucket = JankTracker::frameTimeForFrameCountIndex(i);
+ } else {
+ int temp = i - mockData.frameCounts.size();
+ expectedCount = (temp % 5) + 1;
+ expectedBucket = JankTracker::frameTimeForSlowFrameCountIndex(temp);
+ }
+ EXPECT_EQ(expectedCount, loadedProto.histogram().Get(i).frame_count());
+ EXPECT_EQ(expectedBucket, loadedProto.histogram().Get(i).render_millis());
+ }
+}
+
+TEST(GraphicsStats, merge) {
+ std::string path = findRootPath() + "/test_merge";
+ std::string packageName = "com.test.merge";
+ ProfileData mockData;
+ mockData.jankFrameCount = 20;
+ mockData.totalFrameCount = 100;
+ mockData.statStartTime = 10000;
+ // Fill with patterned data we can recognize but which won't map to a
+ // memset or basic for iteration count
+ for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+ mockData.frameCounts[i] = ((i % 10) + 1) * 2;
+ }
+ for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+ mockData.slowFrameCounts[i] = (i % 5) + 1;
+ }
+ GraphicsStatsService::saveBuffer(path, packageName, 5, 3000, 7000, &mockData);
+ mockData.jankFrameCount = 50;
+ mockData.totalFrameCount = 500;
+ for (size_t i = 0; i < mockData.frameCounts.size(); i++) {
+ mockData.frameCounts[i] = (i % 5) + 1;
+ }
+ for (size_t i = 0; i < mockData.slowFrameCounts.size(); i++) {
+ mockData.slowFrameCounts[i] = ((i % 10) + 1) * 2;
+ }
+ GraphicsStatsService::saveBuffer(path, packageName, 5, 7050, 10000, &mockData);
+
+ service::GraphicsStatsProto loadedProto;
+ EXPECT_TRUE(GraphicsStatsService::parseFromFile(path, &loadedProto));
+ // Clean up the file
+ unlink(path.c_str());
+
+ EXPECT_EQ(packageName, loadedProto.package_name());
+ EXPECT_EQ(5, loadedProto.version_code());
+ EXPECT_EQ(3000, loadedProto.stats_start());
+ EXPECT_EQ(10000, loadedProto.stats_end());
+ // ASSERT here so we don't continue with a nullptr deref crash if this is false
+ ASSERT_TRUE(loadedProto.has_summary());
+ EXPECT_EQ(20 + 50, loadedProto.summary().janky_frames());
+ EXPECT_EQ(100 + 500, loadedProto.summary().total_frames());
+ EXPECT_EQ(mockData.frameCounts.size() + mockData.slowFrameCounts.size(),
+ (size_t) loadedProto.histogram_size());
+ for (size_t i = 0; i < (size_t) loadedProto.histogram_size(); i++) {
+ int expectedCount, expectedBucket;
+ if (i < mockData.frameCounts.size()) {
+ expectedCount = ((i % 10) + 1) * 2;
+ expectedCount += (i % 5) + 1;
+ expectedBucket = JankTracker::frameTimeForFrameCountIndex(i);
+ } else {
+ int temp = i - mockData.frameCounts.size();
+ expectedCount = (temp % 5) + 1;
+ expectedCount += ((temp % 10) + 1) * 2;
+ expectedBucket = JankTracker::frameTimeForSlowFrameCountIndex(temp);
+ }
+ EXPECT_EQ(expectedCount, loadedProto.histogram().Get(i).frame_count());
+ EXPECT_EQ(expectedBucket, loadedProto.histogram().Get(i).render_millis());
+ }
+}
\ No newline at end of file
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);
}
}
}
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index 2c3cda5..ac95db5 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -30,6 +30,7 @@
$(LOCAL_REL_DIR)/com_android_server_UsbHostManager.cpp \
$(LOCAL_REL_DIR)/com_android_server_VibratorService.cpp \
$(LOCAL_REL_DIR)/com_android_server_PersistentDataBlockService.cpp \
+ $(LOCAL_REL_DIR)/com_android_server_GraphicsStatsService.cpp \
$(LOCAL_REL_DIR)/onload.cpp
LOCAL_C_INCLUDES += \
@@ -37,7 +38,6 @@
external/scrypt/lib/crypto \
frameworks/base/services \
frameworks/base/libs \
- frameworks/base/libs/hwui \
frameworks/base/core/jni \
frameworks/native/services \
system/core/libappfuse/include \
@@ -76,6 +76,7 @@
libhidltransport \
libhwbinder \
libutils \
+ libhwui \
android.hardware.audio.common@2.0 \
android.hardware.contexthub@1.0 \
android.hardware.gnss@1.0 \
diff --git a/services/core/jni/com_android_server_GraphicsStatsService.cpp b/services/core/jni/com_android_server_GraphicsStatsService.cpp
new file mode 100644
index 0000000..5d5728d
--- /dev/null
+++ b/services/core/jni/com_android_server_GraphicsStatsService.cpp
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "GraphicsStatsService"
+
+#include <jni.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
+#include <ScopedPrimitiveArray.h>
+#include <ScopedUtfChars.h>
+#include <JankTracker.h>
+#include <service/GraphicsStatsService.h>
+
+namespace android {
+
+using namespace android::uirenderer;
+
+static jint getAshmemSize(JNIEnv*, jobject) {
+ return sizeof(ProfileData);
+}
+
+static jlong createDump(JNIEnv*, jobject, jint fd, jboolean isProto) {
+ GraphicsStatsService::Dump* dump = GraphicsStatsService::createDump(fd, isProto
+ ? GraphicsStatsService::DumpType::Protobuf : GraphicsStatsService::DumpType::Text);
+ return reinterpret_cast<jlong>(dump);
+}
+
+static void addToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath, jstring jpackage,
+ jint versionCode, jlong startTime, jlong endTime, jbyteArray jdata) {
+ std::string path;
+ const ProfileData* data = nullptr;
+ LOG_ALWAYS_FATAL_IF(jdata == nullptr && jpath == nullptr, "Path and data can't both be null");
+ if (jdata != nullptr) {
+ ScopedByteArrayRO buffer(env, jdata);
+ LOG_ALWAYS_FATAL_IF(buffer.size() != sizeof(ProfileData),
+ "Buffer size %zu doesn't match expected %zu!", buffer.size(), sizeof(ProfileData));
+ data = reinterpret_cast<const ProfileData*>(buffer.get());
+ }
+ if (jpath != nullptr) {
+ ScopedUtfChars pathChars(env, jpath);
+ LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+ path.assign(pathChars.c_str(), pathChars.size());
+ }
+ ScopedUtfChars packageChars(env, jpackage);
+ LOG_ALWAYS_FATAL_IF(packageChars.size() <= 0 || !packageChars.c_str(), "Failed to get path chars");
+ GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+ LOG_ALWAYS_FATAL_IF(!dump, "null passed for dump pointer");
+
+ const std::string package(packageChars.c_str(), packageChars.size());
+ GraphicsStatsService::addToDump(dump, path, package, versionCode, startTime, endTime, data);
+}
+
+static void addFileToDump(JNIEnv* env, jobject, jlong dumpPtr, jstring jpath) {
+ ScopedUtfChars pathChars(env, jpath);
+ LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+ const std::string path(pathChars.c_str(), pathChars.size());
+ GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+ GraphicsStatsService::addToDump(dump, path);
+}
+
+static void finishDump(JNIEnv*, jobject, jlong dumpPtr) {
+ GraphicsStatsService::Dump* dump = reinterpret_cast<GraphicsStatsService::Dump*>(dumpPtr);
+ GraphicsStatsService::finishDump(dump);
+}
+
+static void saveBuffer(JNIEnv* env, jobject clazz, jstring jpath, jstring jpackage,
+ jint versionCode, jlong startTime, jlong endTime, jbyteArray jdata) {
+ ScopedByteArrayRO buffer(env, jdata);
+ LOG_ALWAYS_FATAL_IF(buffer.size() != sizeof(ProfileData),
+ "Buffer size %zu doesn't match expected %zu!", buffer.size(), sizeof(ProfileData));
+ ScopedUtfChars pathChars(env, jpath);
+ LOG_ALWAYS_FATAL_IF(pathChars.size() <= 0 || !pathChars.c_str(), "Failed to get path chars");
+ ScopedUtfChars packageChars(env, jpackage);
+ LOG_ALWAYS_FATAL_IF(packageChars.size() <= 0 || !packageChars.c_str(), "Failed to get path chars");
+
+ const std::string path(pathChars.c_str(), pathChars.size());
+ const std::string package(packageChars.c_str(), packageChars.size());
+ const ProfileData* data = reinterpret_cast<const ProfileData*>(buffer.get());
+ GraphicsStatsService::saveBuffer(path, package, versionCode, startTime, endTime, data);
+}
+
+static const JNINativeMethod sMethods[] = {
+ { "nGetAshmemSize", "()I", (void*) getAshmemSize },
+ { "nCreateDump", "(IZ)J", (void*) createDump },
+ { "nAddToDump", "(JLjava/lang/String;Ljava/lang/String;IJJ[B)V", (void*) addToDump },
+ { "nAddToDump", "(JLjava/lang/String;)V", (void*) addFileToDump },
+ { "nFinishDump", "(J)V", (void*) finishDump },
+ { "nSaveBuffer", "(Ljava/lang/String;Ljava/lang/String;IJJ[B)V", (void*) saveBuffer },
+};
+
+int register_android_server_GraphicsStatsService(JNIEnv* env)
+{
+ return jniRegisterNativeMethods(env, "com/android/server/GraphicsStatsService",
+ sMethods, NELEM(sMethods));
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 899640e..f22b330 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -46,6 +46,7 @@
int register_android_server_Watchdog(JNIEnv* env);
int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
int register_android_server_SyntheticPasswordManager(JNIEnv* env);
+int register_android_server_GraphicsStatsService(JNIEnv* env);
};
using namespace android;
@@ -87,6 +88,7 @@
register_android_server_HardwarePropertiesManagerService(env);
register_android_server_storage_AppFuse(env);
register_android_server_SyntheticPasswordManager(env);
+ register_android_server_GraphicsStatsService(env);
return JNI_VERSION_1_4;
}