fix: additional profiling data races#3035
Conversation
| # endif | ||
|
|
||
| const int kSentryProfilerFrequencyHz = 101; | ||
| NSString *const kTestStringConst = @"test"; |
There was a problem hiding this comment.
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.
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
We now take a single lock to make the entire mutation which simplifies the code, there is no fine grained locking.
| 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.
This logic is preserved from the changes in #3018
| std::shared_ptr<SamplingProfiler> _profiler; | ||
| SentryMetricProfiler *_metricProfiler; | ||
| SentryDebugImageProvider *_debugImageProvider; | ||
| thread::TIDType _mainThreadID; |
| 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.
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.
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 |
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 |
philipphofmann
left a comment
There was a problem hiding this comment.
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.
@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
processBacktracemutated 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.countandstacks.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 aprocessBacktracefunction 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:
sendDefaultPIIis enabled.🔮 Next steps