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:
John Reck
2017-01-19 15:56:21 -08:00
parent f8a420097e
commit df1742ed47
24 changed files with 1172 additions and 168 deletions

View File

@@ -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

View File

@@ -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);
}

View 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();
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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},

View File

@@ -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;
}

View 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;
}

View File

@@ -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 \

View File

@@ -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");

View File

@@ -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 */

View File

@@ -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

View File

@@ -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;

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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

View 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 */

View 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 */

View 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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 \

View 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

View File

@@ -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;
}