Skip to content

Libdatadog String interning#485

Open
r1viollet wants to merge 16 commits intomainfrom
r1viollet/string-interning
Open

Libdatadog String interning#485
r1viollet wants to merge 16 commits intomainfrom
r1viollet/string-interning

Conversation

@r1viollet
Copy link
Copy Markdown
Collaborator

@r1viollet r1viollet commented Jan 29, 2026

What does this PR do?

Switches the pprof pipeline to libdatadog's ProfilesDictionary so strings, functions, and mappings are interned once instead of copied per sample.

  • Symbol stores a FunctionId2 handle; MapInfoTable is now vector<MappingId2> — no owned strings in either.
  • All symbol/mapping lookups intern into the dictionary on first insert and return opaque handles. Subsequent lookups are cache hits at the ddprof level.
  • Labels are interned once (key IDs at profile creation, values as CharSlice at sample time).
  • Aggregation goes through Profile_add2 on a Profile_with_dictionary-backed profile — no re-interning at sample time.
  • ProfilesDictionary held by SymbolHdr (declared first, destroyed last, outlives all handles).
  • Mapping cache (MapInfoLookup) detects remaps at the same start address by reading identity back from the dictionary.

Motivation

Copying stack symbol strings on every sample was the wrong design. Interned IDs let pprof aggregation be pointer-sized work.

How to test the change?

  • ddprof_pprof-ut includes a new aggregate → reset → aggregate cycle exercising ID stability across pprof_reset.
  • mapinfo_lookup-ut covers cache hits, remap detection, build-id mismatch, pid isolation, and erase behaviour.
  • New pprof_aggregate-bench micro-benchmark: on aarch64 Release, mean per-sample aggregation drops 877 ns → 333 ns (~2.6×).
  • Existing ddprof_exporter-ut already destroys SymbolHdr before serialization — covers dictionary-outliving-samples under ASan.

@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented Feb 3, 2026

Benchmark results for collatz

Parameters

Baseline Candidate
config baseline candidate
profiler-version ddprof 0.23.0+77955e2e.103721732 ddprof 0.23.0+0e677aed.103753196

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics.

See unchanged results
scenario Δ mean execution_time
scenario:ddprof -S bench-collatz --preset cpu_only collatz_runner.sh same

@pr-commenter
Copy link
Copy Markdown

pr-commenter Bot commented Feb 3, 2026

Benchmark results for BadBoggleSolver_run

Parameters

Baseline Candidate
config baseline candidate
profiler-version ddprof 0.23.0+77955e2e.103721732 ddprof 0.23.0+0e677aed.103753196

Summary

Found 0 performance improvements and 0 performance regressions! Performance is the same for 1 metrics, 0 unstable metrics.

See unchanged results
scenario Δ mean execution_time
scenario:ddprof -S bench-bad-boggle-solver BadBoggleSolver_run work 1000 same

@r1viollet r1viollet changed the title R1viollet/string interning Libdatadog String interning Feb 3, 2026
@r1viollet
Copy link
Copy Markdown
Collaborator Author

Deploying to relenv to see what this yields

@r1viollet r1viollet force-pushed the r1viollet/string-interning branch from 341431b to 0e677ae Compare March 20, 2026 15:33
r1viollet and others added 3 commits April 24, 2026 16:31
Mirrors TEST(DDProfPProf, aggregate) but drives the aggregation in a tight
loop via google benchmark so we can measure ns/sample on the interned path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exercises pprof_reset between two pprof_aggregate_interned_sample calls to
confirm that the interned StringId2/FunctionId2/MappingId2 handles stored in
the SymbolHdr-owned ProfilesDictionary remain valid across a profile reset
and get serialized correctly on the second cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@r1viollet r1viollet marked this pull request as ready for review April 24, 2026 15:40
@r1viollet r1viollet requested a review from nsavoire as a code owner April 24, 2026 15:40
The libdatadog ProfilesDictionaryHandle is already a pointer type
(ProfilesDictionary*), and the C API takes it by pointer-to-handle so it
can out-write / zero the slot. The previous code heap-allocated that slot
and wrapped it in a unique_ptr<Handle, Deleter> — effectively a smart
pointer to a heap-allocated ProfilesDictionary** with no benefit.

