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

feat: upload list of slow/frozen rendered frame timestamps during a profile #1910

Merged
merged 43 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e209a56
feat: upload list of all rendered frame timestamps during a profile
armcknight Jun 21, 2022
5abf281
docs: update changelog
armcknight Jun 21, 2022
bbe4255
Format code
getsentry-bot Jun 21, 2022
cdd764b
fix: PR feedback; conditionally compile more stuff only for profiling
armcknight Jun 23, 2022
7c40ab3
fix: unlock profiler after querying if it's running
armcknight Jun 23, 2022
df4b44d
fix: bad conflict resolution
armcknight Jun 23, 2022
4bf5c7c
fix: use a ring buffer to place an upper bound on the amount of times…
armcknight Jun 23, 2022
8da1ed8
Format code
getsentry-bot Jun 23, 2022
f6bd805
fix: don't advance previousFrameTimestamp until after recording for p…
armcknight Jun 23, 2022
796d38d
Format code
getsentry-bot Jun 24, 2022
5faee87
fix: bad conflict resolution
armcknight Jun 24, 2022
6bb11a1
fix: compile fix for platforms without uikit
armcknight Jun 27, 2022
872e983
Format code
getsentry-bot Jun 27, 2022
5283d8d
fix: conditional compilation for other platforms not using profiling
armcknight Jun 27, 2022
41e434e
Format code
getsentry-bot Jun 27, 2022
bc82892
fix: changelog after bad merge
armcknight Jun 27, 2022
8bb9d28
fix: only init timestamp array if profiling
armcknight Jun 27, 2022
758ee4e
Format code
getsentry-bot Jun 27, 2022
3d5223a
fix: use a ring buffer to place an upper bound on the amount of times…
armcknight Jun 23, 2022
84cfe76
fix: changelog after bad merge
armcknight Jun 27, 2022
f29aa9d
fix: reset timestamps for each profiled transaction
armcknight Jun 27, 2022
52e3177
fix: convert timestamps to be relative to transaction start in nanose…
armcknight Jun 27, 2022
d00d45c
fix: revert to plain array instead of ringbuffer
armcknight Jun 29, 2022
11c8cdf
ref: also send time series of refresh rate changes during profiled tr…
armcknight Jun 30, 2022
03d41ad
Format code
getsentry-bot Jun 30, 2022
0ea16a0
fix: compilation without UIKit
armcknight Jun 30, 2022
08624b9
Format code
getsentry-bot Jun 30, 2022
b4b3be9
fix: bad conflict resolution
armcknight Jun 30, 2022
0fee73e
fix: properly set refresh rate relative timestamps
armcknight Jul 1, 2022
c5c4899
docs: fix changelog after bad merge
armcknight Jul 1, 2022
643eb5a
fix: pr feedback around proper state management of frame tracker for …
armcknight Jul 6, 2022
2a60e11
Format code
getsentry-bot Jul 6, 2022
480c869
Update Sources/Sentry/SentryFramesTracker.m
armcknight Jul 6, 2022
bf9fc96
Update Sources/Sentry/SentryFramesTracker.m
armcknight Jul 6, 2022
035a578
fix: more renames
armcknight Jul 6, 2022
d2dfd83
fix: non-ios builds
armcknight Jul 6, 2022
75fefa3
Format code
getsentry-bot Jul 12, 2022
4a44bf3
ref: move all possible logic to first locked profiler sector in finis…
armcknight Jul 12, 2022
0cf376a
Format code
getsentry-bot Jul 12, 2022
9a1f77a
Update Sources/Sentry/Public/SentryScreenFrames.h
armcknight Jul 12, 2022
ed687fd
docs: fix changelog after bad merge
armcknight Jul 12, 2022
25b8904
fix: allow null
armcknight Jul 12, 2022
f020c7e
Format code
getsentry-bot Jul 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add enableAutoBreadcrumbTracking option (#1958)
- Upload frame rendering timestamps to correlate to sampled backtraces (#1910)

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3038,7 +3038,7 @@
TargetAttributes = {
63AA759A1EB8AEF500D153DE = {
CreatedOnToolsVersion = 8.3.2;
LastSwiftMigration = 1330;
LastSwiftMigration = 1340;
ProvisioningStyle = Manual;
};
63AA76641EB8CB2F00D153DE = {
Expand Down
29 changes: 29 additions & 0 deletions Sources/Sentry/Public/SentryScreenFrames.h
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
#import "SentryDefines.h"
#import "SentryProfilingConditionals.h"

NS_ASSUME_NONNULL_BEGIN

#if SENTRY_HAS_UIKIT

/** An array of dictionaries that each contain a start and end timestamp for a rendered frame. */
# if SENTRY_TARGET_PROFILING_SUPPORTED
typedef NSArray<NSDictionary<NSString *, NSNumber *> *> SentryFrameInfoTimeSeries;
armcknight marked this conversation as resolved.
Show resolved Hide resolved
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

@interface SentryScreenFrames : NSObject
SENTRY_NO_INIT

- (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow;

# if SENTRY_TARGET_PROFILING_SUPPORTED
- (instancetype)initWithTotal:(NSUInteger)total
armcknight marked this conversation as resolved.
Show resolved Hide resolved
frozen:(NSUInteger)frozen
slow:(NSUInteger)slow
frameTimestamps:(SentryFrameInfoTimeSeries *)frameTimestamps
frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps;
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

@property (nonatomic, assign, readonly) NSUInteger total;
@property (nonatomic, assign, readonly) NSUInteger frozen;
@property (nonatomic, assign, readonly) NSUInteger slow;

# if SENTRY_TARGET_PROFILING_SUPPORTED
/**
* Array of dictionaries describing slow/frozen frames' timestamps. Each dictionary start and end
* timestamp for every such frame, keyed under @c start_timestamp and @c end_timestamp.
*/
@property (nonatomic, copy, readonly) SentryFrameInfoTimeSeries *frameTimestamps;
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved

/**
* Array of dictionaries describing the screen refresh rate at all points in time that it changes,
* which can happen when modern devices e.g. go into low power mode. Each dictionary contains keys
* @c timestamp and @c frame_rate.
*/
@property (nonatomic, copy, readonly) SentryFrameInfoTimeSeries *frameRateTimestamps;
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

@end

#endif
Expand Down
64 changes: 60 additions & 4 deletions Sources/Sentry/SentryFramesTracker.m
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
#import "SentryFramesTracker.h"
#import "SentryDisplayLinkWrapper.h"
#import <SentryLog.h>
#import "SentryProfilingConditionals.h"
#import "SentryTracer.h"
#import <SentryScreenFrames.h>
#include <stdatomic.h>

#if SENTRY_HAS_UIKIT
# import <UIKit/UIKit.h>

# if SENTRY_TARGET_PROFILING_SUPPORTED
/** A mutable version of @c SentryFrameInfoTimeSeries so we can accumulate results. */
typedef NSMutableArray<NSDictionary<NSString *, NSNumber *> *> SentryMutableFrameInfoTimeSeries;
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

static CFTimeInterval const SentryFrozenFrameThreshold = 0.7;
static CFTimeInterval const SentryPreviousFrameInitialValue = -1;

Expand All @@ -21,6 +27,10 @@

@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 *frameRateTimestamps;
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

@end

Expand Down Expand Up @@ -71,7 +81,18 @@ - (void)resetFrames
atomic_store_explicit(&_slowFrames, 0, SentryFramesMemoryOrder);

self.previousFrameTimestamp = SentryPreviousFrameInitialValue;
# if SENTRY_TARGET_PROFILING_SUPPORTED
[self resetProfilingTimestamps];
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
}

# if SENTRY_TARGET_PROFILING_SUPPORTED
- (void)resetProfilingTimestamps
{
self.frameTimestamps = [SentryMutableFrameInfoTimeSeries array];
self.frameRateTimestamps = [SentryMutableFrameInfoTimeSeries array];
}
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

- (void)start
{
Expand Down Expand Up @@ -101,6 +122,18 @@ - (void)displayLinkCallback
= 1 / (self.displayLinkWrapper.targetTimestamp - self.displayLinkWrapper.timestamp);
}

# if SENTRY_TARGET_PROFILING_SUPPORTED
if (self.currentTracer.isProfiling
armcknight marked this conversation as resolved.
Show resolved Hide resolved
&& (self.frameRateTimestamps.count == 0
|| self.frameRateTimestamps.lastObject[@"frame_rate"].doubleValue
!= actualFramesPerSecond)) {
[self.frameRateTimestamps addObject:@{
@"timestamp" : @(self.displayLinkWrapper.timestamp),
@"frame_rate" : @(actualFramesPerSecond),
}];
}
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

// Most frames take just a few microseconds longer than the optimal caculated duration.
// Therefore we substract one, because otherwise almost all frames would be slow.
CFTimeInterval slowFrameThreshold = 1 / (actualFramesPerSecond - 1);
Expand All @@ -110,22 +143,45 @@ - (void)displayLinkCallback

if (frameDuration > slowFrameThreshold && frameDuration <= SentryFrozenFrameThreshold) {
atomic_fetch_add_explicit(&_slowFrames, 1, SentryFramesMemoryOrder);
}

if (frameDuration > SentryFrozenFrameThreshold) {
# if SENTRY_TARGET_PROFILING_SUPPORTED
[self recordTimestampStart:@(self.previousFrameTimestamp) end:@(lastFrameTimestamp)];
# 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:@(lastFrameTimestamp)];
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
armcknight marked this conversation as resolved.
Show resolved Hide resolved
}

atomic_fetch_add_explicit(&_totalFrames, 1, SentryFramesMemoryOrder);

self.previousFrameTimestamp = lastFrameTimestamp;
}

# if SENTRY_TARGET_PROFILING_SUPPORTED
- (void)recordTimestampStart:(NSNumber *)start end:(NSNumber *)end
{
if (self.currentTracer.isProfiling) {
[self.frameTimestamps addObject:@{ @"start_timestamp" : start, @"end_timestamp" : end }];
}
}
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved

- (SentryScreenFrames *)currentFrames
{
NSUInteger total = atomic_load_explicit(&_totalFrames, SentryFramesMemoryOrder);
NSUInteger slow = atomic_load_explicit(&_slowFrames, SentryFramesMemoryOrder);
NSUInteger frozen = atomic_load_explicit(&_frozenFrames, SentryFramesMemoryOrder);

# if SENTRY_TARGET_PROFILING_SUPPORTED
return [[SentryScreenFrames alloc] initWithTotal:total
frozen:frozen
slow:slow
frameTimestamps:self.frameTimestamps
frameRateTimestamps:self.frameRateTimestamps];
# else
return [[SentryScreenFrames alloc] initWithTotal:total frozen:frozen slow:slow];
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
}

- (void)stop
Expand Down
41 changes: 41 additions & 0 deletions Sources/Sentry/SentryProfiler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
# import "SentryDependencyContainer.h"
# import "SentryEnvelope.h"
# import "SentryEnvelopeItemType.h"
# import "SentryFramesTracker.h"
# import "SentryHexAddressFormatter.h"
# import "SentryId.h"
# import "SentryLog.h"
# import "SentryProfilingLogging.hpp"
# import "SentrySamplingProfiler.hpp"
# import "SentryScreenFrames.h"
# import "SentrySerialization.h"
# import "SentryTime.h"
# import "SentryTransaction.h"
Expand Down Expand Up @@ -208,6 +210,7 @@ - (void)stop
}

- (SentryEnvelopeItem *)buildEnvelopeItemForTransaction:(SentryTransaction *)transaction
frameInfo:(SentryScreenFrames *)frameInfo
{
NSMutableDictionary<NSString *, id> *profile = nil;
@synchronized(self) {
Expand Down Expand Up @@ -253,6 +256,39 @@ - (SentryEnvelopeItem *)buildEnvelopeItemForTransaction:(SentryTransaction *)tra
profile[@"version_code"] = [bundle objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];
profile[@"version_name"] = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];

# if SENTRY_HAS_UIKIT
auto relativeFrameTimestampsNs = [NSMutableArray array];
[frameInfo.frameTimestamps enumerateObjectsUsingBlock:^(
NSDictionary<NSString *, NSNumber *> *_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);
[relativeFrameTimestampsNs addObject:@{
@"start_timestamp_relative_ns" : @(getDurationNs(_startTimestamp, begin)),
@"end_timestamp_relative_ns" : @(getDurationNs(_startTimestamp, end)),
}];
}];
profile[@"adverse_frame_render_timestamps"] = relativeFrameTimestampsNs;

relativeFrameTimestampsNs = [NSMutableArray array];
[frameInfo.frameRateTimestamps enumerateObjectsUsingBlock:^(
NSDictionary<NSString *, NSNumber *> *_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;
# endif // SENTRY_HAS_UIKIT

NSError *error = nil;
const auto JSONData = [SentrySerialization dataWithJSONObject:profile error:&error];
if (JSONData == nil) {
Expand All @@ -268,6 +304,11 @@ - (SentryEnvelopeItem *)buildEnvelopeItemForTransaction:(SentryTransaction *)tra
return [[SentryEnvelopeItem alloc] initWithHeader:header data:JSONData];
}

- (BOOL)isRunning
{
return _profiler->isSampling();
}

@end

#endif
27 changes: 27 additions & 0 deletions Sources/Sentry/SentryScreenFrames.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,42 @@ @implementation SentryScreenFrames

- (instancetype)initWithTotal:(NSUInteger)total frozen:(NSUInteger)frozen slow:(NSUInteger)slow
{
# if SENTRY_TARGET_PROFILING_SUPPORTED
return [self initWithTotal:total
frozen:frozen
slow:slow
frameTimestamps:@[]
frameRateTimestamps:@[]];
# else
if (self = [super init]) {
_total = total;
_slow = slow;
_frozen = frozen;
}

return self;
# endif // SENTRY_TARGET_PROFILING_SUPPORTED
}

# if SENTRY_TARGET_PROFILING_SUPPORTED
- (instancetype)initWithTotal:(NSUInteger)total
frozen:(NSUInteger)frozen
slow:(NSUInteger)slow
frameTimestamps:(SentryFrameInfoTimeSeries *)frameTimestamps
frameRateTimestamps:(SentryFrameInfoTimeSeries *)frameRateTimestamps
{
if (self = [super init]) {
_total = total;
_slow = slow;
_frozen = frozen;
_frameTimestamps = frameTimestamps;
_frameRateTimestamps = frameRateTimestamps;
}

return self;
}
# endif // SENTRY_TARGET_PROFILING_SUPPORTED

@end

#endif
37 changes: 30 additions & 7 deletions Sources/Sentry/SentryTracer.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
@property (nonatomic) BOOL isWaitingForChildren;
@property (nonatomic) NSTimeInterval idleTimeout;
@property (nonatomic, nullable, strong) SentryDispatchQueueWrapper *dispatchQueueWrapper;
@property (nonatomic, assign, readwrite) BOOL isProfiling;

@end

Expand Down Expand Up @@ -146,18 +147,22 @@ - (instancetype)initWithTransactionContext:(SentryTransactionContext *)transacti
initSlowFrames = currentFrames.slow;
initFrozenFrames = currentFrames.frozen;
}
#endif
#endif // SENTRY_HAS_UIKIT
#if SENTRY_TARGET_PROFILING_SUPPORTED
if ([_hub getClient].options.enableProfiling) {
[profilerLock lock];
if (profiler == nil) {
profiler = [[SentryProfiler alloc] init];
[SentryLog logWithMessage:@"Starting profiler." andLevel:kSentryLevelDebug];
# if SENTRY_HAS_UIKIT
framesTracker.currentTracer = self;
[framesTracker resetProfilingTimestamps];
# endif // SENTRY_HAS_UIKIT
[profiler start];
}
[profilerLock unlock];
}
#endif
#endif // SENTRY_TARGET_PROFILING_SUPPORTED
}

return self;
Expand Down Expand Up @@ -397,17 +402,23 @@ - (void)finishInternal
self.finishCallback = nil;
}

if (_hub == nil)
return;

#if SENTRY_TARGET_PROFILING_SUPPORTED
SentryScreenFrames *frameInfo;
if ([_hub getClient].options.enableProfiling) {
[SentryLog logWithMessage:@"Stopping profiler." andLevel:kSentryLevelDebug];
[profilerLock lock];
[profiler stop];
# if SENTRY_HAS_UIKIT
frameInfo = SentryFramesTracker.sharedInstance.currentFrames;
[SentryFramesTracker.sharedInstance resetProfilingTimestamps];
SentryFramesTracker.sharedInstance.currentTracer = nil;
# endif // SENTRY_HAS_UIKIT
armcknight marked this conversation as resolved.
Show resolved Hide resolved
[profilerLock unlock];
}
#endif

if (_hub == nil)
return;
#endif // SENTRY_TARGET_PROFILING_SUPPORTED

[_hub.scope useSpan:^(id<SentrySpan> _Nullable span) {
if (span == self) {
Expand Down Expand Up @@ -452,11 +463,13 @@ - (void)finishInternal
}

NSMutableArray<SentryEnvelopeItem *> *additionalEnvelopeItems = [NSMutableArray array];

#if SENTRY_TARGET_PROFILING_SUPPORTED
if ([_hub getClient].options.enableProfiling) {
[profilerLock lock];
if (profiler != nil) {
SentryEnvelopeItem *profile = [profiler buildEnvelopeItemForTransaction:transaction];
SentryEnvelopeItem *profile = [profiler buildEnvelopeItemForTransaction:transaction
frameInfo:frameInfo];
if (profile != nil) {
[additionalEnvelopeItems addObject:profile];
}
Expand Down Expand Up @@ -727,6 +740,16 @@ + (nullable SentryTracer *)getTracer:(id<SentrySpan>)span
return nil;
}

#if SENTRY_TARGET_PROFILING_SUPPORTED
- (BOOL)isProfiling
{
[profilerLock lock];
BOOL isRunning = profiler.isRunning;
[profilerLock unlock];
return isRunning;
}
#endif // SENTRY_TARGET_PROFILING_SUPPORTED

@end

NS_ASSUME_NONNULL_END