Skip to content

ref(node): Improve span flushing #16577

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

Merged
merged 7 commits into from
Jun 17, 2025
Merged

ref(node): Improve span flushing #16577

merged 7 commits into from
Jun 17, 2025

Conversation

mydea
Copy link
Member

@mydea mydea commented Jun 13, 2025

Follow up to #16416, slightly moving things around and making some small improvements to performance and logic (some not related to that PR but another improvement we noticed when BYK looked int this):

  1. Actually debounce the flushing properly, including a maxWait of 100ms. Previously, it was technically possible to debounce flushing forever, if a span was flushed every ms. Now, at least after 100ms it should always flush.
  2. Simplify overhead by avoid checking for the 5min timeout of sent spans. As this check isSpanAlreadySent has been called for every single span that ended, it is quite high impact, and meant a little bit of additional work (although it should not necessarily run too often, but still). Instead, we now simply check for map.has(id) which should be good enough for what we want to achieve, IMHO. We still clean up the sent spans when flushing, so old stuff should still go away eventually.
  3. Made stuff that is not needed as public API private on the span exporter class. this is technically breaking but I think it is OK, this should not be public API surface as it does not need to be called from outside.

For this I moved the already existing debounce function from replay to core. We re-implement this in replay with a different setTimeout impl. which is needed for some angular stuff.

@mydea mydea requested review from BYK, AbhiPrasad and andreiborza June 13, 2025 07:34
@mydea mydea self-assigned this Jun 13, 2025
@mydea mydea requested a review from a team as a code owner June 13, 2025 07:34
Copy link
Contributor

github-actions bot commented Jun 13, 2025

size-limit report 📦

Path Size % Change Change
@sentry/browser 23.99 kB - -
@sentry/browser - with treeshaking flags 23.76 kB - -
@sentry/browser (incl. Tracing) 38.79 kB - -
@sentry/browser (incl. Tracing, Replay) 76.92 kB +0.04% +28 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70 kB +0.05% +30 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 81.68 kB +0.04% +28 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 93.75 kB +0.03% +28 B 🔺
@sentry/browser (incl. Feedback) 40.73 kB - -
@sentry/browser (incl. sendFeedback) 28.7 kB - -
@sentry/browser (incl. FeedbackAsync) 33.59 kB - -
@sentry/react 25.76 kB - -
@sentry/react (incl. Tracing) 40.78 kB - -
@sentry/vue 28.36 kB - -
@sentry/vue (incl. Tracing) 40.66 kB - -
@sentry/svelte 24.01 kB - -
CDN Bundle 25.48 kB - -
CDN Bundle (incl. Tracing) 38.96 kB - -
CDN Bundle (incl. Tracing, Replay) 74.81 kB +0.05% +33 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 80.23 kB +0.05% +34 B 🔺
CDN Bundle - uncompressed 74.48 kB - -
CDN Bundle (incl. Tracing) - uncompressed 115.3 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 229.35 kB +0.04% +84 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 242.18 kB +0.04% +84 B 🔺
@sentry/nextjs (client) 42.44 kB - -
@sentry/sveltekit (client) 39.28 kB - -
@sentry/node 150.76 kB +0.08% +109 B 🔺
@sentry/node - without tracing 98.52 kB +0.12% +113 B 🔺
@sentry/aws-serverless 124.27 kB +0.1% +112 B 🔺

View base workflow run

this._flushTimeout = setTimeout(() => {
this.flush();
}, 1);
if (!localParentId || this._sentSpans.has(localParentId)) {
Copy link
Member

Choose a reason for hiding this comment

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

should we also run cleanup on sent spans here (the _flushSentSpanCache method)?

The _sentSpans is checked for both flush and export, but only cleaned up in flush.

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm but export calls flush (via _debouncedFlush) so it should also cleanup, I think?

@mydea mydea force-pushed the fn/span-exporter-perf branch from 3ecf73e to 3ad8cc2 Compare June 16, 2025 07:47
Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

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

Like the debounce touch as we talked about it earlier. Not sure if I agree with the "code cleanup" part as much and am particularly concerned about the for-loop based building of sent spans rather than using flatMap() regarding perf.

Comment on lines 129 to 144
const finishedSpans: ReadableSpan[] = [];
for (const bucket of this._finishedSpanBuckets) {
if (bucket) {
for (const span of bucket.spans) {
finishedSpans.push(span);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this relly chaeper than what we had? As far as I'm aware .map()/.flatMap() is always faster than building an array on the fly due to resizing and the interpreter not knowing the type of the items ahead of time.

Also, what's the expected amount of items here as if it is a handful we might be bikeshedding.

Copy link
Member Author

Choose a reason for hiding this comment

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

buckets can be up to 1000 items large (which also does happen in reality). So not massive but also not tiny, basically :)

It is still always creating a new array (Array.from()), I think map/flatMap themselves are fine! 🤔

Copy link
Member

Choose a reason for hiding this comment

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

buckets can be up to 1000 items large (which also does happen in reality). So not massive but also not tiny, basically :)

Yeah, I think that size is big enough to care about these performance measures.

It is still always creating a new array (Array.from()), I think map/flatMap themselves are fine! 🤔

I'll leave this to you then. I'm speaking from experience but lack any hard evidence to back the claim up, especially anything recent. Up to you to take it, go with your own way, or put the time down to actually measure :)

@mydea
Copy link
Member Author

mydea commented Jun 16, 2025

I added some more code comments based on @BYK's comments, to make it (hopefully) easier to follow what the exporter does! 🙏

/**
* Try to flush any pending spans immediately.
* This is called internally by the exporter (via _debouncedFlush),
* but can also be triggered externally if we force-flush.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* but can also be triggered externally if we force-flush.
* but can also be triggered externally if we want a force-flush.

Copy link
Member

@BYK BYK left a comment

Choose a reason for hiding this comment

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

Looks great, still a go from me!

function invokeFunc(): unknown {
cancelTimers();
callbackReturnValue = func();
return callbackReturnValue;

Choose a reason for hiding this comment

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

Just giving my two cents, it's not a massive deal.
I feel like we could be saving a line by assigning the value at the same time as we return it: 🙂

Suggested change
return callbackReturnValue;
return callbackReturnValue = func();

I personally find this more readable

Copy link
Member

Choose a reason for hiding this comment

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

usually minification algorithms (terser) will do this for us anyway, I prefer the new line because it usually make git blame a bitter easier to use.

Choose a reason for hiding this comment

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

Thanks for the explanation! that makes sense!
I haven't thought about this.

I guess it would be easier to debug too now that I think about it

function invokeFunc(): unknown {
cancelTimers();
callbackReturnValue = func();
return callbackReturnValue;
Copy link
Member

Choose a reason for hiding this comment

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

usually minification algorithms (terser) will do this for us anyway, I prefer the new line because it usually make git blame a bitter easier to use.

@mydea mydea force-pushed the fn/span-exporter-perf branch from 0fbb5ab to c6e9af2 Compare June 17, 2025 07:33
@mydea
Copy link
Member Author

mydea commented Jun 17, 2025

It was actually a great pointer from @BYK that we should really actually profile/measure if the for loop is faster than the flatMap, instead of just assuming so. I ran a simple benchmark here: https://jsbench.me/84mc07ydz3/1

I re-ran it a few times, the difference is not huge and it also changed from time to time which was faster. So overall likely not worth the change!

@mydea mydea merged commit cb5265c into develop Jun 17, 2025
163 checks passed
@mydea mydea deleted the fn/span-exporter-perf branch June 17, 2025 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants