Draft: Associate label sets with wall profiles#98
Conversation
Overall package sizeSelf size: 3.2 MB Dependency sizes
🤖 This report was automatically generated by heaviest-objects-in-the-universe |
88a883f to
6cb5345
Compare
7ff3d57 to
832a591
Compare
d7f74a4 to
97a6d61
Compare
|
|
||
| const T& front() const { return buffer[front_index_]; } | ||
|
|
||
| template <typename T2> |
There was a problem hiding this comment.
❔ question: Why are push_back/push_front templated ?
There was a problem hiding this comment.
To make their arguments universal references, so we can use std::forward
| auto newProfilers = new ProfilerMap(*currProfilers); | ||
| updateFn(newProfilers); | ||
| if (profilers.compare_exchange_weak(currProfilers, newProfilers)) { | ||
| delete currProfilers; |
There was a problem hiding this comment.
I think that at this point another thread might still reference currProfilers (eg. if it was interrupted just after line 360).
If I understand correctly, you are trying to do some kind of RCU, but I am not sure if it's feasible in userspace.
There was a problem hiding this comment.
Made some changes, using atomic sets to nullptr for the sighandler to temporarily take ownership of the map while it's using it
There was a problem hiding this comment.
Not sure about this, it seems that it does not solve the issue that when deleting currProfilers on line 403 another thread might be copying it on 399 because it loaded its value before CAS operation succeeded on line 401.
There was a problem hiding this comment.
yep, you're right. I added a mutex to updateProfilers. It still uses the same logic to coordinate with the signal handler though. With the latest change I also accounted for the fact we can now have at most one mutator thread, so I simplified the logic around compare-and-exchange, as the once constructed new map is always valid, so we just need to try to busily install it, and don't have to loop through the whole method one more time and construct the new map again (as it'll still be based on the previous map.) Basically, there's no need for delete newProfilers anymore.
| SampleContext() {} | ||
|
|
||
| SampleContext(std::shared_ptr<v8::Global<v8::Value>> l, uint64_t t) | ||
| : labels(l), timestamp(t) {} |
There was a problem hiding this comment.
| : labels(l), timestamp(t) {} | |
| : labels(std::move(l)), timestamp(t) {} |
There was a problem hiding this comment.
Hm… might it actually be even better to declare the argument as a shared_ptr&? Then it wouldn't be copied on the call site but would otherwise be fed correctly to the labels copy constructor here.
56abe94 to
29e6175
Compare
8e2f906 to
911ff76
Compare
911ff76 to
547a14e
Compare
| static void sighandler(int sig, siginfo_t* info, void* context) { | ||
| auto isolate = Isolate::GetCurrent(); | ||
| WallProfiler* prof = nullptr; | ||
| auto prof_map = profilers.exchange(nullptr); |
There was a problem hiding this comment.
Very elegant solution !
This way sighandler will always be able to "lock" the map without using a mutex, nor using a CAS loop.
All the complexity / latency is moved to updateProfilers, but this is not a problem since it is rarely called.
5a19209 to
4a97423
Compare
| // to it in time, and stop as soon as it sees a context that's too recent | ||
| // for this sample. | ||
| for (;;) { | ||
| auto contextTimestamp = sampleContext.timestamp - zeroContextTime; |
There was a problem hiding this comment.
contextTimestamp is an uint64_t (because sampleContext.timestamp and zeroContextTime are both uint64_t), but zeroContextTime might be bigger than sampleContext.timestamp (if a context from a previous period was not matched for example), leading to a contextTimestamp holding a very large value and test (contextTimestamp < earliest) failing whereas is should.
I find it best to avoid unsigned arithmetic types in c++, because they lead to surprising behaviours in the face of integer promotion rules (eg. -1 > 1u being true).
There was a problem hiding this comment.
Our only source of unsigned ints was uv_hrtime() but fortunately we could now completely ditch it in favor of v8::base::TimeTicks so this'll no longer be an issue 🎉
1a1e721 to
630d700
Compare
7a66f02 to
2bddc62
Compare
3bf3471 to
8b06d8c
Compare
Make ring buffer operations interrupt safe.
8b06d8c to
42a55ad
Compare
|
Superseded by #105 |
Adds ability to associate custom label sets to wall samples. This can then be used by dd-trace-js to associate span ids with said samples.
Implementation notes
The implementation adds JS API to set and clear labels, as well as API to query whether labels were used when capturing a sample. In practice, the
WallProfilerJS class will gain getters and setters for propertieslabels, a method namedunsetLabelsand a getter for read-only propertylabelsCaptured.Internally, the implementation uses a PPROF signal handler executing before V8's signal handler used for profiling. It will associate the current label set with a timestamp, and put it in a preallocated ring buffer (so allocations are avoided in the signal handler.) This happens once every 10ms while profiling. The ring buffer is sized generously, for 2x the number of samples for a single profile. This allows capturing the next profile's labels while the previous profile is post-processed.
When the profile is post-processed once every minute, the labels are matched to samples based on the timestamp, and emitted into the serialized profile.
Timestamp matching
Timestamp translation is necessary between V8's timestamps and libuv's high-resolution timer. The timestamps don't need to be exactly matched (and most likely won't be.) V8 uses microsecond timestamps, libuv uses nanosecond timestamps, and the bases are different. After matching the bases and scaling, the labels with closest timestamp in the ring buffer within a ±5ms range of a sample are associated with the sample.