Skip to content

perf: copy-on-write for sentry_value_t clone#1794

Open
jpnurmi wants to merge 8 commits into
masterfrom
jpnurmi/ref/copy-on-write
Open

perf: copy-on-write for sentry_value_t clone#1794
jpnurmi wants to merge 8 commits into
masterfrom
jpnurmi/ref/copy-on-write

Conversation

@jpnurmi

@jpnurmi jpnurmi commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

Both out-of-process handlers, crashpad and native, flush the global scope to __sentry-event whenever any scope properties change. This involves creating an event object and merging the current scope into it. The purpose of this is to make the event available for the separate handler/daemon process.

In sentry__scope_apply_to_event, the process of merging the scope into an event object is heavy. Passing references (event { tags: ref(scope->tags) }) is out of question, because that way any before_send modifications would flow back to the scope and cause data loss. Therefore, full clones (event { tags: clone(scope->tags) }) are passed instead. The problem is that the more data the scope has (tags, context, ...), the slower it gets to clone all that. Furthermore, in many cases, the clones are never modified, such as when flushing the scope for crashpad or native, or any native SDK user with a non-modifying or no before_send hook.

Solution

For container types, sentry_value_t already stores the actual data separately in list_t and obj_t. Instead of deep-copying the list items or object key-value pairs on clone, make list_t and obj_t refcounted and share them between copies. Only if any clone modifies the data while refcount > 1, then the modified clone is detached from the shared data, and a deep copy is performed.

Results

pytest -v "tests/benchmark.py::test_benchmark_scope[crashpad-set_tag]"
Median before Median after Improvement
Windows 3.617 ms 1.256 ms 65.3%
macOS 0.966 ms 0.157 ms 83.7%
Linux N/A N/A N/A
pytest -v "tests/benchmark.py::test_benchmark_scope[native-set_tag]"
Median before Median after Improvement
Windows 2.376 ms 0.529 ms 77.7%
macOS 0.907 ms 0.096 ms 89.4%
Linux 0.703 ms 0.040 ms 94.3%

Close: #1551

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ec86d1f. Configure here.

Comment thread src/sentry_value.c
Comment thread src/sentry_value.c Outdated
@jpnurmi jpnurmi changed the title ref: copy-on-write for sentry_value_t clone wip(ref): copy-on-write for sentry_value_t clone Jun 5, 2026
@jpnurmi jpnurmi force-pushed the jpnurmi/ref/copy-on-write branch from ec86d1f to 3e7dd7b Compare July 6, 2026 09:53
@jpnurmi jpnurmi changed the title wip(ref): copy-on-write for sentry_value_t clone WIP: perf: copy-on-write for sentry_value_t clone Jul 6, 2026
Comment thread src/sentry_value.c
@jpnurmi jpnurmi force-pushed the jpnurmi/ref/copy-on-write branch from 3e7dd7b to bffeb8e Compare July 7, 2026 14:27
@jpnurmi jpnurmi changed the title WIP: perf: copy-on-write for sentry_value_t clone perf: copy-on-write for sentry_value_t clone Jul 7, 2026
jpnurmi and others added 5 commits July 8, 2026 16:03
Add copy-on-write (COW) semantics to sentry__value_clone() for list and
object values. Instead of deep-copying items/pairs on clone, the new
thing_t shares the underlying list_t/obj_t data via a data-level
refcount. The actual copy is deferred to mutation time (thing_detach),
making clone O(1) for flat containers.

Two-level refcounting:
- thing_t.refcount: how many sentry_value_t handles reference the thing
- list_t.refcount / obj_t.refcount: how many thing_ts share the data

Container children (nested lists/objects) are still recursively cloned
to maintain value semantics at each nesting level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In `thing_clone_children`, if `sentry__value_clone` fails for a nested
container, the error was ignored. This resulted in clones that appeared
usable but were missing nested data. Add a check to return false on
clone failure, ensuring the entire operation fails rather than producing
an incomplete result.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep object copy-on-write detach atomic when cloning keys fails. Track
initialized pairs so partial clones can be freed without installing null keys.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
@jpnurmi jpnurmi force-pushed the jpnurmi/ref/copy-on-write branch from bffeb8e to 837e73d Compare July 8, 2026 14:03
@jpnurmi jpnurmi requested review from JoshuaMoelans and mujacica July 8, 2026 17:09
@jpnurmi jpnurmi requested a review from limbonaut July 9, 2026 10:12
@limbonaut

Copy link
Copy Markdown
Collaborator

Linux numbers 0.040 ms 😎
Nice, I'll take a look tomorrow!

@limbonaut limbonaut left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Superb 🤘This should be quite a noticeable performance improvement, I recon.
The deep clone trade-off could be minimized by using shared keys, which is worth consideration, imo.

Comment thread src/sentry_value.c
return NULL;
}
for (size_t i = 0; i < obj->len; i++) {
clone->pairs[i].k = sentry__string_clone(obj->pairs[i].k);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Potentially: Since keys are immutable, we could store them with a refcount and, instead of a copy here, increase the key's refcount. That would reduce allocations on mutate, especially in deep/wide objects where a single set_by_key could trigger N string copies. Just an idea to consider.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the proposal. 👍 Sounds like worth experimenting! Replacing plain C string keys with thing_t strings could achieve this at the cost of one additional allocation per key. 🤔

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.

before_send event modifications leak to the scope

3 participants