Skip to content

Commit

Permalink
Added performance instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
axemclion committed Jul 8, 2019
1 parent 40b1210 commit b9633cd
Show file tree
Hide file tree
Showing 7 changed files with 484 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.microsoft.appcenter.reactnative.crashes.AppCenterReactNativeCrashesPackage;
import com.microsoft.appcenter.reactnative.analytics.AppCenterReactNativeAnalyticsPackage;
import com.microsoft.appcenter.reactnative.appcenter.AppCenterReactNativePackage;
import com.nparashuram.PerfLogger;

import java.util.Arrays;
import java.util.List;
Expand All @@ -37,9 +38,9 @@ public boolean getUseDeveloperSupport() {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new NetInfoPackage(),
new RNDeviceInfo(),
new AsyncStoragePackage(),
new NetInfoPackage(),
new RNDeviceInfo(),
new AsyncStoragePackage(),
new RNLocalizePackage(),
new RNScreensPackage(),
new RNGestureHandlerPackage(),
Expand Down Expand Up @@ -69,5 +70,6 @@ public ReactNativeHost getReactNativeHost() {
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
new PerfLogger(getReactNativeHost()).initialize();
}
}
201 changes: 201 additions & 0 deletions android/app/src/main/java/com/nparashuram/PerfLogger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.nparashuram;

import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.uimanager.util.ReactFindViewUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

/**
* A class to record the Perf metrics that are emitted by {@link ReactMarker.MarkerListener} It
* records the metrics and sends them over to the server
*/
public class PerfLogger {
// TODO (axe) This also used as a global JS variable. Need a better way to expose this
private static final String TAG = "AXE_PERFLOGGER";
private final long mStartTime;
private final ReactNativeHost mReactNativeHost;

/** Class holds the individual records from the performance metrics */
private class PerfLoggerRecord {
private final long mTime;
private final String mName;
private final String mTag;
private final int mInstanceKey;
private final int mTid;
private final int mPid;

PerfLoggerRecord(String name, String tag, int instanceKey) {
mTime = System.currentTimeMillis();
mName = name;
mTag = tag;
mInstanceKey = instanceKey;
mPid = Process.myPid();
mTid = Process.myTid();
}

public JSONObject toJSON() {
JSONObject result = new JSONObject();
try {
result.put("time", mTime);
result.put("name", mName);
result.put("tag", mTag);
result.put("instanceKey", mInstanceKey);
result.put("pid", mPid);
result.put("tid", mTid);
return result;
} catch (JSONException e) {
return new JSONObject();
}
}

public String toString() {
return TextUtils.join(
",",
new String[] {
Long.toString(mTime),
mName,
mTag,
Integer.toString(mInstanceKey),
Integer.toString(mTid),
Integer.toString(mPid)
});
}
}

@Nullable private List<PerfLoggerRecord> mPerfLoggerRecords;

public PerfLogger(ReactNativeHost reactNativeHost) {
mStartTime = System.currentTimeMillis();
mPerfLoggerRecords = new ArrayList<>();
mReactNativeHost = reactNativeHost;
}

public void initialize() {
addReactMarkerListener();
addTTIEndListener();
setVariableForJS(null);
}

/**
* Currently, we set a global JS variable to send the data from {@link ReactMarker.MarkerListener}
* to JS This should ideally be a native module. The global variable is {@link PerfLogger#TAG}
*
* <p>Currently, we call it on start and when the initial loading is complete, but it can
* technically be called at the end of any scenario that needs to be profiled
*
* @param records
*/
private void setVariableForJS(List<PerfLoggerRecord> records) {
final ReactInstanceManager reactInstanceManager = mReactNativeHost.getReactInstanceManager();
ReactContext context = reactInstanceManager.getCurrentReactContext();
if (context != null) {
// Called when React Native is ready. In this file, its when TTI is complete
context.getCatalystInstance().setGlobalVariable(TAG, getPerfRecordsJSON(mStartTime, records));
} else {
// Called when React Native is not readt, in this file during {@link PerfLogger#initialize}
// In this case, we wait for React Native, and then set the global JS
reactInstanceManager.addReactInstanceEventListener(
new ReactInstanceManager.ReactInstanceEventListener() {
@Override
public void onReactContextInitialized(ReactContext context) {
reactInstanceManager.removeReactInstanceEventListener(this);
context
.getCatalystInstance()
// TODO (axe) Use a native module instead of setting a global JS
.setGlobalVariable(TAG, getPerfRecordsJSON(mStartTime, null));
}
});
}
}

private static String getPerfRecordsJSON(
long startTime, @Nullable List<PerfLoggerRecord> records) {
JSONObject result = new JSONObject();
try {
result.put("startTime", startTime);
if (records != null) {
JSONArray jsonRecords = new JSONArray();
for (PerfLoggerRecord record : records) {
// Log.d(TAG, record.toString());
jsonRecords.put(record.toJSON());
}
result.put("data", jsonRecords);
}
return result.toString();
} catch (JSONException e) {
Log.w(TAG, "Could not convert perf records to JSON", e);
return "{}";
}
}

/**
* This is the main functionality of this file. It basically listens to all the events and stores
* them
*/
private void addReactMarkerListener() {
ReactMarker.addListener(
new ReactMarker.MarkerListener() {
@Override
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
mPerfLoggerRecords.add(new PerfLoggerRecord(name.toString(), tag, instanceKey));
}
});
}

