-
-
Notifications
You must be signed in to change notification settings - Fork 318
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
fix: additional profiling data races #3035
Conversation
@@ -49,7 +50,6 @@ | |||
# endif | |||
|
|||
const int kSentryProfilerFrequencyHz = 101; | |||
NSString *const kTestStringConst = @"test"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constant was unused so I removed it
|
||
- (void)appendBacktrace:(const Backtrace &)backtrace { | ||
[self mutate:^(SentryProfilingMutableState *state) { | ||
const auto threadID = sentry_stringForUInt64(backtrace.threadMetadata.threadID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic inside mutate
is essentially unchanged from the original logic of processBacktrace
prior to the changes in #3018. The original code is here:
sentry-cocoa/Sources/Sentry/SentryProfiler.mm
Lines 91 to 160 in b7bd719
const auto threadID = sentry_stringForUInt64(backtrace.threadMetadata.threadID); | |
NSString *queueAddress = nil; | |
if (backtrace.queueMetadata.address != 0) { | |
queueAddress = sentry_formatHexAddressUInt64(backtrace.queueMetadata.address); | |
} | |
NSMutableDictionary<NSString *, id> *metadata = threadMetadata[threadID]; | |
if (metadata == nil) { | |
metadata = [NSMutableDictionary<NSString *, id> dictionary]; | |
threadMetadata[threadID] = metadata; | |
} | |
if (!backtrace.threadMetadata.name.empty() && metadata[@"name"] == nil) { | |
metadata[@"name"] = [NSString stringWithUTF8String:backtrace.threadMetadata.name.c_str()]; | |
} | |
if (backtrace.threadMetadata.priority != -1 && metadata[@"priority"] == nil) { | |
metadata[@"priority"] = @(backtrace.threadMetadata.priority); | |
} | |
if (queueAddress != nil && queueMetadata[queueAddress] == nil | |
&& backtrace.queueMetadata.label != nullptr) { | |
queueMetadata[queueAddress] = | |
@ { @"label" : [NSString stringWithUTF8String:backtrace.queueMetadata.label->c_str()] }; | |
} | |
# if defined(DEBUG) | |
const auto symbols | |
= backtrace_symbols(reinterpret_cast<void *const *>(backtrace.addresses.data()), | |
static_cast<int>(backtrace.addresses.size())); | |
# endif | |
const auto stack = [NSMutableArray<NSNumber *> array]; | |
for (std::vector<uintptr_t>::size_type backtraceAddressIdx = 0; | |
backtraceAddressIdx < backtrace.addresses.size(); backtraceAddressIdx++) { | |
const auto instructionAddress | |
= sentry_formatHexAddressUInt64(backtrace.addresses[backtraceAddressIdx]); | |
const auto frameIndex = frameIndexLookup[instructionAddress]; | |
if (frameIndex == nil) { | |
const auto frame = [NSMutableDictionary<NSString *, id> dictionary]; | |
frame[@"instruction_addr"] = instructionAddress; | |
# if defined(DEBUG) | |
frame[@"function"] = parseBacktraceSymbolsFunctionName(symbols[backtraceAddressIdx]); | |
# endif | |
[stack addObject:@(frames.count)]; | |
frameIndexLookup[instructionAddress] = @(frames.count); | |
[frames addObject:frame]; | |
} else { | |
[stack addObject:frameIndex]; | |
} | |
} | |
const auto sample = [[SentrySample alloc] init]; | |
sample.absoluteTimestamp = backtrace.absoluteTimestamp; | |
sample.threadID = backtrace.threadMetadata.threadID; | |
if (queueAddress != nil) { | |
sample.queueAddress = queueAddress; | |
} | |
const auto stackKey = [stack componentsJoinedByString:@"|"]; | |
const auto stackIndex = stackIndexLookup[stackKey]; | |
if (stackIndex) { | |
sample.stackIndex = stackIndex; | |
} else { | |
const auto nextStackIndex = @(stacks.count); | |
sample.stackIndex = nextStackIndex; | |
stackIndexLookup[stackKey] = nextStackIndex; | |
[stacks addObject:stack]; | |
} | |
{ | |
std::lock_guard<std::mutex> l(_gSamplesArrayLock); | |
[samples addObject:sample]; | |
} |
We now take a single lock to make the entire mutation which simplifies the code, there is no fine grained locking.
Sources/Sentry/SentryProfiler.mm
Outdated
NSMutableArray<SentrySample *> *const samples = [_mutableState.samples copy]; | ||
NSMutableArray<NSArray<NSNumber *> *> *const stacks = [_mutableState.stacks copy]; | ||
NSMutableArray<NSDictionary<NSString *, id> *> *const frames = [_mutableState.frames copy]; | ||
NSMutableDictionary<NSString *, NSDictionary *> *const queueMetadata = [_mutableState.queueMetadata copy]; | ||
|
||
// thread metadata contains a mutable substructure, so it's not enough to perform a copy of | ||
// the top-level dictionary, we need to go deeper to copy the mutable subdictionaries | ||
const auto threadMetadata = [NSMutableDictionary<NSString *, NSDictionary *> dictionary]; | ||
[_mutableState.threadMetadata | ||
enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSDictionary *_Nonnull obj, | ||
BOOL *_Nonnull stop) { threadMetadata[key] = [obj copy]; }]; | ||
|
||
return @{ | ||
@"profile": @{ | ||
@"samples": samples, | ||
@"stacks": stacks, | ||
@"frames": frames, | ||
@"thread_metadata": threadMetadata, | ||
@"queue_metadata": queueMetadata | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic is preserved from the changes in #3018
std::shared_ptr<SamplingProfiler> _profiler; | ||
SentryMetricProfiler *_metricProfiler; | ||
SentryDebugImageProvider *_debugImageProvider; | ||
thread::TIDType _mainThreadID; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was unused
const auto frameIndexLookup = [NSMutableDictionary<NSString *, NSNumber *> dictionary]; | ||
const auto stackIndexLookup = [NSMutableDictionary<NSString *, NSNumber *> dictionary]; | ||
|
||
SentryProfilingState *state = [[SentryProfilingState alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SentryProfilingState
simplifies these tests
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #3035 +/- ##
=============================================
+ Coverage 88.835% 88.868% +0.033%
=============================================
Files 492 492
Lines 53063 53005 -58
Branches 19000 18991 -9
=============================================
- Hits 47139 47105 -34
+ Misses 4964 4942 -22
+ Partials 960 958 -2
... and 14 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
|
[stack addObject:@(frames.count)]; | ||
frameIndexLookup[instructionAddress] = @(frames.count); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the old implementation the logic was changed to use the framesToAdd
array as a temporary container before applying the append all at once in the critical section, but that means that frames.count
will now be incorrect here.
Performance metrics 🚀
|
Revision | Plain | With Sentry | Diff |
---|---|---|---|
28333b6 | 1247.29 ms | 1262.51 ms | 15.22 ms |
eaa1002 | 1205.50 ms | 1235.08 ms | 29.58 ms |
1e065bc | 1239.69 ms | 1258.18 ms | 18.49 ms |
47b41ed | 1252.31 ms | 1253.96 ms | 1.64 ms |
15b8c61 | 1223.16 ms | 1244.83 ms | 21.67 ms |
9e96fd6 | 1207.20 ms | 1229.40 ms | 22.20 ms |
1bbcb9c | 1192.51 ms | 1231.96 ms | 39.45 ms |
dc0fe58 | 1198.56 ms | 1220.98 ms | 22.42 ms |
371db89 | 1226.40 ms | 1251.54 ms | 25.14 ms |
78d5983 | 1229.98 ms | 1245.60 ms | 15.62 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
28333b6 | 20.76 KiB | 424.69 KiB | 403.93 KiB |
eaa1002 | 20.76 KiB | 423.19 KiB | 402.43 KiB |
1e065bc | 20.76 KiB | 425.78 KiB | 405.01 KiB |
47b41ed | 20.76 KiB | 436.66 KiB | 415.90 KiB |
15b8c61 | 20.76 KiB | 419.67 KiB | 398.91 KiB |
9e96fd6 | 20.76 KiB | 425.80 KiB | 405.04 KiB |
1bbcb9c | 20.76 KiB | 426.10 KiB | 405.34 KiB |
dc0fe58 | 20.76 KiB | 436.50 KiB | 415.74 KiB |
371db89 | 20.76 KiB | 427.31 KiB | 406.55 KiB |
78d5983 | 20.76 KiB | 427.80 KiB | 407.04 KiB |
Previous results on branch: indragiek/fix-profiling-concurrency
Startup times
Revision | Plain | With Sentry | Diff |
---|---|---|---|
6826b48 | 1229.86 ms | 1236.84 ms | 6.98 ms |
6a989e4 | 1248.90 ms | 1273.30 ms | 24.40 ms |
App size
Revision | Plain | With Sentry | Diff |
---|---|---|---|
6826b48 | 20.76 KiB | 435.10 KiB | 414.34 KiB |
6a989e4 | 20.76 KiB | 435.04 KiB | 414.28 KiB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
- additional profiling data races ([#3035](https://github.com/getsentry/sentry-cocoa/pull/3035)) If none of the above apply, you can opt out of this check by adding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the refactoring. I reviewed this while you merged it. Still LGTM 👍
processBacktrace(backtrace, resolvedThreadMetadata, resolvedQueueMetadata, resolvedSamples, | ||
resolvedStacks, resolvedFrames, frameIndexLookup, stackIndexLookup); | ||
[mutateExpectation fulfill]; | ||
[state mutate:^( | ||
__unused SentryProfilingMutableState *mutableState) { [mutateExpectation fulfill]; }]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@indragiek I just noticed this change when going to write some new tests. Is the last backtrace created above (here) no longer appended to the profile data?
📜 Description
While debugging the memory leaks reported in #2980 I found additional data race issues on top of the ones fixed in #3018. This is a larger refactor that fixes those issues but also simplifies the way that synchronization works.
In #3018, there was a change to add locking where
processBacktrace
mutated data structures that could be accessed concurrently. It attempted to separate the code that did not access the shared data structures, and moved all mutating code to a section at the end of the function. However, it missed a few reads from those data structures that also needed to be synchronized (e.g.frames.count
andstacks.count
). Generally, separating the critical section from the rest of the logic in that function is not worth the pain because the taking the lock for the entire duration of the function is not expensive.This refactor adds a new type,
SentryProfilingState
, that centralizes all logic for reading/writing the shared data structures. This has the additional benefit that you no longer need aprocessBacktrace
function that takes many parameters where the lifetime of those parameters is unclear (this will be helpful for fixing the memory leaks in a subsequent PR).All notable changes have been commented on inline.
💚 How did you test it?
Ran all existing tests, they pass.
📝 Checklist
You have to check all boxes before merging:
sendDefaultPII
is enabled.🔮 Next steps