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
This commit is contained in:
19
Android.mk
19
Android.mk
@@ -322,6 +322,7 @@ LOCAL_SRC_FILES += \
|
||||
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 @@ endif
|
||||
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
24
core/java/android/view/IGraphicsStatsCallback.aidl
Normal file
24
core/java/android/view/IGraphicsStatsCallback.aidl
Normal file
@@ -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();
|
||||
}
|
||||
@@ -25,9 +25,9 @@ import android.graphics.Bitmap;
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
*
|
||||
* @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 @@ public final class ThreadedRenderer {
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
mRootNode = RenderNode.adopt(rootNodePtr);
|
||||
mRootNode.setClipToBounds(false);
|
||||
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
|
||||
nSetName(mNativeProxy, name);
|
||||
|
||||
ProcessInitializer.sInstance.init(context, mNativeProxy);
|
||||
|
||||
@@ -814,15 +811,6 @@ public final class ThreadedRenderer {
|
||||
nCancelLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -884,20 +872,29 @@ public final class ThreadedRenderer {
|
||||
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
|
||||
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 @@ public final class ThreadedRenderer {
|
||||
|
||||
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);
|
||||
|
||||
@@ -887,9 +887,9 @@ public final class ViewRootImpl implements ViewParent,
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 @@ static jboolean android_view_ThreadedRenderer_supportsOpenGL(JNIEnv* env, jobjec
|
||||
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 @@ static void android_view_ThreadedRenderer_dumpProfileInfo(JNIEnv* env, jobject c
|
||||
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 @@ const char* const kClassPathName = "android/view/ThreadedRenderer";
|
||||
|
||||
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 @@ static const JNINativeMethod gMethods[] = {
|
||||
{ "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},
|
||||
|
||||
@@ -21,6 +21,7 @@ option java_outer_classname = "IncidentProtoMetadata";
|
||||
|
||||
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 @@ message IncidentProto {
|
||||
android.providers.settings.SettingsServiceDumpProto settings = 3002;
|
||||
android.service.appwidget.AppWidgetServiceDumpProto appwidget = 3003;
|
||||
android.service.notification.NotificationServiceDumpProto notification = 3004;
|
||||
android.service.GraphicsStatsServiceDumpProto graphicsstats = 3005;
|
||||
}
|
||||
|
||||
78
core/proto/android/service/graphicsstats.proto
Normal file
78
core/proto/android/service/graphicsstats.proto
Normal file
@@ -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;
|
||||
}
|
||||
@@ -47,6 +47,7 @@ hwui_src_files := \
|
||||
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 @@ LOCAL_SRC_FILES += \
|
||||
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 \
|
||||
|
||||
@@ -110,7 +110,7 @@ static uint32_t frameCountIndexForFrameTime(nsecs_t frameTime) {
|
||||
}
|
||||
|
||||
// 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 @@ static uint32_t frameTimeForFrameCountIndex(uint32_t index) {
|
||||
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 @@ void JankTracker::freeData() {
|
||||
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::addFrame(const FrameInfo& frame) {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ void JankTracker::dumpData(const ProfileData* data, int fd) {
|
||||
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");
|
||||
|
||||
@@ -54,29 +54,50 @@ struct ProfileData {
|
||||
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 @@ private:
|
||||
nsecs_t mDequeueTimeForgiveness = 0;
|
||||
ProfileData* mData;
|
||||
bool mIsMapped = false;
|
||||
ProfileDataDescription mDescription;
|
||||
};
|
||||
|
||||
} /* namespace uirenderer */
|
||||
|
||||
@@ -22,11 +22,12 @@ LOCAL_SHARED_LIBRARIES += \
|
||||
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
|
||||
|
||||
@@ -590,6 +590,7 @@ DeferredLayerUpdater* CanvasContext::createTextureLayer() {
|
||||
}
|
||||
|
||||
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 @@ void CanvasContext::resetFrameStats() {
|
||||
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;
|
||||
|
||||
@@ -155,8 +155,7 @@ public:
|
||||
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();
|
||||
|
||||
|
||||
@@ -487,9 +487,22 @@ CREATE_BRIDGE2(setProcessStatsBuffer, RenderThread* thread, int fd) {
|
||||
|
||||
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() {
|
||||
|
||||
@@ -113,7 +113,8 @@ public:
|
||||
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();
|
||||
|
||||
@@ -74,6 +74,7 @@ protected:
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
287
libs/hwui/service/GraphicsStatsService.cpp
Normal file
287
libs/hwui/service/GraphicsStatsService.cpp
Normal file
@@ -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 */
|
||||
60
libs/hwui/service/GraphicsStatsService.h
Normal file
60
libs/hwui/service/GraphicsStatsService.h
Normal file
@@ -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 */
|
||||
159
libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
Normal file
159
libs/hwui/tests/unit/GraphicsStatsServiceTests.cpp
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
|
||||
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 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
|
||||
|
||||
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 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
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 @@ public class GraphicsStatsService extends IGraphicsStats.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ LOCAL_SRC_FILES += \
|
||||
$(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 @@ LOCAL_C_INCLUDES += \
|
||||
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 @@ LOCAL_SHARED_LIBRARIES += \
|
||||
libhidltransport \
|
||||
libhwbinder \
|
||||
libutils \
|
||||
libhwui \
|
||||
android.hardware.audio.common@2.0 \
|
||||
android.hardware.contexthub@1.0 \
|
||||
android.hardware.gnss@1.0 \
|
||||
|
||||
110
services/core/jni/com_android_server_GraphicsStatsService.cpp
Normal file
110
services/core/jni/com_android_server_GraphicsStatsService.cpp
Normal file
@@ -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
|
||||
@@ -46,6 +46,7 @@ int register_android_server_PersistentDataBlockService(JNIEnv* env);
|
||||
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 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user