diff --git a/CHANGELOG.md b/CHANGELOG.md index 320181a9a9..327f9d1874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `sample_rand` to baggage (#4751) - Add timeIntervalSince1970 to log messages (#4781) - Add `waitForFullDisplay` to `sentryTrace` view modifier (#4797) +- Increase continuous profiling buffer size to 60 seconds (#4826) ### Fixes diff --git a/SentryTestUtils/TestSentryNSTimerFactory.swift b/SentryTestUtils/TestSentryNSTimerFactory.swift index 7e9fe92f2c..5cfc4ec242 100644 --- a/SentryTestUtils/TestSentryNSTimerFactory.swift +++ b/SentryTestUtils/TestSentryNSTimerFactory.swift @@ -1,52 +1,96 @@ import Foundation -import Sentry +@testable import Sentry // We must not subclass NSTimer, see https://developer.apple.com/documentation/foundation/nstimer#1770465. // Therefore we return a NSTimer instance here with TimeInterval.infinity. public class TestSentryNSTimerFactory: SentryNSTimerFactory { public struct Overrides { - private var _timer: Timer? - - public var timer: Timer { - get { - _timer ?? Timer() - } - set(newValue) { - _timer = newValue - } - } - + private var _interval: TimeInterval? + + public var timer: Timer var block: ((Timer) -> Void)? - + + var interval: TimeInterval { + get { + _interval ?? TimeInterval.infinity + } + set { + _interval = newValue + } + } + + var lastFireDate: Date + struct InvocationInfo { var target: NSObject var selector: Selector } var invocationInfo: InvocationInfo? + + init(timer: Timer, interval: TimeInterval? = nil, block: ((Timer) -> Void)? = nil, lastFireDate: Date, invocationInfo: InvocationInfo? = nil) { + self.timer = timer + self._interval = interval + self.block = block + self.lastFireDate = lastFireDate + self.invocationInfo = invocationInfo + } } + + public var overrides: Overrides? + + private var currentDateProvider: SentryCurrentDateProvider + + public init(currentDateProvider: SentryCurrentDateProvider) { + self.currentDateProvider = currentDateProvider + super.init() + } +} - public var overrides = Overrides() - - public override func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer { +// MARK: Superclass overrides +public extension TestSentryNSTimerFactory { + override func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer { let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval.infinity, repeats: repeats, block: block) - overrides.timer = timer - overrides.block = block + overrides = Overrides(timer: timer, interval: interval, block: block, lastFireDate: currentDateProvider.date()) return timer } - - public override func scheduledTimer(withTimeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer { + + override func scheduledTimer(withTimeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer { let timer = Timer.scheduledTimer(timeInterval: ti, target: aTarget, selector: aSelector, userInfo: userInfo, repeats: yesOrNo) //swiftlint:disable force_cast - overrides.invocationInfo = Overrides.InvocationInfo(target: aTarget as! NSObject, selector: aSelector) + let invocationInfo = Overrides.InvocationInfo(target: aTarget as! NSObject, selector: aSelector) //swiftlint:enable force_cast + overrides = Overrides(timer: timer, interval: ti, lastFireDate: currentDateProvider.date(), invocationInfo: invocationInfo) return timer } +} + +// MARK: Extensions +public extension TestSentryNSTimerFactory { + enum TestTimerError: Error { + case timerNotInitialized + } + + // check the current time against the last fire time and interval, and if enough time has elapsed, execute any block/invocation registered with the timer + func check() throws { + guard let overrides = overrides else { throw TestTimerError.timerNotInitialized } + let currentDate = currentDateProvider.date() + if currentDate.timeIntervalSince(overrides.lastFireDate) >= overrides.interval { + try fire() + } + } - public func fire() { + // immediately execute any block/invocation registered with the timer + func fire() throws { + guard var overrides = overrides else { throw TestTimerError.timerNotInitialized } + overrides.lastFireDate = currentDateProvider.date() if let block = overrides.block { block(overrides.timer) } else if let invocationInfo = overrides.invocationInfo { - try! Invocation(target: invocationInfo.target, selector: invocationInfo.selector).invoke() + try Invocation(target: invocationInfo.target, selector: invocationInfo.selector).invoke() } } + + func isTimerInitialized() -> Bool { + return overrides != nil + } } diff --git a/Sources/Sentry/Profiling/SentryProfilerDefines.h b/Sources/Sentry/Profiling/SentryProfilerDefines.h index d06b153985..e561ee8a0e 100644 --- a/Sources/Sentry/Profiling/SentryProfilerDefines.h +++ b/Sources/Sentry/Profiling/SentryProfilerDefines.h @@ -15,7 +15,7 @@ typedef NS_ENUM(NSUInteger, SentryProfilerTruncationReason) { SentryProfilerTruncationReasonAppMovedToBackground, }; -static NSTimeInterval kSentryProfilerChunkExpirationInterval = 10; +static NSTimeInterval kSentryProfilerChunkExpirationInterval = 60; static NSTimeInterval kSentryProfilerTimeoutInterval = 30; NS_ASSUME_NONNULL_BEGIN diff --git a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift index 2a14874b5e..ea2f7d5e71 100644 --- a/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift +++ b/Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift @@ -23,7 +23,11 @@ final class SentryContinuousProfilerTests: XCTestCase { super.tearDown() clearTestState() } - + + func testSentryProfilerChunkExpirationInterval() { + XCTAssertEqual(60, kSentryProfilerChunkExpirationInterval) + } + func testStartingAndStoppingContinuousProfiler() throws { try performContinuousProfilingTest() } @@ -94,12 +98,12 @@ final class SentryContinuousProfilerTests: XCTestCase { XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling()) } - func testClosingSDKStopsProfile() { + func testClosingSDKStopsProfile() throws { XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling()) SentryContinuousProfiler.start() XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling()) SentrySDK.close() - assertContinuousProfileStoppage() + try assertContinuousProfileStoppage() } func testStartingAPerformanceTransactionDoesNotStartProfiler() throws { @@ -118,15 +122,15 @@ final class SentryContinuousProfilerTests: XCTestCase { XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling()) // assert that the first chunk was sent - fixture.currentDateProvider.advanceBy(interval: kSentryProfilerChunkExpirationInterval) - fixture.timeoutTimerFactory.fire() + fixture.currentDateProvider.advanceBy(interval: 60) + try fixture.timeoutTimerFactory.check() let envelope = try XCTUnwrap(self.fixture.client?.captureEnvelopeInvocations.last) let profileItem = try XCTUnwrap(envelope.items.first) XCTAssertEqual("profile_chunk", profileItem.header.type) // assert that the profiler doesn't stop until after the next timer period elapses SentryContinuousProfiler.stop() - assertContinuousProfileStoppage() + try assertContinuousProfileStoppage() // check that the last full chunk was sent let lastEnvelope = try XCTUnwrap(self.fixture.client?.captureEnvelopeInvocations.last) @@ -136,13 +140,34 @@ final class SentryContinuousProfilerTests: XCTestCase { // check that two chunks were sent in total XCTAssertEqual(2, self.fixture.client?.captureEnvelopeInvocations.count) } + + func testChunkSerializationAfterBufferInterval() throws { + SentryContinuousProfiler.start() + XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling()) + + // Advance time by the buffer interval to trigger chunk serialization + fixture.currentDateProvider.advanceBy(interval: 60) + try fixture.timeoutTimerFactory.check() + + // Check that a chunk was serialized and sent + let envelope = try XCTUnwrap(self.fixture.client?.captureEnvelopeInvocations.last) + let profileItem = try XCTUnwrap(envelope.items.first) + XCTAssertEqual("profile_chunk", profileItem.header.type) + + // Ensure the profiler is still running + XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling()) + + // Stop the profiler + SentryContinuousProfiler.stop() + try assertContinuousProfileStoppage() + } } private extension SentryContinuousProfilerTests { func addMockSamples(mockAddresses: [NSNumber]) throws { let mockThreadMetadata = SentryProfileTestFixture.ThreadMetadata(id: 1, priority: 2, name: "main") let state = try XCTUnwrap(SentryContinuousProfiler.profiler()?.state) - for _ in 0..