Skip to content

Commit

Permalink
feat: expose cocoa profiling via method channels
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind committed Aug 18, 2023
1 parent bba0ffa commit f9fd5d8
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 179 deletions.
45 changes: 41 additions & 4 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
let key = arguments?["key"] as? String
removeTag(key: key, result: result)

#if !os(tvOS) && !os(watchOS)
case "startProfiling":
startProfiling(call, result)

case "collectProfile":
collectProfile(call, result)
#endif

default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -450,8 +458,8 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
private func captureEnvelope(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let arguments = call.arguments as? [Any],
!arguments.isEmpty,
let data = (arguments.first as? FlutterStandardTypedData)?.data else {
print("Envelope is null or empty !")
let data = (arguments.first as? FlutterStandardTypedData)?.data else {
print("Envelope is null or empty!")
result(FlutterError(code: "2", message: "Envelope is null or empty", details: nil))
return
}
Expand Down Expand Up @@ -483,8 +491,8 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {

result(item)
#else
print("note: appStartMeasurement not available on this platform")
result(nil)
print("note: appStartMeasurement not available on this platform")
result(nil)
#endif
}

Expand Down Expand Up @@ -648,6 +656,35 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
result("")
}
}

private func startProfiling(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let traceId = call.arguments as? String else {
print("Cannot start profiling: trace ID missing")
result(FlutterError(code: "5", message: "Cannot start profiling: trace ID missing", details: nil))
return
}

let startTime = PrivateSentrySDKOnly.startProfiling(forTrace: SentryId(uuidString: traceId))
result(startTime)
}

private func collectProfile(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let arguments = call.arguments as? [String: Any],
let traceId = arguments["traceId"] as? String else {
print("Cannot collect profile: trace ID missing")
result(FlutterError(code: "6", message: "Cannot collect profile: trace ID missing", details: nil))
return
}

guard let startTime = arguments["startTime"] as? uint64 else {
print("Cannot collect profile: start time missing")
result(FlutterError(code: "7", message: "Cannot collect profile: start time missing", details: nil))
return
}

let payload = PrivateSentrySDKOnly.collectProfile(forTrace: SentryId(uuidString: traceId), since: startTime)
result(payload)
}
}

// swiftlint:enable function_body_length
8 changes: 8 additions & 0 deletions flutter/lib/src/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ class SentryNative {
return await _nativeChannel?.removeTag(key);
}

Future<int?> startProfiling(SentryId traceId) async {
return await _nativeChannel?.startProfiling(traceId);
}

Future<dynamic> collectProfile(SentryId traceId, int startTimeNs) async {
return await _nativeChannel?.collectProfile(traceId, startTimeNs);
}

/// Reset state
void reset() {
appStartEnd = null;
Expand Down
22 changes: 22 additions & 0 deletions flutter/lib/src/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ class SentryNativeChannel {
}
}

Future<int?> startProfiling(SentryId traceId) async {
try {
return await _channel.invokeMethod('startProfiling', traceId.toString())
as int?;
} catch (error, stackTrace) {
_logError('startProfiling', error, stackTrace);
return null;
}
}

Future<dynamic> collectProfile(SentryId traceId, int startTimeNs) async {
try {
return await _channel.invokeMethod('collectProfile', {
'traceId': traceId.toString(),
'startTime': startTimeNs
}) as Map<String, dynamic>?;
} catch (error, stackTrace) {
_logError('collectProfile', error, stackTrace);
return null;
}
}

// Helper

