From b2f82fa9222d2b8fb2d404eba1119b0675f41998 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 18 Jan 2023 12:42:56 -0500 Subject: [PATCH] feat: monitor for new metric values via sampling and notifications (#2493) * feat: monitor for new metric values via sampling and notifications * wrap syscalls/NSProcess and NSTimer APIs with tests/mocks * typedef system type to SentryRAMBytes for better understandability * split slow/frozen GPU frame render timestamps; use singular unit names * wrap bare nanosecond conversions in function * add slow/frozen frames to test case, test amount of GPU frame render timestamps recorded for profiling metrics, using mock in test * fix typo "TestDiplayLinkWrapper"; simplify lazy property init --- CHANGELOG.md | 9 + Sentry.xcodeproj/project.pbxproj | 152 ++++--- Sources/Sentry/Public/SentryError.h | 3 + .../Sentry/{SentryError.m => SentryError.mm} | 10 + Sources/Sentry/SentryFramesTracker.m | 21 +- Sources/Sentry/SentryMachLogging.cpp | 404 +++++++++--------- Sources/Sentry/SentryMetricProfiler.mm | 173 ++++++++ .../SentryNSNotificationCenterWrapper.m | 24 +- Sources/Sentry/SentryNSProcessInfoWrapper.mm | 10 + Sources/Sentry/SentryNSTimerWrapper.m | 7 - Sources/Sentry/SentryProfiler.mm | 234 +++++++--- Sources/Sentry/SentryScreenFrames.m | 9 +- Sources/Sentry/SentrySystemWrapper.mm | 62 +++ Sources/Sentry/SentryTime.mm | 9 + .../include/HybridPublic/SentryScreenFrames.h | 13 +- Sources/Sentry/include/SentryMachLogging.hpp | 39 +- Sources/Sentry/include/SentryMetricProfiler.h | 39 ++ .../SentryNSNotificationCenterWrapper.h | 8 +- .../include/SentryNSProcessInfoWrapper.h | 11 + Sources/Sentry/include/SentryProfiler.h | 10 +- Sources/Sentry/include/SentrySystemWrapper.h | 31 ++ Sources/Sentry/include/SentryTime.h | 6 + .../SentryNSProcessInfoWrapperTests.swift | 12 + .../Helper/SentryNSTimerWrapperTest.swift | 31 ++ .../Helper/SentryProfiler+SwiftTest.h | 32 ++ .../Helper/SentrySystemWrapperTests.swift | 26 ++ .../TestSentryNSProcessInfoWrapper.swift | 13 + .../Helper/TestSentryNSTimerWrapper.swift | 2 +- .../Helper/TestSentrySystemWrapper.swift | 28 ++ .../SentryFramesTrackerTests.swift | 8 +- ...SentryFramesTrackingIntegrationTests.swift | 2 +- .../TestDisplayLinkWrapper.swift | 2 +- .../Performance/SentryTracerTests.swift | 4 +- .../PrivateSentrySDKOnlyTests.swift | 2 +- .../Profiling/SentryProfilerSwiftTests.swift | 195 +++++++-- .../Protocol/SentryNSErrorTests.swift | 11 + Tests/SentryTests/SentryClientTests.swift | 2 +- .../TestSentryUIDeviceWrapper.swift | 4 +- .../SentryTests}/SentryNSTimerWrapper+Test.h | 2 - .../SentryTests/SentryTests-Bridging-Header.h | 7 +- 40 files changed, 1236 insertions(+), 431 deletions(-) rename Sources/Sentry/{SentryError.m => SentryError.mm} (70%) create mode 100644 Sources/Sentry/SentryMetricProfiler.mm create mode 100644 Sources/Sentry/SentryNSProcessInfoWrapper.mm create mode 100644 Sources/Sentry/SentrySystemWrapper.mm create mode 100644 Sources/Sentry/include/SentryMetricProfiler.h create mode 100644 Sources/Sentry/include/SentryNSProcessInfoWrapper.h create mode 100644 Sources/Sentry/include/SentrySystemWrapper.h create mode 100644 Tests/SentryTests/Helper/SentryNSProcessInfoWrapperTests.swift create mode 100644 Tests/SentryTests/Helper/SentryNSTimerWrapperTest.swift create mode 100644 Tests/SentryTests/Helper/SentryProfiler+SwiftTest.h create mode 100644 Tests/SentryTests/Helper/SentrySystemWrapperTests.swift create mode 100644 Tests/SentryTests/Helper/TestSentryNSProcessInfoWrapper.swift create mode 100644 Tests/SentryTests/Helper/TestSentrySystemWrapper.swift rename {Sources/Sentry/include => Tests/SentryTests}/SentryNSTimerWrapper+Test.h (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 373ef06c38b..711f4acc7e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # Changelog +## Unreleased + +### Features + +- Gather profiling timeseries metrics for CPU usage and memory footprint, and thermal and memory pressure events (#2493) + ## 8.0.0 +### Features + +This version adds a dependency on Swift. This version adds a dependency on Swift. We renamed the default branch from `master` to `main`. We are going to keep the `master` branch for backwards compatibility for package managers pointing to the `master` branch. ### Features diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index ac35822e624..ba22f09ab06 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -49,10 +49,6 @@ 0A2D8DA9289BC905008720F6 /* SentryViewHierarchy.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A2D8DA7289BC905008720F6 /* SentryViewHierarchy.m */; }; 0A4EDEA928D3461B00FA67CB /* SentryPerformanceTracker+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A4EDEA828D3461B00FA67CB /* SentryPerformanceTracker+Private.h */; }; 0A5370A128A3EC2400B2DCDE /* SentryViewHierarchyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5370A028A3EC2400B2DCDE /* SentryViewHierarchyTests.swift */; }; - 0A54C3B0294B3F2100318F31 /* SentryNSTimerWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A54C3AF294B3F2100318F31 /* SentryNSTimerWrapper.m */; }; - 0A54C3B2294B3F4D00318F31 /* SentryNSTimerWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A54C3B1294B3F4D00318F31 /* SentryNSTimerWrapper.h */; }; - 0A54C3B6294B3FCD00318F31 /* SentryNSTimerWrapper+Test.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A54C3B5294B3FCD00318F31 /* SentryNSTimerWrapper+Test.h */; }; - 0A54C3B7294B403F00318F31 /* TestSentryNSTimerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A54C3B3294B3FA000318F31 /* TestSentryNSTimerWrapper.swift */; }; 0A56DA5F28ABA01B00C400D5 /* SentryTransactionContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A56DA5E28ABA01B00C400D5 /* SentryTransactionContext+Private.h */; }; 0A6EEADD28A657970076B469 /* UIViewRecursiveDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */; }; 0A80E433291017C300095219 /* SentryWatchdogTerminationScopeObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A80E432291017C300095219 /* SentryWatchdogTerminationScopeObserver.m */; }; @@ -145,7 +141,7 @@ 63AA76991EB9C1C200D153DE /* SentryDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = 63AA76951EB9C1C200D153DE /* SentryDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; 63AA769A1EB9C1C200D153DE /* SentryLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 63AA76961EB9C1C200D153DE /* SentryLog.h */; settings = {ATTRIBUTES = (Private, ); }; }; 63AA769D1EB9C57A00D153DE /* SentryError.h in Headers */ = {isa = PBXBuildFile; fileRef = 63AA769B1EB9C57A00D153DE /* SentryError.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 63AA769E1EB9C57A00D153DE /* SentryError.m in Sources */ = {isa = PBXBuildFile; fileRef = 63AA769C1EB9C57A00D153DE /* SentryError.m */; }; + 63AA769E1EB9C57A00D153DE /* SentryError.mm in Sources */ = {isa = PBXBuildFile; fileRef = 63AA769C1EB9C57A00D153DE /* SentryError.mm */; }; 63AA76A31EB9CBAA00D153DE /* SentryDsn.m in Sources */ = {isa = PBXBuildFile; fileRef = 63AA76A11EB9CBAA00D153DE /* SentryDsn.m */; }; 63AA76A51EB9CBC200D153DE /* SentryDsn.h in Headers */ = {isa = PBXBuildFile; fileRef = 63AA76A41EB9CBC200D153DE /* SentryDsn.h */; settings = {ATTRIBUTES = (Public, ); }; }; 63AF656C1ED87B8C00EBCFF7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 6304360D1EC05CEF00C4D3FA /* libz.tbd */; }; @@ -610,8 +606,22 @@ 7DC8310A2398283C0043DD9A /* SentryCrashIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DC831082398283C0043DD9A /* SentryCrashIntegration.h */; }; 7DC8310C2398283C0043DD9A /* SentryCrashIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DC831092398283C0043DD9A /* SentryCrashIntegration.m */; }; 8419C0C428C1889D001C8259 /* SentryProfilerSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419C0C328C1889D001C8259 /* SentryProfilerSwiftTests.swift */; }; + 844EDC6F294143B900C86F34 /* SentryNSProcessInfoWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 844EDC6D294143B900C86F34 /* SentryNSProcessInfoWrapper.h */; }; + 844EDC70294143B900C86F34 /* SentryNSProcessInfoWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC6E294143B900C86F34 /* SentryNSProcessInfoWrapper.mm */; }; + 844EDC73294144B200C86F34 /* TestSentryNSProcessInfoWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC712941442200C86F34 /* TestSentryNSProcessInfoWrapper.swift */; }; + 844EDC76294144DB00C86F34 /* SentrySystemWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 844EDC74294144DB00C86F34 /* SentrySystemWrapper.h */; }; + 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC75294144DB00C86F34 /* SentrySystemWrapper.mm */; }; + 844EDC7A29415AE800C86F34 /* TestSentrySystemWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDC7829415AB300C86F34 /* TestSentrySystemWrapper.swift */; }; + 844EDCE52947DC3100C86F34 /* SentryNSTimerWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 844EDCE32947DC3100C86F34 /* SentryNSTimerWrapper.h */; }; + 844EDCE62947DC3100C86F34 /* SentryNSTimerWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 844EDCE42947DC3100C86F34 /* SentryNSTimerWrapper.m */; }; + 844EDCE82947DCD700C86F34 /* TestSentryNSTimerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844EDCE72947DCD700C86F34 /* TestSentryNSTimerWrapper.swift */; }; + 844EDD6C2949387000C86F34 /* SentryMetricProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 844EDD6B2949387000C86F34 /* SentryMetricProfiler.h */; }; 8453421228BE855D00C22EEC /* SentrySampleDecision.m in Sources */ = {isa = PBXBuildFile; fileRef = 8453421128BE855D00C22EEC /* SentrySampleDecision.m */; }; 8453421628BE8A9500C22EEC /* SentrySpanStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 8453421528BE8A9500C22EEC /* SentrySpanStatus.m */; }; + 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */; }; + 849472812971C107002603DE /* SentrySystemWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472802971C107002603DE /* SentrySystemWrapperTests.swift */; }; + 849472832971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472822971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift */; }; + 849472852971C41A002603DE /* SentryNSTimerWrapperTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849472842971C41A002603DE /* SentryNSTimerWrapperTest.swift */; }; 84A888FD28D9B11700C51DFD /* SentryProfiler+Test.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */; }; 84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A8891A28DBD28900C51DFD /* SentryDevice.h */; }; 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84A8891B28DBD28900C51DFD /* SentryDevice.mm */; }; @@ -829,10 +839,6 @@ 0A2D8DA7289BC905008720F6 /* SentryViewHierarchy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryViewHierarchy.m; sourceTree = ""; }; 0A4EDEA828D3461B00FA67CB /* SentryPerformanceTracker+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryPerformanceTracker+Private.h"; path = "include/SentryPerformanceTracker+Private.h"; sourceTree = ""; }; 0A5370A028A3EC2400B2DCDE /* SentryViewHierarchyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewHierarchyTests.swift; sourceTree = ""; }; - 0A54C3AF294B3F2100318F31 /* SentryNSTimerWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSTimerWrapper.m; sourceTree = ""; }; - 0A54C3B1294B3F4D00318F31 /* SentryNSTimerWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSTimerWrapper.h; path = include/SentryNSTimerWrapper.h; sourceTree = ""; }; - 0A54C3B3294B3FA000318F31 /* TestSentryNSTimerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryNSTimerWrapper.swift; sourceTree = ""; }; - 0A54C3B5294B3FCD00318F31 /* SentryNSTimerWrapper+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryNSTimerWrapper+Test.h"; path = "include/SentryNSTimerWrapper+Test.h"; sourceTree = ""; }; 0A56DA5E28ABA01B00C400D5 /* SentryTransactionContext+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryTransactionContext+Private.h"; path = "include/SentryTransactionContext+Private.h"; sourceTree = ""; }; 0A6EEADC28A657970076B469 /* UIViewRecursiveDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewRecursiveDescriptionTests.swift; sourceTree = ""; }; 0A80E432291017C300095219 /* SentryWatchdogTerminationScopeObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryWatchdogTerminationScopeObserver.m; sourceTree = ""; }; @@ -933,7 +939,7 @@ 63AA76951EB9C1C200D153DE /* SentryDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryDefines.h; path = Public/SentryDefines.h; sourceTree = ""; }; 63AA76961EB9C1C200D153DE /* SentryLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryLog.h; path = include/SentryLog.h; sourceTree = ""; }; 63AA769B1EB9C57A00D153DE /* SentryError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryError.h; path = Public/SentryError.h; sourceTree = ""; }; - 63AA769C1EB9C57A00D153DE /* SentryError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryError.m; sourceTree = ""; }; + 63AA769C1EB9C57A00D153DE /* SentryError.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryError.mm; sourceTree = ""; }; 63AA76A11EB9CBAA00D153DE /* SentryDsn.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryDsn.m; sourceTree = ""; }; 63AA76A41EB9CBC200D153DE /* SentryDsn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryDsn.h; path = Public/SentryDsn.h; sourceTree = ""; }; 63AA76AE1EB9D5CD00D153DE /* SentryTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SentryTests.xcconfig; sourceTree = ""; }; @@ -1451,8 +1457,24 @@ 844DA81D28246DAE00E6B62E /* develop-docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "develop-docs"; sourceTree = ""; }; 844DA81E28246DB900E6B62E /* fastlane */ = {isa = PBXFileReference; lastKnownFileType = folder; path = fastlane; sourceTree = ""; }; 844DA81F28246DE300E6B62E /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = ""; }; + 844EDC6D294143B900C86F34 /* SentryNSProcessInfoWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSProcessInfoWrapper.h; path = include/SentryNSProcessInfoWrapper.h; sourceTree = ""; }; + 844EDC6E294143B900C86F34 /* SentryNSProcessInfoWrapper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryNSProcessInfoWrapper.mm; sourceTree = ""; }; + 844EDC712941442200C86F34 /* TestSentryNSProcessInfoWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryNSProcessInfoWrapper.swift; sourceTree = ""; }; + 844EDC74294144DB00C86F34 /* SentrySystemWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySystemWrapper.h; path = include/SentrySystemWrapper.h; sourceTree = ""; }; + 844EDC75294144DB00C86F34 /* SentrySystemWrapper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentrySystemWrapper.mm; sourceTree = ""; }; + 844EDC7829415AB300C86F34 /* TestSentrySystemWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentrySystemWrapper.swift; sourceTree = ""; }; + 844EDC7B2942843400C86F34 /* SentryProfiler+SwiftTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryProfiler+SwiftTest.h"; path = "Tests/SentryTests/Helper/SentryProfiler+SwiftTest.h"; sourceTree = SOURCE_ROOT; }; + 844EDCE32947DC3100C86F34 /* SentryNSTimerWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryNSTimerWrapper.h; path = include/SentryNSTimerWrapper.h; sourceTree = ""; }; + 844EDCE42947DC3100C86F34 /* SentryNSTimerWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSTimerWrapper.m; sourceTree = ""; }; + 844EDCE72947DCD700C86F34 /* TestSentryNSTimerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentryNSTimerWrapper.swift; sourceTree = ""; }; + 844EDCE92947E78B00C86F34 /* SentryNSTimerWrapper+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryNSTimerWrapper+Test.h"; sourceTree = ""; }; + 844EDD6B2949387000C86F34 /* SentryMetricProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryMetricProfiler.h; path = Sources/Sentry/include/SentryMetricProfiler.h; sourceTree = SOURCE_ROOT; }; 8453421128BE855D00C22EEC /* SentrySampleDecision.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySampleDecision.m; sourceTree = ""; }; 8453421528BE8A9500C22EEC /* SentrySpanStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySpanStatus.m; sourceTree = ""; }; + 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SentryMetricProfiler.mm; path = Sources/Sentry/SentryMetricProfiler.mm; sourceTree = SOURCE_ROOT; }; + 849472802971C107002603DE /* SentrySystemWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySystemWrapperTests.swift; sourceTree = ""; }; + 849472822971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSProcessInfoWrapperTests.swift; sourceTree = ""; }; + 849472842971C41A002603DE /* SentryNSTimerWrapperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSTimerWrapperTest.swift; sourceTree = ""; }; 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryProfiler+Test.h"; path = "Sources/Sentry/include/SentryProfiler+Test.h"; sourceTree = SOURCE_ROOT; }; 84A8891A28DBD28900C51DFD /* SentryDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDevice.h; path = include/SentryDevice.h; sourceTree = ""; }; 84A8891B28DBD28900C51DFD /* SentryDevice.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryDevice.mm; sourceTree = ""; }; @@ -1940,7 +1962,7 @@ 63AA76951EB9C1C200D153DE /* SentryDefines.h */, D8BD2E67293619F600D96C6A /* PrivatesHeader.h */, 63AA769B1EB9C57A00D153DE /* SentryError.h */, - 63AA769C1EB9C57A00D153DE /* SentryError.m */, + 63AA769C1EB9C57A00D153DE /* SentryError.mm */, 7B42C47F27E08F33009B58C2 /* SentryDependencyContainer.h */, 7B42C48127E08F4B009B58C2 /* SentryDependencyContainer.m */, 7BC8522E24581096005A70F0 /* SentryFileContents.h */, @@ -1988,9 +2010,12 @@ 84A8891A28DBD28900C51DFD /* SentryDevice.h */, 84A8891B28DBD28900C51DFD /* SentryDevice.mm */, 0A9E917028DC7E7000FB4182 /* SentryInternalDefines.h */, - 0A54C3B1294B3F4D00318F31 /* SentryNSTimerWrapper.h */, - 0A54C3AF294B3F2100318F31 /* SentryNSTimerWrapper.m */, - 0A54C3B5294B3FCD00318F31 /* SentryNSTimerWrapper+Test.h */, + 844EDC6D294143B900C86F34 /* SentryNSProcessInfoWrapper.h */, + 844EDC6E294143B900C86F34 /* SentryNSProcessInfoWrapper.mm */, + 844EDC74294144DB00C86F34 /* SentrySystemWrapper.h */, + 844EDC75294144DB00C86F34 /* SentrySystemWrapper.mm */, + 844EDCE32947DC3100C86F34 /* SentryNSTimerWrapper.h */, + 844EDCE42947DC3100C86F34 /* SentryNSTimerWrapper.m */, ); name = Helper; sourceTree = ""; @@ -2022,50 +2047,51 @@ 63AA75931EB8AEDB00D153DE /* SentryTests */ = { isa = PBXGroup; children = ( - 035E73C627D5661A005EEB11 /* Profiling */, - 7B6438AD26A710E6000D0F65 /* Categories */, - 7B6C5ED4264E62B60010D138 /* Transaction */, - 7B42602C26302DE500B36EDD /* Performance */, - 8E4A038125F76A4900000D77 /* Dynamic */, - 7BF536D224BEF240004FA6A2 /* TestUtils */, - 7B3D0474249A3D5800E106B6 /* Protocol */, - 7B944FA924697E9700A10721 /* Integrations */, - 7BD7299B24654CD500EA3610 /* Helper */, - 7BBD18AF24517E5D00427C76 /* Networking */, - 63FE71D220DA66C500CDBAE8 /* SentryCrash */, - 7B944FAC2469B41600A10721 /* State */, - D81FDF0F280E9FEC0045E0E4 /* Tools */, - 63AA75941EB8AEDB00D153DE /* Info.plist */, - 639889D21EDF06C100EA7442 /* SentryTests-Bridging-Header.h */, - 63AA75951EB8AEDB00D153DE /* SentryTests.m */, + 7B3878E92490D90400EBDEA2 /* SentryClient+TestInit.h */, + 844EDCE92947E78B00C86F34 /* SentryNSTimerWrapper+Test.h */, + 7B569DFE2590EEF600B653FC /* SentryScope+Equality.h */, + 7B569E052590F04700B653FC /* SentryScope+Properties.h */, 7B9421C4260CA393001F9349 /* SentrySDK+Tests.h */, - 7BD47B4C268F0B080076A663 /* ClearTestState.swift */, - 7BA8409F24A1EC6E00B718AA /* SentrySDKTests.swift */, - 630436151EC0AD3100C4D3FA /* SentryNSDataCompressionTests.m */, - 630C01931EC3402C00C52CEF /* SentryKSCrashReportConverterTests.m */, + 639889D21EDF06C100EA7442 /* SentryTests-Bridging-Header.h */, + 15360CF22433C59500112302 /* SentryInstallationTests.m */, 63B819131EC352A7002FDF4C /* SentryInterfacesTests.m */, + 630C01931EC3402C00C52CEF /* SentryKSCrashReportConverterTests.m */, + 630436151EC0AD3100C4D3FA /* SentryNSDataCompressionTests.m */, 63EED6C22237989300E02400 /* SentryOptionsTest.m */, - 632331F52404FFA8008D91D6 /* SentryScopeTests.m */, - 7B569DFE2590EEF600B653FC /* SentryScope+Equality.h */, 7B569DFF2590EEF600B653FC /* SentryScope+Equality.m */, - 7B569E052590F04700B653FC /* SentryScope+Properties.h */, - 7B6CC50124EE5A42001816D7 /* SentryHubTests.swift */, - 15360CF22433C59500112302 /* SentryInstallationTests.m */, + 632331F52404FFA8008D91D6 /* SentryScopeTests.m */, + 7B0002312477F0520035FEF1 /* SentrySessionTests.m */, + 63AA75951EB8AEDB00D153DE /* SentryTests.m */, + 63AA75941EB8AEDB00D153DE /* Info.plist */, + 7BD47B4C268F0B080076A663 /* ClearTestState.swift */, + 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */, + 7B4260332630315C00B36EDD /* SampleError.swift */, 7BAF3DB4243C743E008A5414 /* SentryClientTests.swift */, - 7B3878E92490D90400EBDEA2 /* SentryClient+TestInit.h */, - 7BAF3DD6243DD4A1008A5414 /* TestConstants.swift */, + A8AFFCD12907DA7600967CD7 /* SentryHttpStatusCodeRangeTests.swift */, + 7B6CC50124EE5A42001816D7 /* SentryHubTests.swift */, 15D0AC872459EE4D006541C2 /* SentryNSURLRequestTests.swift */, - 7B0002312477F0520035FEF1 /* SentrySessionTests.m */, - 7B0002332477F52D0035FEF1 /* SentrySessionTests.swift */, - 7B944FAF2469B46000A10721 /* TestClient.swift */, 7B0A54552523178700A71716 /* SentryScopeSwiftTests.swift */, + D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */, + 7BA8409F24A1EC6E00B718AA /* SentrySDKTests.swift */, + 7B0002332477F52D0035FEF1 /* SentrySessionTests.swift */, 8E70B10025CB8695002B3155 /* SentrySpanIdTests.swift */, - 7B4260332630315C00B36EDD /* SampleError.swift */, - 7B6D1262265F7CC600C9BE4B /* PrivateSentrySDKOnlyTests.swift */, 8ED3D305264DFE700049393B /* SwiftDescriptorTests.swift */, - D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */, + 7B944FAF2469B46000A10721 /* TestClient.swift */, + 7BAF3DD6243DD4A1008A5414 /* TestConstants.swift */, 0A1B497228E597DD00D7BFA3 /* TestLogOutput.swift */, - A8AFFCD12907DA7600967CD7 /* SentryHttpStatusCodeRangeTests.swift */, + 7B6438AD26A710E6000D0F65 /* Categories */, + 8E4A038125F76A4900000D77 /* Dynamic */, + 7BD7299B24654CD500EA3610 /* Helper */, + 7B944FA924697E9700A10721 /* Integrations */, + 7BBD18AF24517E5D00427C76 /* Networking */, + 7B42602C26302DE500B36EDD /* Performance */, + 035E73C627D5661A005EEB11 /* Profiling */, + 7B3D0474249A3D5800E106B6 /* Protocol */, + 63FE71D220DA66C500CDBAE8 /* SentryCrash */, + 7B944FAC2469B41600A10721 /* State */, + 7BF536D224BEF240004FA6A2 /* TestUtils */, + D81FDF0F280E9FEC0045E0E4 /* Tools */, + 7B6C5ED4264E62B60010D138 /* Transaction */, ); path = SentryTests; sourceTree = ""; @@ -2606,7 +2632,12 @@ 7B18DE4328D9F8F6004845C6 /* TestNSNotificationCenterWrapper.swift */, 7B18DE4928DA0C8B004845C6 /* SentryNSNotificationCenterWrapperTests.swift */, 84A8892028DBD8D600C51DFD /* SentryDeviceTests.mm */, - 0A54C3B3294B3FA000318F31 /* TestSentryNSTimerWrapper.swift */, + 844EDC712941442200C86F34 /* TestSentryNSProcessInfoWrapper.swift */, + 844EDC7829415AB300C86F34 /* TestSentrySystemWrapper.swift */, + 844EDCE72947DCD700C86F34 /* TestSentryNSTimerWrapper.swift */, + 849472802971C107002603DE /* SentrySystemWrapperTests.swift */, + 849472822971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift */, + 849472842971C41A002603DE /* SentryNSTimerWrapperTest.swift */, ); path = Helper; sourceTree = ""; @@ -2912,7 +2943,10 @@ 03F84D1B27DD414C008FE43F /* SentryMachLogging.hpp */, 03F84D2C27DD4191008FE43F /* SentryMachLogging.cpp */, 03F84D1127DD414C008FE43F /* SentryProfiler.h */, + 844EDD6B2949387000C86F34 /* SentryMetricProfiler.h */, + 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */, 84A888FC28D9B11700C51DFD /* SentryProfiler+Test.h */, + 844EDC7B2942843400C86F34 /* SentryProfiler+SwiftTest.h */, 03F84D2B27DD4191008FE43F /* SentryProfiler.mm */, 03BCC38D27E2A377003232C7 /* SentryProfilingConditionals.h */, 03F84D2927DD416B008FE43F /* SentryProfilingLogging.hpp */, @@ -3192,7 +3226,6 @@ 63B818F91EC34639002FDF4C /* SentryDebugMeta.h in Headers */, 6360850D1ED2AFE100E8599E /* SentryBreadcrumb.h in Headers */, 7BAF3DD92440AEC8008A5414 /* SentryRequestManager.h in Headers */, - 0A54C3B2294B3F4D00318F31 /* SentryNSTimerWrapper.h in Headers */, 7BE3C77B2446111500A38442 /* SentryRateLimitParser.h in Headers */, 84A888FD28D9B11700C51DFD /* SentryProfiler+Test.h in Headers */, 7D0637032382B34300B30749 /* SentryScope.h in Headers */, @@ -3223,10 +3256,12 @@ 635B3F381EBC6E2500A6176D /* SentryAsynchronousOperation.h in Headers */, 7BD4BD4727EB2A3D0071F4FF /* SentryDiscardedEvent.h in Headers */, 7BBD18972449DC1D00427C76 /* SentryRateLimits.h in Headers */, + 844EDC76294144DB00C86F34 /* SentrySystemWrapper.h in Headers */, 7B3B473025D6CBFC00D01640 /* SentryNSError.h in Headers */, 630436101EC0600A00C4D3FA /* SentrySerializable.h in Headers */, 63FE70DD20DA4C1000CDBAE8 /* SentryCrashMonitor_Signal.h in Headers */, 63FE710320DA4C1000CDBAE8 /* SentryCrashMachineContext_Apple.h in Headers */, + 844EDC6F294143B900C86F34 /* SentryNSProcessInfoWrapper.h in Headers */, D8479328278873A100BE8E99 /* SentryByteCountFormatter.h in Headers */, 63AA76981EB9C1C200D153DE /* SentryClient.h in Headers */, 63AA76971EB9C1C200D153DE /* Sentry.h in Headers */, @@ -3247,7 +3282,6 @@ 7B8713AE26415ADF006D6004 /* SentryAppStartTrackingIntegration.h in Headers */, 7B7D873224864BB900D2ECFF /* SentryCrashMachineContextWrapper.h in Headers */, 861265F92404EC1500C4AFDE /* NSArray+SentrySanitize.h in Headers */, - 0A54C3B6294B3FCD00318F31 /* SentryNSTimerWrapper+Test.h in Headers */, 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, @@ -3324,6 +3358,7 @@ 7BD86EC5264A63F6005439DB /* SentrySysctl.h in Headers */, 63BE85701ECEC6DE00DC44F5 /* NSDate+SentryExtras.h in Headers */, 63FE709520DA4C1000CDBAE8 /* SentryCrashReportFilterBasic.h in Headers */, + 844EDCE52947DC3100C86F34 /* SentryNSTimerWrapper.h in Headers */, 63FE70A120DA4C1000CDBAE8 /* SentryCrashCString.h in Headers */, 7B63459D280EBA6300CFA05A /* SentryUIEventTracker.h in Headers */, 7B7D873424864C6600D2ECFF /* SentryCrashDefaultMachineContextWrapper.h in Headers */, @@ -3392,6 +3427,7 @@ 639FCF9C1EBC7F9500778193 /* SentryThread.h in Headers */, 63FE716B20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.h in Headers */, 63AA76A51EB9CBC200D153DE /* SentryDsn.h in Headers */, + 844EDD6C2949387000C86F34 /* SentryMetricProfiler.h in Headers */, 636085131ED47BE600E8599E /* SentryFileManager.h in Headers */, 7BD729962463E83300EA3610 /* SentryDateUtil.h in Headers */, 63FE707B20DA4C1000CDBAE8 /* Container+SentryDeepSearch.h in Headers */, @@ -3640,7 +3676,7 @@ D8CB741B2947286500A5F964 /* SentryEnvelopeItemHeader.m in Sources */, D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */, D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */, - 63AA769E1EB9C57A00D153DE /* SentryError.m in Sources */, + 63AA769E1EB9C57A00D153DE /* SentryError.mm in Sources */, 7B8713B026415B22006D6004 /* SentryAppStartTrackingIntegration.m in Sources */, 8E133FA225E72DEF00ABD0BF /* SentrySamplingContext.m in Sources */, 7B6C5F8726034395007F7DFF /* SentryWatchdogTerminationLogic.m in Sources */, @@ -3657,6 +3693,7 @@ D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */, 7B30B67E26527894006B2752 /* SentryDisplayLinkWrapper.m in Sources */, 63FE711D20DA4C1000CDBAE8 /* SentryCrashCPU_arm64.c in Sources */, + 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, 630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */, 7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */, 639FCFA11EBC804600778193 /* SentryException.m in Sources */, @@ -3699,7 +3736,6 @@ 63FE70D520DA4C1000CDBAE8 /* SentryCrashMonitor_NSException.m in Sources */, 0AAE201E28ED9B9400D0CD80 /* SentryReachability.m in Sources */, 7B0A54282521C22C00A71716 /* SentryFrameRemover.m in Sources */, - 0A54C3B0294B3F2100318F31 /* SentryNSTimerWrapper.m in Sources */, 7BC63F0A28081288009D9E37 /* SentrySwizzleWrapper.m in Sources */, 7B6C5EDC264E8DA80010D138 /* SentryFramesTrackingIntegration.m in Sources */, 63295AF71EF3C7DB002D4490 /* NSDictionary+SentrySanitize.m in Sources */, @@ -3714,6 +3750,7 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */, 7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */, + 844EDCE62947DC3100C86F34 /* SentryNSTimerWrapper.m in Sources */, 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.m in Sources */, 63BE85711ECEC6DE00DC44F5 /* NSDate+SentryExtras.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, @@ -3738,6 +3775,7 @@ 6344DDB11EC308E400D9160D /* SentryCrashInstallationReporter.m in Sources */, D85596F3280580F10041FF8B /* SentryScreenshotIntegration.m in Sources */, 7BAF3DCE243DCBFE008A5414 /* SentryTransportFactory.m in Sources */, + 844EDC70294143B900C86F34 /* SentryNSProcessInfoWrapper.mm in Sources */, 7BB65501253DC1B500887E87 /* SentryUserFeedback.m in Sources */, 7D5C441A237C2E1F00DAB0A3 /* SentrySDK.m in Sources */, 7D65260E237F649E00113EA2 /* SentryScope.m in Sources */, @@ -3746,6 +3784,7 @@ 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentryTracesSampler.m in Sources */, + 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */, 63FE714120DA4C1100CDBAE8 /* SentryCrashDate.c in Sources */, 63FE70DB20DA4C1000CDBAE8 /* SentryCrashMonitor_System.m in Sources */, 7BA61CBB247BC5D800C130A8 /* SentryCrashDefaultBinaryImageProvider.m in Sources */, @@ -3848,6 +3887,8 @@ 632331F62404FFA8008D91D6 /* SentryScopeTests.m in Sources */, D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */, 0A283E79291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift in Sources */, + 849472852971C41A002603DE /* SentryNSTimerWrapperTest.swift in Sources */, + 849472812971C107002603DE /* SentrySystemWrapperTests.swift in Sources */, 63FE720D20DA66EC00CDBAE8 /* NSError+SimpleConstructor_Tests.m in Sources */, 69BEE6F72620729E006DF9DF /* UrlSessionDelegateSpy.swift in Sources */, 035E73C827D56757005EEB11 /* SentryBacktraceTests.mm in Sources */, @@ -3913,6 +3954,7 @@ 7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */, 7B72D23A28D074BC0014798A /* TestExtensions.swift in Sources */, 7BBD18BB24530D2600427C76 /* SentryFileManagerTests.swift in Sources */, + 849472832971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift in Sources */, 63FE722020DA66EC00CDBAE8 /* SentryCrashObjC_Tests.m in Sources */, 7B58816727FC5D790098B121 /* SentryDiscardReasonMapperTests.swift in Sources */, 63FE720320DA66EC00CDBAE8 /* SentryCrashCPU_Tests.m in Sources */, @@ -3941,6 +3983,7 @@ 7B7725D8292F5DC20015BBF9 /* SentryCrashInstallationTests.m in Sources */, 7B82D54924E2A2D400EE670F /* SentryIdTests.swift in Sources */, 7BD47B4E268F0B470076A663 /* ClearTestState.swift in Sources */, + 844EDC7A29415AE800C86F34 /* TestSentrySystemWrapper.swift in Sources */, 7B87C916295ECFD700510C52 /* SentryMetricKitEventTests.swift in Sources */, 7B6D98ED24C703F8005502FA /* Async.swift in Sources */, 7BA0C04C28056556003E0326 /* SentryTransportAdapterTests.swift in Sources */, @@ -3998,7 +4041,6 @@ 8E70B0FD25CB72BE002B3155 /* SentrySpanTests.swift in Sources */, 7BBD188F2448469A00427C76 /* HttpDateFormatter.swift in Sources */, 63FE720C20DA66EC00CDBAE8 /* SentryCrashMonitor_Tests.m in Sources */, - 0A54C3B7294B403F00318F31 /* TestSentryNSTimerWrapper.swift in Sources */, D855B3EA27D652C700BCED76 /* TestCoreDataStack.swift in Sources */, 63FE721820DA66EC00CDBAE8 /* TestThread.m in Sources */, 7B4D308A26FC616B00C94DE9 /* SentryHttpTransportTests.swift in Sources */, @@ -4009,6 +4051,7 @@ 7BC6EC14255C415E0059822A /* SentryExceptionTests.swift in Sources */, 7B82722927A319E900F4BFF4 /* SentryAutoSessionTrackingIntegrationTests.swift in Sources */, D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */, + 844EDC73294144B200C86F34 /* TestSentryNSProcessInfoWrapper.swift in Sources */, 8ED2D28026A6581C00CA8329 /* NSURLProtocolSwizzle.m in Sources */, D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */, 7BC6EC04255C235F0059822A /* SentryFrameTests.swift in Sources */, @@ -4038,6 +4081,7 @@ D8CB742E294B294B00A5F964 /* MockUIScene.m in Sources */, 7BA61CBD247BC6B900C130A8 /* TestSentryCrashBinaryImageProvider.swift in Sources */, 7BFA69F627E0840400233199 /* SentryANRTrackingIntegrationTests.swift in Sources */, + 844EDCE82947DCD700C86F34 /* TestSentryNSTimerWrapper.swift in Sources */, 7BBD18B62451807600427C76 /* SentryDefaultRateLimitsTests.swift in Sources */, 63FE720620DA66EC00CDBAE8 /* SentryCrashMonitor_AppState_Tests.m in Sources */, 7B4E375B2582313100059C93 /* SentryAttachmentTests.swift in Sources */, diff --git a/Sources/Sentry/Public/SentryError.h b/Sources/Sentry/Public/SentryError.h index 442a098ec5e..2dc46decb53 100644 --- a/Sources/Sentry/Public/SentryError.h +++ b/Sources/Sentry/Public/SentryError.h @@ -13,6 +13,7 @@ typedef NS_ENUM(NSInteger, SentryError) { kSentryErrorRequestError = 106, kSentryErrorEventNotSent = 107, kSentryErrorFileIO = 108, + kSentryErrorKernel = 109, }; SENTRY_EXTERN NSError *_Nullable NSErrorFromSentryError(SentryError error, NSString *description); @@ -20,6 +21,8 @@ SENTRY_EXTERN NSError *_Nullable NSErrorFromSentryErrorWithUnderlyingError( SentryError error, NSString *description, NSError *underlyingError); SENTRY_EXTERN NSError *_Nullable NSErrorFromSentryErrorWithException( SentryError error, NSString *description, NSException *exception); +SENTRY_EXTERN NSError *_Nullable NSErrorFromSentryErrorWithKernelError( + SentryError error, NSString *description, kern_return_t kernelErrorCode); SENTRY_EXTERN NSString *const SentryErrorDomain; diff --git a/Sources/Sentry/SentryError.m b/Sources/Sentry/SentryError.mm similarity index 70% rename from Sources/Sentry/SentryError.m rename to Sources/Sentry/SentryError.mm index d00e8a51f5c..998f8220984 100644 --- a/Sources/Sentry/SentryError.m +++ b/Sources/Sentry/SentryError.mm @@ -1,4 +1,5 @@ #import "SentryError.h" +#import "SentryMachLogging.hpp" NS_ASSUME_NONNULL_BEGIN @@ -25,6 +26,15 @@ }); } +SENTRY_EXTERN NSError *_Nullable NSErrorFromSentryErrorWithKernelError( + SentryError error, NSString *description, kern_return_t kernelErrorCode) +{ + return _SentryError(error, @ { + NSLocalizedDescriptionKey : [NSString stringWithFormat:@"%@ (%s)", description, + sentry::kernelReturnCodeDescription(kernelErrorCode)], + }); +} + NSError *_Nullable NSErrorFromSentryError(SentryError error, NSString *description) { return _SentryError(error, @ { NSLocalizedDescriptionKey : description }); diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index 4fae7a71600..1c8da9e248b 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -30,7 +30,8 @@ @property (nonatomic, strong, readonly) SentryDisplayLinkWrapper *displayLinkWrapper; @property (nonatomic, assign) CFTimeInterval previousFrameTimestamp; # if SENTRY_TARGET_PROFILING_SUPPORTED -@property (nonatomic, readwrite) SentryMutableFrameInfoTimeSeries *frameTimestamps; +@property (nonatomic, readwrite) SentryMutableFrameInfoTimeSeries *frozenFrameTimestamps; +@property (nonatomic, readwrite) SentryMutableFrameInfoTimeSeries *slowFrameTimestamps; @property (nonatomic, readwrite) SentryMutableFrameInfoTimeSeries *frameRateTimestamps; # endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -91,7 +92,8 @@ - (void)resetFrames # if SENTRY_TARGET_PROFILING_SUPPORTED - (void)resetProfilingTimestamps { - self.frameTimestamps = [SentryMutableFrameInfoTimeSeries array]; + self.frozenFrameTimestamps = [SentryMutableFrameInfoTimeSeries array]; + self.slowFrameTimestamps = [SentryMutableFrameInfoTimeSeries array]; self.frameRateTimestamps = [SentryMutableFrameInfoTimeSeries array]; } # endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -157,12 +159,16 @@ - (void)displayLinkCallback if (frameDuration > slowFrameThreshold && frameDuration <= SentryFrozenFrameThreshold) { atomic_fetch_add_explicit(&_slowFrames, 1, SentryFramesMemoryOrder); # if SENTRY_TARGET_PROFILING_SUPPORTED - [self recordTimestampStart:@(self.previousFrameTimestamp) end:@(thisFrameTimestamp)]; + [self recordTimestampStart:@(self.previousFrameTimestamp) + end:@(thisFrameTimestamp) + array:self.slowFrameTimestamps]; # endif // SENTRY_TARGET_PROFILING_SUPPORTED } else if (frameDuration > SentryFrozenFrameThreshold) { atomic_fetch_add_explicit(&_frozenFrames, 1, SentryFramesMemoryOrder); # if SENTRY_TARGET_PROFILING_SUPPORTED - [self recordTimestampStart:@(self.previousFrameTimestamp) end:@(thisFrameTimestamp)]; + [self recordTimestampStart:@(self.previousFrameTimestamp) + end:@(thisFrameTimestamp) + array:self.frozenFrameTimestamps]; # endif // SENTRY_TARGET_PROFILING_SUPPORTED } @@ -171,14 +177,14 @@ - (void)displayLinkCallback } # if SENTRY_TARGET_PROFILING_SUPPORTED -- (void)recordTimestampStart:(NSNumber *)start end:(NSNumber *)end +- (void)recordTimestampStart:(NSNumber *)start end:(NSNumber *)end array:(NSMutableArray *)array { BOOL shouldRecord = [SentryProfiler isRunning]; # if defined(TEST) || defined(TESTCI) shouldRecord = YES; # endif if (shouldRecord) { - [self.frameTimestamps addObject:@{ @"start_timestamp" : start, @"end_timestamp" : end }]; + [array addObject:@{ @"start_timestamp" : start, @"end_timestamp" : end }]; } } # endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -193,7 +199,8 @@ - (SentryScreenFrames *)currentFrames return [[SentryScreenFrames alloc] initWithTotal:total frozen:frozen slow:slow - frameTimestamps:self.frameTimestamps + slowFrameTimestamps:self.slowFrameTimestamps + frozenFrameTimestamps:self.frozenFrameTimestamps frameRateTimestamps:self.frameRateTimestamps]; # else return [[SentryScreenFrames alloc] initWithTotal:total frozen:frozen slow:slow]; diff --git a/Sources/Sentry/SentryMachLogging.cpp b/Sources/Sentry/SentryMachLogging.cpp index a35dce1214d..af35c5f6d9c 100644 --- a/Sources/Sentry/SentryMachLogging.cpp +++ b/Sources/Sentry/SentryMachLogging.cpp @@ -1,212 +1,210 @@ #include "SentryMachLogging.hpp" namespace sentry { -namespace profiling { - const char * - kernelReturnCodeDescription(kern_return_t kr) noexcept - { - switch (kr) { - case KERN_SUCCESS: - return "Success."; - case KERN_INVALID_ADDRESS: - return "Specified address is not currently valid."; - case KERN_PROTECTION_FAILURE: - return "Specified memory is valid, but does not permit the required forms of access."; - case KERN_NO_SPACE: - return "The address range specified is already in use, or no address range of the size " - "specified could be found."; - case KERN_INVALID_ARGUMENT: - return "The function requested was not applicable to this type of argument, or an " - "argument is invalid."; - case KERN_FAILURE: - return "The function could not be performed."; - case KERN_RESOURCE_SHORTAGE: - return "A system resource could not be allocated to fulfill this request."; - case KERN_NOT_RECEIVER: - return "The task in question does not hold receive rights for the port argument."; - case KERN_NO_ACCESS: - return "Bogus access restriction."; - case KERN_MEMORY_FAILURE: - return "During a page fault, the target address refers to a memory object that has " - "been destroyed."; - case KERN_MEMORY_ERROR: - return "During a page fault, the memory object indicated that the data could not be " - "returned."; - case KERN_ALREADY_IN_SET: - return "The receive right is already a member of the portset."; - case KERN_NOT_IN_SET: - return "The receive right is not a member of a port set."; - case KERN_NAME_EXISTS: - return "The name already denotes a right in the task."; - case KERN_ABORTED: - return "The operation was aborted."; - case KERN_INVALID_NAME: - return "The name doesn't denote a right in the task."; - case KERN_INVALID_TASK: - return "Target task isn't an active task."; - case KERN_INVALID_RIGHT: - return "The name denotes a right, but not an appropriate right."; - case KERN_INVALID_VALUE: - return "A blatant range error."; - case KERN_UREFS_OVERFLOW: - return "Operation would overflow limit on user-references."; - case KERN_INVALID_CAPABILITY: - return "The supplied (port) capability is improper."; - case KERN_RIGHT_EXISTS: - return "The task already has send or receive rights for the port under another name."; - case KERN_INVALID_HOST: - return "Target host isn't actually a host."; - case KERN_MEMORY_PRESENT: - return "An attempt was made to supply \"precious\" data for memory that is already " - "present in a memory object."; - case KERN_MEMORY_DATA_MOVED: - return "See code documentation for KERN_MEMORY_DATA_MOVED"; - case KERN_MEMORY_RESTART_COPY: - return "See code documentation for KERN_MEMORY_RESTART_COPY"; - case KERN_INVALID_PROCESSOR_SET: - return "An argument applied to assert processor set privilege was not a processor set " - "control port."; - case KERN_POLICY_LIMIT: - return "The specified scheduling attributes exceed the thread's limits."; - case KERN_INVALID_POLICY: - return "The specified scheduling policy is not currently enabled for the processor " - "set."; - case KERN_INVALID_OBJECT: - return "The external memory manager failed to initialize the memory object."; - case KERN_ALREADY_WAITING: - return "A thread is attempting to wait for an event for which there is already a " - "waiting thread."; - case KERN_DEFAULT_SET: - return "An attempt was made to destroy the default processor set"; - case KERN_EXCEPTION_PROTECTED: - return "An attempt was made to fetch an exception port that is protected, or to abort " - "a thread while processing a protected exception."; - case KERN_INVALID_LEDGER: - return "A ledger was required but not supplied."; - case KERN_INVALID_MEMORY_CONTROL: - return "The port was not a memory cache control port."; - case KERN_INVALID_SECURITY: - return "An argument supplied to assert security privilege was not a host security " - "port."; - case KERN_NOT_DEPRESSED: - return "thread_depress_abort was called on a thread which was not currently depressed."; - case KERN_TERMINATED: - return "Object has been terminated and is no longer available"; - case KERN_LOCK_SET_DESTROYED: - return "Lock set has been destroyed and is no longer available."; - case KERN_LOCK_UNSTABLE: - return "The thread holding the lock terminated before releasing"; - case KERN_LOCK_OWNED: - return "The lock is already owned by another thread"; - case KERN_LOCK_OWNED_SELF: - return "The lock is already owned by the calling thread"; - case KERN_SEMAPHORE_DESTROYED: - return "Semaphore has been destroyed and is no longer available."; - case KERN_RPC_SERVER_TERMINATED: - return "Return from RPC indicating the target server was terminated before it " - "successfully replied."; - case KERN_RPC_TERMINATE_ORPHAN: - return "Terminate an orphaned activation."; - case KERN_RPC_CONTINUE_ORPHAN: - return "Allow an orphaned activation to continue executing."; - case KERN_NOT_SUPPORTED: - return "Empty thread activation (No thread linked to it)"; - case KERN_NODE_DOWN: - return "Remote node down or inaccessible."; - case KERN_NOT_WAITING: - return "A signalled thread was not actually waiting."; - case KERN_OPERATION_TIMED_OUT: - return "Some thread-oriented operation (semaphore_wait) timed out"; - case KERN_CODESIGN_ERROR: - return "During a page fault, indicates that the page was rejected as a result of a " - "signature check."; - case KERN_POLICY_STATIC: - return "The requested property cannot be changed at this time."; - case KERN_INSUFFICIENT_BUFFER_SIZE: - return "The provided buffer is of insufficient size for the requested data."; - default: - return "Unknown error."; - } +const char * +kernelReturnCodeDescription(kern_return_t kr) noexcept +{ + switch (kr) { + case KERN_SUCCESS: + return "Success."; + case KERN_INVALID_ADDRESS: + return "Specified address is not currently valid."; + case KERN_PROTECTION_FAILURE: + return "Specified memory is valid, but does not permit the required forms of access."; + case KERN_NO_SPACE: + return "The address range specified is already in use, or no address range of the size " + "specified could be found."; + case KERN_INVALID_ARGUMENT: + return "The function requested was not applicable to this type of argument, or an " + "argument is invalid."; + case KERN_FAILURE: + return "The function could not be performed."; + case KERN_RESOURCE_SHORTAGE: + return "A system resource could not be allocated to fulfill this request."; + case KERN_NOT_RECEIVER: + return "The task in question does not hold receive rights for the port argument."; + case KERN_NO_ACCESS: + return "Bogus access restriction."; + case KERN_MEMORY_FAILURE: + return "During a page fault, the target address refers to a memory object that has " + "been destroyed."; + case KERN_MEMORY_ERROR: + return "During a page fault, the memory object indicated that the data could not be " + "returned."; + case KERN_ALREADY_IN_SET: + return "The receive right is already a member of the portset."; + case KERN_NOT_IN_SET: + return "The receive right is not a member of a port set."; + case KERN_NAME_EXISTS: + return "The name already denotes a right in the task."; + case KERN_ABORTED: + return "The operation was aborted."; + case KERN_INVALID_NAME: + return "The name doesn't denote a right in the task."; + case KERN_INVALID_TASK: + return "Target task isn't an active task."; + case KERN_INVALID_RIGHT: + return "The name denotes a right, but not an appropriate right."; + case KERN_INVALID_VALUE: + return "A blatant range error."; + case KERN_UREFS_OVERFLOW: + return "Operation would overflow limit on user-references."; + case KERN_INVALID_CAPABILITY: + return "The supplied (port) capability is improper."; + case KERN_RIGHT_EXISTS: + return "The task already has send or receive rights for the port under another name."; + case KERN_INVALID_HOST: + return "Target host isn't actually a host."; + case KERN_MEMORY_PRESENT: + return "An attempt was made to supply \"precious\" data for memory that is already " + "present in a memory object."; + case KERN_MEMORY_DATA_MOVED: + return "See code documentation for KERN_MEMORY_DATA_MOVED"; + case KERN_MEMORY_RESTART_COPY: + return "See code documentation for KERN_MEMORY_RESTART_COPY"; + case KERN_INVALID_PROCESSOR_SET: + return "An argument applied to assert processor set privilege was not a processor set " + "control port."; + case KERN_POLICY_LIMIT: + return "The specified scheduling attributes exceed the thread's limits."; + case KERN_INVALID_POLICY: + return "The specified scheduling policy is not currently enabled for the processor " + "set."; + case KERN_INVALID_OBJECT: + return "The external memory manager failed to initialize the memory object."; + case KERN_ALREADY_WAITING: + return "A thread is attempting to wait for an event for which there is already a " + "waiting thread."; + case KERN_DEFAULT_SET: + return "An attempt was made to destroy the default processor set"; + case KERN_EXCEPTION_PROTECTED: + return "An attempt was made to fetch an exception port that is protected, or to abort " + "a thread while processing a protected exception."; + case KERN_INVALID_LEDGER: + return "A ledger was required but not supplied."; + case KERN_INVALID_MEMORY_CONTROL: + return "The port was not a memory cache control port."; + case KERN_INVALID_SECURITY: + return "An argument supplied to assert security privilege was not a host security " + "port."; + case KERN_NOT_DEPRESSED: + return "thread_depress_abort was called on a thread which was not currently depressed."; + case KERN_TERMINATED: + return "Object has been terminated and is no longer available"; + case KERN_LOCK_SET_DESTROYED: + return "Lock set has been destroyed and is no longer available."; + case KERN_LOCK_UNSTABLE: + return "The thread holding the lock terminated before releasing"; + case KERN_LOCK_OWNED: + return "The lock is already owned by another thread"; + case KERN_LOCK_OWNED_SELF: + return "The lock is already owned by the calling thread"; + case KERN_SEMAPHORE_DESTROYED: + return "Semaphore has been destroyed and is no longer available."; + case KERN_RPC_SERVER_TERMINATED: + return "Return from RPC indicating the target server was terminated before it " + "successfully replied."; + case KERN_RPC_TERMINATE_ORPHAN: + return "Terminate an orphaned activation."; + case KERN_RPC_CONTINUE_ORPHAN: + return "Allow an orphaned activation to continue executing."; + case KERN_NOT_SUPPORTED: + return "Empty thread activation (No thread linked to it)"; + case KERN_NODE_DOWN: + return "Remote node down or inaccessible."; + case KERN_NOT_WAITING: + return "A signalled thread was not actually waiting."; + case KERN_OPERATION_TIMED_OUT: + return "Some thread-oriented operation (semaphore_wait) timed out"; + case KERN_CODESIGN_ERROR: + return "During a page fault, indicates that the page was rejected as a result of a " + "signature check."; + case KERN_POLICY_STATIC: + return "The requested property cannot be changed at this time."; + case KERN_INSUFFICIENT_BUFFER_SIZE: + return "The provided buffer is of insufficient size for the requested data."; + default: + return "Unknown error."; } +} - const char * - machMessageReturnCodeDescription(mach_msg_return_t mr) noexcept - { - switch (mr) { - case MACH_MSG_SUCCESS: - return "Success."; - case MACH_SEND_NO_BUFFER: - return "A resource shortage prevented the kernel from allocating a message buffer."; - case MACH_SEND_INVALID_DATA: - return "The supplied message buffer was not readable."; - case MACH_SEND_INVALID_HEADER: - return "The msgh_bits value was invalid."; - case MACH_SEND_INVALID_DEST: - return "The msgh_remote_port value was invalid."; - case MACH_SEND_INVALID_NOTIFY: - return "When using MACH_SEND_CANCEL, the notify argument did not denote a valid " - "receive right."; - case MACH_SEND_INVALID_REPLY: - return "The msgh_local_port value was invalid."; - case MACH_SEND_INVALID_TRAILER: - return "The trailer to be sent does not correspond to the current kernel format, or " - "the sending task does not have the privilege to supply the message attributes."; - case MACH_SEND_INVALID_MEMORY: - return "The message body specified out-of-line data that was not readable."; - case MACH_SEND_INVALID_RIGHT: - return "The message body specified a port right which the caller didn't possess."; - case MACH_SEND_INVALID_TYPE: - return "A kernel processed descriptor was invalid."; - case MACH_SEND_MSG_TOO_SMALL: - return "The last data item in the message ran over the end of the message."; - case MACH_SEND_TIMED_OUT: - return "The timeout interval expired."; - case MACH_SEND_INTERRUPTED: - return "A software interrupt occurred."; - case MACH_RCV_INVALID_NAME: - return "The specified receive_name was invalid."; - case MACH_RCV_IN_SET: - return "The specified port was a member of a port set."; - case MACH_RCV_TIMED_OUT: - return "The timeout interval expired."; - case MACH_RCV_INTERRUPTED: - return "A software interrupt occurred."; - case MACH_RCV_PORT_DIED: - return "The caller lost the rights specified by receive_name."; - case MACH_RCV_PORT_CHANGED: - return "receive_name specified a receive right which was moved into a port set during " - "the call."; - case MACH_RCV_TOO_LARGE: - return "When using MACH_RCV_LARGE, the message was larger than receive_limit. The " - "message is left queued, and its actual size is returned in the message " - "header/message body."; - case MACH_RCV_INVALID_TRAILER: - return "The trailer type desired, or the number of trailer elements desired, is not " - "supported by the kernel."; - case MACH_RCV_HEADER_ERROR: - return "A resource shortage prevented the reception of the port rights in the message " - "header."; - case MACH_RCV_INVALID_NOTIFY: - return "When using MACH_RCV_NOTIFY, the notify argument did not denote a valid receive " - "right."; - case MACH_RCV_INVALID_DATA: - return "The specified message buffer was not writable."; - case MACH_RCV_SCATTER_SMALL: - return "When not using MACH_RCV_LARGE with MACH_RCV_OVERWRITE, one or more scatter " - "list descriptors specified an overwrite region smaller than the corresponding " - "incoming region. The message was de-queued and destroyed."; - case MACH_RCV_INVALID_TYPE: - return "When using MACH_RCV_OVERWRITE, one or more scatter list descriptors did not " - "have the type matching the corresponding incoming message descriptor or had an " - "invalid copy (disposition) field."; - case MACH_RCV_BODY_ERROR: - return "A resource shortage prevented the reception of a port right or out-of- line " - "memory region in the message body."; - default: - return "Unknown error."; - } +const char * +machMessageReturnCodeDescription(mach_msg_return_t mr) noexcept +{ + switch (mr) { + case MACH_MSG_SUCCESS: + return "Success."; + case MACH_SEND_NO_BUFFER: + return "A resource shortage prevented the kernel from allocating a message buffer."; + case MACH_SEND_INVALID_DATA: + return "The supplied message buffer was not readable."; + case MACH_SEND_INVALID_HEADER: + return "The msgh_bits value was invalid."; + case MACH_SEND_INVALID_DEST: + return "The msgh_remote_port value was invalid."; + case MACH_SEND_INVALID_NOTIFY: + return "When using MACH_SEND_CANCEL, the notify argument did not denote a valid " + "receive right."; + case MACH_SEND_INVALID_REPLY: + return "The msgh_local_port value was invalid."; + case MACH_SEND_INVALID_TRAILER: + return "The trailer to be sent does not correspond to the current kernel format, or " + "the sending task does not have the privilege to supply the message attributes."; + case MACH_SEND_INVALID_MEMORY: + return "The message body specified out-of-line data that was not readable."; + case MACH_SEND_INVALID_RIGHT: + return "The message body specified a port right which the caller didn't possess."; + case MACH_SEND_INVALID_TYPE: + return "A kernel processed descriptor was invalid."; + case MACH_SEND_MSG_TOO_SMALL: + return "The last data item in the message ran over the end of the message."; + case MACH_SEND_TIMED_OUT: + return "The timeout interval expired."; + case MACH_SEND_INTERRUPTED: + return "A software interrupt occurred."; + case MACH_RCV_INVALID_NAME: + return "The specified receive_name was invalid."; + case MACH_RCV_IN_SET: + return "The specified port was a member of a port set."; + case MACH_RCV_TIMED_OUT: + return "The timeout interval expired."; + case MACH_RCV_INTERRUPTED: + return "A software interrupt occurred."; + case MACH_RCV_PORT_DIED: + return "The caller lost the rights specified by receive_name."; + case MACH_RCV_PORT_CHANGED: + return "receive_name specified a receive right which was moved into a port set during " + "the call."; + case MACH_RCV_TOO_LARGE: + return "When using MACH_RCV_LARGE, the message was larger than receive_limit. The " + "message is left queued, and its actual size is returned in the message " + "header/message body."; + case MACH_RCV_INVALID_TRAILER: + return "The trailer type desired, or the number of trailer elements desired, is not " + "supported by the kernel."; + case MACH_RCV_HEADER_ERROR: + return "A resource shortage prevented the reception of the port rights in the message " + "header."; + case MACH_RCV_INVALID_NOTIFY: + return "When using MACH_RCV_NOTIFY, the notify argument did not denote a valid receive " + "right."; + case MACH_RCV_INVALID_DATA: + return "The specified message buffer was not writable."; + case MACH_RCV_SCATTER_SMALL: + return "When not using MACH_RCV_LARGE with MACH_RCV_OVERWRITE, one or more scatter " + "list descriptors specified an overwrite region smaller than the corresponding " + "incoming region. The message was de-queued and destroyed."; + case MACH_RCV_INVALID_TYPE: + return "When using MACH_RCV_OVERWRITE, one or more scatter list descriptors did not " + "have the type matching the corresponding incoming message descriptor or had an " + "invalid copy (disposition) field."; + case MACH_RCV_BODY_ERROR: + return "A resource shortage prevented the reception of a port right or out-of- line " + "memory region in the message body."; + default: + return "Unknown error."; } +} -} // namespace profiling } // namespace sentry diff --git a/Sources/Sentry/SentryMetricProfiler.mm b/Sources/Sentry/SentryMetricProfiler.mm new file mode 100644 index 00000000000..42a4272a095 --- /dev/null +++ b/Sources/Sentry/SentryMetricProfiler.mm @@ -0,0 +1,173 @@ +#import "SentryMetricProfiler.h" + +#if SENTRY_TARGET_PROFILING_SUPPORTED + +# import "SentryLog.h" +# import "SentryNSProcessInfoWrapper.h" +# import "SentryNSTimerWrapper.h" +# import "SentrySystemWrapper.h" +# import "SentryTime.h" + +/** + * Currently set to 10 Hz as we don't anticipate much utility out of a higher resolution when + * sampling CPU usage and memory footprint, and we want to minimize the overhead of making the + * necessary system calls to gather that information. + */ +static const NSTimeInterval kSentryMetricProfilerTimeseriesInterval = 0.1; + +NSString *const kSentryMetricProfilerSerializationKeyMemoryFootprint = @"memory_footprint"; +NSString *const kSentryMetricProfilerSerializationKeyCPUUsageFormat = @"cpu_usage_%d"; + +NSString *const kSentryMetricProfilerSerializationUnitBytes = @"byte"; +NSString *const kSentryMetricProfilerSerializationUnitPercentage = @"percent"; + +namespace { +NSDictionary * +serializedValues(NSArray *> *values, NSString *unit) +{ + return @ { @"unit" : unit, @"values" : values }; +} +} // namespace + +@implementation SentryMetricProfiler { + NSTimer *_timer; + + SentryNSProcessInfoWrapper *_processInfoWrapper; + SentrySystemWrapper *_systemWrapper; + SentryNSTimerWrapper *_timerWrapper; + + /// arrays of readings keyed on NSNumbers representing the core number for the set of readings + NSMutableDictionary *> *> + *_cpuUsage; + + NSMutableArray *> *_memoryFootprint; + uint64_t _profileStartTime; +} + +- (instancetype)initWithProfileStartTime:(uint64_t)profileStartTime + processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper + systemWrapper:(SentrySystemWrapper *)systemWrapper + timerWrapper:(SentryNSTimerWrapper *)timerWrapper +{ + if (self = [super init]) { + _cpuUsage = [NSMutableDictionary *> *> + dictionary]; + const auto processorCount = processInfoWrapper.processorCount; + SENTRY_LOG_DEBUG( + @"Preparing %lu arrays for CPU core usage readings", (long unsigned)processorCount); + for (NSUInteger core = 0; core < processorCount; core++) { + _cpuUsage[@(core)] = [NSMutableArray *> array]; + } + + _systemWrapper = systemWrapper; + _processInfoWrapper = processInfoWrapper; + _timerWrapper = timerWrapper; + + _memoryFootprint = [NSMutableArray *> array]; + + _profileStartTime = profileStartTime; + } + return self; +} + +- (void)dealloc +{ + [self stop]; +} + +# pragma mark - Public + +- (void)start +{ + [self registerSampler]; +} + +- (void)stop +{ + [_timer invalidate]; +} + +- (NSMutableDictionary *)serialize +{ + NSMutableDictionary *dict; + @synchronized(self) { + dict = [NSMutableDictionary dictionary]; + } + + if (_memoryFootprint.count > 0) { + dict[kSentryMetricProfilerSerializationKeyMemoryFootprint] + = serializedValues(_memoryFootprint, kSentryMetricProfilerSerializationUnitBytes); + } + + [_cpuUsage enumerateKeysAndObjectsUsingBlock:^(NSNumber *_Nonnull core, + NSMutableArray *> *_Nonnull readings, + BOOL *_Nonnull stop) { + if (readings.count > 0) { + dict[[NSString stringWithFormat:kSentryMetricProfilerSerializationKeyCPUUsageFormat, + core.intValue]] + = serializedValues(readings, kSentryMetricProfilerSerializationUnitPercentage); + } + }]; + + return dict; +} + +# pragma mark - Private + +- (void)registerSampler +{ + __weak auto weakSelf = self; + _timer = [_timerWrapper scheduledTimerWithTimeInterval:kSentryMetricProfilerTimeseriesInterval + repeats:YES + block:^(NSTimer *_Nonnull timer) { + [weakSelf recordCPUPercentagePerCore]; + [weakSelf recordMemoryFootprint]; + }]; +} + +- (void)recordMemoryFootprint +{ + NSError *error; + const auto footprintBytes = [_systemWrapper memoryFootprintBytes:&error]; + + if (error) { + SENTRY_LOG_ERROR(@"Failed to read memory footprint: %@", error); + return; + } + + @synchronized(self) { + [_memoryFootprint addObject:[self metricEntryForValue:@(footprintBytes)]]; + } +} + +- (void)recordCPUPercentagePerCore +{ + NSError *error; + const auto result = [_systemWrapper cpuUsagePerCore:&error]; + + if (error) { + SENTRY_LOG_ERROR(@"Failed to read CPU usages: %@", error); + return; + } + + @synchronized(self) { + [result enumerateObjectsUsingBlock:^( + NSNumber *_Nonnull usage, NSUInteger core, BOOL *_Nonnull stop) { + [_cpuUsage[@(core)] addObject:[self metricEntryForValue:usage]]; + }]; + } +} + +- (NSDictionary *)metricEntryForValue:(NSNumber *)value +{ + return @{ + @"value" : value, + @"elapsed_since_start_ns" : + [@(getDurationNs(_profileStartTime, getAbsoluteTime())) stringValue] + }; +} + +@end + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/SentryNSNotificationCenterWrapper.m b/Sources/Sentry/SentryNSNotificationCenterWrapper.m index 763da0186a1..f235f2de55f 100644 --- a/Sources/Sentry/SentryNSNotificationCenterWrapper.m +++ b/Sources/Sentry/SentryNSNotificationCenterWrapper.m @@ -43,23 +43,23 @@ + (NSNotificationName)willTerminateNotificationName } #endif -- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName +- (void)addObserver:(id)observer + selector:(SEL)aSelector + name:(NSNotificationName)aName + object:(id)anObject { [NSNotificationCenter.defaultCenter addObserver:observer selector:aSelector name:aName - object:nil]; + object:anObject]; } -- (void)addObserver:(id)observer - selector:(SEL)aSelector - name:(NSNotificationName)aName - object:(id)anObject +- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName { [NSNotificationCenter.defaultCenter addObserver:observer selector:aSelector name:aName - object:anObject]; + object:nil]; } - (void)removeObserver:(id)observer name:(NSNotificationName)aName @@ -67,11 +67,21 @@ - (void)removeObserver:(id)observer name:(NSNotificationName)aName [NSNotificationCenter.defaultCenter removeObserver:observer name:aName object:nil]; } +- (void)removeObserver:(id)observer name:(NSNotificationName)aName object:(id)anObject +{ + [NSNotificationCenter.defaultCenter removeObserver:observer name:aName object:anObject]; +} + - (void)removeObserver:(id)observer { [NSNotificationCenter.defaultCenter removeObserver:observer]; } +- (void)postNotificationName:(NSNotificationName)aName object:(id)anObject +{ + [NSNotificationCenter.defaultCenter postNotificationName:aName object:anObject]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryNSProcessInfoWrapper.mm b/Sources/Sentry/SentryNSProcessInfoWrapper.mm new file mode 100644 index 00000000000..ba63cc3bbd5 --- /dev/null +++ b/Sources/Sentry/SentryNSProcessInfoWrapper.mm @@ -0,0 +1,10 @@ +#import "SentryNSProcessInfoWrapper.h" + +@implementation SentryNSProcessInfoWrapper + +- (NSUInteger)processorCount +{ + return NSProcessInfo.processInfo.processorCount; +} + +@end diff --git a/Sources/Sentry/SentryNSTimerWrapper.m b/Sources/Sentry/SentryNSTimerWrapper.m index d1e1202e325..5e6af70ad1b 100644 --- a/Sources/Sentry/SentryNSTimerWrapper.m +++ b/Sources/Sentry/SentryNSTimerWrapper.m @@ -9,11 +9,4 @@ - (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval return [NSTimer scheduledTimerWithTimeInterval:interval repeats:repeats block:block]; } -#pragma mark - Testing - -- (void)fire -{ - // no-op -} - @end diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index 99d0c7ad6ee..3e8917d9592 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -17,11 +17,15 @@ # import "SentryHub+Private.h" # import "SentryId.h" # import "SentryLog.h" +# import "SentryMetricProfiler.h" +# import "SentryNSProcessInfoWrapper.h" +# import "SentryNSTimerWrapper.h" # import "SentrySamplingProfiler.hpp" # import "SentryScope+Private.h" # import "SentryScreenFrames.h" # import "SentrySerialization.h" # import "SentrySpanId.h" +# import "SentrySystemWrapper.h" # import "SentryThread.h" # import "SentryTime.h" # import "SentryTransaction.h" @@ -41,6 +45,10 @@ const int kSentryProfilerFrequencyHz = 101; NSString *const kTestStringConst = @"test"; +NSString *const kSentryProfilerSerializationKeySlowFrameRenders = @"slow_frame_renders"; +NSString *const kSentryProfilerSerializationKeyFrozenFrameRenders = @"frozen_frame_renders"; +NSString *const kSentryProfilerSerializationKeyFrameRates = @"screen_frame_rates"; + using namespace sentry::profiling; NSString * @@ -147,6 +155,12 @@ std::mutex _gProfilerLock; NSMutableDictionary *_gProfilersPerSpanID; SentryProfiler *_Nullable _gCurrentProfiler; +SentryNSProcessInfoWrapper *_gCurrentProcessInfoWrapper; +SentrySystemWrapper *_gCurrentSystemWrapper; +SentryNSTimerWrapper *_gCurrentTimerWrapper; +# if SENTRY_HAS_UIKIT +SentryFramesTracker *_gCurrentFramesTracker; +# endif // SENTRY_HAS_UIKIT NSString * profilerTruncationReasonName(SentryProfilerTruncationReason reason) @@ -161,6 +175,72 @@ } } +# if SENTRY_HAS_UIKIT +NSArray * +processFrameRenders( + SentryFrameInfoTimeSeries *frameInfo, uint64_t profileStart, uint64_t profileDuration) +{ + auto relativeFrameInfo = [NSMutableArray array]; + [frameInfo enumerateObjectsUsingBlock:^( + NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + const auto frameRenderStart + = timeIntervalToNanoseconds(obj[@"start_timestamp"].doubleValue); + +# if defined(TEST) || defined(TESTCI) + // we don't currently validate the timestamps in tests, and the mock doesn't provide + // realistic ones, so they'd fail the checks below. just write them directly into the data + // structure so we can count *how many* were recorded + [relativeFrameInfo addObject:@{ + @"elapsed_since_start_ns" : @(frameRenderStart), + @"value" : @(frameRenderStart), + }]; + return; +# else // if not testing, ie, development or production + if (frameRenderStart < profileStart) { + return; + } + const auto frameRenderEnd = timeIntervalToNanoseconds(obj[@"end_timestamp"].doubleValue); + const auto frameRenderEndRelativeToProfileStart = getDurationNs(profileStart, frameRenderEnd); + if (frameRenderEndRelativeToProfileStart > profileDuration) { + SENTRY_LOG_DEBUG(@"The last slow/frozen frame extended past the end of the profile, " + @"will not report it."); + return; + } + const auto frameRenderStartRelativeToProfileStartNs = getDurationNs(profileStart, frameRenderStart); + const auto frameRenderDurationNs = frameRenderEndRelativeToProfileStart - frameRenderStartRelativeToProfileStartNs; + [relativeFrameInfo addObject:@{ + @"elapsed_since_start_ns" : @(frameRenderStartRelativeToProfileStartNs), + @"value" : @(frameRenderDurationNs), + }]; +# endif // defined(TEST) || defined(TESTCI) + }]; + return relativeFrameInfo; +} + +NSArray * +processFrameRates(SentryFrameInfoTimeSeries *frameRates, uint64_t start) +{ + if (frameRates.count == 0) { + return nil; + } + auto relativeFrameRates = [NSMutableArray array]; + [frameRates enumerateObjectsUsingBlock:^( + NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + const auto timestamp = (uint64_t)(obj[@"timestamp"].doubleValue * 1e9); + const auto refreshRate = obj[@"frame_rate"]; + uint64_t relativeTimestamp = 0; + if (timestamp >= start) { + relativeTimestamp = getDurationNs(start, timestamp); + } + [relativeFrameRates addObject:@{ + @"elapsed_since_start_ns" : @(relativeTimestamp), + @"value" : refreshRate, + }]; + }]; + return relativeFrameRates; +} +# endif // SENTRY_HAS_UIKIT + @implementation SentryProfiler { NSMutableDictionary *_profile; uint64_t _startTimestamp; @@ -168,6 +248,7 @@ @implementation SentryProfiler { uint64_t _endTimestamp; NSDate *_endDate; std::shared_ptr _profiler; + SentryMetricProfiler *_metricProfiler; SentryDebugImageProvider *_debugImageProvider; thread::TIDType _mainThreadID; @@ -181,14 +262,11 @@ @implementation SentryProfiler { + (void)initialize { -# if SENTRY_TARGET_PROFILING_SUPPORTED if (self == [SentryProfiler class]) { _gProfilersPerSpanID = [NSMutableDictionary dictionary]; } -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } -# if SENTRY_TARGET_PROFILING_SUPPORTED - (instancetype)init { if (!(self = [super init])) { @@ -202,26 +280,22 @@ - (instancetype)init _transactions = [NSMutableArray array]; return self; } -# endif # pragma mark - Public + (void)startForSpanID:(SentrySpanId *)spanID hub:(SentryHub *)hub { -# if SENTRY_TARGET_PROFILING_SUPPORTED NSTimeInterval timeoutInterval = 30; -# if defined(TEST) || defined(TESTCI) +# if defined(TEST) || defined(TESTCI) timeoutInterval = 1; -# endif - [self startForSpanID:spanID hub:hub timeoutInterval:timeoutInterval]; # endif + [self startForSpanID:spanID hub:hub timeoutInterval:timeoutInterval]; } + (void)startForSpanID:(SentrySpanId *)spanID hub:(SentryHub *)hub timeoutInterval:(NSTimeInterval)timeoutInterval { -# if SENTRY_TARGET_PROFILING_SUPPORTED std::lock_guard l(_gProfilerLock); if (_gCurrentProfiler == nil) { @@ -230,9 +304,9 @@ + (void)startForSpanID:(SentrySpanId *)spanID SENTRY_LOG_WARN(@"Profiler was not initialized, will not proceed."); return; } -# if SENTRY_HAS_UIKIT - [SentryFramesTracker.sharedInstance resetProfilingTimestamps]; -# endif // SENTRY_HAS_UIKIT +# if SENTRY_HAS_UIKIT + [_gCurrentFramesTracker resetProfilingTimestamps]; +# endif // SENTRY_HAS_UIKIT [_gCurrentProfiler start]; _gCurrentProfiler->_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeoutInterval @@ -240,12 +314,12 @@ + (void)startForSpanID:(SentrySpanId *)spanID selector:@selector(timeoutAbort) userInfo:nil repeats:NO]; -# if SENTRY_HAS_UIKIT +# if SENTRY_HAS_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundAbort) name:UIApplicationWillResignActiveNotification object:nil]; -# endif // SENTRY_HAS_UIKIT +# endif // SENTRY_HAS_UIKIT _gCurrentProfiler->_hub = hub; } @@ -253,12 +327,10 @@ + (void)startForSpanID:(SentrySpanId *)spanID @"Tracking span with ID %@ with profiler %@", spanID.sentrySpanIdString, _gCurrentProfiler); [_gCurrentProfiler->_spansInFlight addObject:spanID]; _gProfilersPerSpanID[spanID] = _gCurrentProfiler; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } + (void)stopProfilingSpan:(id)span { -# if SENTRY_TARGET_PROFILING_SUPPORTED std::lock_guard l(_gProfilerLock); if (_gCurrentProfiler == nil) { @@ -272,12 +344,10 @@ + (void)stopProfilingSpan:(id)span _gCurrentProfiler, span.spanId.sentrySpanIdString); [self stopProfilerForReason:SentryProfilerTruncationReasonNormal]; } -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } + (void)dropTransaction:(SentryTransaction *)transaction { -# if SENTRY_TARGET_PROFILING_SUPPORTED std::lock_guard l(_gProfilerLock); const auto spanID = transaction.trace.spanId; @@ -288,12 +358,10 @@ + (void)dropTransaction:(SentryTransaction *)transaction } [self captureEnvelopeIfFinished:profiler spanID:spanID]; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } + (void)linkTransaction:(SentryTransaction *)transaction { -# if SENTRY_TARGET_PROFILING_SUPPORTED std::lock_guard l(_gProfilerLock); const auto spanID = transaction.trace.spanId; @@ -308,17 +376,42 @@ + (void)linkTransaction:(SentryTransaction *)transaction [profiler addTransaction:transaction]; [self captureEnvelopeIfFinished:profiler spanID:spanID]; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } + (BOOL)isRunning { -# if SENTRY_TARGET_PROFILING_SUPPORTED std::lock_guard l(_gProfilerLock); return [_gCurrentProfiler isRunning]; -# endif // SENTRY_TARGET_PROFILING_SUPPORTED } +# pragma mark - Testing + ++ (void)useSystemWrapper:(SentrySystemWrapper *)systemWrapper +{ + std::lock_guard l(_gProfilerLock); + _gCurrentSystemWrapper = systemWrapper; +} + ++ (void)useProcessInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper +{ + std::lock_guard l(_gProfilerLock); + _gCurrentProcessInfoWrapper = processInfoWrapper; +} + ++ (void)useTimerWrapper:(SentryNSTimerWrapper *)timerWrapper +{ + std::lock_guard l(_gProfilerLock); + _gCurrentTimerWrapper = timerWrapper; +} + +# if SENTRY_HAS_UIKIT ++ (void)useFramesTracker:(SentryFramesTracker *)framesTracker +{ + std::lock_guard l(_gProfilerLock); + _gCurrentFramesTracker = framesTracker; +} +# endif // SENTRY_HAS_UIKIT + # pragma mark - Private + (void)captureEnvelopeIfFinished:(SentryProfiler *)profiler spanID:(SentrySpanId *)spanID @@ -367,12 +460,31 @@ + (void)stopProfilerForReason:(SentryProfilerTruncationReason)reason [_gCurrentProfiler stop]; _gCurrentProfiler->_truncationReason = reason; # if SENTRY_HAS_UIKIT - _gCurrentProfiler->_frameInfo = SentryFramesTracker.sharedInstance.currentFrames; - [SentryFramesTracker.sharedInstance resetProfilingTimestamps]; + _gCurrentProfiler->_frameInfo = _gCurrentFramesTracker.currentFrames; + [_gCurrentFramesTracker resetProfilingTimestamps]; # endif // SENTRY_HAS_UIKIT _gCurrentProfiler = nil; } +- (void)startMetricProfiler +{ + if (_gCurrentSystemWrapper == nil) { + _gCurrentSystemWrapper = [[SentrySystemWrapper alloc] init]; + } + if (_gCurrentProcessInfoWrapper == nil) { + _gCurrentProcessInfoWrapper = [[SentryNSProcessInfoWrapper alloc] init]; + } + if (_gCurrentTimerWrapper == nil) { + _gCurrentTimerWrapper = [[SentryNSTimerWrapper alloc] init]; + } + _metricProfiler = + [[SentryMetricProfiler alloc] initWithProfileStartTime:_startTimestamp + processInfoWrapper:_gCurrentProcessInfoWrapper + systemWrapper:_gCurrentSystemWrapper + timerWrapper:_gCurrentTimerWrapper]; + [_metricProfiler start]; +} + - (void)start { // Disable profiling when running with TSAN because it produces a TSAN false @@ -463,6 +575,8 @@ - (void)start }, kSentryProfilerFrequencyHz); _profiler->startSampling(); + + [self startMetricProfiler]; } } @@ -492,6 +606,7 @@ - (void)stop _profiler->stopSampling(); _endTimestamp = getAbsoluteTime(); _endDate = [SentryCurrentDate date]; + [_metricProfiler stop]; SENTRY_LOG_DEBUG(@"Stopped profiler %@ at system time: %llu.", self, _endTimestamp); } } @@ -499,8 +614,10 @@ - (void)stop - (void)captureEnvelope { NSMutableDictionary *profile = nil; + NSMutableDictionary *metrics; @synchronized(self) { profile = [_profile mutableCopy]; + metrics = [_metricProfiler serialize]; } if ([((NSArray *)profile[@"profile"][@"samples"]) count] < 2) { @@ -550,43 +667,28 @@ - (void)captureEnvelope profile[@"timestamp"] = [[SentryCurrentDate date] sentry_toIso8601String]; profile[@"release"] = _hub.getClient.options.releaseName; + profile[@"measurements"] = metrics; + # if SENTRY_HAS_UIKIT - auto relativeFrameTimestampsNs = [NSMutableArray array]; - [_frameInfo.frameTimestamps enumerateObjectsUsingBlock:^( - NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - const auto begin = (uint64_t)(obj[@"start_timestamp"].doubleValue * 1e9); - if (begin < _startTimestamp) { - return; - } - const auto end = (uint64_t)(obj[@"end_timestamp"].doubleValue * 1e9); - const auto relativeEnd = getDurationNs(_startTimestamp, end); - if (relativeEnd > profileDuration) { - SENTRY_LOG_DEBUG(@"The last slow/frozen frame extended past the end of the profile, " - @"will not report it."); - return; - } - [relativeFrameTimestampsNs addObject:@{ - @"start_timestamp_relative_ns" : @(getDurationNs(_startTimestamp, begin)), - @"end_timestamp_relative_ns" : @(relativeEnd), - }]; - }]; - profile[@"adverse_frame_render_timestamps"] = relativeFrameTimestampsNs; + const auto slowTimestamps + = processFrameRenders(_frameInfo.slowFrameTimestamps, _startTimestamp, profileDuration); + if (slowTimestamps.count > 0) { + metrics[kSentryProfilerSerializationKeySlowFrameRenders] = + @{ @"unit" : @"nanosecond", @"values" : slowTimestamps }; + } - relativeFrameTimestampsNs = [NSMutableArray array]; - [_frameInfo.frameRateTimestamps enumerateObjectsUsingBlock:^( - NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - const auto timestamp = (uint64_t)(obj[@"timestamp"].doubleValue * 1e9); - const auto refreshRate = obj[@"frame_rate"]; - uint64_t relativeTimestamp = 0; - if (timestamp >= _startTimestamp) { - relativeTimestamp = getDurationNs(_startTimestamp, timestamp); - } - [relativeFrameTimestampsNs addObject:@{ - @"start_timestamp_relative_ns" : @(relativeTimestamp), - @"frame_rate" : refreshRate, - }]; - }]; - profile[@"screen_frame_rates"] = relativeFrameTimestampsNs; + const auto frozenTimestamps + = processFrameRenders(_frameInfo.frozenFrameTimestamps, _startTimestamp, profileDuration); + if (frozenTimestamps.count > 0) { + metrics[kSentryProfilerSerializationKeyFrozenFrameRenders] = + @{ @"unit" : @"nanosecond", @"values" : frozenTimestamps }; + } + + const auto frameRates = processFrameRates(_frameInfo.frameRateTimestamps, _startTimestamp); + if (frameRates.count > 0) { + metrics[kSentryProfilerSerializationKeyFrameRates] = + @{ @"unit" : @"hz", @"values" : frameRates }; + } # endif // SENTRY_HAS_UIKIT // populate info from all transactions that occurred while profiler was running @@ -604,21 +706,23 @@ - (void)captureEnvelope [NSString stringWithFormat:@"%llu", [transaction.startTimestamp compare:_startDate] == NSOrderedAscending ? 0 - : (unsigned long long)( - [transaction.startTimestamp timeIntervalSinceDate:_startDate] * 1e9)]; + : timeIntervalToNanoseconds( + [transaction.startTimestamp timeIntervalSinceDate:_startDate])]; NSString *relativeEnd; if ([transaction.timestamp compare:_endDate] == NSOrderedDescending) { relativeEnd = [NSString stringWithFormat:@"%llu", profileDuration]; } else { - const auto profileStartToTransactionEnd_ns = - [transaction.timestamp timeIntervalSinceDate:_startDate] * 1e9; - if (profileStartToTransactionEnd_ns < 0) { + const auto profileStartToTransactionEndInterval = + [transaction.timestamp timeIntervalSinceDate:_startDate]; + if (profileStartToTransactionEndInterval < 0) { SENTRY_LOG_DEBUG(@"Transaction %@ ended before the profiler started, won't " @"associate it with this profile.", transaction.trace.traceId.sentryIdString); continue; } else { + const auto profileStartToTransactionEnd_ns + = timeIntervalToNanoseconds(profileStartToTransactionEndInterval); relativeEnd = [NSString stringWithFormat:@"%llu", (unsigned long long)profileStartToTransactionEnd_ns]; } diff --git a/Sources/Sentry/SentryScreenFrames.m b/Sources/Sentry/SentryScreenFrames.m index 37e594ecc0d..c019a06af98 100644 --- a/Sources/Sentry/SentryScreenFrames.m +++ b/Sources/Sentry/SentryScreenFrames.m @@ -10,7 +10,8 @@ - (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:( return [self initWithTotal:total frozen:frozen slow:slow - frameTimestamps:@[] + slowFrameTimestamps:@[] + frozenFrameTimestamps:@[] frameRateTimestamps:@[]]; # else if (self = [super init]) { @@ -27,14 +28,16 @@ - (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:( - (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow - frameTimestamps:(SentryFrameInfoTimeSeries *)frameTimestamps + slowFrameTimestamps:(SentryFrameInfoTimeSeries *)slowFrameTimestamps + frozenFrameTimestamps:(SentryFrameInfoTimeSeries *)frozenFrameTimestamps frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps { if (self = [super init]) { _total = total; _slow = slow; _frozen = frozen; - _frameTimestamps = frameTimestamps; + _slowFrameTimestamps = slowFrameTimestamps; + _frozenFrameTimestamps = frozenFrameTimestamps; _frameRateTimestamps = frameRateTimestamps; } diff --git a/Sources/Sentry/SentrySystemWrapper.mm b/Sources/Sentry/SentrySystemWrapper.mm new file mode 100644 index 00000000000..6e626700e8e --- /dev/null +++ b/Sources/Sentry/SentrySystemWrapper.mm @@ -0,0 +1,62 @@ +#import "SentrySystemWrapper.h" +#import "SentryError.h" +#import + +@implementation SentrySystemWrapper + +- (SentryRAMBytes)memoryFootprintBytes:(NSError *__autoreleasing _Nullable *)error +{ + task_vm_info_data_t info; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + + const auto status = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&info, &count); + if (status != KERN_SUCCESS) { + if (error) { + *error = NSErrorFromSentryErrorWithKernelError( + kSentryErrorKernel, @"task_info reported an error.", status); + } + return 0; + } + + SentryRAMBytes footprintBytes; + if (count >= TASK_VM_INFO_REV1_COUNT) { + footprintBytes = info.phys_footprint; + } else { + footprintBytes = info.resident_size; + } + + return footprintBytes; +} + +- (NSArray *)cpuUsagePerCore:(NSError **)error +{ + natural_t numCPUs = 0U; + processor_info_array_t cpuInfo; + mach_msg_type_number_t numCPUInfo; + const auto status = host_processor_info( + mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUs, &cpuInfo, &numCPUInfo); + if (status != KERN_SUCCESS) { + if (error) { + *error = NSErrorFromSentryErrorWithKernelError( + kSentryErrorKernel, @"host_processor_info reported an error.", status); + } + return nil; + } + + NSMutableArray *result = [NSMutableArray arrayWithCapacity:numCPUs]; + for (natural_t core = 0U; core < numCPUs; ++core) { + const auto indexBase = CPU_STATE_MAX * core; + const float user = cpuInfo[indexBase + CPU_STATE_USER]; + const float sys = cpuInfo[indexBase + CPU_STATE_SYSTEM]; + const float nice = cpuInfo[indexBase + CPU_STATE_NICE]; + const float idle = cpuInfo[indexBase + CPU_STATE_IDLE]; + const auto inUse = user + sys + nice; + const auto total = inUse + idle; + const auto usagePercent = inUse / total * 100.f; + [result addObject:@(usagePercent)]; + } + + return result; +} + +@end diff --git a/Sources/Sentry/SentryTime.mm b/Sources/Sentry/SentryTime.mm index 6df4fde6396..16c534b6356 100644 --- a/Sources/Sentry/SentryTime.mm +++ b/Sources/Sentry/SentryTime.mm @@ -6,6 +6,15 @@ #import "SentryMachLogging.hpp" +uint64_t +timeIntervalToNanoseconds(double seconds) +{ + NSCAssert(seconds >= 0, @"Seconds must be a positive value"); + NSCAssert(seconds <= UINT64_MAX / 1e9, + @"Value of seconds is too great; will overflow if casted to a uint64_t"); + return (uint64_t)(seconds * 1e9); +} + uint64_t getAbsoluteTime(void) { diff --git a/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h b/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h index 9c64e51dadc..a0116b76a31 100644 --- a/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h +++ b/Sources/Sentry/include/HybridPublic/SentryScreenFrames.h @@ -18,7 +18,8 @@ SENTRY_NO_INIT - (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow - frameTimestamps:(SentryFrameInfoTimeSeries *)frameTimestamps + slowFrameTimestamps:(SentryFrameInfoTimeSeries *)slowFrameTimestamps + frozenFrameTimestamps:(SentryFrameInfoTimeSeries *)frozenFrameTimestamps frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps; # endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -28,10 +29,16 @@ SENTRY_NO_INIT # if SENTRY_TARGET_PROFILING_SUPPORTED /** - * Array of dictionaries describing slow/frozen frames' timestamps. Each dictionary start and end + * 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 *frameTimestamps; +@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, diff --git a/Sources/Sentry/include/SentryMachLogging.hpp b/Sources/Sentry/include/SentryMachLogging.hpp index 39ce68186c3..625f2e065de 100644 --- a/Sources/Sentry/include/SentryMachLogging.hpp +++ b/Sources/Sentry/include/SentryMachLogging.hpp @@ -6,27 +6,25 @@ #include namespace sentry { -namespace profiling { - /** - * Returns a human readable description string for a kernel return code. - * - * @param kr The kernel return code to get a description for. - * @return A string containing the description, or an unknown error message if - * the error code is not known. - */ - const char *kernelReturnCodeDescription(kern_return_t kr) noexcept; +/** + * Returns a human readable description string for a kernel return code. + * + * @param kr The kernel return code to get a description for. + * @return A string containing the description, or an unknown error message if + * the error code is not known. + */ +const char *kernelReturnCodeDescription(kern_return_t kr) noexcept; - /** - * Returns a human readable description string for a mach message return code. - * - * @param mr The mach message return code to get a description for. - * @return A string containing the description, or an unknown error message if - * the error code is not known. - */ - const char *machMessageReturnCodeDescription(mach_msg_return_t mr) noexcept; +/** + * Returns a human readable description string for a mach message return code. + * + * @param mr The mach message return code to get a description for. + * @return A string containing the description, or an unknown error message if + * the error code is not known. + */ +const char *machMessageReturnCodeDescription(mach_msg_return_t mr) noexcept; -} // namespace profiling } // namespace sentry #define SENTRY_PROF_LOG_KERN_RETURN(statement) \ @@ -34,7 +32,7 @@ namespace profiling { const kern_return_t __log_kr = statement; \ if (__log_kr != KERN_SUCCESS) { \ SENTRY_PROF_LOG_ERROR("%s failed with kern return code: %d, description: %s", \ - #statement, __log_kr, sentry::profiling::kernelReturnCodeDescription(__log_kr)); \ + #statement, __log_kr, sentry::kernelReturnCodeDescription(__log_kr)); \ } \ __log_kr; \ }) @@ -44,8 +42,7 @@ namespace profiling { const mach_msg_return_t __log_mr = statement; \ if (__log_mr != MACH_MSG_SUCCESS) { \ SENTRY_PROF_LOG_ERROR("%s failed with mach_msg return code: %d, description: %s", \ - #statement, __log_mr, \ - sentry::profiling::machMessageReturnCodeDescription(__log_mr)); \ + #statement, __log_mr, sentry::machMessageReturnCodeDescription(__log_mr)); \ } \ __log_mr; \ }) diff --git a/Sources/Sentry/include/SentryMetricProfiler.h b/Sources/Sentry/include/SentryMetricProfiler.h new file mode 100644 index 00000000000..a67895c22fe --- /dev/null +++ b/Sources/Sentry/include/SentryMetricProfiler.h @@ -0,0 +1,39 @@ +#import "SentryDefines.h" +#import "SentryProfilingConditionals.h" +#import + +#if SENTRY_TARGET_PROFILING_SUPPORTED + +@class SentryNSProcessInfoWrapper; +@class SentryNSTimerWrapper; +@class SentrySystemWrapper; + +NS_ASSUME_NONNULL_BEGIN + +SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationKeyMemoryFootprint; +SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationKeyCPUUsageFormat; + +SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationUnitBytes; +SENTRY_EXTERN NSString *const kSentryMetricProfilerSerializationUnitPercentage; + +/** + * A profiler that gathers various time-series and event-based metrics on the app process, such as + * CPU and memory usage timeseries and thermal and memory pressure warning notifications. + */ +@interface SentryMetricProfiler : NSObject + +- (instancetype)initWithProfileStartTime:(uint64_t)profileStartTime + processInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper + systemWrapper:(SentrySystemWrapper *)systemWrapper + timerWrapper:(SentryNSTimerWrapper *)timerWrapper; +- (void)start; +- (void)stop; + +/** @return All data gathered during the profiling run. */ +- (NSMutableDictionary *)serialize; + +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h b/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h index 39ddcc09988..0bb69def33f 100644 --- a/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h +++ b/Sources/Sentry/include/SentryNSNotificationCenterWrapper.h @@ -18,17 +18,21 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, copy, class) NSNotificationName willTerminateNotificationName; #endif -- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName; - - (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName object:(id)anObject; +- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSNotificationName)aName; + +- (void)removeObserver:(id)observer name:(NSNotificationName)aName object:(id)anObject; + - (void)removeObserver:(id)observer name:(NSNotificationName)aName; - (void)removeObserver:(id)observer; +- (void)postNotificationName:(NSNotificationName)aName object:(id)anObject; + NS_ASSUME_NONNULL_END @end diff --git a/Sources/Sentry/include/SentryNSProcessInfoWrapper.h b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h new file mode 100644 index 00000000000..12539afe3d0 --- /dev/null +++ b/Sources/Sentry/include/SentryNSProcessInfoWrapper.h @@ -0,0 +1,11 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface SentryNSProcessInfoWrapper : NSObject + +@property (readonly) NSUInteger processorCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryProfiler.h b/Sources/Sentry/include/SentryProfiler.h index 52316906529..97cc71bc3c6 100644 --- a/Sources/Sentry/include/SentryProfiler.h +++ b/Sources/Sentry/include/SentryProfiler.h @@ -7,10 +7,12 @@ @class SentryFramesTracker; #endif // SENTRY_HAS_UIKIT @class SentryHub; +@class SentryNSProcessInfoWrapper; @class SentryProfilesSamplerDecision; @class SentryScreenFrames; @class SentryEnvelope; @class SentrySpanId; +@class SentrySystemWrapper; @class SentryTransaction; #if SENTRY_TARGET_PROFILING_SUPPORTED @@ -23,8 +25,12 @@ typedef NS_ENUM(NSUInteger, SentryProfilerTruncationReason) { NS_ASSUME_NONNULL_BEGIN -FOUNDATION_EXPORT const int kSentryProfilerFrequencyHz; -FOUNDATION_EXPORT NSString *const kTestStringConst; +SENTRY_EXTERN const int kSentryProfilerFrequencyHz; +SENTRY_EXTERN NSString *const kTestStringConst; + +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeySlowFrameRenders; +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrozenFrameRenders; +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrameRates; SENTRY_EXTERN_C_BEGIN diff --git a/Sources/Sentry/include/SentrySystemWrapper.h b/Sources/Sentry/include/SentrySystemWrapper.h new file mode 100644 index 00000000000..03ecc7d52f0 --- /dev/null +++ b/Sources/Sentry/include/SentrySystemWrapper.h @@ -0,0 +1,31 @@ +#import "SentryDefines.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^SentryMemoryPressureNotification)(uintptr_t); + +/** + * @c mach_vm_size_t Is a type defined in mach headers as an unsigned 64-bit type used to express + * the amount of working memory the process currently has allocated. + */ +typedef mach_vm_size_t SentryRAMBytes; + +/** + * A wrapper around low-level system APIs that are found in headers such as @c and @c + * . + */ +@interface SentrySystemWrapper : NSObject + +- (SentryRAMBytes)memoryFootprintBytes:(NSError **)error; + +/** + * @return The CPU usage per core, where the order of results corresponds to the core number as + * returned by the underlying system call, e.g. @c @[ @c , @c , + * @c ...] . + */ +- (nullable NSArray *)cpuUsagePerCore:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryTime.h b/Sources/Sentry/include/SentryTime.h index 2f5860793f8..f21e7dcb328 100644 --- a/Sources/Sentry/include/SentryTime.h +++ b/Sources/Sentry/include/SentryTime.h @@ -4,6 +4,12 @@ SENTRY_EXTERN_C_BEGIN +/** + * Given a fractional amount of seconds in a @c double from a Cocoa API like @c -[NSDate @c + * timeIntervalSinceDate:], return an integer representing the amount of nanoseconds. + */ +uint64_t timeIntervalToNanoseconds(double seconds); + /** * Returns the absolute timestamp, which has no defined reference point or unit * as it is platform dependent. diff --git a/Tests/SentryTests/Helper/SentryNSProcessInfoWrapperTests.swift b/Tests/SentryTests/Helper/SentryNSProcessInfoWrapperTests.swift new file mode 100644 index 00000000000..e7e1fbd6a9b --- /dev/null +++ b/Tests/SentryTests/Helper/SentryNSProcessInfoWrapperTests.swift @@ -0,0 +1,12 @@ +import XCTest + +class SentryNSProcessInfoWrapperTests: XCTestCase { + struct Fixture { + lazy var processInfoWrapper = SentryNSProcessInfoWrapper() + } + lazy var fixture = Fixture() + + func testProcessorCount() { + XCTAssert((0...UInt.max).contains(fixture.processInfoWrapper.processorCount)) + } +} diff --git a/Tests/SentryTests/Helper/SentryNSTimerWrapperTest.swift b/Tests/SentryTests/Helper/SentryNSTimerWrapperTest.swift new file mode 100644 index 00000000000..3ba112b7277 --- /dev/null +++ b/Tests/SentryTests/Helper/SentryNSTimerWrapperTest.swift @@ -0,0 +1,31 @@ +import XCTest + +class SentryNSTimerWrapperTests: XCTestCase { + struct Fixture { + lazy var timerWrapper = SentryNSTimerWrapper() + } + lazy var fixture = Fixture() + + func testNonrepeatingTimer() { + let exp = expectation(description: "timer fires exactly once") + fixture.timerWrapper.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in + exp.fulfill() + } + waitForExpectations(timeout: 1) + } + + func testRepeatingTimer() { + var count = 0 + let exp = expectation(description: "timer fires multiple times") + exp.expectedFulfillmentCount = 2 + fixture.timerWrapper.scheduledTimer(withTimeInterval: 0.1, repeats: true) { + guard count < exp.expectedFulfillmentCount else { + $0.invalidate() + return + } + count += 1 + exp.fulfill() + } + waitForExpectations(timeout: 1) + } +} diff --git a/Tests/SentryTests/Helper/SentryProfiler+SwiftTest.h b/Tests/SentryTests/Helper/SentryProfiler+SwiftTest.h new file mode 100644 index 00000000000..35d8aae8b25 --- /dev/null +++ b/Tests/SentryTests/Helper/SentryProfiler+SwiftTest.h @@ -0,0 +1,32 @@ +// This is a separate header extension that's able to be included into the Swift bridging header for +// the tests. SentryProfiler+Test.h contains C++ symbols which are not able to be exported to Swift. + +#include "SentryProfiler.h" + +#if SENTRY_TARGET_PROFILING_SUPPORTED + +@interface +SentryProfiler () + +/** + * By default, the profiler will use an instance of @c SentrySystemWrapper. Use this method to swap + * out for a different instance, like @c TestSentrySystemWrapper. + */ ++ (void)useSystemWrapper:(SentrySystemWrapper *)systemWrapper NS_SWIFT_NAME(useSystemWrapper(_:)); + +/** + * By default, the profiler will use an instance of @c SentrySystemWrapper. Use this method to swap + * out for a different instance, like @c TestSentrySystemWrapper. + */ ++ (void)useProcessInfoWrapper:(SentryNSProcessInfoWrapper *)processInfoWrapper + NS_SWIFT_NAME(useProcessInfoWrapper(_:)); + ++ (void)useTimerWrapper:(SentryNSTimerWrapper *)timerWrapper NS_SWIFT_NAME(useTimerWrapper(_:)); + +# if SENTRY_HAS_UIKIT ++ (void)useFramesTracker:(SentryFramesTracker *)framesTracker NS_SWIFT_NAME(useFramesTracker(_:)); +# endif // SENTRY_HAS_UIKIT + +@end + +#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Tests/SentryTests/Helper/SentrySystemWrapperTests.swift b/Tests/SentryTests/Helper/SentrySystemWrapperTests.swift new file mode 100644 index 00000000000..2607ecba33d --- /dev/null +++ b/Tests/SentryTests/Helper/SentrySystemWrapperTests.swift @@ -0,0 +1,26 @@ +import XCTest + +class SentrySystemWrapperTests: XCTestCase { + struct Fixture { + lazy var systemWrapper = SentrySystemWrapper() + } + lazy var fixture = Fixture() + + func testCPUUsageReportsData() throws { + XCTAssertNoThrow({ + let cpuUsages = try self.fixture.systemWrapper.cpuUsagePerCore() + XCTAssertGreaterThan(cpuUsages.count, 0) + let range = 0.0 ... 100.0 + cpuUsages.forEach { + XCTAssert(range.contains($0.doubleValue)) + } + }) + } + + func testMemoryFootprint() { + let error: NSErrorPointer = nil + let memoryFootprint = fixture.systemWrapper.memoryFootprintBytes(error) + XCTAssertNil(error?.pointee) + XCTAssert((0...UINT64_MAX).contains(memoryFootprint)) + } +} diff --git a/Tests/SentryTests/Helper/TestSentryNSProcessInfoWrapper.swift b/Tests/SentryTests/Helper/TestSentryNSProcessInfoWrapper.swift new file mode 100644 index 00000000000..310f4afe7d2 --- /dev/null +++ b/Tests/SentryTests/Helper/TestSentryNSProcessInfoWrapper.swift @@ -0,0 +1,13 @@ +import Sentry + +class TestSentryNSProcessInfoWrapper: SentryNSProcessInfoWrapper { + struct Override { + var processorCount: UInt? + } + + var overrides = Override() + + override var processorCount: UInt { + overrides.processorCount ?? super.processorCount + } +} diff --git a/Tests/SentryTests/Helper/TestSentryNSTimerWrapper.swift b/Tests/SentryTests/Helper/TestSentryNSTimerWrapper.swift index dc40dd77863..3ea72a25edb 100644 --- a/Tests/SentryTests/Helper/TestSentryNSTimerWrapper.swift +++ b/Tests/SentryTests/Helper/TestSentryNSTimerWrapper.swift @@ -25,7 +25,7 @@ class TestSentryNSTimerWrapper: SentryNSTimerWrapper { return timer } - override func fire() { + func fire() { overrides.block?(overrides.timer) } } diff --git a/Tests/SentryTests/Helper/TestSentrySystemWrapper.swift b/Tests/SentryTests/Helper/TestSentrySystemWrapper.swift new file mode 100644 index 00000000000..5e515b79329 --- /dev/null +++ b/Tests/SentryTests/Helper/TestSentrySystemWrapper.swift @@ -0,0 +1,28 @@ +import Sentry + +class TestSentrySystemWrapper: SentrySystemWrapper { + struct Override { + var memoryFootprintError: NSError? + var memoryFootprintBytes: SentryRAMBytes? + + var cpuUsageError: NSError? + var cpuUsagePerCore: [NSNumber]? + } + + var overrides = Override() + + override func memoryFootprintBytes(_ error: NSErrorPointer) -> SentryRAMBytes { + if let errorOverride = overrides.memoryFootprintError { + error?.pointee = errorOverride + return 0 + } + return overrides.memoryFootprintBytes ?? super.memoryFootprintBytes(error) + } + + override func cpuUsagePerCore() throws -> [NSNumber] { + if let errorOverride = overrides.cpuUsageError { + throw errorOverride + } + return try overrides.cpuUsagePerCore ?? super.cpuUsagePerCore() + } +} diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift index 98a44962196..8a0a509eaf4 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackerTests.swift @@ -5,17 +5,15 @@ class SentryFramesTrackerTests: XCTestCase { private class Fixture { - var displayLinkWrapper: TestDiplayLinkWrapper + var displayLinkWrapper: TestDisplayLinkWrapper var queue: DispatchQueue init() { - displayLinkWrapper = TestDiplayLinkWrapper() + displayLinkWrapper = TestDisplayLinkWrapper() queue = DispatchQueue(label: "SentryFramesTrackerTests", qos: .background, attributes: [.concurrent]) } - lazy var sut: SentryFramesTracker = { - return SentryFramesTracker(displayLinkWrapper: displayLinkWrapper) - }() + lazy var sut: SentryFramesTracker = SentryFramesTracker(displayLinkWrapper: displayLinkWrapper) } private var fixture: Fixture! diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift index 59b6eead8e3..5d560ccd54a 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/SentryFramesTrackingIntegrationTests.swift @@ -5,7 +5,7 @@ class SentryFramesTrackingIntegrationTests: XCTestCase { private class Fixture { let options = Options() - let displayLink = TestDiplayLinkWrapper() + let displayLink = TestDisplayLinkWrapper() init() { options.dsn = TestConstants.dsnAsString(username: "SentryFramesTrackingIntegrationTests") diff --git a/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift b/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift index a2808407509..987d0d1d816 100644 --- a/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift +++ b/Tests/SentryTests/Integrations/Performance/FramesTracking/TestDisplayLinkWrapper.swift @@ -1,7 +1,7 @@ import Foundation #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) -class TestDiplayLinkWrapper: SentryDisplayLinkWrapper { +class TestDisplayLinkWrapper: SentryDisplayLinkWrapper { var target: AnyObject! var selector: Selector! diff --git a/Tests/SentryTests/Performance/SentryTracerTests.swift b/Tests/SentryTests/Performance/SentryTracerTests.swift index f91bb1fc07c..f89a1e1673e 100644 --- a/Tests/SentryTests/Performance/SentryTracerTests.swift +++ b/Tests/SentryTests/Performance/SentryTracerTests.swift @@ -38,7 +38,7 @@ class SentryTracerTests: XCTestCase { let idleTimeout: TimeInterval = 1.0 #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - var displayLinkWrapper: TestDiplayLinkWrapper + var displayLinkWrapper: TestDisplayLinkWrapper #endif init() { @@ -58,7 +58,7 @@ class SentryTracerTests: XCTestCase { CurrentDate.setCurrentDateProvider(currentDateProvider) #if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) - displayLinkWrapper = TestDiplayLinkWrapper() + displayLinkWrapper = TestDisplayLinkWrapper() SentryFramesTracker.sharedInstance().setDisplayLinkWrapper(displayLinkWrapper) SentryFramesTracker.sharedInstance().start() diff --git a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift index e4e72e7be7a..bf1afd7dd7a 100644 --- a/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift +++ b/Tests/SentryTests/PrivateSentrySDKOnlyTests.swift @@ -127,7 +127,7 @@ class PrivateSentrySDKOnlyTests: XCTestCase { func testGetFrames() { let tracker = SentryFramesTracker.sharedInstance() - let displayLink = TestDiplayLinkWrapper() + let displayLink = TestDisplayLinkWrapper() tracker.setDisplayLinkWrapper(displayLink) tracker.start() diff --git a/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift b/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift index e383e94327f..1fd7fb29465 100644 --- a/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift +++ b/Tests/SentryTests/Profiling/SentryProfilerSwiftTests.swift @@ -23,6 +23,19 @@ class SentryProfilerSwiftTests: XCTestCase { let message = "some message" let transactionName = "Some Transaction" let transactionOperation = "Some Operation" + + lazy var systemWrapper = TestSentrySystemWrapper() + lazy var processInfoWrapper = TestSentryNSProcessInfoWrapper() + lazy var timerWrapper = TestSentryNSTimerWrapper() + +#if !os(macOS) + lazy var displayLinkWrapper = TestDisplayLinkWrapper() + lazy var framesTracker = SentryFramesTracker(displayLinkWrapper: displayLinkWrapper) +#endif + + func newTransaction() -> Span { + hub.startTransaction(name: transactionName, operation: transactionOperation) + } } private var fixture: Fixture! @@ -38,13 +51,73 @@ class SentryProfilerSwiftTests: XCTestCase { super.tearDown() clearTestState() SentryTracer.resetAppStartMeasurementRead() -#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst) +#if !os(macOS) SentryFramesTracker.sharedInstance().resetFrames() SentryFramesTracker.sharedInstance().stop() #endif } - func testConcurrentProfilingTransactions() { + func testMetricProfiler() { + let options = fixture.options + options.profilesSampleRate = 1.0 + options.tracesSampleRate = 1.0 + SentryProfiler.useSystemWrapper(fixture.systemWrapper) + SentryProfiler.useProcessInfoWrapper(fixture.processInfoWrapper) + SentryProfiler.useTimerWrapper(fixture.timerWrapper) +#if !os(macOS) + SentryProfiler.useFramesTracker(fixture.framesTracker) +#endif + + // mock cpu usage + let cpuUsages = [12.4, 63.5, 1.4, 4.6] + fixture.systemWrapper.overrides.cpuUsagePerCore = cpuUsages.map { NSNumber(value: $0) } + fixture.processInfoWrapper.overrides.processorCount = UInt(cpuUsages.count) + + // mock memory footprint + let memoryFootprint: SentryRAMBytes = 123_455 + fixture.systemWrapper.overrides.memoryFootprintBytes = memoryFootprint + + let span = fixture.newTransaction() + forceProfilerSample() + + // gather mock cpu usages and memory footprints + for _ in 0..<2 { + self.fixture.timerWrapper.fire() + } + +#if !os(macOS) + // gather mock GPU frame render timestamps + fixture.framesTracker.start() + fixture.displayLinkWrapper.call() // call once directly to capture previous frame timestamp for comparison with later ones + fixture.displayLinkWrapper.slowFrame() + fixture.displayLinkWrapper.normalFrame() + fixture.displayLinkWrapper.almostFrozenFrame() + fixture.displayLinkWrapper.normalFrame() + fixture.displayLinkWrapper.frozenFrame() + fixture.framesTracker.stop() +#endif + + // mock errors gathering cpu usage and memory footprint to ensure they don't add more information to the payload + fixture.systemWrapper.overrides.cpuUsageError = NSError(domain: "test-error", code: 0) + fixture.systemWrapper.overrides.memoryFootprintError = NSError(domain: "test-error", code: 1) + self.fixture.timerWrapper.fire() + + // finish profile + let exp = expectation(description: "Receives profile payload") + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + span.finish() + + do { + try self.assertMetricsPayload(expectedCPUUsages: cpuUsages, usageReadings: 2, expectedMemoryFootprint: memoryFootprint, expectedSlowFrameCount: 2, expectedFrozenFrameCount: 1, expectedFrameRateCount: 1) + exp.fulfill() + } catch { + XCTFail("Encountered error: \(error)") + } + } + waitForExpectations(timeout: 3) + } + + func testConcurrentProfilingTransactions() throws { let options = fixture.options options.profilesSampleRate = 1.0 options.tracesSampleRate = 1.0 @@ -52,24 +125,15 @@ class SentryProfilerSwiftTests: XCTestCase { let numberOfTransactions = 10 var spans = [Span]() for _ in 0 ..< numberOfTransactions { - spans.append(fixture.hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation)) + spans.append(fixture.newTransaction()) } forceProfilerSample() spans.forEach { $0.finish() } - guard let envelope = self.fixture.client.captureEnvelopeInvocations.first else { - XCTFail("Expected to capture 1 event") - return - } - XCTAssertEqual(1, envelope.items.count) - guard let profileItem = envelope.items.first else { - XCTFail("Expected an envelope item") - return - } - XCTAssertEqual("profile", profileItem.header.type) - self.assertValidProfileData(data: profileItem.data, numberOfTransactions: numberOfTransactions) + let profileData = try getProfileData() + self.assertValidProfileData(data: profileData, numberOfTransactions: numberOfTransactions) } /// Test a situation where a long-running span starts the profiler, which winds up timing out, and then another span starts that begins a new profile, then finishes, and then the long-running span finishes; both profiles should be separately captured in envelopes. @@ -80,12 +144,12 @@ class SentryProfilerSwiftTests: XCTestCase { /// transaction B |-------| /// profiler B |-------| <- normal finish /// ``` - func testConcurrentSpansWithTimeout() { + func testConcurrentSpansWithTimeout_disabled() throws { let options = fixture.options options.profilesSampleRate = 1.0 options.tracesSampleRate = 1.0 - let spanA = fixture.hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) + let spanA = fixture.newTransaction() forceProfilerSample() @@ -96,7 +160,7 @@ class SentryProfilerSwiftTests: XCTestCase { } waitForExpectations(timeout: 3) - let spanB = self.fixture.hub.startTransaction(name: self.fixture.transactionName, operation: self.fixture.transactionOperation) + let spanB = fixture.newTransaction() forceProfilerSample() @@ -108,8 +172,7 @@ class SentryProfilerSwiftTests: XCTestCase { for envelope in self.fixture.client.captureEnvelopeInvocations.invocations { XCTAssertEqual(1, envelope.items.count) guard let profileItem = envelope.items.first else { - XCTFail("Expected an envelope item") - return + throw TestError.noProfileEnvelopeItem } XCTAssertEqual("profile", profileItem.header.type) self.assertValidProfileData(data: profileItem.data, shouldTimeout: currentEnvelope == 1) @@ -117,34 +180,34 @@ class SentryProfilerSwiftTests: XCTestCase { } } - func testProfileTimeoutTimer() { + func testProfileTimeoutTimer_disabled() throws { fixture.options.profilesSampleRate = 1.0 fixture.options.tracesSampleRate = 1.0 - performTest(shouldTimeOut: true) + try performTest(shouldTimeOut: true) } - func testStartTransaction_ProfilingDataIsValid() { + func testStartTransaction_ProfilingDataIsValid() throws { fixture.options.profilesSampleRate = 1.0 fixture.options.tracesSampleRate = 1.0 - performTest() + try performTest() } - func testProfilingDataContainsEnvironmentSetFromOptions() { + func testProfilingDataContainsEnvironmentSetFromOptions() throws { fixture.options.profilesSampleRate = 1.0 fixture.options.tracesSampleRate = 1.0 let expectedEnvironment = "test-environment" fixture.options.environment = expectedEnvironment - performTest(transactionEnvironment: expectedEnvironment) + try performTest(transactionEnvironment: expectedEnvironment) } - func testProfilingDataContainsEnvironmentSetFromConfigureScope() { + func testProfilingDataContainsEnvironmentSetFromConfigureScope() throws { fixture.options.profilesSampleRate = 1.0 fixture.options.tracesSampleRate = 1.0 let expectedEnvironment = "test-environment" fixture.hub.configureScope { scope in scope.setEnvironment(expectedEnvironment) } - performTest(transactionEnvironment: expectedEnvironment) + try performTest(transactionEnvironment: expectedEnvironment) } func testStartTransaction_NotSamplingProfileUsingEnableProfiling() { @@ -198,6 +261,30 @@ class SentryProfilerSwiftTests: XCTestCase { } private extension SentryProfilerSwiftTests { + enum TestError: Error { + case unexpectedProfileDeserializationType + case unexpectedMeasurementsDeserializationType + case noEnvelopeCaptured + case noProfileEnvelopeItem + case malformedMetricValueEntry + case noMetricsReported + case noMetricValuesFound + } + + func getProfileData() throws -> Data { + guard let envelope = self.fixture.client.captureEnvelopeInvocations.first else { + throw(TestError.noEnvelopeCaptured) + } + + XCTAssertEqual(1, envelope.items.count) + guard let profileItem = envelope.items.first else { + throw(TestError.noProfileEnvelopeItem) + } + + XCTAssertEqual("profile", profileItem.header.type) + return profileItem.data + } + /// Keep a thread busy over a long enough period of time (long enough for 3 samples) for the sampler to pick it up. func forceProfilerSample() { let str = "a" @@ -207,8 +294,8 @@ private extension SentryProfilerSwiftTests { } } - func performTest(transactionEnvironment: String = kSentryDefaultEnvironment, numberOfTransactions: Int = 1, shouldTimeOut: Bool = false) { - let span = fixture.hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) + func performTest(transactionEnvironment: String = kSentryDefaultEnvironment, numberOfTransactions: Int = 1, shouldTimeOut: Bool = false) throws { + let span = fixture.newTransaction() forceProfilerSample() @@ -225,18 +312,48 @@ private extension SentryProfilerSwiftTests { waitForExpectations(timeout: 10) - guard let envelope = self.fixture.client.captureEnvelopeInvocations.first else { - XCTFail("Expected to capture at least 1 event") - return + let profileData = try getProfileData() + self.assertValidProfileData(data: profileData, transactionEnvironment: transactionEnvironment, numberOfTransactions: numberOfTransactions, shouldTimeout: shouldTimeOut) + } + + func assertMetricsPayload(expectedCPUUsages: [Double], usageReadings: Int, expectedMemoryFootprint: SentryRAMBytes, expectedSlowFrameCount: Int, expectedFrozenFrameCount: Int, expectedFrameRateCount: Int) throws { + let profileData = try self.getProfileData() + guard let profile = try JSONSerialization.jsonObject(with: profileData) as? [String: Any] else { + throw TestError.unexpectedProfileDeserializationType } - XCTAssertEqual(1, envelope.items.count) - guard let profileItem = envelope.items.first else { - XCTFail("Expected an envelope item") - return + guard let measurements = profile["measurements"] as? [String: Any] else { + throw TestError.unexpectedMeasurementsDeserializationType } - XCTAssertEqual("profile", profileItem.header.type) - self.assertValidProfileData(data: profileItem.data, transactionEnvironment: transactionEnvironment, numberOfTransactions: numberOfTransactions, shouldTimeout: shouldTimeOut) + for (i, expectedUsage) in expectedCPUUsages.enumerated() { + let key = NSString(format: kSentryMetricProfilerSerializationKeyCPUUsageFormat as NSString, i) as String + try assertMetricValue(measurements: measurements, key: key, numberOfReadings: usageReadings, expectedValue: expectedUsage) + } + + try assertMetricValue(measurements: measurements, key: kSentryMetricProfilerSerializationKeyMemoryFootprint, numberOfReadings: usageReadings, expectedValue: expectedMemoryFootprint) + +#if !os(macOS) + let dummyValue: UInt64? = nil + try assertMetricValue(measurements: measurements, key: kSentryProfilerSerializationKeySlowFrameRenders, numberOfReadings: expectedSlowFrameCount, expectedValue: dummyValue) + try assertMetricValue(measurements: measurements, key: kSentryProfilerSerializationKeyFrozenFrameRenders, numberOfReadings: expectedFrozenFrameCount, expectedValue: dummyValue) + try assertMetricValue(measurements: measurements, key: kSentryProfilerSerializationKeyFrameRates, numberOfReadings: expectedFrameRateCount, expectedValue: dummyValue) +#endif + } + + func assertMetricValue(measurements: [String: Any], key: String, numberOfReadings: Int, expectedValue: T?) throws { + guard let metricContainer = measurements[key] as? [String: Any] else { + throw TestError.noMetricsReported + } + guard let values = metricContainer["values"] as? [[String: Any]] else { + throw TestError.malformedMetricValueEntry + } + XCTAssertEqual(values.count, numberOfReadings, "Wrong number of values under \(key)") + if let expectedValue = expectedValue { + guard let memoryFootprintValue = values[0]["value"] as? T else { + throw TestError.noMetricValuesFound + } + XCTAssertEqual(memoryFootprintValue, expectedValue, "Wrong value for \(key)") + } } func assertValidProfileData(data: Data, transactionEnvironment: String = kSentryDefaultEnvironment, numberOfTransactions: Int = 1, shouldTimeout: Bool = false) { @@ -362,7 +479,7 @@ private extension SentryProfilerSwiftTests { let hub = fixture.hub Dynamic(hub).tracesSampler.random = TestRandom(value: 1.0) - let span = hub.startTransaction(name: fixture.transactionName, operation: fixture.transactionOperation) + let span = fixture.newTransaction() let exp = expectation(description: "Span finishes") DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { span.finish() diff --git a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift index 2e5605f84c1..95757c8544c 100644 --- a/Tests/SentryTests/Protocol/SentryNSErrorTests.swift +++ b/Tests/SentryTests/Protocol/SentryNSErrorTests.swift @@ -42,4 +42,15 @@ class SentryNSErrorTests: XCTestCase { XCTAssertEqual(actualDescription, "\(inputDescription) (\(inputExceptionReason))") } + func testWithKernelError() { + let inputKernelErrorCode = KERN_NOT_RECEIVER + let inputDescription = "some test kernel error" + let actualError = NSErrorFromSentryErrorWithKernelError(SentryError.unknownError, inputDescription, inputKernelErrorCode) + + guard let actualDescription = actualError?.localizedDescription else { + XCTFail("Expected a localizedDescription in the resulting error") + return + } + XCTAssertEqual(actualDescription, "\(inputDescription) (The task in question does not hold receive rights for the port argument.)") + } } diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index 2739d379b8b..5f48283fc7f 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -633,7 +633,7 @@ class SentryClientTest: XCTestCase { func testCaptureEvent_DeviceProperties_OtherValues() { #if os(iOS) fixture.deviceWrapper.internalOrientation = .landscapeLeft - fixture.deviceWrapper.interalBatteryState = .full + fixture.deviceWrapper.internalBatteryState = .full fixture.getSut().capture(event: TestData.event) diff --git a/Tests/SentryTests/SentryCrash/TestSentryUIDeviceWrapper.swift b/Tests/SentryTests/SentryCrash/TestSentryUIDeviceWrapper.swift index c3caae43b64..dcb9a2378d2 100644 --- a/Tests/SentryTests/SentryCrash/TestSentryUIDeviceWrapper.swift +++ b/Tests/SentryTests/SentryCrash/TestSentryUIDeviceWrapper.swift @@ -5,7 +5,7 @@ class TestSentryUIDeviceWrapper: SentryUIDeviceWrapper { var internalOrientation = UIDeviceOrientation.portrait var internalIsBatteryMonitoringEnabled = true var internalBatteryLevel: Float = 0.6 - var interalBatteryState = UIDevice.BatteryState.charging + var internalBatteryState = UIDevice.BatteryState.charging override func orientation() -> UIDeviceOrientation { return internalOrientation @@ -20,7 +20,7 @@ class TestSentryUIDeviceWrapper: SentryUIDeviceWrapper { } override func batteryState() -> UIDevice.BatteryState { - return interalBatteryState + return internalBatteryState } #endif } diff --git a/Sources/Sentry/include/SentryNSTimerWrapper+Test.h b/Tests/SentryTests/SentryNSTimerWrapper+Test.h similarity index 83% rename from Sources/Sentry/include/SentryNSTimerWrapper+Test.h rename to Tests/SentryTests/SentryNSTimerWrapper+Test.h index 245668340d7..ba40732d01d 100644 --- a/Sources/Sentry/include/SentryNSTimerWrapper+Test.h +++ b/Tests/SentryTests/SentryNSTimerWrapper+Test.h @@ -3,6 +3,4 @@ @interface SentryNSTimerWrapper () -- (void)fire; - @end diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index e5e89fa4580..f68060c2858 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -100,11 +100,13 @@ #import "SentryMechanismMeta.h" #import "SentryMeta.h" #import "SentryMetricKitIntegration.h" +#import "SentryMetricProfiler.h" #import "SentryMigrateSessionInit.h" #import "SentryNSDataTracker.h" #import "SentryNSError.h" #import "SentryNSNotificationCenterWrapper.h" -#import "SentryNSTimerWrapper+Test.h" +#import "SentryNSProcessInfoWrapper.h" +#import "SentryNSTimerWrapper.h" #import "SentryNSURLRequest.h" #import "SentryNSURLRequestBuilder.h" #import "SentryNSURLSessionTaskSearch.h" @@ -117,7 +119,7 @@ #import "SentryPerformanceTracker.h" #import "SentryPerformanceTrackingIntegration.h" #import "SentryPredicateDescriptor.h" -#import "SentryProfiler.h" +#import "SentryProfiler+SwiftTest.h" #import "SentryQueueableRequestManager.h" #import "SentryRandom.h" #import "SentryRateLimitParser.h" @@ -144,6 +146,7 @@ #import "SentrySwizzleWrapper.h" #import "SentrySysctl.h" #import "SentrySystemEventBreadcrumbs.h" +#import "SentrySystemWrapper.h" #import "SentryTestIntegration.h" #import "SentryTestObjCRuntimeWrapper.h" #import "SentryThread.h"