Skip to content

Commit

Permalink
Merge 00a1a3a into 44332bf
Browse files Browse the repository at this point in the history
  • Loading branch information
armcknight authored Feb 12, 2025
2 parents 44332bf + 00a1a3a commit bf8d5b4
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 55 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
89 changes: 69 additions & 20 deletions SentryTestUtils/TestSentryNSTimerFactory.swift
Original file line number Diff line number Diff line change
@@ -1,52 +1,101 @@
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()
}
private var _timer: Timer?
private var _interval: TimeInterval?

public var timer: Timer {
get {
_timer ?? Timer()
}
set(newValue) {
_timer = newValue
}
}

}
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()
}
}
}
2 changes: 1 addition & 1 deletion Sources/Sentry/Profiling/SentryProfilerDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 29 additions & 8 deletions Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,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 {
Expand All @@ -119,14 +119,14 @@ final class SentryContinuousProfilerTests: XCTestCase {

// assert that the first chunk was sent
fixture.currentDateProvider.advanceBy(interval: kSentryProfilerChunkExpirationInterval)
fixture.timeoutTimerFactory.fire()
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)
Expand All @@ -136,6 +136,27 @@ 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 {
Expand All @@ -159,7 +180,7 @@ private extension SentryContinuousProfilerTests {
try fixture.gatherMockedContinuousProfileMetrics()
try addMockSamples(mockAddresses: expectedAddresses)
fixture.currentDateProvider.advanceBy(interval: 1)
fixture.timeoutTimerFactory.fire()
try fixture.timeoutTimerFactory.check()
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
try assertValidData(expectedEnvironment: expectedEnvironment, expectedAddresses: expectedAddresses, countMetricsReadingAtProfileStart: countMetricsReadingAtProfileStart)
#if os(iOS) || os(tvOS) || targetEnvironment(macCatalyst)
Expand All @@ -174,13 +195,13 @@ private extension SentryContinuousProfilerTests {

XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
SentryContinuousProfiler.stop()
assertContinuousProfileStoppage()
try assertContinuousProfileStoppage()
}

func assertContinuousProfileStoppage() {
func assertContinuousProfileStoppage() throws {
XCTAssert(SentryContinuousProfiler.isCurrentlyProfiling())
fixture.currentDateProvider.advance(by: kSentryProfilerTimeoutInterval)
fixture.timeoutTimerFactory.fire()
try fixture.timeoutTimerFactory.check()
XCTAssertFalse(SentryContinuousProfiler.isCurrentlyProfiling())
}

Expand Down
6 changes: 4 additions & 2 deletions Tests/SentryProfilerTests/SentryProfileTestFixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SentryProfileTestFixture {
let processInfoWrapper = TestSentryNSProcessInfoWrapper()
let dispatchFactory = TestDispatchFactory()
var metricTimerFactory: TestDispatchSourceWrapper?
let timeoutTimerFactory = TestSentryNSTimerFactory()
var timeoutTimerFactory: TestSentryNSTimerFactory
let dispatchQueueWrapper = TestSentryDispatchQueueWrapper()
let notificationCenter = TestNSNotificationCenterWrapper()

Expand All @@ -46,8 +46,10 @@ class SentryProfileTestFixture {
SentryDependencyContainer.sharedInstance().systemWrapper = systemWrapper
SentryDependencyContainer.sharedInstance().processInfoWrapper = processInfoWrapper
SentryDependencyContainer.sharedInstance().dispatchFactory = dispatchFactory
SentryDependencyContainer.sharedInstance().timerFactory = timeoutTimerFactory
SentryDependencyContainer.sharedInstance().notificationCenterWrapper = notificationCenter

timeoutTimerFactory = TestSentryNSTimerFactory(currentDateProvider: self.currentDateProvider)
SentryDependencyContainer.sharedInstance().timerFactory = timeoutTimerFactory

let image = DebugMeta()
image.name = "sentrytest"
Expand Down
4 changes: 2 additions & 2 deletions Tests/SentryProfilerTests/SentryTraceProfilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class SentryTraceProfilerTests: XCTestCase {

// time out profiler for span A
fixture.currentDateProvider.advanceBy(nanoseconds: 30.toNanoSeconds())
fixture.timeoutTimerFactory.fire()
try fixture.timeoutTimerFactory.fire()

fixture.currentDateProvider.advanceBy(nanoseconds: 0.5.toNanoSeconds())

Expand Down Expand Up @@ -405,7 +405,7 @@ private extension SentryTraceProfilerTests {
try addMockSamples()
fixture.currentDateProvider.advance(by: 31)
if shouldTimeOut {
self.fixture.timeoutTimerFactory.fire()
try self.fixture.timeoutTimerFactory.fire()
}

let exp = expectation(description: "finished span")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {

private class Fixture {
let dateProvider: TestCurrentDateProvider = TestCurrentDateProvider()
let timerFactory = TestSentryNSTimerFactory()
lazy var timerFactory = TestSentryNSTimerFactory(currentDateProvider: dateProvider)

var displayLinkWrapper = TestDisplayLinkWrapper()
var framesTracker: SentryFramesTracker
Expand Down Expand Up @@ -280,7 +280,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11))

// Timeout for tracer times out
fixture.timerFactory.fire()
try fixture.timerFactory.fire()

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 12))
sut.reportFullyDisplayed()
Expand Down Expand Up @@ -330,7 +330,7 @@ class SentryTimeToDisplayTrackerTest: XCTestCase {
fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 11))

// Timeout for tracer times out
fixture.timerFactory.fire()
try fixture.timerFactory.fire()

fixture.dateProvider.setDate(date: Date(timeIntervalSince1970: 12))
sut.reportFullyDisplayed()
Expand Down
Loading

0 comments on commit bf8d5b4

Please sign in to comment.