void _logError(String nativeMethodName, Object error, StackTrace stackTrace) {
Expand Down
31 changes: 30 additions & 1 deletion flutter/test/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ ISentrySpan startTransactionShim(
// ignore: invalid_use_of_internal_member
SentryTracer,
MethodChannel,
SentryNative,
], customMocks: [
MockSpec<Hub>(fallbackGenerators: {#startTransaction: startTransactionShim})
])
Expand Down Expand Up @@ -156,6 +155,7 @@ class NoOpHub with NoSuchMethodProvider implements Hub {
bool get isEnabled => false;
}

// TODO can this be replaced with https://pub.dev/packages/mockito#verifying-exact-number-of-invocations--at-least-x--never
class TestMockSentryNative implements SentryNative {
@override
DateTime? appStartEnd;
Expand Down Expand Up @@ -189,6 +189,8 @@ class TestMockSentryNative implements SentryNative {
var numberOfSetTagCalls = 0;
SentryUser? sentryUser;
var numberOfSetUserCalls = 0;
var numberOfStartProfilingCalls = 0;
var numberOfCollectProfileCalls = 0;

@override
Future<void> addBreadcrumb(Breadcrumb breadcrumb) async {
Expand Down Expand Up @@ -265,8 +267,21 @@ class TestMockSentryNative implements SentryNative {
this.sentryUser = sentryUser;
numberOfSetUserCalls++;
}

@override
Future<dynamic> collectProfile(SentryId traceId, int startTimeNs) {
numberOfCollectProfileCalls++;
return Future.value(null);
}

@override
Future<int?> startProfiling(SentryId traceId) {
numberOfStartProfilingCalls++;
return Future.value(null);
}
}

// TODO can this be replaced with https://pub.dev/packages/mockito#verifying-exact-number-of-invocations--at-least-x--never
class MockNativeChannel implements SentryNativeChannel {
NativeAppStart? nativeAppStart;
NativeFrames? nativeFrames;
Expand All @@ -283,6 +298,8 @@ class MockNativeChannel implements SentryNativeChannel {
int numberOfSetContextsCalls = 0;
int numberOfSetExtraCalls = 0;
int numberOfSetTagCalls = 0;
int numberOfStartProfilingCalls = 0;
int numberOfCollectProfileCalls = 0;

@override
Future<NativeAppStart?> fetchNativeAppStart() async => nativeAppStart;
Expand Down Expand Up @@ -343,6 +360,18 @@ class MockNativeChannel implements SentryNativeChannel {
Future<void> setTag(String key, value) async {
numberOfSetTagCalls += 1;
}

@override
Future<dynamic> collectProfile(SentryId traceId, int startTimeNs) {
numberOfCollectProfileCalls++;
return Future.value(null);
}

@override
Future<int?> startProfiling(SentryId traceId) {
numberOfStartProfilingCalls++;
return Future.value(null);
}
}

class MockRendererWrapper implements RendererWrapper {
Expand Down
176 changes: 2 additions & 174 deletions flutter/test/mocks.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import 'package:sentry/sentry.dart' as _i2;
import 'package:sentry/src/protocol.dart' as _i3;
import 'package:sentry/src/sentry_envelope.dart' as _i7;
import 'package:sentry/src/sentry_tracer.dart' as _i8;
import 'package:sentry_flutter/src/sentry_native.dart' as _i10;
import 'package:sentry_flutter/src/sentry_native_channel.dart' as _i11;

import 'mocks.dart' as _i12;
import 'mocks.dart' as _i10;

// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
Expand Down Expand Up @@ -495,176 +493,6 @@ class MockMethodChannel extends _i1.Mock implements _i9.MethodChannel {
);
}

/// A class which mocks [SentryNative].
///
/// See the documentation for Mockito's code generation for more information.
class MockSentryNative extends _i1.Mock implements _i10.SentryNative {
MockSentryNative() {
_i1.throwOnMissingStub(this);
}

@override
set appStartEnd(DateTime? _appStartEnd) => super.noSuchMethod(
Invocation.setter(
#appStartEnd,
_appStartEnd,
),
returnValueForMissingStub: null,
);
@override
set nativeChannel(_i11.SentryNativeChannel? nativeChannel) =>
super.noSuchMethod(
Invocation.setter(
#nativeChannel,
nativeChannel,
),
returnValueForMissingStub: null,
);
@override
bool get didFetchAppStart => (super.noSuchMethod(
Invocation.getter(#didFetchAppStart),
returnValue: false,
) as bool);
@override
_i6.Future<_i11.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod(
Invocation.method(
#fetchNativeAppStart,
[],
),
returnValue: _i6.Future<_i11.NativeAppStart?>.value(),
) as _i6.Future<_i11.NativeAppStart?>);
@override
_i6.Future<void> beginNativeFramesCollection() => (super.noSuchMethod(
Invocation.method(
#beginNativeFramesCollection,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<_i11.NativeFrames?> endNativeFramesCollection(
_i3.SentryId? traceId) =>
(super.noSuchMethod(
Invocation.method(
#endNativeFramesCollection,
[traceId],
),
returnValue: _i6.Future<_i11.NativeFrames?>.value(),
) as _i6.Future<_i11.NativeFrames?>);
@override
_i6.Future<void> setContexts(
String? key,
dynamic value,
) =>
(super.noSuchMethod(
Invocation.method(
#setContexts,
[
key,
value,
],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> removeContexts(String? key) => (super.noSuchMethod(
Invocation.method(
#removeContexts,
[key],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> setUser(_i3.SentryUser? sentryUser) => (super.noSuchMethod(
Invocation.method(
#setUser,
[sentryUser],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> addBreadcrumb(_i3.Breadcrumb? breadcrumb) =>
(super.noSuchMethod(
Invocation.method(
#addBreadcrumb,
[breadcrumb],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> clearBreadcrumbs() => (super.noSuchMethod(
Invocation.method(
#clearBreadcrumbs,
[],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> setExtra(
String? key,
dynamic value,
) =>
(super.noSuchMethod(
Invocation.method(
#setExtra,
[
key,
value,
],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> removeExtra(String? key) => (super.noSuchMethod(
Invocation.method(
#removeExtra,
[key],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> setTag(
String? key,
String? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setTag,
[
key,
value,
],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i6.Future<void> removeTag(String? key) => (super.noSuchMethod(
Invocation.method(
#removeTag,
[key],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
void reset() => super.noSuchMethod(
Invocation.method(
#reset,
[],
),
returnValueForMissingStub: null,
);
}

/// A class which mocks [Hub].
///
/// See the documentation for Mockito's code generation for more information.
Expand Down Expand Up @@ -882,7 +710,7 @@ class MockHub extends _i1.Mock implements _i2.Hub {
#customSamplingContext: customSamplingContext,
},
),
returnValue: _i12.startTransactionShim(
returnValue: _i10.startTransactionShim(
name,
operation,
description: description,
Expand Down
Loading

0 comments on commit f9fd5d8

Please sign in to comment.