Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d32ebb8
[Performance] Track async/await URLSession requests on iOS 16+ (#11861)
JesusRojass Mar 26, 2026
1e7eef9
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Mar 26, 2026
c132579
Extract shared async/await trace logic into reusable helpers
JesusRojass Mar 26, 2026
7ef6337
Remove > 0 guards on byte count assignment from task metrics
JesusRojass Mar 26, 2026
eede0d8
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Mar 31, 2026
c0e172c
Tidy up for failing CI/CD Checks
JesusRojass Apr 1, 2026
e0f53bb
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 1, 2026
70b2b4a
Update CHANGELOG.md
JesusRojass Apr 1, 2026
8d3c5ee
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 6, 2026
53035f0
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 10, 2026
2452f47
Update branch
JesusRojass Apr 15, 2026
deda323
Merge remote-tracking branch 'origin/JesusRojass/#11861' into JesusRo…
JesusRojass Apr 15, 2026
2b843a1
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 16, 2026
7607b7f
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 20, 2026
c261996
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 22, 2026
918b600
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass Apr 27, 2026
d8e6b41
Sync branch
JesusRojass May 8, 2026
3281b85
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass May 11, 2026
d83f417
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass May 13, 2026
acee1b3
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass May 13, 2026
3cee975
Merge branch 'firebase:main' into JesusRojass/#11861
JesusRojass May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,71 @@

@implementation FPRNSURLSessionDelegate

#pragma mark - Async/await support

/** Fires for every task created on the session, including tasks created by Swift async/await
* methods (iOS 16+). This is the only reliable hook that fires for async created "convenience
* tasks" where delegate data transfer callbacks are suppressed by the system.
* Only creates a trace when one has not already been attached by the ObjC task creation swizzle.
*/
- (void)URLSession:(NSURLSession *)session
didCreateTask:(NSURLSessionTask *)task API_AVAILABLE(ios(16.0), tvos(16.0)) {
@try {
// Skip if trace was already attached by the ObjC task creation swizzle path.
if ([FPRNetworkTrace networkTraceFromObject:task] != nil) {
return;
}
// Guard against nil request.
if (!task.originalRequest) {
return;
}
FPRNetworkTrace *trace = [[FPRNetworkTrace alloc] initWithURLRequest:task.originalRequest];
[trace start];
[FPRNetworkTrace addNetworkTrace:trace toObject:task];
} @catch (NSException *exception) {
FPRLogWarning(kFPRNetworkTraceNotTrackable, @"Unable to track network request.");
}
}

/** Fires after every task completes (iOS 10+), including async/await-created tasks where
* didCompleteWithError: is suppressed by the system ("convenience task" semantics). Using metrics
* also provides more accurate byte counts than incremental delegate callbacks.
* The traceCompleted guard inside FPRNetworkTrace ensures this is idempotent: for ObjC
* completion handler tasks the trace is already removed before metrics fire; for ObjC
* delegate based tasks the trace is completed here and didCompleteWithError: becomes a no op.
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
@try {
FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:task];
if (!trace) {
return;
}
// Use task metrics for accurate byte counts (more reliable than incremental callbacks).
NSURLSessionTaskTransactionMetrics *transactionMetrics = metrics.transactionMetrics.lastObject;
if (transactionMetrics) {
if (transactionMetrics.countOfResponseBodyBytesReceived > 0) {
trace.responseSize = transactionMetrics.countOfResponseBodyBytesReceived;
}
if (transactionMetrics.countOfRequestBodyBytesSent > 0) {
trace.requestSize = transactionMetrics.countOfRequestBodyBytesSent;
}
Comment thread
JesusRojass marked this conversation as resolved.
Outdated
}
// Ensure intermediate checkpoints are recorded before completion (idempotent).
[trace checkpointState:FPRNetworkTraceCheckpointStateRequestCompleted];
[trace checkpointState:FPRNetworkTraceCheckpointStateResponseReceived];
// Complete the trace. For ObjC delegate tasks this fires before didCompleteWithError:,
// making that subsequent call a no op via the traceCompleted guard.
[trace didCompleteRequestWithResponse:task.response error:task.error];
[FPRNetworkTrace removeNetworkTraceFromObject:task];
} @catch (NSException *exception) {
FPRLogWarning(kFPRNetworkTraceNotTrackable, @"Unable to track network request.");
}
}

#pragma mark - NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,89 @@ void InstrumentURLSessionDataTaskDidReceiveData(FPRClassInstrumentor *instrument
}
}

#pragma mark - Async/await support (NSURLSessionTaskDelegate)

/** Instruments URLSession:didCreateTask: (iOS 16+ / tvOS 16+).
* Fires for every task created on the session, including tasks created by Swift async/await
* methods, which bypass the ObjC task creation swizzle entirely on iOS 16+.
*
* @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
*/
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentURLSessionDidCreateTask(FPRClassInstrumentor *instrumentor) {
SEL selector = @selector(URLSession:didCreateTask:);
FPRSelectorInstrumentor *selectorInstrumentor =
[instrumentor instrumentorForInstanceSelector:selector];
if (selectorInstrumentor) {
IMP currentIMP = selectorInstrumentor.currentIMP;
[selectorInstrumentor
setReplacingBlock:^(id object, NSURLSession *session, NSURLSessionTask *task) {
@try {
// Only create a trace when the ObjC task creation swizzle has not already done so.
if ([FPRNetworkTrace networkTraceFromObject:task] == nil && task.originalRequest) {
FPRNetworkTrace *trace =
[[FPRNetworkTrace alloc] initWithURLRequest:task.originalRequest];
[trace start];
[FPRNetworkTrace addNetworkTrace:trace toObject:task];
}
} @catch (NSException *exception) {
FPRLogWarning(kFPRNetworkTraceNotTrackable, @"Unable to track network request.");
} @finally {
typedef void (*OriginalImp)(id, SEL, NSURLSession *, NSURLSessionTask *);
((OriginalImp)currentIMP)(object, selector, session, task);
}
}];
}
}

/** Instruments URLSession:task:didFinishCollectingMetrics: (iOS 10+).
* Fires after every task completes, including async/await-created tasks where
* didCompleteWithError: is suppressed by the system. The traceCompleted guard inside
* FPRNetworkTrace makes this idempotent with the existing ObjC completion paths.
*
* @param instrumentor The FPRClassInstrumentor to add the selector instrumentor to.
*/
FOUNDATION_STATIC_INLINE
NS_EXTENSION_UNAVAILABLE("Firebase Performance is not supported for extensions.")
void InstrumentURLSessionTaskDidFinishCollectingMetrics(FPRClassInstrumentor *instrumentor) {
SEL selector = @selector(URLSession:task:didFinishCollectingMetrics:);
FPRSelectorInstrumentor *selectorInstrumentor =
[instrumentor instrumentorForInstanceSelector:selector];
if (selectorInstrumentor) {
IMP currentIMP = selectorInstrumentor.currentIMP;
[selectorInstrumentor
setReplacingBlock:^(id object, NSURLSession *session, NSURLSessionTask *task,
NSURLSessionTaskMetrics *metrics) {
@try {
FPRNetworkTrace *trace = [FPRNetworkTrace networkTraceFromObject:task];
if (trace) {
NSURLSessionTaskTransactionMetrics *transactionMetrics =
metrics.transactionMetrics.lastObject;
if (transactionMetrics) {
if (transactionMetrics.countOfResponseBodyBytesReceived > 0) {
trace.responseSize = transactionMetrics.countOfResponseBodyBytesReceived;
}
if (transactionMetrics.countOfRequestBodyBytesSent > 0) {
trace.requestSize = transactionMetrics.countOfRequestBodyBytesSent;
}
Comment thread
JesusRojass marked this conversation as resolved.
Outdated
}
[trace checkpointState:FPRNetworkTraceCheckpointStateRequestCompleted];
[trace checkpointState:FPRNetworkTraceCheckpointStateResponseReceived];
[trace didCompleteRequestWithResponse:task.response error:task.error];
[FPRNetworkTrace removeNetworkTraceFromObject:task];
}
} @catch (NSException *exception) {
FPRLogWarning(kFPRNetworkTraceNotTrackable, @"Unable to track network request.");
} @finally {
typedef void (*OriginalImp)(id, SEL, NSURLSession *, NSURLSessionTask *,
NSURLSessionTaskMetrics *);
((OriginalImp)currentIMP)(object, selector, session, task, metrics);
}
}];
}
}
Comment thread
JesusRojass marked this conversation as resolved.

#pragma mark - NSURLSessionDownloadDelegate methods.

/** Instruments URLSession:downloadTask:didFinishDownloadingToURL:.
Expand Down Expand Up @@ -230,6 +313,14 @@ - (void)registerClass:(Class)aClass {
InstrumentURLSessionDownloadTaskDidWriteDataTotalBytesWrittenTotalBytesExpectedToWrite(
instrumentor);

// Async/await support: captures tasks created by Swift async/await methods (iOS 16+).
if (@available(iOS 16.0, tvOS 16.0, *)) {
InstrumentURLSessionDidCreateTask(instrumentor);
}
// Task metrics (iOS 10+): reliable completion hook for async tasks and more accurate byte
// counts for all task types. Idempotent with ObjC completion paths via traceCompleted guard.
InstrumentURLSessionTaskDidFinishCollectingMetrics(instrumentor);

[instrumentor swizzle];
});
}
Expand Down Expand Up @@ -260,6 +351,12 @@ - (void)registerObject:(id)object {
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:),
instrumentor);

// Async/await support.
if (@available(iOS 16.0, tvOS 16.0, *)) {
CopySelector(@selector(URLSession:didCreateTask:), instrumentor);
}
CopySelector(@selector(URLSession:task:didFinishCollectingMetrics:), instrumentor);

[instrumentor swizzle];
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,15 +284,13 @@ - (void)testConnectionDidFailWithError {
self.appFake.fakeIsDataCollectionDefaultEnabled = YES;
FPRNSURLConnectionInstrument *instrument = [[FPRNSURLConnectionInstrument alloc] init];
[instrument registerInstrumentors];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://nonurl/"]];
[self.testServer stop];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]];
NSURLRequest *request = [NSURLRequest requestWithURL:self.testServer.serverURL];
FPRNSURLConnectionCompleteTestDelegate *delegate =
[[FPRNSURLConnectionCompleteTestDelegate alloc] init];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:delegate];
[connection start];
XCTAssertNotNil([FPRNetworkTrace networkTraceFromObject:connection]);
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
XCTAssertTrue(delegate.connectionDidFailWithErrorCalled);
XCTAssertNil([FPRNetworkTrace networkTraceFromObject:connection]);
[self.testServer start];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,8 @@ - (void)testDelegateURLSessionTaskDidCompleteWithError {
[instrument registerInstrumentors];
FPRNSURLSessionCompleteTestDelegate *delegate =
[[FPRNSURLSessionCompleteTestDelegate alloc] init];
// This request needs to fail.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://nonurl"]];
// This request needs to fail. Point at the stopped local server for deterministic refusal.
NSURLRequest *request = [NSURLRequest requestWithURL:self.testServer.serverURL];
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
Expand All @@ -363,8 +363,7 @@ - (void)testDelegateURLSessionTaskDidCompleteWithError {
@autoreleasepool {
task = [session dataTaskWithRequest:request];
[task resume];
XCTAssertNotNil([FPRNetworkTrace networkTraceFromObject:task]);
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
}
XCTAssertNil([FPRNetworkTrace networkTraceFromObject:task]);
XCTAssertTrue(delegate.URLSessionTaskDidCompleteWithErrorCalled);
Expand Down Expand Up @@ -835,6 +834,106 @@ - (void)testMutableRequestURLs {
[instrument deregisterInstrumentors];
}

#pragma mark - Testing async/await support (didCreateTask: + didFinishCollectingMetrics:)

/** Tests that URLSession:task:didFinishCollectingMetrics: is called and wrapped for a
* delegate-based task, and that the network trace is completed and removed after it fires.
*/
- (void)testDelegateURLSessionTaskDidFinishCollectingMetrics {
FPRNSURLSessionInstrument *instrument = [[FPRNSURLSessionInstrument alloc] init];
[instrument registerInstrumentors];
FPRNSURLSessionCompleteTestDelegate *delegate =
[[FPRNSURLSessionCompleteTestDelegate alloc] init];
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithURL:self.testServer.serverURL];
[task resume];
[self waitAndRunBlockAfterResponse:^(id self, GCDWebServerRequest *_Nonnull request,
GCDWebServerResponse *_Nonnull response) {
XCTAssertNil([FPRNetworkTrace networkTraceFromObject:task]);
}];
XCTAssertTrue(delegate.URLSessionTaskDidFinishCollectingMetricsCalled);
[instrument deregisterInstrumentors];
}