Introduce a small move-only RAII class that holds the handle inline. No
behavior change; callers that need the pointer-to-handle the C API wants
still get it via .get().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@r1viollet r1viollet force-pushed the r1viollet/string-interning branch from 55f0990 to a6eb45d Compare April 24, 2026 15:51
@nsavoire
Copy link
Copy Markdown
Collaborator

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6eb45d0f1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ddog_profiling_utils.cc Outdated
Comment on lines +37 to +38
ddog_prof_Status status = ddog_prof_ProfilesDictionary_insert_str(
&string_id, dict, to_CharSlice(str), DDOG_PROF_UTF8_OPTION_ASSUME);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate strings before interning

When a profiled process has a mapped filename, debug source path, or symbol text containing non-UTF-8 bytes, this helper now passes that untrusted data to libdatadog with DDOG_PROF_UTF8_OPTION_ASSUME; libdatadog's ASSUME mode is documented/implemented as undefined behavior for invalid UTF-8 (it uses unchecked conversion), whereas the old Profile_add path validated strings. Use validate or lossy conversion here so arbitrary Linux path bytes cannot make the profiler invoke UB.

Useful? React with 👍 / 👎.

Comment thread include/mapinfo_table.hpp Outdated
Offset_t _offset{0};
std::string _sopath;
BuildIdStr _build_id;
ddog_prof_Mapping2 *_mapping_id{nullptr};
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.

Suggested change
ddog_prof_Mapping2 *_mapping_id{nullptr};
ddog_prof_MappingId2 _mapping_id{nullptr};

Comment thread include/mapinfo_table.hpp Outdated
Comment thread include/symbol_hdr.hpp
explicit operator bool() const { return _handle != nullptr; }

// The C API expects a pointer-to-handle (so it can mutate the slot).
[[nodiscard]] const ddog_prof_ProfilesDictionaryHandle *get() const {
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.

Not sure if this is necessary, the only used C API that requires a pointer-to-handle is ddog_prof_Profile_with_dictionary and it should not mutate the slot.

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.

I think this comment was about the get, I did not really adjust this, tell me if you feel strongly. I might have missed why this was important.

Comment thread src/ddog_profiling_utils.cc Outdated
Symbol make_symbol(std::string symname, const std::string &demangled_name,
uint32_t lineno, const std::string &srcpath,
const ddog_prof_ProfilesDictionary *dict) {
[[maybe_unused]] const std::string ignored_symname = std::move(symname);
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.

symname is never used

@r1viollet r1viollet marked this pull request as draft April 29, 2026 12:44
Move the storage to the libdatadog objects
@r1viollet
Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@r1viollet
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb1e5f2e65

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ddprof_worker.cc
Comment on lines +713 to +715
DDRES_CHECK_FWD(pprof_create_profile(
ctx.worker_ctx.pprof[0], ctx,
ctx.worker_ctx.us->symbol_hdr._profiles_dictionary.get()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sharing the intern dictionary across export buffers

When asynchronous export is used, both pprof buffers are created with this same SymbolHdr dictionary (and pprof[1] gets the same pointer just below). ddprof_worker_cycle then swaps buffers and starts ddprof_worker_export_thread to serialize the old profile while new samples continue aggregating into the other profile; those aggregations call pprof_aggregate_interned_sample and intern strings/functions/mappings into this shared dictionary. That defeats the existing double-buffering and leaves the exporter reading the dictionary concurrently with writes, which can corrupt serialized profiles or race in libdatadog. Use a per-buffer/export-snapshot dictionary or guard all serialization and interning with the same lock.

Useful? React with 👍 / 👎.

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.

The key libdatadog header says ProfilesDictionary contains data common to multiple profiles, “whether that’s multiple profiles simultaneously or multiple profiles through time”, and that the current implementation is thread-safe. That matches our double-buffer setup: two Profiles sharing one long-lived SymbolHdr dictionary.

I think we are fine, though codex does not have access to the libdatadog APIs.

@r1viollet r1viollet marked this pull request as ready for review April 29, 2026 13:27
@r1viollet
Copy link
Copy Markdown
Collaborator Author

To answer a question, mappings are never cleared afaik, so we still partially rely on the hard worker reset.

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.

2 participants