diff --git a/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h b/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h index 167ef5144..edf61082c 100644 --- a/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h +++ b/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h @@ -1,5 +1,7 @@ #import #import "BugsnagPerformanceConfiguration.h" +#import "BugsnagPerformanceSpan.h" +#import "BugsnagPerformanceSpanOptions.h" NS_ASSUME_NONNULL_BEGIN @@ -18,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN - (BugsnagPerformanceConfiguration * _Nullable)getConfiguration; +- (BugsnagPerformanceSpan * _Nullable)startSpan:(NSString *)name options:(BugsnagPerformanceSpanOptions *)options; + #pragma mark Shared Instance + (instancetype _Nullable) sharedInstance; diff --git a/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m b/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m index 66a1a1131..bb94f0e62 100644 --- a/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m +++ b/packages/platforms/react-native/ios/BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m @@ -28,6 +28,16 @@ + (void)initialize { } } + if ((err = [cls mapAPINamed:@"startSpanV1" + toSelector:@selector(startSpan:options:)]) != nil) { + NSLog(@"Failed to map Bugsnag Performance API startSpanV1: %@", err); + NSString *mapped = err.userInfo[BSGUserInfoKeyMapped]; + if (![mapped isEqualToString:BSGUserInfoValueMappedYes]) { + // Must abort because this method is not mapped, so we'd crash if we tried to call it. + return; + } + } + // Our "sharedInstance" will actually be the cross-talk API whose class we loaded. bugsnagPerformanceCrossTalkAPI = [cls sharedInstance]; } diff --git a/packages/platforms/react-native/ios/BugsnagPerformanceSpan.h b/packages/platforms/react-native/ios/BugsnagPerformanceSpan.h new file mode 100644 index 000000000..0d720056b --- /dev/null +++ b/packages/platforms/react-native/ios/BugsnagPerformanceSpan.h @@ -0,0 +1,28 @@ +#import "BugsnagPerformanceSpanContext.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface BugsnagPerformanceSpan : BugsnagPerformanceSpanContext + +@property(nonatomic,readonly) BOOL isValid; + +@property (nonatomic,readonly) NSString *name; +@property (nonatomic,readonly) NSDate *_Nullable startTime; +@property (nonatomic,readonly) NSDate *_Nullable endTime; + +@property (nonatomic,readwrite) SpanId parentId; +@property (nonatomic,readonly) NSMutableDictionary *attributes; + +- (void)abortIfOpen; + +- (void)abortUnconditionally; + +- (void)end; + +- (void)endWithEndTime:(NSDate *)endTime NS_SWIFT_NAME(end(endTime:)); + +- (void)setAttribute:(NSString *)attributeName withValue:(_Nullable id)value; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/packages/platforms/react-native/ios/BugsnagPerformanceSpanContext.h b/packages/platforms/react-native/ios/BugsnagPerformanceSpanContext.h new file mode 100644 index 000000000..7032e2e7c --- /dev/null +++ b/packages/platforms/react-native/ios/BugsnagPerformanceSpanContext.h @@ -0,0 +1,24 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef union { + __uint128_t value; + struct { + uint64_t lo; + uint64_t hi; + }; +} TraceId; + +typedef uint64_t SpanId; + +@interface BugsnagPerformanceSpanContext : NSObject + +@property(nonatomic) TraceId traceId; +@property(nonatomic) SpanId spanId; + +- (instancetype) initWithTraceIdHi:(uint64_t)traceIdHi traceIdLo:(uint64_t)traceIdLo spanId:(SpanId)spanId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/platforms/react-native/ios/BugsnagPerformanceSpanOptions.h b/packages/platforms/react-native/ios/BugsnagPerformanceSpanOptions.h new file mode 100644 index 000000000..c2565cc2f --- /dev/null +++ b/packages/platforms/react-native/ios/BugsnagPerformanceSpanOptions.h @@ -0,0 +1,27 @@ +#import "BugsnagPerformanceSpanContext.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(uint8_t, BSGFirstClass) { + BSGFirstClassNo = 0, + BSGFirstClassYes = 1, + BSGFirstClassUnset = 2, +}; + +// Affects whether or not a span should include rendering metrics +typedef NS_ENUM(uint8_t, BSGInstrumentRendering) { + BSGInstrumentRenderingNo = 0, // Never include rendering metrics + BSGInstrumentRenderingYes = 1, // Always include rendering metrics, as long as the autoInstrumentRendering configuration option is on + BSGInstrumentRenderingUnset = 2, // Include rendering metrics only if the span is first class, start and end times were not set when creating/closing the span and the autoInstrumentRendering configuration option is on +}; + +@interface BugsnagPerformanceSpanOptions : NSObject + +@property(nonatomic) NSDate * _Nullable startTime; +@property(nonatomic) BugsnagPerformanceSpanContext * _Nullable parentContext; +@property(nonatomic) BOOL makeCurrentContext; +@property(nonatomic) BSGFirstClass firstClass; +@property(nonatomic) BSGInstrumentRendering instrumentRendering; + +@end +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm index 5a93bb691..c43677cf1 100644 --- a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm +++ b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.mm @@ -41,6 +41,15 @@ @implementation BugsnagReactNativePerformance #endif } +static uint64_t hexStringToUInt64(NSString *hexString) { + uint64_t result = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:0]; + [scanner scanHexLongLong:&result]; + + return result; +} + static NSString *getRandomBytes() noexcept { const int POOL_SIZE = 1024; UInt8 bytes[POOL_SIZE]; @@ -129,6 +138,46 @@ @implementation BugsnagReactNativePerformance return config; } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(startNativeSpan:(NSString *)name + options:(NSDictionary *)options) { + + BugsnagPerformanceSpanOptions *spanOptions = [BugsnagPerformanceSpanOptions new]; + spanOptions.makeCurrentContext = NO; + spanOptions.firstClass = BSGFirstClassYes; + spanOptions.parentContext = nil; + + // Javascript start times are Unix nanosecond timestamps + NSNumber *startTime = options[@"startTime"]; + spanOptions.startTime = [NSDate dateWithTimeIntervalSince1970:([startTime doubleValue] / NSEC_PER_SEC)]; + + NSDictionary *parentContext = options[@"parentContext"]; + if (parentContext != nil) { + NSString *parentSpanId = parentContext[@"id"]; + NSString *parentTraceId = parentContext[@"traceId"]; + + uint64_t spanId = hexStringToUInt64(parentSpanId); + uint64_t traceIdHi = hexStringToUInt64([parentTraceId substringToIndex:16]); + uint64_t traceIdLo = hexStringToUInt64([parentTraceId substringFromIndex:16]); + + spanOptions.parentContext = [[BugsnagPerformanceSpanContext alloc] initWithTraceIdHi:traceIdHi + traceIdLo:traceIdLo spanId:spanId]; + } + + BugsnagPerformanceSpan *nativeSpan = [BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.sharedInstance startSpan:name options:spanOptions]; + [nativeSpan.attributes removeAllObjects]; + + NSMutableDictionary *span = [NSMutableDictionary new]; + span[@"name"] = nativeSpan.name; + span[@"id"] = [NSString stringWithFormat:@"%llx", nativeSpan.spanId]; + span[@"traceId"] = [NSString stringWithFormat:@"%llx%llx", nativeSpan.traceId.hi, nativeSpan.traceId.lo]; + span[@"startTime"] = [NSNumber numberWithDouble: [nativeSpan.startTime timeIntervalSince1970] * NSEC_PER_SEC]; + if (nativeSpan.parentId > 0) { + span[@"parentSpanId"] = [NSString stringWithFormat:@"%llx", nativeSpan.parentId]; + } + + return span; +} + #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params diff --git a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.xcodeproj/project.pbxproj b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.xcodeproj/project.pbxproj index a17c0721c..d11b21f59 100644 --- a/packages/platforms/react-native/ios/BugsnagReactNativePerformance.xcodeproj/project.pbxproj +++ b/packages/platforms/react-native/ios/BugsnagReactNativePerformance.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ DA396E142CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */; }; - DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.m in Sources */ = {isa = PBXBuildFile; fileRef = DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */; }; + DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.mm in Sources */ = {isa = PBXBuildFile; fileRef = DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */; }; DAE18DD72C58E02C00D52529 /* BugsnagReactNativePerformance.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */; }; /* End PBXBuildFile section */ @@ -30,8 +30,11 @@ DA396E122CCFD242009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h; sourceTree = ""; }; DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m; sourceTree = ""; }; DA396E152CCFD72E009B37C2 /* BugsnagPerformanceConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceConfiguration.h; sourceTree = ""; }; + DAC1DC592CF87E770009C7F9 /* BugsnagPerformanceSpanContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpanContext.h; sourceTree = ""; }; + DAC1DC5A2CF87EBA0009C7F9 /* BugsnagPerformanceSpanOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpanOptions.h; sourceTree = ""; }; + DAC1DC5B2CF8859A0009C7F9 /* BugsnagPerformanceSpan.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagPerformanceSpan.h; sourceTree = ""; }; DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BugsnagReactNativePerformance.h; sourceTree = ""; }; - DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagReactNativePerformance.m; sourceTree = ""; }; + DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BugsnagReactNativePerformance.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,11 +59,14 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( + DAC1DC5B2CF8859A0009C7F9 /* BugsnagPerformanceSpan.h */, + DAC1DC5A2CF87EBA0009C7F9 /* BugsnagPerformanceSpanOptions.h */, + DAC1DC592CF87E770009C7F9 /* BugsnagPerformanceSpanContext.h */, DA396E152CCFD72E009B37C2 /* BugsnagPerformanceConfiguration.h */, DA396E132CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m */, DA396E122CCFD242009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.h */, DAE18DD22C58DF2500D52529 /* BugsnagReactNativePerformance.h */, - DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.m */, + DAE18DD32C58DF2500D52529 /* BugsnagReactNativePerformance.mm */, 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; @@ -123,7 +129,7 @@ buildActionMask = 2147483647; files = ( DA396E142CCFD327009B37C2 /* BugsnagCocoaPerformanceFromBugsnagReactNativePerformance.m in Sources */, - DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.m in Sources */, + DAE18DD42C58DF2500D52529 /* BugsnagReactNativePerformance.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts index 1c9ab7cc4..45dcaf6be 100644 --- a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts +++ b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport' +import type { UnsafeObject } from 'react-native/Libraries/Types/CodegenTypes' import { TurboModuleRegistry } from 'react-native' export type DeviceInfo = { @@ -28,17 +29,12 @@ export type ParentContext = { traceId: string } -export type NativeSpanOptions = { - startTime: number | undefined - parentContext: ParentContext | null -} - export type NativeSpan = { name: string id: string traceId: string startTime: number - parentSpanId: string + parentSpanId: string | undefined } export interface Spec extends TurboModule { @@ -47,7 +43,7 @@ export interface Spec extends TurboModule { requestEntropyAsync: () => Promise isNativePerformanceAvailable: () => boolean getNativeConfiguration: () => NativeConfiguration | null - startNativeSpan: (name: string, options: NativeSpanOptions) => NativeSpan + startNativeSpan: (name: string, options: UnsafeObject) => NativeSpan } export default TurboModuleRegistry.get( diff --git a/packages/platforms/react-native/lib/clock.ts b/packages/platforms/react-native/lib/clock.ts index 597cc6f4f..4d142d26b 100644 --- a/packages/platforms/react-native/lib/clock.ts +++ b/packages/platforms/react-native/lib/clock.ts @@ -5,22 +5,27 @@ interface Performance { now: () => number } -const createClock = (performance: Performance): Clock => { +interface ReactNativeClock extends Clock { + toUnixNanoseconds: (time: number) => number +} + +const createClock = (performance: Performance): ReactNativeClock => { // Measurable "monotonic" time // In React Native, `performance.now` often returns some very high values, but does not expose the `timeOrigin` it uses to calculate what "now" is. // by storing the value of `performance.now` when the app starts, we can remove that value from any further `.now` calculations, and add it to the current "wall time" to get a useful timestamp. const startPerfTime = performance.now() const startWallTime = Date.now() + const toUnixNanoseconds = (time: number) => millisecondsToNanoseconds(time - startPerfTime + startWallTime) + return { now: () => performance.now(), date: () => new Date(performance.now() - startPerfTime + startWallTime), convert: (date: Date) => date.getTime() - startWallTime + startPerfTime, + // convert milliseconds since timeOrigin to unix time in nanoseconds + toUnixNanoseconds, // convert milliseconds since timeOrigin to full timestamp - toUnixTimestampNanoseconds: (time: number) => - millisecondsToNanoseconds( - time - startPerfTime + startWallTime - ).toString() + toUnixTimestampNanoseconds: (time: number) => toUnixNanoseconds(time).toString() } }