/** Tests that URLSession:task:didFinishCollectingMetrics: is instrumented on user-provided
* delegate classes that don't originally implement it (via CopySelector).
*/
- (void)testDelegateURLSessionTaskDidFinishCollectingMetricsCopiedForNonImplementingDelegate {
FPRNSURLSessionInstrument *instrument = [[FPRNSURLSessionInstrument alloc] init];
[instrument registerInstrumentors];
// FPRNSURLSessionTestDelegate only implements NSURLSessionDelegate, not the task delegate.
FPRNSURLSessionTestDelegate *delegate = [[FPRNSURLSessionTestDelegate alloc] init];
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
// The delegate should now respond to didFinishCollectingMetrics: after CopySelector.
XCTAssertTrue(
[delegate respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]);
(void)session;
[instrument deregisterInstrumentors];
}

/** Tests that a network trace attached by the ObjC task-creation swizzle is not duplicated
* when URLSession:didCreateTask: also fires on iOS 16+. The trace from the ObjC path takes
* precedence and didCreateTask: should be a no-op for that task.
*/
- (void)testDidCreateTaskDoesNotDoubleTraceObjCCreatedTask {
Comment thread
JesusRojass marked this conversation as resolved.
if (@available(iOS 16.0, tvOS 16.0, *)) {
FPRNSURLSessionInstrument *instrument = [[FPRNSURLSessionInstrument alloc] init];
[instrument registerInstrumentors];
FPRNSURLSessionCompleteTestDelegate *delegate =
[[FPRNSURLSessionCompleteTestDelegate alloc] init];
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
NSURL *URL = [self.testServer.serverURL URLByAppendingPathComponent:@"test"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
// Task is created via ObjC swizzled method, trace attached immediately.
NSURLSessionTask *task = [session dataTaskWithRequest:request];
FPRNetworkTrace *traceFromObjCPath = [FPRNetworkTrace networkTraceFromObject:task];
XCTAssertNotNil(traceFromObjCPath);
// didCreateTask: fires here (iOS 16+). The trace already exists, so it must be a no-op.
// Verify the same trace object is still on the task (not replaced).
XCTAssertEqual([FPRNetworkTrace networkTraceFromObject:task], traceFromObjCPath);
[task resume];
[self waitAndRunBlockAfterResponse:^(id self, GCDWebServerRequest *_Nonnull req,
GCDWebServerResponse *_Nonnull resp) {
XCTAssertNil([FPRNetworkTrace networkTraceFromObject:task]);
}];
[instrument deregisterInstrumentors];
}
}

/** Tests that URLSession:didCreateTask: is called and wrapped for a user-provided delegate
* on iOS 16+.
*/
- (void)testDelegateURLSessionDidCreateTaskCalledOnIOS16 {
if (@available(iOS 16.0, tvOS 16.0, *)) {
FPRNSURLSessionInstrument *instrument = [[FPRNSURLSessionInstrument alloc] init];
[instrument registerInstrumentors];
FPRNSURLSessionCompleteTestDelegate *delegate =
[[FPRNSURLSessionCompleteTestDelegate alloc] init];
NSURLSessionConfiguration *configuration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
NSURL *URL = [self.testServer.serverURL URLByAppendingPathComponent:@"test"];
NSURLSessionTask *task = [session dataTaskWithRequest:[NSURLRequest requestWithURL:URL]];
(void)task;
XCTAssertTrue(delegate.URLSessionDidCreateTaskCalled);
[instrument deregisterInstrumentors];
}
}

@end

#endif // SWIFT_PACKAGE
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property(nonatomic) BOOL URLSessionDownloadTaskDidWriteDataTotalBytesWrittenTotalBytesCalled;

/** Set to YES when URLSession:didCreateTask: is called (iOS 16+), used for testing. */
@property(nonatomic) BOOL URLSessionDidCreateTaskCalled;

/** Set to YES when URLSession:task:didFinishCollectingMetrics: is called, used for testing. */
@property(nonatomic) BOOL URLSessionTaskDidFinishCollectingMetricsCalled;

@end

/** This class implements the methods necessary to cancel and resume a download. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ - (void)URLSession:(NSURLSession *)session
self.URLSessionDownloadTaskDidWriteDataTotalBytesWrittenTotalBytesCalled = YES;
}

#pragma mark - Async/await support

- (void)URLSession:(NSURLSession *)session
didCreateTask:(NSURLSessionTask *)task API_AVAILABLE(ios(16.0), tvos(16.0)) {
self.URLSessionDidCreateTaskCalled = YES;
}

- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
self.URLSessionTaskDidFinishCollectingMetricsCalled = YES;
}

@end

@interface FPRNSURLSessionTestDownloadDelegate ()
Expand Down