Skip to content

Conversation

szegedi
Copy link

@szegedi szegedi commented May 15, 2023

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 WallProfiler JS class will gain getters and setters for properties labels, a method named unsetLabels and a getter for read-only property labelsCaptured.

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.

@szegedi szegedi added enhancement New feature or request semver-minor Usually minor non-breaking improvements labels May 15, 2023
@github-actions
Copy link

github-actions bot commented May 15, 2023

Overall package size

Self size: 3.2 MB
Deduped: 4.08 MB
No deduping: 4.08 MB

Dependency sizes

name version self size total size
pprof-format 2.0.7 588.12 kB 588.12 kB
source-map 0.7.4 226 kB 226 kB
split 1.0.1 12.29 kB 24.82 kB
p-limit 3.1.0 7.75 kB 13.78 kB
delay 5.0.0 11.17 kB 11.17 kB
pify 5.0.0 8.87 kB 8.87 kB
node-gyp-build 3.9.0 8.81 kB 8.81 kB

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@szegedi szegedi force-pushed the szegedi/use_timestamps branch 8 times, most recently from 88a883f to 6cb5345 Compare May 17, 2023 15:11
@szegedi szegedi force-pushed the szegedi/use_timestamps branch from 7ff3d57 to 832a591 Compare May 25, 2023 12:00
@szegedi szegedi requested review from nsavoire and Qard as code owners May 25, 2023 12:00
@szegedi szegedi force-pushed the szegedi/use_timestamps branch 3 times, most recently from d7f74a4 to 97a6d61 Compare May 31, 2023 10:07

const T& front() const { return buffer[front_index_]; }

template <typename T2>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: ‏ Why are push_back/push_front templated ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some changes, using atomic sets to nullptr for the sighandler to temporarily take ownership of the map while it's using it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
: labels(l), timestamp(t) {}
: labels(std::move(l)), timestamp(t) {}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@szegedi szegedi force-pushed the szegedi/use_timestamps branch 2 times, most recently from 56abe94 to 29e6175 Compare June 2, 2023 08:27
@szegedi szegedi force-pushed the szegedi/use_timestamps branch 2 times, most recently from 8e2f906 to 911ff76 Compare June 2, 2023 12:54
@szegedi szegedi force-pushed the szegedi/use_timestamps branch from 911ff76 to 547a14e Compare June 5, 2023 07:45
static void sighandler(int sig, siginfo_t* info, void* context) {
auto isolate = Isolate::GetCurrent();
WallProfiler* prof = nullptr;
auto prof_map = profilers.exchange(nullptr);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@szegedi szegedi force-pushed the szegedi/use_timestamps branch 3 times, most recently from 5a19209 to 4a97423 Compare June 5, 2023 12:30
// 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;
Copy link

@nsavoire nsavoire Jun 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 🎉

@szegedi szegedi force-pushed the szegedi/use_timestamps branch 3 times, most recently from 1a1e721 to 630d700 Compare June 9, 2023 15:51
@szegedi szegedi force-pushed the szegedi/use_timestamps branch from 7a66f02 to 2bddc62 Compare June 12, 2023 14:49
@szegedi szegedi force-pushed the szegedi/use_timestamps branch 2 times, most recently from 3bf3471 to 8b06d8c Compare June 13, 2023 13:59
@szegedi szegedi force-pushed the szegedi/use_timestamps branch from 8b06d8c to 42a55ad Compare June 13, 2023 14:00
@szegedi szegedi closed this Jul 3, 2023
@szegedi szegedi deleted the szegedi/use_timestamps branch July 3, 2023 11:19
@szegedi
Copy link
Author

szegedi commented Jul 3, 2023

Superseded by #105

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request semver-minor Usually minor non-breaking improvements
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants