Commit 0e40462e authored by John Reck's avatar John Reck Committed by The Android Automerger
Browse files

Revert "DO NOT MERGE Backport of limited jank-tracking metrics"

This reverts commit 2614bd22.

Change-Id: I344b4cbaa0bb0caf50bceb806d1446ee27ea52d8
parent d5a4a1aa
......@@ -1083,7 +1083,7 @@ public final class ActivityThread {
@Override
public void dumpGfxInfo(FileDescriptor fd, String[] args) {
dumpGraphicsInfo(fd);
WindowManagerGlobal.getInstance().dumpGfxInfo(fd, args);
WindowManagerGlobal.getInstance().dumpGfxInfo(fd);
}
@Override
......
......@@ -140,19 +140,6 @@ public final class Choreographer {
private long mLastFrameTimeNanos;
private long mFrameIntervalNanos;
/**
* Contains information about the current frame for jank-tracking,
* mainly timings of key events along with a bit of metadata about
* view tree state
*
* TODO: Is there a better home for this? Currently Choreographer
* is the only one with CALLBACK_ANIMATION start time, hence why this
* resides here.
*
* @hide
*/
FrameInfo mFrameInfo = new FrameInfo();
/**
* Callback type: Input callback. Runs first.
* @hide
......@@ -526,7 +513,6 @@ public final class Choreographer {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
......@@ -555,18 +541,12 @@ public final class Choreographer {
return;
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
if (DEBUG) {
......
/*
* Copyright (C) 2015 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;
import android.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Class that contains all the timing information for the current frame. This
* is used in conjunction with the hardware renderer to provide
* continous-monitoring jank events
*
* All times in nanoseconds from CLOCK_MONOTONIC/System.nanoTime()
*
* To minimize overhead from System.nanoTime() calls we infer durations of
* things by knowing the ordering of the events. For example, to know how
* long layout & measure took it's displayListRecordStart - performTraversalsStart.
*
* These constants must be kept in sync with FrameInfo.h in libhwui and are
* used for indexing into AttachInfo's mFrameInfo long[], which is intended
* to be quick to pass down to native via JNI, hence a pre-packed format
*
* @hide
*/
final class FrameInfo {
long[] mFrameInfo = new long[9];
// Various flags set to provide extra metadata about the current frame
private static final int FLAGS = 0;
// Is this the first-draw following a window layout?
public static final long FLAG_WINDOW_LAYOUT_CHANGED = 1;
@IntDef(flag = true, value = {
FLAG_WINDOW_LAYOUT_CHANGED })
@Retention(RetentionPolicy.SOURCE)
public @interface FrameInfoFlags {}
// The intended vsync time, unadjusted by jitter
private static final int INTENDED_VSYNC = 1;
// Jitter-adjusted vsync time, this is what was used as input into the
// animation & drawing system
private static final int VSYNC = 2;
// The time of the oldest input event
private static final int OLDEST_INPUT_EVENT = 3;
// The time of the newest input event
private static final int NEWEST_INPUT_EVENT = 4;
// When input event handling started
private static final int HANDLE_INPUT_START = 5;
// When animation evaluations started
private static final int ANIMATION_START = 6;
// When ViewRootImpl#performTraversals() started
private static final int PERFORM_TRAVERSALS_START = 7;
// When View:draw() started
private static final int DRAW_START = 8;
public void setVsync(long intendedVsync, long usedVsync) {
mFrameInfo[INTENDED_VSYNC] = intendedVsync;
mFrameInfo[VSYNC] = usedVsync;
mFrameInfo[OLDEST_INPUT_EVENT] = Long.MAX_VALUE;
mFrameInfo[NEWEST_INPUT_EVENT] = 0;
mFrameInfo[FLAGS] = 0;
}
public void updateInputEventTime(long inputEventTime, long inputEventOldestTime) {
if (inputEventOldestTime < mFrameInfo[OLDEST_INPUT_EVENT]) {
mFrameInfo[OLDEST_INPUT_EVENT] = inputEventOldestTime;
}
if (inputEventTime > mFrameInfo[NEWEST_INPUT_EVENT]) {
mFrameInfo[NEWEST_INPUT_EVENT] = inputEventTime;
}
}
public void markInputHandlingStart() {
mFrameInfo[HANDLE_INPUT_START] = System.nanoTime();
}
public void markAnimationsStart() {
mFrameInfo[ANIMATION_START] = System.nanoTime();
}
public void markPerformTraversalsStart() {
mFrameInfo[PERFORM_TRAVERSALS_START] = System.nanoTime();
}
public void markDrawStart() {
mFrameInfo[DRAW_START] = System.nanoTime();
}
public void addFlags(@FrameInfoFlags long flags) {
mFrameInfo[FLAGS] |= flags;
}
}
......@@ -278,7 +278,7 @@ public abstract class HardwareRenderer {
/**
* Outputs extra debugging information in the specified file descriptor.
*/
abstract void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args);
abstract void dumpGfxInfo(PrintWriter pw, FileDescriptor fd);
/**
* Loads system properties used by the renderer. This method is invoked
......
......@@ -16,7 +16,8 @@
package android.view;
import android.annotation.IntDef;
import com.android.internal.R;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
......@@ -26,18 +27,16 @@ import android.graphics.drawable.Drawable;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.Trace;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.TimeUtils;
import android.view.Surface.OutOfResourcesException;
import android.view.View.AttachInfo;
import com.android.internal.R;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashSet;
......@@ -75,14 +74,6 @@ public class ThreadedRenderer extends HardwareRenderer {
PROFILE_PROPERTY_VISUALIZE_BARS,
};
private static final int FLAG_DUMP_FRAMESTATS = 1 << 0;
private static final int FLAG_DUMP_RESET = 1 << 1;
@IntDef(flag = true, value = {
FLAG_DUMP_FRAMESTATS, FLAG_DUMP_RESET })
@Retention(RetentionPolicy.SOURCE)
public @interface DumpFlags {}
// Size of the rendered content.
private int mWidth, mHeight;
......@@ -102,12 +93,12 @@ public class ThreadedRenderer extends HardwareRenderer {
private final float mLightRadius;
private final int mAmbientShadowAlpha;
private final int mSpotShadowAlpha;
private final float mDensity;
private long mNativeProxy;
private boolean mInitialized = false;
private RenderNode mRootNode;
private Choreographer mChoreographer;
private boolean mProfilingEnabled;
private boolean mRootNodeNeedsUpdate;
ThreadedRenderer(Context context, boolean translucent) {
......@@ -119,7 +110,6 @@ public class ThreadedRenderer extends HardwareRenderer {
(int) (255 * a.getFloat(R.styleable.Lighting_ambientShadowAlpha, 0) + 0.5f);
mSpotShadowAlpha = (int) (255 * a.getFloat(R.styleable.Lighting_spotShadowAlpha, 0) + 0.5f);
a.recycle();
mDensity = context.getResources().getDisplayMetrics().density;
long rootNodePtr = nCreateRootRenderNode();
mRootNode = RenderNode.adopt(rootNodePtr);
......@@ -224,7 +214,7 @@ public class ThreadedRenderer extends HardwareRenderer {
mRootNode.setLeftTopRightBottom(-mInsetLeft, -mInsetTop, mSurfaceWidth, mSurfaceHeight);
nSetup(mNativeProxy, mSurfaceWidth, mSurfaceHeight,
lightX, mLightY, mLightZ, mLightRadius,
mAmbientShadowAlpha, mSpotShadowAlpha, mDensity);
mAmbientShadowAlpha, mSpotShadowAlpha);
}
@Override
......@@ -243,25 +233,32 @@ public class ThreadedRenderer extends HardwareRenderer {
}
@Override
void dumpGfxInfo(PrintWriter pw, FileDescriptor fd, String[] args) {
void dumpGfxInfo(PrintWriter pw, FileDescriptor fd) {
pw.flush();
int flags = 0;
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "framestats":
flags |= FLAG_DUMP_FRAMESTATS;
break;
case "reset":
flags |= FLAG_DUMP_RESET;
break;
}
nDumpProfileInfo(mNativeProxy, fd);
}
private static int search(String[] values, String value) {
for (int i = 0; i < values.length; i++) {
if (values[i].equals(value)) return i;
}
nDumpProfileInfo(mNativeProxy, fd, flags);
return -1;
}
private static boolean checkIfProfilingRequested() {
String profiling = SystemProperties.get(HardwareRenderer.PROFILE_PROPERTY);
int graphType = search(VISUALIZERS, profiling);
return (graphType >= 0) || Boolean.parseBoolean(profiling);
}
@Override
boolean loadSystemProperties() {
boolean changed = nLoadSystemProperties(mNativeProxy);
boolean wantProfiling = checkIfProfilingRequested();
if (wantProfiling != mProfilingEnabled) {
mProfilingEnabled = wantProfiling;
changed = true;
}
if (changed) {
invalidateRoot();
}
......@@ -310,12 +307,20 @@ public class ThreadedRenderer extends HardwareRenderer {
@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
attachInfo.mIgnoreDirtyState = true;
long frameTimeNanos = mChoreographer.getFrameTimeNanos();
attachInfo.mDrawingTime = frameTimeNanos / TimeUtils.NANOS_PER_MS;
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();
long recordDuration = 0;
if (mProfilingEnabled) {
recordDuration = System.nanoTime();
}
updateRootDisplayList(view, callbacks);
if (mProfilingEnabled) {
recordDuration = System.nanoTime() - recordDuration;
}
attachInfo.mIgnoreDirtyState = false;
// register animating rendernodes which started animating prior to renderer
......@@ -332,8 +337,8 @@ public class ThreadedRenderer extends HardwareRenderer {
attachInfo.mPendingAnimatingRenderNodes = null;
}
final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo;
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos,
recordDuration, view.getResources().getDisplayMetrics().density);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
......@@ -495,9 +500,10 @@ public class ThreadedRenderer extends HardwareRenderer {
private static native boolean nPauseSurface(long nativeProxy, Surface window);
private static native void nSetup(long nativeProxy, int width, int height,
float lightX, float lightY, float lightZ, float lightRadius,
int ambientShadowAlpha, int spotShadowAlpha, float density);
int ambientShadowAlpha, int spotShadowAlpha);
private static native void nSetOpaque(long nativeProxy, boolean opaque);
private static native int nSyncAndDrawFrame(long nativeProxy, long[] frameInfo, int size);
private static native int nSyncAndDrawFrame(long nativeProxy,
long frameTimeNanos, long recordDuration, float density);
private static native void nDestroy(long nativeProxy);
private static native void nRegisterAnimatingRenderNode(long rootRenderNode, long animatingNode);
......@@ -517,6 +523,5 @@ public class ThreadedRenderer extends HardwareRenderer {
private static native void nStopDrawing(long nativeProxy);
private static native void nNotifyFramePending(long nativeProxy);
private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd,
@DumpFlags int dumpFlags);
private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd);
}
......@@ -56,7 +56,6 @@ import android.util.AndroidRuntimeException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.TypedValue;
import android.view.Surface.OutOfResourcesException;
import android.view.View.AttachInfo;
......@@ -1503,7 +1502,6 @@ public final class ViewRootImpl implements ViewParent,
// to resume them
mDirty.set(0, 0, mWidth, mHeight);
}
mChoreographer.mFrameInfo.addFlags(FrameInfo.FLAG_WINDOW_LAYOUT_CHANGED);
}
final int surfaceGenerationId = mSurface.getGenerationId();
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......@@ -2507,9 +2505,6 @@ public final class ViewRootImpl implements ViewParent,
}
}
mAttachInfo.mDrawingTime =
mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
// If accessibility focus moved, always invalidate the root.
......@@ -2629,6 +2624,7 @@ public final class ViewRootImpl implements ViewParent,
dirty.setEmpty();
mIsAnimating = false;
attachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.PFLAG_DRAWN;
if (DEBUG_DRAW) {
......@@ -5782,16 +5778,6 @@ public final class ViewRootImpl implements ViewParent,
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
deliverInputEvent(q);
}
......
......@@ -459,7 +459,7 @@ public final class WindowManagerGlobal {
}
}
public void dumpGfxInfo(FileDescriptor fd, String[] args) {
public void dumpGfxInfo(FileDescriptor fd) {
FileOutputStream fout = new FileOutputStream(fd);
PrintWriter pw = new FastPrintWriter(fout);
try {
......@@ -476,7 +476,7 @@ public final class WindowManagerGlobal {
HardwareRenderer renderer =
root.getView().mAttachInfo.mHardwareRenderer;
if (renderer != null) {
renderer.dumpGfxInfo(pw, fd, args);
renderer.dumpGfxInfo(pw, fd);
}
}
......
......@@ -49,7 +49,6 @@
#include <AnimationContext.h>
#include <DisplayListRenderer.h>
#include <FrameInfo.h>
#include <RenderNode.h>
#include <renderthread/RenderProxy.h>
......@@ -395,7 +394,7 @@ static jlong create(JNIEnv* env, jclass clazz, jlong rootNodePtr, jlong surfaceP
proxy->initialize(surface);
// Shadows can't be used via this interface, so just set the light source
// to all 0s. (and width & height are unused, TODO remove them)
proxy->setup(0, 0, (Vector3){0, 0, 0}, 0, 0, 0, 1.0f);
proxy->setup(0, 0, (Vector3){0, 0, 0}, 0, 0, 0);
return (jlong) proxy;
}
......@@ -407,11 +406,8 @@ static void setSurface(JNIEnv* env, jclass clazz, jlong rendererPtr, jlong surfa
static void draw(JNIEnv* env, jclass clazz, jlong rendererPtr) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(rendererPtr);
nsecs_t vsync = systemTime(CLOCK_MONOTONIC);
UiFrameInfoBuilder(proxy->frameInfo())
.setVsync(vsync, vsync)
.addFlag(FrameInfoFlags::kSurfaceCanvas);
proxy->syncAndDrawFrame();
nsecs_t frameTimeNs = systemTime(CLOCK_MONOTONIC);
proxy->syncAndDrawFrame(frameTimeNs, 0, 1.0f);
}
static void destroy(JNIEnv* env, jclass clazz, jlong rendererPtr) {
......
......@@ -283,10 +283,10 @@ static jboolean android_view_ThreadedRenderer_pauseSurface(JNIEnv* env, jobject
static void android_view_ThreadedRenderer_setup(JNIEnv* env, jobject clazz, jlong proxyPtr,
jint width, jint height,
jfloat lightX, jfloat lightY, jfloat lightZ, jfloat lightRadius,
jint ambientShadowAlpha, jint spotShadowAlpha, jfloat density) {
jint ambientShadowAlpha, jint spotShadowAlpha) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setup(width, height, (Vector3){lightX, lightY, lightZ}, lightRadius,
ambientShadowAlpha, spotShadowAlpha, density);
ambientShadowAlpha, spotShadowAlpha);
}
static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz,
......@@ -296,13 +296,9 @@ static void android_view_ThreadedRenderer_setOpaque(JNIEnv* env, jobject clazz,
}
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) {
LOG_ALWAYS_FATAL_IF(frameInfoSize != UI_THREAD_FRAME_INFO_SIZE,
"Mismatched size expectations, given %d expected %d",
frameInfoSize, UI_THREAD_FRAME_INFO_SIZE);
jlong proxyPtr, jlong frameTimeNanos, jlong recordDuration, jfloat density) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo());
return proxy->syncAndDrawFrame();
return proxy->syncAndDrawFrame(frameTimeNanos, recordDuration, density);
}
static void android_view_ThreadedRenderer_destroy(JNIEnv* env, jobject clazz,
......@@ -397,10 +393,10 @@ static void android_view_ThreadedRenderer_notifyFramePending(JNIEnv* env, jobjec
}
static void android_view_ThreadedRenderer_dumpProfileInfo(JNIEnv* env, jobject clazz,
jlong proxyPtr, jobject javaFileDescriptor, jint dumpFlags) {
jlong proxyPtr, jobject javaFileDescriptor) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
int fd = jniGetFDFromFileDescriptor(env, javaFileDescriptor);
proxy->dumpProfileInfo(fd, dumpFlags);
proxy->dumpProfileInfo(fd);
}
#endif
......@@ -434,9 +430,9 @@ static JNINativeMethod gMethods[] = {
{ "nInitialize", "(JLandroid/view/Surface;)Z", (void*) android_view_ThreadedRenderer_initialize },
{ "nUpdateSurface", "(JLandroid/view/Surface;)V", (void*) android_view_ThreadedRenderer_updateSurface },
{ "nPauseSurface", "(JLandroid/view/Surface;)Z", (void*) android_view_ThreadedRenderer_pauseSurface },
{ "nSetup", "(JIIFFFFIIF)V", (void*) android_view_ThreadedRenderer_setup },
{ "nSetup", "(JIIFFFFII)V", (void*) android_view_ThreadedRenderer_setup },
{ "nSetOpaque", "(JZ)V", (void*) android_view_ThreadedRenderer_setOpaque },
{ "nSyncAndDrawFrame", "(J[JI)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
{ "nSyncAndDrawFrame", "(JJJF)I", (void*) android_view_ThreadedRenderer_syncAndDrawFrame },
{ "nDestroy", "(J)V", (void*) android_view_ThreadedRenderer_destroy },
{ "nRegisterAnimatingRenderNode", "(JJ)V", (void*) android_view_ThreadedRenderer_registerAnimatingRenderNode },
{ "nInvokeFunctor", "(JZ)V", (void*) android_view_ThreadedRenderer_invokeFunctor },
......@@ -451,7 +447,7 @@ static JNINativeMethod gMethods[] = {
{ "nFence", "(J)V", (void*) android_view_ThreadedRenderer_fence },
{ "nStopDrawing", "(J)V", (void*) android_view_ThreadedRenderer_stopDrawing },
{ "nNotifyFramePending", "(J)V", (void*) android_view_ThreadedRenderer_notifyFramePending },
{ "nDumpProfileInfo", "(JLjava/io/FileDescriptor;I)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo },
{ "nDumpProfileInfo", "(JLjava/io/FileDescriptor;)V", (void*) android_view_ThreadedRenderer_dumpProfileInfo },
#endif
{ "setupShadersDiskCache", "(Ljava/lang/String;)V",
(void*) android_view_ThreadedRenderer_setupShadersDiskCache },
......
......@@ -18,7 +18,6 @@ ifeq ($(USE_OPENGL_RENDERER),true)
AssetAtlas.cpp \
DamageAccumulator.cpp \
FontRenderer.cpp \
FrameInfo.cpp \
GammaFontRenderer.cpp \
Caches.cpp \
DisplayList.cpp \
......@@ -33,7 +32,6 @@ ifeq ($(USE_OPENGL_RENDERER),true)
GradientCache.cpp \
Image.cpp \
Interpolator.cpp \
JankTracker.cpp \
Layer.cpp \
LayerCache.cpp \
LayerRenderer.cpp \
......
/*
* Copyright (C) 2015 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 "FrameInfo.h"
#include <cstring>
namespace android {
namespace uirenderer {
void FrameInfo::importUiThreadInfo(int64_t* info) {
memcpy(mFrameInfo, info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
}
} /* namespace uirenderer */
} /* namespace android */
/*
* Copyright (C) 2015 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.
*/
#ifndef FRAMEINFO_H_
#define FRAMEINFO_H_
#include "utils/Macros.h"
#include <cutils/compiler.h>
#include <utils/Timers.h>
#include <memory.h>
namespace android {
namespace uirenderer {
#define UI_THREAD_FRAME_INFO_SIZE 9
HWUI_ENUM(FrameInfoIndex,
kFlags = 0,
kIntendedVsync,
kVsync,
kOldestInputEvent,
kNewestInputEvent,
kHandleInputStart,
kAnimationStart,
kPerformTraversalsStart,
kDrawStart,
// End of UI frame info
kSyncStart,
kIssueDrawCommandsStart,
kSwapBuffers,
kFrameCompleted,
// Must be the last value!
kNumIndexes
);
HWUI_ENUM(FrameInfoFlags,
kWindowLayoutChanged = 1 << 0,
kRTAnimation = 1 << 1,
kSurfaceCanvas = 1 << 2,
);
class ANDROID_API UiFrameInfoBuilder {
public:
UiFrameInfoBuilder(int64_t* buffer) : mBuffer(buffer) {
memset(mBuffer, 0, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));
}
UiFrameInfoBuilder& setVsync(nsecs_t vsyncTime, nsecs_t intendedVsync) {
mBuffer[FrameInfoIndex::kVsync] = vsyncTime;
mBuffer[FrameInfoIndex::kIntendedVsync] = intendedVsync;
return *this;
}
UiFrameInfoBuilder& addFlag(FrameInfoFlagsEnum flag) {
mBuffer[FrameInfoIndex::kFlags] |= static_cast<uint64_t>(flag);
return *this;
}
private:
int64_t* mBuffer;
};
class FrameInfo {
public:
void importUiThreadInfo(int64_t* info);
void markSyncStart() {
mFrameInfo[FrameInfoIndex::kSyncStart] = systemTime(CLOCK_MONOTONIC);
}
void markIssueDrawCommandsStart() {
mFrameInfo[FrameInfoIndex::kIssueDrawCommandsStart] = systemTime(CLOCK_MONOTONIC);
}
void markSwapBuffers() {
mFrameInfo[FrameInfoIndex::kSwapBuffers] = systemTime(CLOCK_MONOTONIC);
}
void markFrameCompleted() {
mFrameInfo[FrameInfoIndex::kFrameCompleted] = systemTime(CLOCK_MONOTONIC);
}
int64_t operator[](FrameInfoIndexEnum index) const {
if (index == FrameInfoIndex::kNumIndexes) return 0;
return mFrameInfo[static_cast<int>(index)];
}
int64_t operator[](int index) const {
if (index < 0 || index >= FrameInfoIndex::kNumIndexes) return 0;
return mFrameInfo[static_cast<int>(index)];
}
private:
int64_t mFrameInfo[FrameInfoIndex::kNumIndexes];
};
} /* namespace uirenderer */
} /* namespace android */
#endif /* FRAMEINFO_H_ */
/*
* Copyright (C) 2015 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 "JankTracker.h"
#include <algorithm>
#include <cstdio>
#include <inttypes.h>
namespace android {
namespace uirenderer {
static const char* JANK_TYPE_NAMES[] = {
"Missed Vsync",
"High input latency",
"Slow UI thread",
"Slow bitmap uploads",
"Slow draw",
};
struct Comparison {
FrameInfoIndexEnum start;
FrameInfoIndexEnum end;
};
static const Comparison COMPARISONS[] = {
{FrameInfoIndex::kIntendedVsync, FrameInfoIndex::kVsync},
{FrameInfoIndex::kOldestInputEvent, FrameInfoIndex::kVsync},
{FrameInfoIndex::kVsync, FrameInfoIndex::kSyncStart},
{FrameInfoIndex::kSyncStart, FrameInfoIndex::kIssueDrawCommandsStart},
{FrameInfoIndex::kIssueDrawCommandsStart, FrameInfoIndex::kFrameCompleted},
};
// If the event exceeds 10 seconds throw it away, this isn't a jank event
// it's an ANR and will be handled as such
static const int64_t IGNORE_EXCEEDING = seconds_to_nanoseconds(10);
/*
* Frames that are exempt from jank metrics.
* First-draw frames, for example, are expected to
* be slow, this is hidden from the user with window animations and
* other tricks
*
* Similarly, we don't track direct-drawing via Surface:lockHardwareCanvas()
* for now
*
* TODO: kSurfaceCanvas can negatively impact other drawing by using up
* time on the RenderThread, figure out how to attribute that as a jank-causer
*/
static const int64_t EXEMPT_FRAMES_FLAGS
= FrameInfoFlags::kWindowLayoutChanged
| FrameInfoFlags::kSurfaceCanvas;
JankTracker::JankTracker(nsecs_t frameIntervalNanos) {
reset();
setFrameInterval(frameIntervalNanos);
}
void JankTracker::setFrameInterval(nsecs_t frameInterval) {
mFrameInterval = frameInterval;
mThresholds[kMissedVsync] = 1;
/*
* Due to interpolation and sample rate differences between the touch
* panel and the display (example, 85hz touch panel driving a 60hz display)
* we call high latency 1.5 * frameinterval
*
* NOTE: Be careful when tuning this! A theoretical 1,000hz touch panel
* on a 60hz display will show kOldestInputEvent - kIntendedVsync of being 15ms
* Thus this must always be larger than frameInterval, or it will fail
*/
mThresholds[kHighInputLatency] = static_cast<int64_t>(1.5 * frameInterval);
// Note that these do not add up to 1. This is intentional. It's to deal
// with variance in values, and should be sort of an upper-bound on what
// is reasonable to expect.
mThresholds[kSlowUI] = static_cast<int64_t>(.5 * frameInterval);
mThresholds[kSlowSync] = static_cast<int64_t>(.2 * frameInterval);
mThresholds[kSlowRT] = static_cast<int64_t>(.75 * frameInterval);
}
void JankTracker::addFrame(const FrameInfo& frame) {
using namespace FrameInfoIndex;
mTotalFrameCount++;
// Fast-path for jank-free frames
int64_t totalDuration = frame[kFrameCompleted] - frame[kIntendedVsync];
uint32_t framebucket = std::min(
static_cast<typeof sizeof(mFrameCounts)>(ns2ms(totalDuration)),
sizeof(mFrameCounts) / sizeof(mFrameCounts[0]));
// Keep the fast path as fast as possible.
if (CC_LIKELY(totalDuration < mFrameInterval)) {
mFrameCounts[framebucket]++;
return;
}
if (frame[kFlags] & EXEMPT_FRAMES_FLAGS) {
return;
}
mFrameCounts[framebucket]++;
mJankFrameCount++;
for (int i = 0; i < NUM_BUCKETS; i++) {
int64_t delta = frame[COMPARISONS[i].end] - frame[COMPARISONS[i].start];
if (delta >= mThresholds[i] && delta < IGNORE_EXCEEDING) {
mBuckets[i].count++;
}
}
}
void JankTracker::dump(int fd) {
FILE* file = fdopen(fd, "a");
fprintf(file, "\nFrame stats:");
fprintf(file, "\n Total frames rendered: %u", mTotalFrameCount);
fprintf(file, "\n Janky frames: %u (%.2f%%)", mJankFrameCount,
(float) mJankFrameCount / (float) mTotalFrameCount * 100.0f);
fprintf(file, "\n 90th percentile: %ums", findPercentile(90));
fprintf(file, "\n 95th percentile: %ums", findPercentile(95));
fprintf(file, "\n 99th percentile: %ums", findPercentile(99));
for (int i = 0; i < NUM_BUCKETS; i++) {
fprintf(file, "\n Number %s: %u", JANK_TYPE_NAMES[i], mBuckets[i].count);
}
fprintf(file, "\n");
fflush(file);
}
void JankTracker::reset() {
memset(mBuckets, 0, sizeof(mBuckets));
memset(mFrameCounts, 0, sizeof(mFrameCounts));
mTotalFrameCount = 0;
mJankFrameCount = 0;
}
uint32_t JankTracker::findPercentile(int percentile) {
int pos = percentile * mTotalFrameCount / 100;
int remaining = mTotalFrameCount - pos;
for (int i = sizeof(mFrameCounts) / sizeof(mFrameCounts[0]) - 1; i >= 0; i--) {
remaining -= mFrameCounts[i];
if (remaining <= 0) {
return i;
}
}
return 0;
}
} /* namespace uirenderer */
} /* namespace android */
/*
* Copyright (C) 2015 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.
*/
#ifndef JANKTRACKER_H_
#define JANKTRACKER_H_
#include "FrameInfo.h"
#include "renderthread/TimeLord.h"
#include "utils/RingBuffer.h"
#include <memory>
namespace android {
namespace uirenderer {
enum JankType {
kMissedVsync = 0,
kHighInputLatency,
kSlowUI,
kSlowSync,
kSlowRT,
// must be last
NUM_BUCKETS,
};
struct JankBucket {
// Number of frames that hit this bucket
uint32_t count;
};
// TODO: Replace DrawProfiler with this
class JankTracker {
public:
JankTracker(nsecs_t frameIntervalNanos);
void setFrameInterval(nsecs_t frameIntervalNanos);
void addFrame(const FrameInfo& frame);
void dump(int fd);
void reset();
private:
uint32_t findPercentile(int p);
JankBucket mBuckets[NUM_BUCKETS];
int64_t mThresholds[NUM_BUCKETS];
uint32_t mFrameCounts[128];
int64_t mFrameInterval;
uint32_t mTotalFrameCount;
uint32_t mJankFrameCount;
};
} /* namespace uirenderer */
} /* namespace android */
#endif /* JANKTRACKER_H_ */
......@@ -47,8 +47,7 @@ CanvasContext::CanvasContext(RenderThread& thread, bool translucent,
, mOpaque(!translucent)
, mCanvas(NULL)
, mHaveNewSurface(false)
, mRootRenderNode(rootRenderNode)
, mCurrentFrameInfo(NULL) {
, mRootRenderNode(rootRenderNode) {
mAnimationContext = contextFactory->createAnimationContext(mRenderThread.timeLord());
mRenderThread.renderState().registerCanvasContext(this);
}
......@@ -153,13 +152,9 @@ void CanvasContext::processLayerUpdate(DeferredLayerUpdater* layerUpdater) {
}
}
void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo) {
void CanvasContext::prepareTree(TreeInfo& info) {
mRenderThread.removeFrameCallback(this);
mCurrentFrameInfo = &mFrames.next();
mCurrentFrameInfo->importUiThreadInfo(uiFrameInfo);
mCurrentFrameInfo->markSyncStart();
info.damageAccumulator = &mDamageAccumulator;
info.renderer = mCanvas;
if (mPrefetechedLayers.size() && info.mode == TreeInfo::MODE_FULL) {
......@@ -209,7 +204,6 @@ void CanvasContext::draw() {
"drawRenderNode called on a context with no canvas or surface!");
profiler().markPlaybackStart();
mCurrentFrameInfo->markIssueDrawCommandsStart();
SkRect dirty;
mDamageAccumulator.finish(&dirty);
......@@ -247,19 +241,12 @@ void CanvasContext::draw() {
profiler().markPlaybackEnd();
// Even if we decided to cancel the frame, from the perspective of jank
// metrics the frame was swapped at this point
mCurrentFrameInfo->markSwapBuffers();
if (status & DrawGlInfo::kStatusDrew) {
swapBuffers();
} else {
mEglManager.cancelFrame();
}
// TODO: Use a fence for real completion?
mCurrentFrameInfo->markFrameCompleted();
mRenderThread.jankTracker().addFrame(*mCurrentFrameInfo);
profiler().finishFrame();
}
......@@ -272,14 +259,9 @@ void CanvasContext::doFrame() {
ATRACE_CALL();
profiler().startFrame();
int64_t frameInfo[UI_THREAD_FRAME_INFO_SIZE];
UiFrameInfoBuilder(frameInfo)
.addFlag(FrameInfoFlags::kRTAnimation)
.setVsync(mRenderThread.timeLord().computeFrameTimeNanos(),
mRenderThread.timeLord().latestVsync());
TreeInfo info(TreeInfo::MODE_RT_ONLY, mRenderThread.renderState());
prepareTree(info, frameInfo);
prepareTree(info);
if (info.out.canDrawThisFrame) {
draw();
}
......@@ -392,28 +374,6 @@ void CanvasContext::setTextureAtlas(RenderThread& thread,
thread.eglManager().setTextureAtlas(buffer, map, mapSize);
}
void CanvasContext::dumpFrames(int fd) {
FILE* file = fdopen(fd, "a");
fprintf(file, "\n\n---PROFILEDATA---");
for (size_t i = 0; i < mFrames.size(); i++) {
FrameInfo& frame = mFrames[i];
if (frame[FrameInfoIndex::kSyncStart] == 0) {
continue;
}
fprintf(file, "\n");
for (int i = 0; i < FrameInfoIndex::kNumIndexes; i++) {
fprintf(file, "%" PRId64 ",", frame[i]);
}
}
fprintf(file, "\n---PROFILEDATA---\n\n");
fflush(file);
}
void CanvasContext::resetFrameStats() {
mFrames.clear();
mRenderThread.jankTracker().reset();
}
} /* namespace renderthread */
} /* namespace uirenderer */
} /* namespace android */
......@@ -17,14 +17,7 @@
#ifndef CANVASCONTEXT_H_
#define CANVASCONTEXT_H_
#include "DamageAccumulator.h"
#include "DrawProfiler.h"
#include "IContextFactory.h"
#include "FrameInfo.h"
#include "RenderNode.h"
#include "utils/RingBuffer.h"
#include "renderthread/RenderTask.h"
#include "renderthread/RenderThread.h"
#include <set>
#include <cutils/compiler.h>
#include <EGL/egl.h>
......@@ -32,7 +25,14 @@
#include <utils/Functor.h>
#include <utils/Vector.h>
#include <set>
#include "../DamageAccumulator.h"
#include "../DrawProfiler.h"
#include "../IContextFactory.h"
#include "../RenderNode.h"
#include "RenderTask.h"
#include "RenderThread.h"
#define FUNCTOR_PROCESS_DELAY 4
namespace android {
namespace uirenderer {
......@@ -75,7 +75,7 @@ public:
void setOpaque(bool opaque);
void makeCurrent();
void processLayerUpdate(DeferredLayerUpdater* layerUpdater);
void prepareTree(TreeInfo& info, int64_t* uiFrameInfo);
void prepareTree(TreeInfo& info);
void draw();
void destroy();
......@@ -103,9 +103,6 @@ public:
DrawProfiler& profiler() { return mProfiler; }
void dumpFrames(int fd);
void resetFrameStats();
private:
friend class RegisterFrameCallbackTask;
// TODO: Replace with something better for layer & other GL object
......@@ -136,9 +133,6 @@ private:
const sp<RenderNode> mRootRenderNode;
DrawProfiler mProfiler;
FrameInfo* mCurrentFrameInfo;
// Ring buffer large enough for 1 second worth of frames
RingBuffer<FrameInfo, 60> mFrames;
std::set<RenderNode*> mPrefetechedLayers;
};
......
......@@ -34,6 +34,8 @@ namespace renderthread {
DrawFrameTask::DrawFrameTask()
: mRenderThread(NULL)
, mContext(NULL)
, mFrameTimeNanos(0)
, mRecordDurationNanos(0)
, mDensity(1.0f) // safe enough default
, mSyncResult(kSync_OK) {
}
......@@ -66,12 +68,18 @@ void DrawFrameTask::removeLayerUpdate(DeferredLayerUpdater* layer) {
}
}
int DrawFrameTask::drawFrame() {
int DrawFrameTask::drawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos) {
LOG_ALWAYS_FATAL_IF(!mContext, "Cannot drawFrame with no CanvasContext!");
mSyncResult = kSync_OK;
mFrameTimeNanos = frameTimeNanos;
mRecordDurationNanos = recordDurationNanos;
postAndWait();
// Reset the single-frame data
mFrameTimeNanos = 0;
mRecordDurationNanos = 0;
return mSyncResult;
}
......@@ -85,7 +93,7 @@ void DrawFrameTask::run() {
ATRACE_NAME("DrawFrame");
mContext->profiler().setDensity(mDensity);
mContext->profiler().startFrame();
mContext->profiler().startFrame(mRecordDurationNanos);
bool canUnblockUiThread;
bool canDrawThisFrame;
......@@ -114,7 +122,7 @@ void DrawFrameTask::run() {
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
ATRACE_CALL();
mRenderThread->timeLord().vsyncReceived(mFrameInfo[FrameInfoIndex::kVsync]);
mRenderThread->timeLord().vsyncReceived(mFrameTimeNanos);
mContext->makeCurrent();
Caches::getInstance().textureCache.resetMarkInUse();
......@@ -122,7 +130,7 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) {
mContext->processLayerUpdate(mLayers[i].get());
}
mLayers.clear();
mContext->prepareTree(info, mFrameInfo);
mContext->prepareTree(info);
// This is after the prepareTree so that any pending operations
// (RenderNode tree state, prefetched layers, etc...) will be flushed.
......
......@@ -25,7 +25,6 @@
#include "RenderTask.h"
#include "../Rect.h"
#include "../FrameInfo.h"
#include "../TreeInfo.h"
namespace android {
......@@ -63,9 +62,7 @@ public:
void removeLayerUpdate(DeferredLayerUpdater* layer);
void setDensity(float density) { mDensity = density; }
int drawFrame();
int64_t* frameInfo() { return mFrameInfo; }
int drawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos);
virtual void run();
......@@ -83,12 +80,12 @@ private:
/*********************************************
* Single frame data
*********************************************/
nsecs_t mFrameTimeNanos;
nsecs_t mRecordDurationNanos;
float mDensity;
std::vector< sp<DeferredLayerUpdater> > mLayers;
int mSyncResult;
int64_t mFrameInfo[UI_THREAD_FRAME_INFO_SIZE];
};
} /* namespace renderthread */
......
......@@ -16,14 +16,14 @@
#include "RenderProxy.h"
#include "DeferredLayerUpdater.h"
#include "DisplayList.h"
#include "LayerRenderer.h"
#include "Rect.h"
#include "renderthread/CanvasContext.h"
#include "renderthread/RenderTask.h"
#include "renderthread/RenderThread.h"
#include "utils/Macros.h"
#include "CanvasContext.h"
#include "RenderTask.h"
#include "RenderThread.h"
#include "../DeferredLayerUpdater.h"
#include "../DisplayList.h"
#include "../LayerRenderer.h"
#include "../Rect.h"
namespace android {
namespace uirenderer {
......@@ -52,11 +52,6 @@ namespace renderthread {
MethodInvokeRenderTask* task = new MethodInvokeRenderTask((RunnableMethod) Bridge_ ## method); \
ARGS(method) *args = (ARGS(method) *) task->payload()
HWUI_ENUM(DumpFlags,
kFrameStats = 1 << 0,
kReset = 1 << 1,
);
CREATE_BRIDGE4(createContext, RenderThread* thread, bool translucent,
RenderNode* rootRenderNode, IContextFactory* contextFactory) {
return new CanvasContext(*args->thread, args->translucent,
......@@ -97,7 +92,7 @@ void RenderProxy::destroyContext() {
}
CREATE_BRIDGE2(setFrameInterval, RenderThread* thread, nsecs_t frameIntervalNanos) {
args->thread->setFrameInterval(args->frameIntervalNanos);
args->thread->timeLord().setFrameInterval(args->frameIntervalNanos);
return NULL;
}
......@@ -180,8 +175,7 @@ CREATE_BRIDGE7(setup, CanvasContext* context, int width, int height,
}
void RenderProxy::setup(int width, int height, const Vector3& lightCenter, float lightRadius,
uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha, float density) {
mDrawFrameTask.setDensity(density);
uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha) {
SETUP_TASK(setup);
args->context = mContext;
args->width = width;
......@@ -205,12 +199,10 @@ void RenderProxy::setOpaque(bool opaque) {
post(task);
}
int64_t* RenderProxy::frameInfo() {
return mDrawFrameTask.frameInfo();
}
int RenderProxy::syncAndDrawFrame() {
return mDrawFrameTask.drawFrame();
int RenderProxy::syncAndDrawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos,
float density) {
mDrawFrameTask.setDensity(density);
return mDrawFrameTask.drawFrame(frameTimeNanos, recordDurationNanos);
}
CREATE_BRIDGE1(destroy, CanvasContext* context) {
......@@ -379,28 +371,19 @@ void RenderProxy::notifyFramePending() {
mRenderThread.queueAtFront(task);
}
CREATE_BRIDGE3(dumpProfileInfo, CanvasContext* context, int fd, int dumpFlags) {
CREATE_BRIDGE2(dumpProfileInfo, CanvasContext* context, int fd) {
args->context->profiler().dumpData(args->fd);
if (args->dumpFlags & DumpFlags::kFrameStats) {
args->context->dumpFrames(args->fd);
}
if (args->dumpFlags & DumpFlags::kReset) {
args->context->resetFrameStats();
}
return NULL;
}
void RenderProxy::dumpProfileInfo(int fd, int dumpFlags) {
void RenderProxy::dumpProfileInfo(int fd) {
SETUP_TASK(dumpProfileInfo);
args->context = mContext;
args->fd = fd;
args->dumpFlags = dumpFlags;
postAndWait(task);
}
CREATE_BRIDGE2(outputLogBuffer, int fd, RenderThread* thread) {
args->thread->jankTracker().dump(args->fd);
CREATE_BRIDGE1(outputLogBuffer, int fd) {
RenderNode::outputLogBuffer(args->fd);
return NULL;
}
......@@ -408,7 +391,6 @@ CREATE_BRIDGE2(outputLogBuffer, int fd, RenderThread* thread) {
void RenderProxy::outputLogBuffer(int fd) {
SETUP_TASK(outputLogBuffer);
args->fd = fd;
args->thread = &RenderThread::getInstance();
staticPostAndWait(task);
}
......
......@@ -71,10 +71,10 @@ public:
ANDROID_API void updateSurface(const sp<ANativeWindow>& window);
ANDROID_API bool pauseSurface(const sp<ANativeWindow>& window);
ANDROID_API void setup(int width, int height, const Vector3& lightCenter, float lightRadius,
uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha, float density);
uint8_t ambientShadowAlpha, uint8_t spotShadowAlpha);
ANDROID_API void setOpaque(bool opaque);
ANDROID_API int64_t* frameInfo();
ANDROID_API int syncAndDrawFrame();
ANDROID_API int syncAndDrawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos,
float density);
ANDROID_API void destroy();
ANDROID_API static void invokeFunctor(Functor* functor, bool waitForCompletion);
......@@ -95,7 +95,7 @@ public:
ANDROID_API void stopDrawing();
ANDROID_API void notifyFramePending();
ANDROID_API void dumpProfileInfo(int fd, int dumpFlags);
ANDROID_API void dumpProfileInfo(int fd);
ANDROID_API static void outputLogBuffer(int fd);
ANDROID_API void setTextureAtlas(const sp<GraphicBuffer>& buffer, int64_t* map, size_t size);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment