Skip to content

Commit 3face70

Browse files
authored
Merge pull request #4 from bitvavo/refactor/better-statup-time-measure
refactor: add a better way to measure startup time
2 parents fd9a135 + 8438b12 commit 3face70

File tree

6 files changed

+120
-103
lines changed

6 files changed

+120
-103
lines changed

packages/react-native-tracing/android/src/main/java/com/grafana/nativeinstrumentation/NativeInstrumentationModule.java

+38-58
Original file line numberDiff line numberDiff line change
@@ -11,93 +11,73 @@
1111
import com.facebook.react.bridge.ReactMarker;
1212
import com.facebook.react.bridge.ReactMarkerConstants;
1313
import com.facebook.react.uimanager.events.RCTEventEmitter;
14+
import android.os.Process;
15+
import android.os.SystemClock;
16+
import java.math.BigInteger;
17+
import android.util.Log;
1418

1519
@ReactModule(name = NativeInstrumentationModule.NAME)
1620
public class NativeInstrumentationModule extends ReactContextBaseJavaModule implements RCTEventEmitter {
17-
public static final String NAME = "NativeInstrumentation";
18-
private static Long startTime = null;
19-
20-
private static Double cachedStartStartupTime = null;
21-
private static Double cachedEndStartupTime = null;
22-
private static Double cachedStartupDuration = null;
21+
private static boolean hasAppRestarted = false;
22+
private static int bundleLoadCounter = 0;
2323

2424
static {
2525
ReactMarker.addListener((name, tag, instanceKey) -> {
26-
long currentTime = System.currentTimeMillis();
27-
2826
if (name == ReactMarkerConstants.PRE_RUN_JS_BUNDLE_START) {
29-
android.util.Log.d(NAME, String.format("JS bundle load started at: %d", currentTime));
30-
initializeNativeInstrumentation();
27+
if (!hasAppRestarted) {
28+
if (bundleLoadCounter > 0) {
29+
hasAppRestarted = true;
30+
}
31+
bundleLoadCounter++;
32+
}
3133
}
3234
});
3335
}
3436

3537
public NativeInstrumentationModule(ReactApplicationContext reactContext) {
3638
super(reactContext);
37-
android.util.Log.d(NAME, "Module constructor called");
3839
}
3940

4041
@Override
4142
public String getName() {
4243
return NAME;
4344
}
4445

45-
public static void initializeNativeInstrumentation() {
46-
android.util.Log.d(NAME, "Initializing native instrumentation...");
47-
cachedStartStartupTime = null;
48-
cachedEndStartupTime = null;
49-
cachedStartupDuration = null;
50-
startTime = System.currentTimeMillis();
51-
android.util.Log.d(NAME, String.format("Initialized with start time: %d (previous metrics cleared)", startTime));
52-
}
46+
@ReactMethod(isBlockingSynchronousMethod = true)
47+
public double getStartupTimeSync() throws Exception {
48+
try {
49+
long currentTime = System.currentTimeMillis();
50+
long processStartTime = Process.getStartUptimeMillis();
51+
long currentUptime = SystemClock.uptimeMillis();
5352

54-
/**
55-
* Creates a fresh WritableMap with startup metrics.
56-
* Note: Each WritableMap can only be consumed once when passed through the React Native bridge.
57-
* This method ensures we always create a new instance for each request.
58-
*
59-
* Each map can be consumed once by the JS side (i.e., going through the bridge).
60-
*
61-
* @return A new WritableMap instance containing the startup metrics
62-
*/
63-
private WritableMap createStartupMetricsMap(double startStartupTime, double endStartupTime, double startupDuration) {
64-
WritableMap params = Arguments.createMap();
65-
params.putDouble("startStartupTime", startStartupTime);
66-
params.putDouble("endStartupTime", endStartupTime);
67-
params.putDouble("startupDuration", startupDuration);
68-
return params;
69-
}
53+
long startupTime = currentTime - currentUptime + processStartTime;
7054

71-
@ReactMethod
72-
public void getStartupTime(Promise promise) {
73-
android.util.Log.d(NAME, "Getting startup time...");
55+
return BigInteger.valueOf(startupTime).doubleValue();
56+
} catch (Exception e) {
57+
Log.e(NAME, "Error calculating startup time", e);
7458

75-
if (startTime == null) {
76-
android.util.Log.e(NAME, "Error: Start time was not initialized");
77-
promise.reject("NO_START_TIME", "[NativeInstrumentation] Start time was not initialized");
78-
return;
59+
throw e;
7960
}
61+
}
8062

81-
if (cachedStartupDuration != null) {
82-
android.util.Log.d(NAME, "Returning cached metrics");
83-
promise.resolve(createStartupMetricsMap(cachedStartStartupTime, cachedEndStartupTime, cachedStartupDuration));
84-
return;
85-
}
63+
@ReactMethod
64+
public void getStartupTime(Promise promise) {
65+
try {
66+
WritableMap response = Arguments.createMap();
8667

87-
long endTime = System.currentTimeMillis();
88-
double duration = (endTime - startTime) / 1000.0;
68+
double startupTime = getStartupTimeSync();
8969

90-
android.util.Log.d(NAME, String.format(
91-
"Calculating metrics - Start: %d, End: %d, Duration: %f seconds",
92-
startTime, endTime, duration
93-
));
70+
response.putDouble("startupTime", startupTime);
9471

95-
cachedStartStartupTime = (double) startTime;
96-
cachedEndStartupTime = (double) endTime;
97-
cachedStartupDuration = duration;
72+
promise.resolve(response);
73+
} catch (Exception e) {
74+
promise.reject("STARTUP_TIME_ERROR", "Failed to get startup time: " + e.getMessage(), e);
75+
}
76+
}
9877

99-
android.util.Log.d(NAME, "Metrics cached and being returned");
100-
promise.resolve(createStartupMetricsMap(cachedStartStartupTime, cachedEndStartupTime, cachedStartupDuration));
78+
@ReactMethod
79+
public void getHasAppRestarted(Promise promise) {
80+
promise.resolve(hasAppRestarted);
10181
}
10282

10383
@ReactMethod

packages/react-native-tracing/android/src/main/java/com/grafana/nativeinstrumentation/NativeInstrumentationPackage.java

-3
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,13 @@
1212
public class NativeInstrumentationPackage implements ReactPackage {
1313
@Override
1414
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
15-
android.util.Log.d("NativeInstrumentation", "Creating view managers (none needed)");
1615
return Collections.emptyList();
1716
}
1817

1918
@Override
2019
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
21-
android.util.Log.d("NativeInstrumentation", "Creating native modules");
2220
List<NativeModule> modules = new ArrayList<>();
2321
modules.add(new NativeInstrumentationModule(reactContext));
24-
android.util.Log.d("NativeInstrumentation", "Native instrumentation module added to modules list");
2522
return modules;
2623
}
2724
}

packages/react-native-tracing/ios/NativeInstrumentation.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ NS_ASSUME_NONNULL_BEGIN
77
- (void)getStartupTime:(RCTPromiseResolveBlock)resolve
88
rejecter:(RCTPromiseRejectBlock)reject;
99

10-
+ (void)initializeNativeInstrumentation;
10+
- (void)getHasAppRestarted:(RCTPromiseResolveBlock)resolve
11+
rejecter:(RCTPromiseRejectBlock)reject;
1112

1213
@end
1314

packages/react-native-tracing/ios/NativeInstrumentation.m

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ @interface RCT_EXTERN_REMAP_MODULE(NativeInstrumentation, NativeInstrumentation,
44

55
RCT_EXTERN_METHOD(getStartupTime:(RCTPromiseResolveBlock)resolve
66
rejecter:(RCTPromiseRejectBlock)reject)
7+
8+
RCT_EXTERN_METHOD(getHasAppRestarted:(RCTPromiseResolveBlock)resolve
9+
rejecter:(RCTPromiseRejectBlock)reject)
710

8-
@end
11+
@end

packages/react-native-tracing/ios/NativeInstrumentation.swift

+58-32
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ import React
33

44
@objc(NativeInstrumentation)
55
public class NativeInstrumentation: NSObject, RCTBridgeModule {
6-
private static var startTime: TimeInterval?
7-
private static var cachedMetrics: [String: Double]?
8-
9-
@objc
10-
public static func initializeNativeInstrumentation() {
11-
NativeInstrumentation.cachedMetrics = nil
12-
NativeInstrumentation.startTime = Date().timeIntervalSince1970
13-
}
14-
15-
override init() {
16-
super.init()
17-
}
6+
private static var hasAppRestarted: Bool = false
187

198
@objc
209
public static func requiresMainQueueSetup() -> Bool {
@@ -26,28 +15,65 @@ public class NativeInstrumentation: NSObject, RCTBridgeModule {
2615
return "NativeInstrumentation"
2716
}
2817

29-
@objc
30-
public func getStartupTime(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
31-
guard let startTime = NativeInstrumentation.startTime else {
32-
reject("NO_START_TIME", "[NativeInstrumentation] Start time was not initialized", nil)
18+
override init() {
19+
super.init()
20+
NotificationCenter.default.addObserver(
21+
self,
22+
selector: #selector(handleBundleLoadStart(_:)),
23+
name: NSNotification.Name("RCTJavaScriptWillStartLoadingNotification"),
24+
object: nil
25+
)
26+
}
27+
28+
@objc private func handleBundleLoadStart(_ notification: Notification) {
29+
if NativeInstrumentation.hasAppRestarted {
3330
return
3431
}
35-
36-
if let metrics = NativeInstrumentation.cachedMetrics {
37-
resolve(metrics)
38-
return
32+
33+
NativeInstrumentation.hasAppRestarted = true
34+
}
35+
36+
@objc
37+
public func getStartupTime(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
38+
do {
39+
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
40+
var size = MemoryLayout<kinfo_proc>.size
41+
var kp = kinfo_proc()
42+
43+
let result = mib.withUnsafeMutableBytes { mibBytes in
44+
withUnsafeMutablePointer(to: &size) { sizeBytes in
45+
withUnsafeMutablePointer(to: &kp) { kpBytes in
46+
sysctl(mibBytes.baseAddress?.assumingMemoryBound(to: Int32.self), 4,
47+
kpBytes,
48+
sizeBytes,
49+
nil, 0)
50+
}
51+
}
52+
}
53+
54+
let startTimeMs: Int64
55+
if result == 0 {
56+
let startTime = kp.kp_proc.p_un.__p_starttime
57+
startTimeMs = Int64(startTime.tv_sec) * 1000 + Int64(startTime.tv_usec) / 1000
58+
} else {
59+
throw NSError(domain: "NativeInstrumentation",
60+
code: Int(result),
61+
userInfo: [NSLocalizedDescriptionKey: "Failed to get process info"])
62+
}
63+
64+
let response = ["startupTime": startTimeMs]
65+
resolve(response)
66+
} catch {
67+
reject("STARTUP_TIME_ERROR", "Failed to get startup time: \(error.localizedDescription)", error)
3968
}
40-
41-
let endTime = Date().timeIntervalSince1970
42-
let duration = endTime - startTime
43-
44-
let metrics: [String: Double] = [
45-
"startStartupTime": startTime,
46-
"endStartupTime": endTime,
47-
"startupDuration": duration
48-
]
49-
50-
NativeInstrumentation.cachedMetrics = metrics
51-
resolve(metrics)
69+
}
70+
71+
@objc
72+
public func getHasAppRestarted(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
73+
resolve(NativeInstrumentation.hasAppRestarted)
74+
}
75+
76+
deinit {
77+
NotificationCenter.default.removeObserver(self)
5278
}
5379
}
+18-8
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
import React, { useEffect } from 'react';
2+
// @ts-ignore
3+
// eslint-disable-next-line import/namespace
4+
import { Alert, NativeModules } from 'react-native';
25

36
import { api } from './dependencies';
47

58
interface StartupMetrics {
6-
startStartupTime: number;
7-
endStartupTime: number;
8-
startupDuration: number;
9+
startupTime: number;
910
}
1011

11-
// TODO(@lucasbento): figure out where to best place this function
1212
const measureStartupTime = async (): Promise<void> => {
1313
try {
14-
const metrics: StartupMetrics =
15-
await require('react-native')['NativeModules']['NativeInstrumentation'].getStartupTime();
14+
const hasAppRestarted = await NativeModules['NativeInstrumentation'].getHasAppRestarted();
15+
16+
if (hasAppRestarted) {
17+
return;
18+
}
19+
20+
const metrics: StartupMetrics = await NativeModules['NativeInstrumentation'].getStartupTime();
21+
22+
const currentTime = Date.now();
23+
const startupDuration = currentTime - metrics.startupTime;
1624

1725
api.pushMeasurement({
1826
type: 'app_startup_time',
1927
values: {
20-
startup_duration: metrics.startupDuration,
28+
startup_duration: startupDuration,
2129
},
30+
timestamp: new Date().toISOString(),
2231
});
23-
} catch (error) {
32+
} catch (error: unknown) {
2433
console.warn('[NativeInstrumentation] Failed to measure startup time:', error);
2534
}
2635
};
@@ -31,6 +40,7 @@ export function wrap<P extends object>(WrappedComponent: React.ComponentType<P>)
3140
measureStartupTime();
3241
}, []);
3342

43+
// @ts-ignore
3444
return <WrappedComponent {...props} />;
3545
};
3646
}

0 commit comments

Comments
 (0)