diff --git a/.github/workflows/test-cross-platform.yml b/.github/workflows/test-cross-platform.yml index ff0f568100f..04e6662959f 100644 --- a/.github/workflows/test-cross-platform.yml +++ b/.github/workflows/test-cross-platform.yml @@ -55,6 +55,7 @@ jobs: with: repository: getsentry/sentry-react-native path: sentry-react-native + ref: itay/fix_sentreyscreenframes - name: Enable Corepack working-directory: sentry-react-native diff --git a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift index e229fe61c69..1acedd8d96a 100644 --- a/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift @@ -2,7 +2,7 @@ import AuthenticationServices import Foundation import SafariServices -import Sentry +@_spi(Private) import Sentry import SentrySampleShared import UIKit diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index eb7c25a2031..21e3143a511 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -425,8 +425,6 @@ 7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B72D23928D074BC0014798A /* TestExtensions.swift */; }; 7B77BE3527EC8445003C9020 /* SentryDiscardReasonMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B77BE3427EC8445003C9020 /* SentryDiscardReasonMapper.h */; }; 7B77BE3727EC8460003C9020 /* SentryDiscardReasonMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B77BE3627EC8460003C9020 /* SentryDiscardReasonMapper.m */; }; - 7B7A599526B692540060A676 /* SentryScreenFrames.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B7A599426B692540060A676 /* SentryScreenFrames.h */; settings = {ATTRIBUTES = (Private, ); }; }; - 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B7A599626B692F00060A676 /* SentryScreenFrames.m */; }; 7B7D872C2486480B00D2ECFF /* SentryStacktraceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 7B7D872B2486480B00D2ECFF /* SentryStacktraceBuilder.h */; }; 7B7D872E2486482600D2ECFF /* SentryStacktraceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B7D872D2486482600D2ECFF /* SentryStacktraceBuilder.m */; }; 7B7D8730248648AD00D2ECFF /* SentryStacktraceBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7D872F248648AD00D2ECFF /* SentryStacktraceBuilderTests.swift */; }; @@ -981,6 +979,10 @@ F429D3AA2E8562EF00DBF387 /* RateLimitParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = F429D3A82E8562EF00DBF387 /* RateLimitParser.swift */; }; F443DB272E09BE8C009A9045 /* LoadValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F443DB262E09BE8C009A9045 /* LoadValidatorTests.swift */; }; F44858132E03579D0013E63B /* SentryCrashDynamicLinker+Test.h in Headers */ = {isa = PBXBuildFile; fileRef = F44858122E0357940013E63B /* SentryCrashDynamicLinker+Test.h */; }; + F44D2B592E6B779E00FF31FA /* SentryScreenFrames.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D2B582E6B779E00FF31FA /* SentryScreenFrames.swift */; }; + F44D2B5C2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = F44D2B5A2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.h */; }; + F44D2B5D2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = F44D2B5B2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.m */; }; + F44D2B602E6B829F00FF31FA /* SentryScreenFramesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D2B5F2E6B829F00FF31FA /* SentryScreenFramesTests.swift */; }; F451FAA62E0B304E0050ACF2 /* LoadValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */; }; F452437E2DE60B71003E8F50 /* SentryUseNSExceptionCallstackWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = F452437D2DE60B71003E8F50 /* SentryUseNSExceptionCallstackWrapper.m */; }; F45243882DE65968003E8F50 /* ExceptionCatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = F45243872DE65968003E8F50 /* ExceptionCatcher.m */; }; @@ -1683,8 +1685,6 @@ 7B77BE3427EC8445003C9020 /* SentryDiscardReasonMapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDiscardReasonMapper.h; path = include/SentryDiscardReasonMapper.h; sourceTree = ""; }; 7B77BE3627EC8460003C9020 /* SentryDiscardReasonMapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryDiscardReasonMapper.m; sourceTree = ""; }; 7B7A30C924B48523005A4C6E /* SentryHub+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryHub+Test.h"; sourceTree = ""; }; - 7B7A599426B692540060A676 /* SentryScreenFrames.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryScreenFrames.h; path = include/HybridPublic/SentryScreenFrames.h; sourceTree = ""; }; - 7B7A599626B692F00060A676 /* SentryScreenFrames.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenFrames.m; sourceTree = ""; }; 7B7D872B2486480B00D2ECFF /* SentryStacktraceBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryStacktraceBuilder.h; path = include/SentryStacktraceBuilder.h; sourceTree = ""; }; 7B7D872D2486482600D2ECFF /* SentryStacktraceBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryStacktraceBuilder.m; sourceTree = ""; }; 7B7D872F248648AD00D2ECFF /* SentryStacktraceBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryStacktraceBuilderTests.swift; sourceTree = ""; }; @@ -2319,6 +2319,10 @@ F429D3A82E8562EF00DBF387 /* RateLimitParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimitParser.swift; sourceTree = ""; }; F443DB262E09BE8C009A9045 /* LoadValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadValidatorTests.swift; sourceTree = ""; }; F44858122E0357940013E63B /* SentryCrashDynamicLinker+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryCrashDynamicLinker+Test.h"; sourceTree = ""; }; + F44D2B582E6B779E00FF31FA /* SentryScreenFrames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenFrames.swift; sourceTree = ""; }; + F44D2B5A2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryProfilingScreenFramesHelper.h; sourceTree = ""; }; + F44D2B5B2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryProfilingScreenFramesHelper.m; sourceTree = ""; }; + F44D2B5F2E6B829F00FF31FA /* SentryScreenFramesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenFramesTests.swift; sourceTree = ""; }; F451FAA52E0B304E0050ACF2 /* LoadValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadValidator.swift; sourceTree = ""; }; F452437D2DE60B71003E8F50 /* SentryUseNSExceptionCallstackWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUseNSExceptionCallstackWrapper.m; sourceTree = ""; }; F45243862DE65968003E8F50 /* ExceptionCatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExceptionCatcher.h; sourceTree = ""; }; @@ -2620,6 +2624,7 @@ 62872B602BA1B84400A4FA7D /* Swift */ = { isa = PBXGroup; children = ( + F44D2B612E6B82A800FF31FA /* Profiling */, F443DB242E09BE61009A9045 /* Core */, 62872B612BA1B84C00A4FA7D /* Extensions */, ); @@ -3661,8 +3666,6 @@ 7B6C5EDB264E8DA80010D138 /* SentryFramesTrackingIntegration.m */, 7B6C5EDF264E8E050010D138 /* SentryFramesTracker.h */, 7B6C5EDD264E8DF00010D138 /* SentryFramesTracker.m */, - 7B7A599426B692540060A676 /* SentryScreenFrames.h */, - 7B7A599626B692F00060A676 /* SentryScreenFrames.m */, 62862B1B2B1DDBC8009B16E3 /* SentryDelayedFrame.h */, 62862B1D2B1DDC35009B16E3 /* SentryDelayedFrame.m */, 62C316802B1F2E93000D7031 /* SentryDelayedFramesTracker.h */, @@ -3899,6 +3902,8 @@ 8459FCC12BD73EEF0038E9C9 /* SentryProfilerSerialization+Test.h */, 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */, 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */, + F44D2B5A2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.h */, + F44D2B5B2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.m */, 840B7EF22BBF83DF008B8120 /* SentryProfiler+Private.h */, 84A898522E163072009A551E /* SentryProfileConfiguration.h */, 84A898532E163072009A551E /* SentryProfileConfiguration.m */, @@ -4724,6 +4729,14 @@ path = Tools; sourceTree = ""; }; + F44D2B612E6B82A800FF31FA /* Profiling */ = { + isa = PBXGroup; + children = ( + F44D2B5F2E6B829F00FF31FA /* SentryScreenFramesTests.swift */, + ); + path = Profiling; + sourceTree = ""; + }; F474CB872E2EC5040001DF41 /* Recovered References */ = { isa = PBXGroup; children = ( @@ -4788,6 +4801,7 @@ FA67DCD22DDBD4EA00896B02 /* FramesTracking */ = { isa = PBXGroup; children = ( + F44D2B582E6B779E00FF31FA /* SentryScreenFrames.swift */, FA67DCD12DDBD4EA00896B02 /* SentryFramesDelayResult.swift */, ); path = FramesTracking; @@ -5041,6 +5055,7 @@ 6383953623ABA42C000C1594 /* SentryHttpTransport.h in Headers */, 84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */, 8E564AEF267AF24400FE117D /* SentryNetworkTracker.h in Headers */, + F44D2B5C2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.h in Headers */, 63FE715120DA4C1100CDBAE8 /* SentryCrashDebug.h in Headers */, 63FE70F520DA4C1000CDBAE8 /* SentryCrashMonitor_System.h in Headers */, FAB359982E05D7E90083D5E3 /* SentryEventSwiftHelper.h in Headers */, @@ -5141,7 +5156,6 @@ 7BA61CAB247BA98100C130A8 /* SentryDebugImageProvider.h in Headers */, 7BC63F0828081242009D9E37 /* SentrySwizzleWrapper.h in Headers */, 638DC9A01EBC6B6400A66E41 /* SentryRequestOperation.h in Headers */, - 7B7A599526B692540060A676 /* SentryScreenFrames.h in Headers */, 6344DDB01EC308E400D9160D /* SentryCrashInstallationReporter.h in Headers */, 7B5CAF7527F5A67C00ED0DB6 /* SentryNSURLRequestBuilder.h in Headers */, 63FE70ED20DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.h in Headers */, @@ -5558,7 +5572,6 @@ D84D2CDF2C2BF9370011AF8A /* SentryReplayType.swift in Sources */, 63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */, D8F67B222BEAB6CC00C9197B /* SentryRRWebEvent.swift in Sources */, - 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */, 84A903712D39F66F00690CE4 /* SentryUserFeedbackFormViewModel.swift in Sources */, 7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */, D81988C92BEC19200020E36C /* SentryRRWebBreadcrumbEvent.swift in Sources */, @@ -5698,6 +5711,7 @@ 627C77892D50B6840055E966 /* SentryBreadcrumbCodable.swift in Sources */, 63FE70D720DA4C1000CDBAE8 /* SentryCrashMonitor_MachException.c in Sources */, 7B96572226830D2400C66E25 /* SentryScopeSyncC.c in Sources */, + F44D2B5D2E6B7E8700FF31FA /* SentryProfilingScreenFramesHelper.m in Sources */, 0A9BF4E228A114940068D266 /* SentryViewHierarchyIntegration.m in Sources */, D8AFC03D2BDA79BF00118BE1 /* SentryReplayVideoMaker.swift in Sources */, 840A11122B61E27500650D02 /* SentrySamplerDecision.m in Sources */, @@ -5792,6 +5806,7 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */, FABE8E172E307A7F0040809A /* Dependencies.swift in Sources */, D4ECA4012E3CBEDE00C757EA /* SentryDummyPublicEmptyClass.m in Sources */, + F44D2B592E6B779E00FF31FA /* SentryScreenFrames.swift in Sources */, D4ECA4022E3CBEDE00C757EA /* SentryDummyPrivateEmptyClass.m in Sources */, D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySessionInternal.m in Sources */, @@ -6229,6 +6244,7 @@ D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */, 8E70B10125CB8695002B3155 /* SentrySpanIdTests.swift in Sources */, 62E2119A2DAE99FC007D7262 /* SentryAsyncSafeLog.m in Sources */, + F44D2B602E6B829F00FF31FA /* SentryScreenFramesTests.swift in Sources */, 84EB21962BF01CEA00EDDA28 /* SentryCrashInstallationTests.swift in Sources */, 7BFE7A0A27A1B6B000D2B66E /* SentryWatchdogTerminationTrackingIntegrationTests.swift in Sources */, D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */, diff --git a/SentryTestUtils/TestFramesTracker.swift b/SentryTestUtils/TestFramesTracker.swift index 1bab5ae7647..c9f820561d2 100644 --- a/SentryTestUtils/TestFramesTracker.swift +++ b/SentryTestUtils/TestFramesTracker.swift @@ -1,10 +1,10 @@ -import Sentry +@_spi(Private) @testable import Sentry #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) public class TestFramesTracker: SentryFramesTracker { - public var expectedFrames: SentryScreenFrames? + @_spi(Private) public var expectedFrames: SentryScreenFrames? - public override func currentFrames() -> SentryScreenFrames { + @_spi(Private) public override func currentFrames() -> SentryScreenFrames { expectedFrames ?? super.currentFrames() } } diff --git a/Sources/Resources/Sentry.modulemap b/Sources/Resources/Sentry.modulemap index 3f474c69cca..ff0db4d34d9 100644 --- a/Sources/Resources/Sentry.modulemap +++ b/Sources/Resources/Sentry.modulemap @@ -15,7 +15,6 @@ framework module Sentry { header "SentryFormatter.h" header "SentryFramesTracker.h" header "SentryOptionsInternal.h" - header "SentryScreenFrames.h" header "SentrySwizzle.h" header "SentryUser+Private.h" diff --git a/Sources/Sentry/Profiling/SentryContinuousProfiler.mm b/Sources/Sentry/Profiling/SentryContinuousProfiler.mm index 79034ad748c..bad8c4225b2 100644 --- a/Sources/Sentry/Profiling/SentryContinuousProfiler.mm +++ b/Sources/Sentry/Profiling/SentryContinuousProfiler.mm @@ -9,6 +9,7 @@ # import "SentryProfiler+Private.h" # import "SentryProfilerSerialization.h" # import "SentryProfilerState.h" +# import "SentryProfilingScreenFramesHelper.h" # import "SentryProfilingSwiftHelpers.h" # import "SentrySDK+Private.h" # import "SentrySample.h" @@ -16,7 +17,6 @@ # if SENTRY_HAS_UIKIT # import "SentryFramesTracker.h" -# import "SentryScreenFrames.h" # import # endif // SENTRY_HAS_UIKIT @@ -72,7 +72,8 @@ # if SENTRY_HAS_UIKIT const auto framesTracker = SentryDependencyContainer.sharedInstance.framesTracker; - SentryScreenFrames *screenFrameData = [framesTracker.currentFrames copy]; + SentryScreenFrames *screenFrameData = + [SentryProfilingScreenFramesHelper copyScreenFrames:framesTracker.currentFrames]; [framesTracker resetProfilingTimestamps]; # endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm index a8ac4dcfa75..d1f91260e4b 100644 --- a/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm +++ b/Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm @@ -30,7 +30,7 @@ # if SENTRY_HAS_UIKIT # import "SentryAppStartMeasurement.h" # import "SentryFramesTracker.h" -# import "SentryScreenFrames.h" +# import "SentryProfilingScreenFramesHelper.h" # endif // SENTRY_HAS_UIKIT /** @@ -237,8 +237,8 @@ _unsafe_cleanUpTraceProfiler(profiler, tracerKey); # if SENTRY_HAS_UIKIT - profiler.screenFrameData = - [SentryDependencyContainer.sharedInstance.framesTracker.currentFrames copy]; + profiler.screenFrameData = [SentryProfilingScreenFramesHelper + copyScreenFrames:SentryDependencyContainer.sharedInstance.framesTracker.currentFrames]; SENTRY_LOG_DEBUG( @"Grabbing copy of frames tracker screen frames data to attach to profiler: %@.", profiler.screenFrameData); diff --git a/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.h b/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.h new file mode 100644 index 00000000000..b84354200a1 --- /dev/null +++ b/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.h @@ -0,0 +1,19 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT + +NS_ASSUME_NONNULL_BEGIN + +@class SentryScreenFrames; + +// Helper to use SentryScreenFrames without importing Swift on ObjC++ files. +// Right now we don't have Clang modules enabled, so we cannot use `@import Sentry;` +// and then no Swift class is visible to Objective C++. +@interface SentryProfilingScreenFramesHelper : NSObject ++ (SentryScreenFrames *)copyScreenFrames:(SentryScreenFrames *)screenFrames; +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.m b/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.m new file mode 100644 index 00000000000..665e7b394fd --- /dev/null +++ b/Sources/Sentry/Profiling/SentryProfilingScreenFramesHelper.m @@ -0,0 +1,15 @@ +#import "SentryProfilingScreenFramesHelper.h" +#import "SentrySwift.h" + +#if SENTRY_HAS_UIKIT + +@implementation SentryProfilingScreenFramesHelper + ++ (SentryScreenFrames *)copyScreenFrames:(SentryScreenFrames *)screenFrames +{ + return [screenFrames copy]; +} + +@end + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index 68169d8602f..125f65119fa 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -12,7 +12,6 @@ # import "SentrySwift.h" # import "SentryTime.h" # import "SentryTracer.h" -# import # include # if SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index ed20ff51f3c..8bb6ec52c8c 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -18,7 +18,6 @@ # import "SentryProfilingSwiftHelpers.h" # import "SentrySDK+Private.h" # import "SentrySamplingProfiler.hpp" -# import "SentryScreenFrames.h" # import "SentryTime.h" # import "SentryTracer+Private.h" diff --git a/Sources/Sentry/SentryScreenFrames.m b/Sources/Sentry/SentryScreenFrames.m deleted file mode 100644 index 6f2bf9c0ea6..00000000000 --- a/Sources/Sentry/SentryScreenFrames.m +++ /dev/null @@ -1,96 +0,0 @@ -#import - -#if SENTRY_UIKIT_AVAILABLE -# import "SentryInternalDefines.h" - -@implementation SentryScreenFrames - -- (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow -{ -# if SENTRY_HAS_UIKIT -# if SENTRY_TARGET_PROFILING_SUPPORTED - return [self initWithTotal:total - frozen:frozen - slow:slow - slowFrameTimestamps:@[] - frozenFrameTimestamps:@[] - frameRateTimestamps:@[]]; -# else - if (self = [super init]) { - _total = total; - _slow = slow; - _frozen = frozen; - } - - return self; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED -# else - SENTRY_GRACEFUL_FATAL( - @"SentryScreenFrames only works with UIKit enabled. Ensure you're using the " - @"right configuration of Sentry that links UIKit."); - return nil; -# endif // SENTRY_HAS_UIKIT -} - -# if SENTRY_TARGET_PROFILING_SUPPORTED -- (instancetype)initWithTotal:(NSUInteger)total - frozen:(NSUInteger)frozen - slow:(NSUInteger)slow - slowFrameTimestamps:(SentryFrameInfoTimeSeries *)slowFrameTimestamps - frozenFrameTimestamps:(SentryFrameInfoTimeSeries *)frozenFrameTimestamps - frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps -{ -# if SENTRY_HAS_UIKIT - if (self = [super init]) { - _total = total; - _slow = slow; - _frozen = frozen; - _slowFrameTimestamps = slowFrameTimestamps; - _frozenFrameTimestamps = frozenFrameTimestamps; - _frameRateTimestamps = frameRateTimestamps; - } - - return self; -# else - SENTRY_GRACEFUL_FATAL( - @"SentryScreenFrames only works with UIKit enabled. Ensure you're using the " - @"right configuration of Sentry that links UIKit."); - return nil; -# endif // SENTRY_HAS_UIKIT -} - -- (nonnull id)copyWithZone:(nullable NSZone *)zone -{ -# if SENTRY_HAS_UIKIT - return [[SentryScreenFrames allocWithZone:zone] initWithTotal:_total - frozen:_frozen - slow:_slow - slowFrameTimestamps:[_slowFrameTimestamps copy] - frozenFrameTimestamps:[_frozenFrameTimestamps copy] - frameRateTimestamps:[_frameRateTimestamps copy]]; -# else - SENTRY_GRACEFUL_FATAL( - @"SentryScreenFrames only works with UIKit enabled. Ensure you're using the " - @"right configuration of Sentry that links UIKit."); - return nil; -# endif // SENTRY_HAS_UIKIT -} - -# endif // SENTRY_TARGET_PROFILING_SUPPORTED - -- (NSString *)description -{ - NSMutableString *result = [NSMutableString - stringWithFormat:@"Total frames: %lu; slow frames: %lu; frozen frames: %lu", - (unsigned long)_total, (unsigned long)_slow, (unsigned long)_frozen]; -# if SENTRY_TARGET_PROFILING_SUPPORTED - [result appendFormat: - @"\nslowFrameTimestamps: %@\nfrozenFrameTimestamps: %@\nframeRateTimestamps: %@", - _slowFrameTimestamps, _frozenFrameTimestamps, _frameRateTimestamps]; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED - return result; -} - -@end - -#endif // SENTRY_UIKIT_AVAILABLE diff --git a/Sources/Sentry/SentrySpan.m b/Sources/Sentry/SentrySpan.m index 86bebaefa1b..df9755a5342 100644 --- a/Sources/Sentry/SentrySpan.m +++ b/Sources/Sentry/SentrySpan.m @@ -20,7 +20,6 @@ #if SENTRY_HAS_UIKIT # import -# import #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index b368f804368..1ade9384c32 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -37,7 +37,6 @@ # import "SentryBuildAppStartSpans.h" # import "SentryFramesTracker.h" # import "SentryUIViewControllerPerformanceTracker.h" -# import #endif // SENTRY_HAS_UIKIT NS_ASSUME_NONNULL_BEGIN diff --git a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h index e2e6bdcc68e..78fa4d111de 100644 --- a/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h +++ b/Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h @@ -4,12 +4,6 @@ # import "PrivatesHeader.h" #endif -#if __has_include() -# import -#else -# import "SentryScreenFrames.h" -#endif - @class SentryDebugMeta; @class SentryScreenFrames; @class SentryAppStartMeasurement; diff --git a/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h b/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h deleted file mode 100644 index d7c0351a4d3..00000000000 --- a/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h +++ /dev/null @@ -1,66 +0,0 @@ -#if __has_include() -# import -#else -# import "PrivatesHeader.h" -#endif - -#if SENTRY_UIKIT_AVAILABLE - -NS_ASSUME_NONNULL_BEGIN - -/** An array of dictionaries that each contain a start and end timestamp for a rendered frame. */ -# if SENTRY_TARGET_PROFILING_SUPPORTED -typedef NSArray *> SentryFrameInfoTimeSeries; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED - -/** - * @warning This feature is not available in @c DebugWithoutUIKit and @c ReleaseWithoutUIKit - * configurations even when targeting iOS or tvOS platforms. - */ -@interface SentryScreenFrames : NSObject -# if SENTRY_TARGET_PROFILING_SUPPORTED - -# endif // SENTRY_TARGET_PROFILING_SUPPORTED -SENTRY_NO_INIT - -- (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow; - -# if SENTRY_TARGET_PROFILING_SUPPORTED -- (instancetype)initWithTotal:(NSUInteger)total - frozen:(NSUInteger)frozen - slow:(NSUInteger)slow - slowFrameTimestamps:(SentryFrameInfoTimeSeries *)slowFrameTimestamps - frozenFrameTimestamps:(SentryFrameInfoTimeSeries *)frozenFrameTimestamps - frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED - -@property (nonatomic, assign, readonly) NSUInteger total; -@property (nonatomic, assign, readonly) NSUInteger frozen; -@property (nonatomic, assign, readonly) NSUInteger slow; - -# if SENTRY_TARGET_PROFILING_SUPPORTED -/** - * Array of dictionaries describing slow frames' timestamps. Each dictionary has a start and end - * timestamp for every such frame, keyed under @c start_timestamp and @c end_timestamp. - */ -@property (nonatomic, copy, readonly) SentryFrameInfoTimeSeries *slowFrameTimestamps; - -/** - * Array of dictionaries describing frozen frames' timestamps. Each dictionary has a start and end - * timestamp for every such frame, keyed under @c start_timestamp and @c end_timestamp. - */ -@property (nonatomic, copy, readonly) SentryFrameInfoTimeSeries *frozenFrameTimestamps; - -/** - * Array of dictionaries describing the screen refresh rate at all points in time that it changes, - * which can happen when modern devices e.g. go into low power mode. Each dictionary contains keys - * @c timestamp and @c frame_rate. - */ -@property (nonatomic, copy, readonly) SentryFrameInfoTimeSeries *frameRateTimestamps; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED - -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_UIKIT_AVAILABLE diff --git a/Sources/Sentry/include/SentryProfileTimeseries.h b/Sources/Sentry/include/SentryProfileTimeseries.h index eebb331c342..009d5cb5a87 100644 --- a/Sources/Sentry/include/SentryProfileTimeseries.h +++ b/Sources/Sentry/include/SentryProfileTimeseries.h @@ -7,7 +7,6 @@ # if SENTRY_HAS_UIKIT # import "SentryProfilerDefines.h" -# import "SentryScreenFrames.h" # endif // SENTRY_HAS_UIKIT @class SentrySample; @@ -19,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN extern "C" { # endif +typedef NSArray *> SentryFrameInfoTimeSeries; + NSArray *_Nullable sentry_slicedProfileSamples( NSArray *samples, uint64_t startSystemTime, uint64_t endSystemTime); diff --git a/Sources/Swift/Core/Integrations/FramesTracking/SentryScreenFrames.swift b/Sources/Swift/Core/Integrations/FramesTracking/SentryScreenFrames.swift new file mode 100644 index 00000000000..4bebc2e0ef3 --- /dev/null +++ b/Sources/Swift/Core/Integrations/FramesTracking/SentryScreenFrames.swift @@ -0,0 +1,187 @@ +import Foundation + +#if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) + +#if os(iOS) +/// An array of dictionaries that each contain a start and end timestamp for a rendered frame. +@_spi(Private) public typealias SentryFrameInfoTimeSeries = [[String: NSNumber]] +#endif // os(iOS) + +/// Represents screen frame metrics including total, slow, and frozen frames. +/// +/// - Warning: This feature is not available in `DebugWithoutUIKit` and `ReleaseWithoutUIKit` +/// configurations even when targeting iOS or tvOS platforms. +@objc @_spi(Private) +public final class SentryScreenFrames: NSObject, NSCopying { + + // MARK: - Properties + + /// Total number of frames rendered + @objc + public let total: UInt + + /// Number of frames that were frozen (took longer than 700ms to render) + @objc + public let frozen: UInt + + /// Number of frames that were slow (took longer than 16.67ms but less than 700ms to render) + @objc + public let slow: UInt + +#if os(iOS) + /// Array of dictionaries describing slow frames' timestamps. + /// Each dictionary has a start and end timestamp for every such frame, + /// keyed under `start_timestamp` and `end_timestamp`. + @objc + public let slowFrameTimestamps: SentryFrameInfoTimeSeries + + /// Array of dictionaries describing frozen frames' timestamps. + /// Each dictionary has a start and end timestamp for every such frame, + /// keyed under `start_timestamp` and `end_timestamp`. + @objc + public let frozenFrameTimestamps: SentryFrameInfoTimeSeries + + /// Array of dictionaries describing the screen refresh rate at all points in time that it changes. + /// This can happen when modern devices go into low power mode, for example. + /// Each dictionary contains keys `timestamp` and `frame_rate`. + @objc + public let frameRateTimestamps: SentryFrameInfoTimeSeries +#endif // os(iOS) + + // MARK: - Initialization + + /// Creates a `SentryScreenFrames` instance with basic frame metrics. + /// - Parameters: + /// - total: Total number of frames rendered + /// - frozen: Number of frozen frames + /// - slow: Number of slow frames + @objc public init(total: UInt, frozen: UInt, slow: UInt) { +#if SENTRY_NO_UIKIT + let warningText = "SentryScreenFrames only works with UIKit enabled. Ensure you're using the right configuration of Sentry that links UIKit." + SentrySDKLog.warning(warningText) + assertionFailure(warningText) +#endif // SENTRY_NO_UIKIT + + #if os(iOS) + self.total = total + self.frozen = frozen + self.slow = slow + self.slowFrameTimestamps = [] + self.frozenFrameTimestamps = [] + self.frameRateTimestamps = [] + #else + self.total = total + self.frozen = frozen + self.slow = slow + #endif // !(os(watchOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) + + super.init() + } + +#if os(iOS) + /// Creates a `SentryScreenFrames` instance with detailed frame metrics including timing data. + /// - Parameters: + /// - total: Total number of frames rendered + /// - frozen: Number of frozen frames + /// - slow: Number of slow frames + /// - slowFrameTimestamps: Array of dictionaries with slow frame timing data + /// - frozenFrameTimestamps: Array of dictionaries with frozen frame timing data + /// - frameRateTimestamps: Array of dictionaries with frame rate change data + @objc public init( + total: UInt, + frozen: UInt, + slow: UInt, + slowFrameTimestamps: SentryFrameInfoTimeSeries, + frozenFrameTimestamps: SentryFrameInfoTimeSeries, + frameRateTimestamps: SentryFrameInfoTimeSeries + ) { + + #if SENTRY_NO_UIKIT + let warningText = "SentryScreenFrames only works with UIKit enabled. Ensure you're using the right configuration of Sentry that links UIKit." + SentrySDKLog.warning(warningText) + assertionFailure(warningText) + #endif // SENTRY_NO_UIKIT + self.total = total + self.frozen = frozen + self.slow = slow + self.slowFrameTimestamps = slowFrameTimestamps + self.frozenFrameTimestamps = frozenFrameTimestamps + self.frameRateTimestamps = frameRateTimestamps + super.init() + } +#endif // os(iOS) + + // MARK: - NSObject Overrides + + public override var description: String { + var result = "Total frames: \(total); slow frames: \(slow); frozen frames: \(frozen)" + +#if os(iOS) + result += "\nslowFrameTimestamps: \(slowFrameTimestamps)" + result += "\nfrozenFrameTimestamps: \(frozenFrameTimestamps)" + result += "\nframeRateTimestamps: \(frameRateTimestamps)" +#endif // os(iOS) + + return result + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? SentryScreenFrames else { return false } + + let basicPropertiesEqual = other.total == self.total + && other.frozen == self.frozen + && other.slow == self.slow + +#if os(iOS) + let timestampsEqual = NSArray(array: other.slowFrameTimestamps).isEqual(self.slowFrameTimestamps) + && NSArray(array: other.frozenFrameTimestamps).isEqual(self.frozenFrameTimestamps) + && NSArray(array: other.frameRateTimestamps).isEqual(self.frameRateTimestamps) + + return basicPropertiesEqual && timestampsEqual +#else + return basicPropertiesEqual +#endif // os(iOS) + } + + public override var hash: Int { + var hasher = Hasher() + hasher.combine(total) + hasher.combine(frozen) + hasher.combine(slow) + +#if os(iOS) + hasher.combine(NSArray(array: slowFrameTimestamps)) + hasher.combine(NSArray(array: frozenFrameTimestamps)) + hasher.combine(NSArray(array: frameRateTimestamps)) +#endif // os(iOS) + + return hasher.finalize() + } + + public func copy(with zone: NSZone? = nil) -> Any { +#if SENTRY_NO_UIKIT + let warningText = "SentryScreenFrames only works with UIKit enabled. Ensure you're using the right configuration of Sentry that links UIKit." + SentrySDKLog.warning(warningText) + assertionFailure(warningText) +#endif // SENTRY_NO_UIKIT + +#if os(iOS) + return SentryScreenFrames( + total: total, + frozen: frozen, + slow: slow, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: frozenFrameTimestamps, + frameRateTimestamps: frameRateTimestamps + ) + #else + return SentryScreenFrames( + total: total, + frozen: frozen, + slow: slow + ) + #endif // os(iOS + } +} + +#endif // (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) diff --git a/Tests/SentryProfilerTests/SentryProfilerTests.mm b/Tests/SentryProfilerTests/SentryProfilerTests.mm index 730b0bfe0a9..03f083b1ff8 100644 --- a/Tests/SentryProfilerTests/SentryProfilerTests.mm +++ b/Tests/SentryProfilerTests/SentryProfilerTests.mm @@ -10,7 +10,7 @@ # import "SentryProfilerMocks.h" # import "SentryProfilerSerialization+Test.h" # import "SentryProfilerState+ObjCpp.h" -# import "SentryScreenFrames.h" +# import "SentrySwift.h" # import "SentryThread.h" # import "SentryTransaction.h" # import "SentryTransactionContext+Private.h" diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 5c8553528f3..e7197964bce 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -161,7 +161,6 @@ #import "SentryScope+Private.h" #import "SentryScopeObserver.h" #import "SentryScopeSyncC.h" -#import "SentryScreenFrames.h" #import "SentryScreenshotIntegration.h" #import "SentrySerialization.h" #import "SentrySessionReplaySyncC.h" diff --git a/Tests/SentryTests/Swift/Profiling/SentryScreenFramesTests.swift b/Tests/SentryTests/Swift/Profiling/SentryScreenFramesTests.swift new file mode 100644 index 00000000000..6baba47e211 --- /dev/null +++ b/Tests/SentryTests/Swift/Profiling/SentryScreenFramesTests.swift @@ -0,0 +1,381 @@ +@_spi(Private) @testable import Sentry +import XCTest + +#if (os(iOS) || os(tvOS) || (swift(>=5.9) && os(visionOS))) + +class SentryScreenFramesTests: XCTestCase { + + // MARK: - Basic Initialization Tests + + func testInitWithBasicMetrics() { + let screenFrames = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + + XCTAssertEqual(screenFrames.total, 100) + XCTAssertEqual(screenFrames.frozen, 2) + XCTAssertEqual(screenFrames.slow, 8) + + #if os(iOS) + XCTAssertTrue(screenFrames.slowFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frozenFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frameRateTimestamps.isEmpty) + #endif // os(iOS) + } + + func testInitWithZeroValues() { + let screenFrames = SentryScreenFrames(total: 0, frozen: 0, slow: 0) + + XCTAssertEqual(screenFrames.total, 0) + XCTAssertEqual(screenFrames.frozen, 0) + XCTAssertEqual(screenFrames.slow, 0) + + #if os(iOS) + XCTAssertTrue(screenFrames.slowFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frozenFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frameRateTimestamps.isEmpty) + #endif // os(iOS) + } + + // MARK: - Profiling Supported Tests + + #if os(iOS) + func testInitWithDetailedMetrics() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 3)], + ["timestamp": NSNumber(value: 2_000.0), "value": NSNumber(value: 2)] + ] + let frozenFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 3_000.0), "value": NSNumber(value: 1)] + ] + let frameRateTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 60.0)], + ["timestamp": NSNumber(value: 5_000.0), "value": NSNumber(value: 30.0)], + ["timestamp": NSNumber(value: 7_000.0), "value": NSNumber(value: 10.0)] + ] + + let screenFrames = SentryScreenFrames( + total: 300, + frozen: 1, + slow: 2, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: frozenFrameTimestamps, + frameRateTimestamps: frameRateTimestamps + ) + + XCTAssertEqual(screenFrames.total, 300) + XCTAssertEqual(screenFrames.frozen, 1) + XCTAssertEqual(screenFrames.slow, 2) + + XCTAssertEqual(screenFrames.slowFrameTimestamps.count, 2) + XCTAssertEqual(screenFrames.slowFrameTimestamps[0]["timestamp"], 1_000) + XCTAssertEqual(screenFrames.slowFrameTimestamps[0]["value"], 3) + XCTAssertEqual(screenFrames.slowFrameTimestamps[1]["timestamp"], 2_000) + XCTAssertEqual(screenFrames.slowFrameTimestamps[1]["value"], 2) + + XCTAssertEqual(screenFrames.frozenFrameTimestamps.count, 1) + XCTAssertEqual(screenFrames.frozenFrameTimestamps[0]["timestamp"], 3_000) + XCTAssertEqual(screenFrames.frozenFrameTimestamps[0]["value"], 1) + + XCTAssertEqual(screenFrames.frameRateTimestamps.count, 3) + XCTAssertEqual(screenFrames.frameRateTimestamps[0]["timestamp"], 1_000) + XCTAssertEqual(screenFrames.frameRateTimestamps[0]["value"], 60) + XCTAssertEqual(screenFrames.frameRateTimestamps[1]["timestamp"], 5_000) + XCTAssertEqual(screenFrames.frameRateTimestamps[1]["value"], 30) + XCTAssertEqual(screenFrames.frameRateTimestamps[2]["timestamp"], 7_000) + XCTAssertEqual(screenFrames.frameRateTimestamps[2]["value"], 10) + } + + func testInitWithEmptyTimestamps() { + let screenFrames = SentryScreenFrames( + total: 50, + frozen: 0, + slow: 0, + slowFrameTimestamps: [], + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + XCTAssertEqual(screenFrames.total, 50) + XCTAssertTrue(screenFrames.slowFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frozenFrameTimestamps.isEmpty) + XCTAssertTrue(screenFrames.frameRateTimestamps.isEmpty) + } + #endif // os(iOS) + + // MARK: - Description Tests + + func testDescription() { + let screenFrames = SentryScreenFrames(total: 100, frozen: 5, slow: 15) + let description = screenFrames.description + + XCTAssertTrue(description.contains("Total frames: 100")) + XCTAssertTrue(description.contains("slow frames: 15")) + XCTAssertTrue(description.contains("frozen frames: 5")) + + #if os(iOS) + XCTAssertTrue(description.contains("slowFrameTimestamps:")) + XCTAssertTrue(description.contains("frozenFrameTimestamps:")) + XCTAssertTrue(description.contains("frameRateTimestamps:")) + #endif // os(iOS) + } + + #if os(iOS) + func testDescriptionWithTimestamps() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 1_020.0)] + ] + + let screenFrames = SentryScreenFrames( + total: 50, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + let description = screenFrames.description + XCTAssertTrue(description.contains("timestamp")) + XCTAssertTrue(description.contains("timestamp")) + XCTAssertTrue(description.contains("1000")) + XCTAssertTrue(description.contains("1020")) + } + #endif // os(iOS) + + // MARK: - Equality Tests + + func testEqualityWithSameBasicProperties() { + let screenFrames1 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + let screenFrames2 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + + XCTAssertTrue(screenFrames1.isEqual(screenFrames2)) + XCTAssertTrue(screenFrames2.isEqual(screenFrames1)) + } + + func testEqualityWithDifferentBasicProperties() { + let screenFrames1 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + let screenFrames2 = SentryScreenFrames(total: 100, frozen: 3, slow: 8) + + XCTAssertFalse(screenFrames1.isEqual(screenFrames2)) + XCTAssertFalse(screenFrames2.isEqual(screenFrames1)) + } + + func testEqualityWithDifferentTypes() { + let screenFrames = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + let otherObject = NSObject() + + XCTAssertFalse(screenFrames.isEqual(otherObject)) + XCTAssertFalse(screenFrames.isEqual(nil)) + } + + #if os(iOS) + func testEqualityWithSameTimestamps() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 1_020.0)] + ] + let frozenFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 2_000.0), "value": NSNumber(value: 2_800.0)] + ] + let frameRateTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 60.0)] + ] + + let screenFrames1 = SentryScreenFrames( + total: 100, + frozen: 1, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: frozenFrameTimestamps, + frameRateTimestamps: frameRateTimestamps + ) + + let screenFrames2 = SentryScreenFrames( + total: 100, + frozen: 1, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: frozenFrameTimestamps, + frameRateTimestamps: frameRateTimestamps + ) + + XCTAssertTrue(screenFrames1.isEqual(screenFrames2)) + } + + func testEqualityWithDifferentTimestamps() { + let slowFrameTimestamps1: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 1_020.0)] + ] + let slowFrameTimestamps2: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 2_000.0), "value": NSNumber(value: 2_020.0)] + ] + + let screenFrames1 = SentryScreenFrames( + total: 100, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps1, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + let screenFrames2 = SentryScreenFrames( + total: 100, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps2, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + XCTAssertFalse(screenFrames1.isEqual(screenFrames2)) + } + #endif // os(iOS) + + // MARK: - Hash Tests + + func testHashConsistency() { + let screenFrames1 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + let screenFrames2 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + + XCTAssertEqual(screenFrames1.hash, screenFrames2.hash) + } + + func testHashDifference() { + let screenFrames1 = SentryScreenFrames(total: 100, frozen: 2, slow: 8) + let screenFrames2 = SentryScreenFrames(total: 100, frozen: 3, slow: 8) + + XCTAssertNotEqual(screenFrames1.hash, screenFrames2.hash) + } + + #if os(iOS) + func testHashWithTimestamps() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 1)] + ] + + let screenFrames1 = SentryScreenFrames( + total: 100, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + let screenFrames2 = SentryScreenFrames( + total: 100, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + XCTAssertEqual(screenFrames1.hash, screenFrames2.hash) + } + #endif // os(iOS) + + // MARK: - NSCopying Tests + + #if os(iOS) + func testCopyWithBasicProperties() { + let original = SentryScreenFrames(total: 100, frozen: 5, slow: 10) + let copy = original.copy() as! SentryScreenFrames + + XCTAssertTrue(original.isEqual(copy)) + XCTAssertEqual(copy.total, 100) + XCTAssertEqual(copy.frozen, 5) + XCTAssertEqual(copy.slow, 10) + + // Ensure they are different objects + XCTAssertFalse(original === copy) + } + + func testCopyWithTimestamps() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 14.0)] + ] + let frozenFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 2_000.0), "value": NSNumber(value: 1.0)] + ] + let frameRateTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.0), "value": NSNumber(value: 60.0)] + ] + + let original = SentryScreenFrames( + total: 200, + frozen: 2, + slow: 5, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: frozenFrameTimestamps, + frameRateTimestamps: frameRateTimestamps + ) + + let copy = original.copy() as! SentryScreenFrames + + XCTAssertTrue(original.isEqual(copy)) + XCTAssertEqual(copy.slowFrameTimestamps.count, 1) + XCTAssertEqual(copy.frozenFrameTimestamps.count, 1) + XCTAssertEqual(copy.frameRateTimestamps.count, 1) + + // Ensure they are different objects + XCTAssertFalse(original === copy) + } + #endif // os(iOS) + + // MARK: - Edge Cases + + func testLargeValues() { + let maxUInt = UInt.max + let screenFrames = SentryScreenFrames(total: maxUInt, frozen: maxUInt, slow: maxUInt) + + XCTAssertEqual(screenFrames.total, maxUInt) + XCTAssertEqual(screenFrames.frozen, maxUInt) + XCTAssertEqual(screenFrames.slow, maxUInt) + } + + #if os(iOS) + func testLargeTimestampArrays() { + // Create a large array to test performance and memory handling + var largeTimestamps: SentryFrameInfoTimeSeries = [] + for i in 0..<1_000 { + largeTimestamps.append([ + "timestamp": NSNumber(value: Double(i * 1_000)), + "value": NSNumber(value: Double(i * 1_000 + 17)) + ]) + } + + let screenFrames = SentryScreenFrames( + total: 1_000, + frozen: 0, + slow: 1_000, + slowFrameTimestamps: largeTimestamps, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + XCTAssertEqual(screenFrames.slowFrameTimestamps.count, 1_000) + XCTAssertNotNil(screenFrames.description) // Should not crash + } + + func testTimestampDataIntegrity() { + let slowFrameTimestamps: SentryFrameInfoTimeSeries = [ + ["timestamp": NSNumber(value: 1_000.5), "value": NSNumber(value: 1_016.7)] + ] + + let screenFrames = SentryScreenFrames( + total: 1, + frozen: 0, + slow: 1, + slowFrameTimestamps: slowFrameTimestamps, + frozenFrameTimestamps: [], + frameRateTimestamps: [] + ) + + let firstFrame = screenFrames.slowFrameTimestamps[0] + XCTAssertEqual(firstFrame["timestamp"], NSNumber(value: 1_000.5)) + XCTAssertEqual(firstFrame["value"], NSNumber(value: 1_016.7)) + } + #endif // os(iOS) +} + +#endif // (os(iOS) || os(tvOS) || os(swift(>=5.9) && os(visionOS)))