/**
* Waits for Loading to complete, also called a Time-To-Interaction (TTI) event. To indicate TTI
* completion, add a prop nativeID="tti_complete" to the component whose appearance indicates that
* the initial TTI or loading is complete
*/
private void addTTIEndListener() {
ReactFindViewUtil.addViewListener(
new ReactFindViewUtil.OnViewFoundListener() {
@Override
public String getNativeId() {
// This is the value of the nativeID property
return "tti_complete";
}

@Override
public void onViewFound(final View view) {
// Once we find the view, we also need to wait for it to be drawn
view.getViewTreeObserver()
// TODO (axe) Should be OnDrawListener instead of this
.addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
view.getViewTreeObserver().removeOnPreDrawListener(this);
mPerfLoggerRecords.add(new PerfLoggerRecord("TTI_COMPLETE", null, 0));
setVariableForJS(mPerfLoggerRecords);
return true;
}
});
}
});

// Send native information even if TTI_COMPLETE flag goes not show up
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
setVariableForJS(mPerfLoggerRecords);
}
}, 10 * 1000);
}
}
37 changes: 22 additions & 15 deletions app/components/schedule-cell/schedule-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Text } from "../text"
import { palette } from "../../theme"
import { formatToTimeZone } from "date-fns-timezone"
import { TIMEZONE } from "../../utils/info"
import { logComponentPerf } from "./../../../perf"

export class ScheduleCell extends React.Component<ScheduleCellProps, {}> {
state = { animatedSize: new Animated.Value(1) }
Expand All @@ -39,7 +40,8 @@ export class ScheduleCell extends React.Component<ScheduleCellProps, {}> {
const isOdd = index % 2 === 0 // index starts at 0
const speakerName = talk.speakers.map(s => s.name).join(", ")
if (!talk) return null
return (
return logComponentPerf(
`ScheduleCell_${talk.title}`,
<TouchableWithoutFeedback
onPressIn={this.handlePressIn}
onPressOut={this.handlePressOut}
Expand All @@ -54,23 +56,28 @@ export class ScheduleCell extends React.Component<ScheduleCellProps, {}> {
>
{noTime ? this.renderTopBorder() : this.renderTime()}
<View style={style.contentWrapper as ViewStyle}>
<View style={style.imageWrapper as ViewStyle}>{this.renderImage()}</View>
<View style={style.content as ViewStyle}>
<Text preset="subheader" text={talk.title} style={style.title as TextStyle} />
{talk.speakers && talk.speakers.length > 0 && talk.speakers[0].name && (
<Text preset="subheader" text={speakerName} style={style.speaker as TextStyle} />
)}
{preset === "afterparty" && (
<Text
preset="subheader"
text={talk.description}
style={style.speaker as TextStyle}
/>
)}
<View style={style.imageWrapper as ViewStyle}>
{logComponentPerf("ScheduleCell_image", this.renderImage())}
</View>
{logComponentPerf(
"ScheduleCell_text",
<View style={style.content as ViewStyle}>
<Text preset="subheader" text={talk.title} style={style.title as TextStyle} />
{talk.speakers && talk.speakers.length > 0 && talk.speakers[0].name && (
<Text preset="subheader" text={speakerName} style={style.speaker as TextStyle} />
)}
{preset === "afterparty" && (
<Text
preset="subheader"
text={talk.description}
style={style.speaker as TextStyle}
/>
)}
</View>,
)}
</View>
</Animated.View>
</TouchableWithoutFeedback>
</TouchableWithoutFeedback>,
)
}

Expand Down
51 changes: 31 additions & 20 deletions app/components/schedule-nav/schedule-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react"
import { TextStyle, TouchableOpacity, View, ViewStyle } from "react-native"
import { Text } from "../text"
import { palette, spacing } from "../../theme"
import { logComponentPerf } from "./../../../perf"

const NAV_WRAPPER: ViewStyle = {
flexDirection: "row",
Expand Down Expand Up @@ -36,27 +37,37 @@ export class ScheduleNav extends React.Component<
> {
render() {
const { selected, onSelected } = this.props
return (
return logComponentPerf(
"NavDays",
<View style={NAV_WRAPPER}>
<TouchableOpacity
style={[NAV_BUTTON, selected === "wednesday" && NAV_BUTTON_SELECTED]}
onPress={() => onSelected("wednesday")}
>
<Text tx="scheduleScreen.nav.wednesday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>
<TouchableOpacity
style={[NAV_BUTTON, selected === "thursday" && NAV_BUTTON_SELECTED]}
onPress={() => onSelected("thursday")}
>
<Text tx="scheduleScreen.nav.thursday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>
<TouchableOpacity
style={[NAV_BUTTON, selected === "friday" && NAV_BUTTON_SELECTED, NAV_BUTTON_LAST]}
onPress={() => onSelected("friday")}
>
<Text tx="scheduleScreen.nav.friday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>
</View>
{logComponentPerf(
"Wednesday",
<TouchableOpacity
style={[NAV_BUTTON, selected === "wednesday" && NAV_BUTTON_SELECTED]}
onPress={() => onSelected("wednesday")}
>
<Text tx="scheduleScreen.nav.wednesday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>,
)}
{logComponentPerf(
"Thursday",
<TouchableOpacity
style={[NAV_BUTTON, selected === "thursday" && NAV_BUTTON_SELECTED]}
onPress={() => onSelected("thursday")}
>
<Text tx="scheduleScreen.nav.thursday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>,
)}
{logComponentPerf(
"Friday",
<TouchableOpacity
style={[NAV_BUTTON, selected === "friday" && NAV_BUTTON_SELECTED, NAV_BUTTON_LAST]}
onPress={() => onSelected("friday")}
>
<Text tx="scheduleScreen.nav.friday" style={NAV_TEXT} preset="sectionHeader" />
</TouchableOpacity>,
)}
</View>,
)
}
}
Loading

0 comments on commit b9633cd

Please sign in to comment.