Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impr(profiling): increase buffer length from 10s to 60s #4826

Merged
merged 8 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
90 changes: 67 additions & 23 deletions SentryTestUtils/TestSentryNSTimerFactory.swift
Original file line number Diff line number Diff line change
@@ -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()
armcknight marked this conversation as resolved.
Show resolved Hide resolved
}
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()
}
}
armcknight marked this conversation as resolved.
Show resolved Hide resolved

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
}
}
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;
armcknight marked this conversation as resolved.
Show resolved Hide resolved
static NSTimeInterval kSentryProfilerTimeoutInterval = 30;

NS_ASSUME_NONNULL_BEGIN
Expand Down
49 changes: 37 additions & 12 deletions Tests/SentryProfilerTests/SentryContinuousProfilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ final class SentryContinuousProfilerTests: XCTestCase {
super.tearDown()
clearTestState()
}


func testSentryProfilerChunkExpirationInterval() {
XCTAssertEqual(60, kSentryProfilerChunkExpirationInterval)
}

func testStartingAndStoppingContinuousProfiler() throws {
try performContinuousProfilingTest()
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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..<Int(kSentryProfilerChunkExpirationInterval) {
for _ in 0..<Int(60) {
fixture.currentDateProvider.advanceBy(interval: 1)
SentryProfilerMocksSwiftCompatible.appendMockBacktrace(to: state, threadID: mockThreadMetadata.id, threadPriority: mockThreadMetadata.priority, threadName: mockThreadMetadata.name, addresses: mockAddresses)
}
Expand All @@ -159,7 +184,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 +199,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)
armcknight marked this conversation as resolved.
Show resolved Hide resolved
fixture.timeoutTimerFactory.fire()
fixture.currentDateProvider.advance(by: 60)
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
8 changes: 6 additions & 2 deletions Tests/SentryProfilerTests/SentryTraceProfilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class SentryTraceProfilerTests: XCTestCase {
clearTestState()
}

func testSentryProfilerTimoutInterval() {
XCTAssertEqual(30, kSentryProfilerTimeoutInterval)
}

func testMetricProfiler() throws {
let span = try fixture.newTransaction()
try addMockSamples()
Expand Down Expand Up @@ -138,7 +142,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 +409,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
Loading