Last active
December 26, 2018 15:34
-
-
Save d4vidi/cef4d84b3d86518ed0861aeb1b3d3ad1 to your computer and use it in GitHub Desktop.
This is an improvement of Parashuram's `PerfLogger` described in http://blog.nparashuram.com/2018/11/react-native-performance-playbook-part-i.html, which allows for custom sections / events reporting alongside RN core's markers, and also enables the alignment of the report's 0-time with `Appication.onCreate()`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.parashuram; | |
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.LinkedList; | |
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 { | |
/** 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(); | |
} | |
PerfLoggerRecord(String name, String tag, int instanceKey, long time) { | |
mTime = time; | |
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) | |
}); | |
} | |
} | |
// 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 List<PerfLoggerRecord> mPerfLoggerRecords = new LinkedList<>(); | |
private ReactNativeHost mReactNativeHost; | |
private static PerfLogger sInstance; | |
public static void createInstance(Long startTime) { | |
sInstance = new PerfLogger(null, startTime); | |
sInstance.initialize(); | |
sInstance.logCustomEventMarker("BEGINNING_OF_TIME", null, startTime); | |
} | |
public static PerfLogger getInstance() { | |
return sInstance; | |
} | |
private PerfLogger(ReactNativeHost reactNativeHost, Long startTime) { | |
mStartTime = startTime; | |
mReactNativeHost = reactNativeHost; | |
} | |
private void initialize() { | |
addReactMarkerListener(); | |
addTTIEndListener(); | |
// setVariableForJS(null); | |
} | |
public void setReactNativeHost(ReactNativeHost host) { | |
mReactNativeHost = host; | |
} | |
public void logCustomStartMarker(String name, String tag) { | |
logCustomStartMarker(name, tag, System.currentTimeMillis()); | |
} | |
public void logCustomStartMarker(String name, String tag, Long time) { | |
logCustomMarker("@" + name + "_START", tag, time); | |
} | |
public void logCustomEndMarker(String name, String tag) { | |
logCustomEndMarker(name, tag, System.currentTimeMillis()); | |
} | |
public void logCustomEndMarker(String name, String tag, Long time) { | |
logCustomMarker("@" + name + "_END", tag, time); | |
} | |
public void logCustomEventMarker(String name, String tag) { | |
logCustomEventMarker(name, tag, System.currentTimeMillis()); | |
} | |
public void logCustomEventMarker(String name, String tag, Long time) { | |
logCustomStartMarker(name, tag, time); | |
logCustomEndMarker(name, tag, time + 1); | |
} | |
private void logCustomMarker(String name, String tag, Long time) { | |
Log.e("ASDASD", name + ": " + tag); | |
mPerfLoggerRecords.add(new PerfLoggerRecord(name, tag, -1, time)); | |
} | |
/** | |
* 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) { | |
Log.e("ZXCZXC", name.toString() + ": " + tag); | |
mPerfLoggerRecords.add(new PerfLoggerRecord(name.toString(), tag, instanceKey)); | |
} | |
}); | |
} | |
/** | |
* 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 "{}"; | |
} | |
} | |
/** | |
* 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; | |
} | |
}); | |
} | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage in app (conceptual code):