diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d813359883..87328c9e1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Add frames delay to transactions (#3487) - Add slow and frozen frames to spans (#3450, #3478) + +### Fixes + +- TTFD waits for next drawn frame (#3505) only when enabling `options.performanceV2 = true`. + ## 8.17.2 ### Fixes diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index cc03cd4b789..9e530c19463 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -31,6 +31,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.attachViewHierarchy = true options.environment = "test-app" options.enableTimeToFullDisplayTracing = true + options.enablePerformanceV2 = true options.add(inAppInclude: "iOS_External") diff --git a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift index b3a113e37ff..d74750bd347 100644 --- a/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ViewControllers/LoremIpsumViewController.swift @@ -14,7 +14,10 @@ class LoremIpsumViewController: UIViewController { if let contents = FileManager.default.contents(atPath: path) { DispatchQueue.main.async { self.textView.text = String(data: contents, encoding: .utf8) - SentrySDK.reportFullyDisplayed() + + dispatchQueue.asyncAfter(deadline: .now() + 0.1) { + SentrySDK.reportFullyDisplayed() + } } } } diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index cff7cab25a0..cc41bbe850d 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -191,6 +191,17 @@ NS_SWIFT_NAME(Options) */ @property (nonatomic, assign) BOOL enableAutoPerformanceTracing; +/** + * @warning This is an experimental feature and may still have bugs. + * + * Sentry works on reworking the whole performance offering with the code Mobile Starfish, which + * aims to provide better insights into the performance of mobile apps and highlight clear actions + * to improve app performance to developers. This feature flag enables experimental features that + * impact the v1 performance offering and would require a major version update. Sentry aims to + * include most features in the next major by default. + */ +@property (nonatomic, assign) BOOL enablePerformanceV2; + /** * A block that configures the initial scope when starting the SDK. * @discussion The block receives a suggested default scope. You can either diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index cfb4d840442..5e82d64566b 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -105,6 +105,7 @@ - (instancetype)init self.maxAttachmentSize = 20 * 1024 * 1024; self.sendDefaultPii = NO; self.enableAutoPerformanceTracing = YES; + self.enablePerformanceV2 = NO; self.enableCaptureFailedRequests = YES; self.environment = kSentryDefaultEnvironment; self.enableTimeToFullDisplayTracing = NO; @@ -375,6 +376,9 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableAutoPerformanceTracing"] block:^(BOOL value) { self->_enableAutoPerformanceTracing = value; }]; + [self setBool:options[@"enablePerformanceV2"] + block:^(BOOL value) { self->_enablePerformanceV2 = value; }]; + [self setBool:options[@"enableCaptureFailedRequests"] block:^(BOOL value) { self->_enableCaptureFailedRequests = value; }]; diff --git a/Sources/Sentry/SentryPerformanceTrackingIntegration.m b/Sources/Sentry/SentryPerformanceTrackingIntegration.m index 506af85ee73..5808e814aac 100644 --- a/Sources/Sentry/SentryPerformanceTrackingIntegration.m +++ b/Sources/Sentry/SentryPerformanceTrackingIntegration.m @@ -47,6 +47,8 @@ - (BOOL)installWithOptions:(SentryOptions *)options [self.swizzling start]; SentryUIViewControllerPerformanceTracker.shared.enableWaitForFullDisplay = options.enableTimeToFullDisplayTracing; + SentryUIViewControllerPerformanceTracker.shared.enablePerformanceV2 + = options.enablePerformanceV2; return YES; } diff --git a/Sources/Sentry/SentryTimeToDisplayTracker.m b/Sources/Sentry/SentryTimeToDisplayTracker.m index 99492abe4a1..45ff30d92b8 100644 --- a/Sources/Sentry/SentryTimeToDisplayTracker.m +++ b/Sources/Sentry/SentryTimeToDisplayTracker.m @@ -26,6 +26,7 @@ @implementation SentryTimeToDisplayTracker { BOOL _waitForFullDisplay; + BOOL _fullDisplayWaitsForNextFrame; BOOL _isReadyToDisplay; BOOL _fullyDisplayedReported; NSString *_controllerName; @@ -33,10 +34,12 @@ @implementation SentryTimeToDisplayTracker { - (instancetype)initForController:(UIViewController *)controller waitForFullDisplay:(BOOL)waitForFullDisplay + fullDisplayWaitsForNextFrame:(BOOL)fullDisplayWaitsForNextFrame { if (self = [super init]) { _controllerName = [SwiftDescriptor getObjectClassName:controller]; _waitForFullDisplay = waitForFullDisplay; + _fullDisplayWaitsForNextFrame = fullDisplayWaitsForNextFrame; _isReadyToDisplay = NO; _fullyDisplayedReported = NO; @@ -78,7 +81,8 @@ - (void)reportReadyToDisplay - (void)reportFullyDisplayed { _fullyDisplayedReported = YES; - if (self.waitForFullDisplay && _isReadyToDisplay) { + + if (self.waitForFullDisplay && _isReadyToDisplay && !_fullDisplayWaitsForNextFrame) { // We need the timestamp to be able to calculate the duration // but we can't finish first and add measure later because // finishing the span may trigger the tracer finishInternal. @@ -108,15 +112,23 @@ - (void)framesTrackerHasNewFrame [self addTimeToDisplayMeasurement:self.initialDisplaySpan name:@"time_to_initial_display"]; [self.initialDisplaySpan finish]; - [SentryDependencyContainer.sharedInstance.framesTracker removeListener:self]; + + if (!_fullDisplayWaitsForNextFrame) { + [SentryDependencyContainer.sharedInstance.framesTracker removeListener:self]; + } } if (_waitForFullDisplay && _fullyDisplayedReported && self.fullDisplaySpan.isFinished == NO) { self.fullDisplaySpan.timestamp = finishTime; - [self addTimeToDisplayMeasurement:self.initialDisplaySpan name:@"time_to_full_display"]; + [self addTimeToDisplayMeasurement:self.fullDisplaySpan name:@"time_to_full_display"]; [self.fullDisplaySpan finish]; } + + if (_fullDisplayWaitsForNextFrame && self.initialDisplaySpan.isFinished == YES + && self.fullDisplaySpan.isFinished == YES) { + [SentryDependencyContainer.sharedInstance.framesTracker removeListener:self]; + } } - (void)trimTTFDIdNecessaryForTracer:(SentryTracer *)tracer diff --git a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m index 922fdf4ac1c..a193ffb4df8 100644 --- a/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m +++ b/Sources/Sentry/SentryUIViewControllerPerformanceTracker.m @@ -45,6 +45,7 @@ - (instancetype)init inAppExcludes:options.inAppExcludes]; _enableWaitForFullDisplay = NO; + _enablePerformanceV2 = NO; } return self; } @@ -141,7 +142,8 @@ - (void)createTimeToDisplay:(UIViewController *)controller SentryTimeToDisplayTracker *ttdTracker = [[SentryTimeToDisplayTracker alloc] initForController:controller - waitForFullDisplay:self.enableWaitForFullDisplay]; + waitForFullDisplay:self.enableWaitForFullDisplay + fullDisplayWaitsForNextFrame:self.enablePerformanceV2]; objc_setAssociatedObject(controller, &SENTRY_UI_PERFORMANCE_TRACKER_TTD_TRACKER, ttdTracker, OBJC_ASSOCIATION_ASSIGN); diff --git a/Sources/Sentry/include/SentryTimeToDisplayTracker.h b/Sources/Sentry/include/SentryTimeToDisplayTracker.h index 1b239aa8f94..3be5a19a3e9 100644 --- a/Sources/Sentry/include/SentryTimeToDisplayTracker.h +++ b/Sources/Sentry/include/SentryTimeToDisplayTracker.h @@ -25,7 +25,8 @@ SENTRY_NO_INIT @property (nonatomic, readonly) BOOL waitForFullDisplay; - (instancetype)initForController:(UIViewController *)controller - waitForFullDisplay:(BOOL)waitForFullDisplay; + waitForFullDisplay:(BOOL)waitForFullDisplay + fullDisplayWaitsForNextFrame:(BOOL)fullDisplayWaitsForNextFrame; - (void)startForTracer:(SentryTracer *)tracer; diff --git a/Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h b/Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h index ed520b34f52..66f198c0418 100644 --- a/Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h +++ b/Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h @@ -31,6 +31,7 @@ static NSString *const SENTRY_UI_PERFORMANCE_TRACKER_TTD_TRACKER @property (nonatomic, strong) SentryInAppLogic *inAppLogic; @property (nonatomic) BOOL enableWaitForFullDisplay; +@property (nonatomic) BOOL enablePerformanceV2; /** * Measures @c controller's @c loadView method. diff --git a/Tests/SentryTests/Integrations/Performance/SentryPerformanceTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/SentryPerformanceTrackingIntegrationTests.swift index 37376eabe95..e7cae218111 100644 --- a/Tests/SentryTests/Integrations/Performance/SentryPerformanceTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/SentryPerformanceTrackingIntegrationTests.swift @@ -1,3 +1,4 @@ +import Nimble import SentryTestUtils import XCTest @@ -88,5 +89,26 @@ class SentryPerformanceTrackingIntegrationTests: XCTestCase { XCTAssertFalse(SentryUIViewControllerPerformanceTracker.shared.enableWaitForFullDisplay) } + func testConfigure_PerformanceV2_Default() { + let sut = SentryPerformanceTrackingIntegration() + + let options = Options() + options.tracesSampleRate = 0.1 + sut.install(with: options) + + expect(SentryUIViewControllerPerformanceTracker.shared.enablePerformanceV2) == false + } + + func testConfigure_PerformanceV2_Enabled() { + let sut = SentryPerformanceTrackingIntegration() + + let options = Options() + options.tracesSampleRate = 0.1 + options.enablePerformanceV2 = true + sut.install(with: options) + + expect(SentryUIViewControllerPerformanceTracker.shared.enablePerformanceV2) == true + } + #endif } diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift index f0bb2434c3f..40e0cc42c66 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryTimeToDisplayTrackerTest.swift @@ -1,4 +1,5 @@ import Foundation +import Nimble import Sentry import SentryTestUtils import XCTest @@ -20,8 +21,8 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { framesTracker.start() } - func getSut(for controller: UIViewController, waitForFullDisplay: Bool) -> SentryTimeToDisplayTracker { - return SentryTimeToDisplayTracker(for: controller, waitForFullDisplay: waitForFullDisplay) + func getSut(for controller: UIViewController, waitForFullDisplay: Bool, fullDisplayWaitsForNextFrame: Bool = false) -> SentryTimeToDisplayTracker { + return SentryTimeToDisplayTracker(for: controller, waitForFullDisplay: waitForFullDisplay, fullDisplayWaitsForNextFrame: fullDisplayWaitsForNextFrame) } } @@ -92,7 +93,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { XCTAssertTrue(ttidSpan.isFinished) } - func testreportInitialDisplay_waitForFullDisplay() { + func testReportInitialDisplay_waitForFullDisplay() { fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 7)) let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true) @@ -122,7 +123,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { XCTAssertEqual(tracer.children.count, 2) } - func testreportFullDisplay_notWaitingForFullDisplay() { + func testReportFullDisplay_notWaitingForFullDisplay() { let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: false) let tracer = fixture.tracer @@ -137,7 +138,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { XCTAssertEqual(tracer.children.count, 1) } - func testreportFullDisplay_waitingForFullDisplay() { + func testReportFullDisplay_waitingForFullDisplay() { fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9)) let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true) @@ -164,6 +165,39 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { XCTAssertEqual(sut.fullDisplaySpan?.operation, SentrySpanOperationUILoadFullDisplay) XCTAssertEqual(sut.fullDisplaySpan?.origin, "manual.ui.time_to_display") } + + func testReportFullDisplay_waitingForFullDisplayAndNextFrame() { + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 9)) + + let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true, fullDisplayWaitsForNextFrame: true) + let tracer = fixture.tracer + + sut.start(for: tracer) + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 10)) + sut.reportReadyToDisplay() + fixture.displayLinkWrapper.normalFrame() + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11)) + sut.reportFullyDisplayed() + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 12)) + fixture.displayLinkWrapper.normalFrame() + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 13)) + tracer.finish() + + expect(sut.fullDisplaySpan) != nil + expect(sut.fullDisplaySpan?.startTimestamp) == Date(timeIntervalSince1970: 9) + expect(sut.fullDisplaySpan?.timestamp) == Date(timeIntervalSince1970: 12) + expect(sut.fullDisplaySpan?.status) == .ok + + expect(sut.fullDisplaySpan?.spanDescription) == "UIViewController full display" + expect(sut.fullDisplaySpan?.operation) == SentrySpanOperationUILoadFullDisplay + expect(sut.fullDisplaySpan?.origin) == "manual.ui.time_to_display" + + assertMeasurement(tracer: tracer, name: "time_to_full_display", duration: 3_000) + } func testReportFullDisplay_waitingForFullDisplay_notReadyToDisplay() { let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true) @@ -176,7 +210,36 @@ class SentryTimeToDisplayTrackerTest: XCTestCase { fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11)) sut.reportFullyDisplayed() - XCTAssertFalse(sut.fullDisplaySpan?.isFinished ?? true) + expect(sut.fullDisplaySpan?.isFinished) == false + } + + func testReportFullDisplay_waitingForFullDisplayAndNextFrame_notReadyToDisplay() { + let sut = fixture.getSut(for: UIViewController(), waitForFullDisplay: true, fullDisplayWaitsForNextFrame: true) + let tracer = fixture.tracer + + sut.start(for: tracer) + + fixture.displayLinkWrapper.normalFrame() + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11)) + sut.reportFullyDisplayed() + + expect(sut.fullDisplaySpan?.isFinished) == false + sut.reportReadyToDisplay() + expect(sut.fullDisplaySpan?.isFinished) == false + + fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 12)) + fixture.displayLinkWrapper.normalFrame() + + expect(sut.initialDisplaySpan?.isFinished) == true + expect(sut.initialDisplaySpan?.timestamp) == Date(timeIntervalSince1970: 12) + expect(sut.initialDisplaySpan?.status) == .ok + + expect(sut.fullDisplaySpan?.isFinished) == true + expect(sut.fullDisplaySpan?.timestamp) == Date(timeIntervalSince1970: 12) + expect(sut.fullDisplaySpan?.status) == .ok + + expect(Dynamic(self.fixture.framesTracker).listeners.count) == 0 } func testReportFullDisplay_expires() { diff --git a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift index c9921cb8f35..bb39acc457b 100644 --- a/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/UIViewController/SentryUIViewControllerPerformanceTrackerTests.swift @@ -1,5 +1,6 @@ #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +import Nimble import ObjectiveC import SentryTestUtils import XCTest @@ -21,6 +22,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { let spanName = "spanName" let spanOperation = "spanOperation" let origin = "auto.ui.view_controller" + let frameDuration = 0.0016 private class Fixture { @@ -251,7 +253,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { callbackExpectation.fulfill() } try assertSpanDuration(span: lastSpan, expectedDuration: 5) - try assertSpanDuration(span: transactionSpan, expectedDuration: 22) + try assertSpanDuration(span: transactionSpan, expectedDuration: 22 + frameDuration) wait(for: [callbackExpectation], timeout: 0) } @@ -259,6 +261,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { func testReportFullyDisplayed() { let sut = fixture.getSut() sut.enableWaitForFullDisplay = true + sut.enablePerformanceV2 = false let viewController = fixture.viewController let tracker = fixture.tracker var tracer: SentryTracer? @@ -267,11 +270,42 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { let spans = self.getStack(tracker) tracer = spans.first as? SentryTracer } + sut.viewControllerViewWillAppear(viewController) { + self.advanceTime(bySeconds: 0.1) + } + + sut.reportFullyDisplayed() + let expectedTTFDTimestamp = fixture.dateProvider.date() + reportFrame() + + let ttfdSpan = tracer?.children[1] + expect(ttfdSpan?.isFinished) == true + expect(ttfdSpan?.timestamp) == expectedTTFDTimestamp + } + + func testReportFullyDisplayed_WithPerformanceV2() { + let sut = fixture.getSut() + sut.enableWaitForFullDisplay = true + sut.enablePerformanceV2 = true + let viewController = fixture.viewController + let tracker = fixture.tracker + var tracer: SentryTracer? + + sut.viewControllerLoadView(viewController) { + let spans = self.getStack(tracker) + tracer = spans.first as? SentryTracer + } + sut.viewControllerViewWillAppear(viewController) { + self.advanceTime(bySeconds: 0.1) + } sut.reportFullyDisplayed() reportFrame() + let expectedTTFDTimestamp = fixture.dateProvider.date() - XCTAssertTrue(tracer?.children[1].isFinished ?? false) + let ttfdSpan = tracer?.children[1] + expect(ttfdSpan?.isFinished) == true + expect(ttfdSpan?.timestamp) == expectedTTFDTimestamp } func testSecondViewController() { @@ -598,7 +632,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { let timestamp = try XCTUnwrap(span.timestamp) let startTimestamp = try XCTUnwrap(span.startTimestamp) let duration = timestamp.timeIntervalSince(startTimestamp) - XCTAssertEqual(duration, expectedDuration) + expect(duration).to(beCloseTo(expectedDuration, within: 0.001)) } private func assertTrackerIsEmpty(_ tracker: SentryPerformanceTracker) { @@ -621,6 +655,7 @@ class SentryUIViewControllerPerformanceTrackerTests: XCTestCase { } private func reportFrame() { + advanceTime(bySeconds: self.frameDuration) Dynamic(SentryDependencyContainer.sharedInstance().framesTracker).displayLinkCallback() } } diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index f5bfcf2821e..6882a1d180f 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -1050,7 +1050,7 @@ class SentryHubTests: XCTestCase { class TestTimeToDisplayTracker: SentryTimeToDisplayTracker { init() { - super.init(for: UIViewController(), waitForFullDisplay: false) + super.init(for: UIViewController(), waitForFullDisplay: false, fullDisplayWaitsForNextFrame: false) } var registerFullDisplayCalled = false diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 6d90438e6b8..28503dddf6f 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -726,6 +726,11 @@ - (void)testEnableAutoPerformanceTracing [self testBooleanField:@"enableAutoPerformanceTracing"]; } +- (void)testEnablePerformanceV2 +{ + [self testBooleanField:@"enablePerformanceV2" defaultValue:NO]; +} + #if SENTRY_HAS_UIKIT - (void)testEnableUIViewControllerTracing {