Skip to content

feat(profiling): add continuous profiler callgraph exploration - #876

Draft
AlexJF wants to merge 2 commits into
DataDog:mainfrom
AlexJF:feat/callgraph-exploration
Draft

AlexJF wants to merge 2 commits into
DataDog:mainfrom
AlexJF:feat/callgraph-exploration

Conversation

@AlexJF

@AlexJF AlexJF commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds pup profiling explore callgraph, which calls the Continuous Profiler call graph exploration endpoint (POST /api/unstable/profiling/pup/explore/callgraph) and returns ranked call graph nodes (self/total values and percentages, top outgoing edges) for a query, trace/span, or single profile.

  • src/commands/profiling.rs: explore_callgraph, structured like explore_flamegraph. It requires one of --query, --trace-id or --profile-id, requires --profile-id/--event-id together, and builds the JSON body.
  • src/main.rs: ProfilingExploreActions::Callgraph (--trace-id/--span-id/--time-hint, --percent-cutoff, --limit-top-nodes, --max-node-details, --frame-filter glob) plus dispatch.
  • Tests: happy path, scope and pairing validation, null optional fields, 400 validation error, no auth.
  • Docs: call graph examples in docs/EXAMPLES.md and the command listing in docs/COMMANDS.md.

Motivation

Follow-up to #819 (pup profiling operations). Call graph exploration was deferred from that MVP. The backend (prof-viz-java) and gateway (prof-gateway) sides are now merged and deployed.

Additional Notes

  • Auth uses the existing continuous_profiler_read scope (same as flamegraph), so there are no new scopes or dependencies.
  • --frame-filter is an anchored glob matched against frame fields server-side, e.g. *MyService*.
  • --trace-id scoping requires --span-id and --time-hint (any pup time format, sent as epoch seconds): the backend rejects a trace context without a timeHint. The shared trace_context_json helper is reused by timeline exploration (feat(profiling): add continuous profiler timeline exploration #877).

Checklist

  • The code change follows the project conventions (see CONTRIBUTING.md)
  • Tests have been added/updated (if applicable)
  • Documentation has been updated (if applicable)
  • All CI checks pass
  • Code coverage is maintained or improved

Related Issues

Follow-up to #819.

Plan
# PROF-16038: Add callgraph exploration support to pup (pup leg)

## Context

Third and final repo for PROF-16038 (follow-up to PROF-15503's pup-CLI-first API). Repo 1
(profiling-backend) is done — draft PR [#9070](https://github.com/ddoghq/profiling-backend/pull/9070)
adds `POST /api/pup/explore/callgraph` to `prof-viz-java`'s `PupController`. Repo 2 (dd-source) is
done — draft PR [#102914](https://github.com/ddoghq/dd-source/pull/102914) proxies it through
`prof-gateway` at `/api/unstable/profiling/pup/explore/callgraph`. This leg adds the pup CLI
subcommand that calls it.

**Important correction vs. the original plan**: PROF-15503's pup-side work (PR #819, adding the
`pup profiling` command group) is already merged into `DataDog/pup:main` — the plan's assumption
that it was "local only, not pushed" was outdated. The `feat/callgraph-exploration` branch is based
on `upstream/main` (which has it), not the stale fork `main`.

**Repo-specific conventions** (per this repo's own `CLAUDE.md`/`docs/REVIEW.md`/`docs/CONTRIBUTING.md`,
not the user's personal global defaults): branch `feat/callgraph-exploration` (no Jira prefix),
commit `feat(profiling): add continuous profiler callgraph exploration`, PR opened from the user's
fork (`origin` = `AlexJF/pup`) into `DataDog/pup:main` using the repo's actual
`.github/pull_request_template.md` (`## What does this PR do?` / `## Motivation` /
`## Additional Notes` / `## Checklist` / `## Related Issues` — matches PR #819's real body, which
differs from the newer generic template shown inline in `CLAUDE.md`).

## Schema reference (from profiling-backend's `CallGraphExplorationRequest.java`)

`filter` (query/from/to), `traceContext` (traceId/spanId), `profileContext` (profileId/eventId),
`profileType`, `percentCutoff`, `limitTopNodes`, `maxNodeDetails`, `frameFilter` (plain string, not
regex — unlike flamegraph's three regex-filter fields), `archiveContext` (server-defaulted, omitted
from the request body — same precedent as `explore_flamegraph`). No `attribute`, `frameFormat`,
`frameGrouping`, or `bypassKindTruncation` (those are flamegraph-only).

## Files changed

- `src/commands/profiling.rs`: added `pub async fn explore_callgraph(...)`, mirroring
  `explore_flamegraph`'s structure (same "one of query/trace-id/profile-id" and "profile-id+event-id
  together" validation, same JSON body construction pattern, POST to `{BASE}/explore/callgraph`,
  output via `formatter::output`). Added 6 tests mirroring the flamegraph test set (ok,
  requires-scope, requires-profile-id-and-event-id-together, tolerates-null-optional-fields,
  validation-error, no-auth).
- `src/main.rs`: added `ProfilingExploreActions::Callgraph { ... }` clap variant (mirroring
  `Flamegraph`'s args minus the flamegraph-only ones, plus `--limit-top-nodes`/`--max-node-details`/
  `--frame-filter`), the matching dispatch arm, and a CAPABILITIES/EXAMPLES line in the `Profiling`
  command's doc comment.
- `docs/EXAMPLES.md`: added an "Explore a Call Graph" section next to the flamegraph one.
- `docs/COMMANDS.md`: updated the `profiling` row/bullet to mention `callgraph`.

## Status

- [x] Repo cloned (`AlexJF/pup` as `origin`, `DataDog/pup` as `upstream`).
- [x] Worktree + branch created from `upstream/main` (`.claude/worktrees/feat-callgraph-exploration`,
      branch `feat/callgraph-exploration`).
- [x] Read `explore_flamegraph`, its tests, `CallGraphExplorationRequest.java`/
      `FlameGraphExplorationRequest.java`, `docs/EXAMPLES.md`, `docs/COMMANDS.md`, and PR #819's
      actual body/template.
- [x] `explore_callgraph` implemented in `src/commands/profiling.rs`.
- [x] CLI wiring (`ProfilingExploreActions::Callgraph` + dispatch arm) added in `src/main.rs`.
- [x] Doc comment (CAPABILITIES/EXAMPLES) updated.
- [x] `docs/EXAMPLES.md` and `docs/COMMANDS.md` updated.
- [x] Tests added (6 new, mirroring flamegraph's set).
- [x] `cargo fmt` — clean.
- [x] `cargo build` — success.
- [x] `cargo clippy --all-targets -- -D warnings` — clean.
- [x] `cargo test profiling::` — 33/33 pass (including all 6 new callgraph tests).
- [x] `cargo test` (full suite) — 2034 passed, 6 pre-existing failures unrelated to this change
      (DNS lookup to a placeholder host, OS keychain/secret-service unavailable in this sandbox, a
      GitHub release-asset naming check) — none touch `profiling.rs` or files this PR changes.
- [x] `cargo audit` — could not fetch the RustSec advisory DB (sandbox blocks outbound SSH to
      GitHub for this tool); not a code issue. This change adds zero new dependencies — it only
      uses crates already imported by `explore_flamegraph` (`anyhow`, `serde_json`, and existing
      internal modules), so there is nothing new for an audit to flag.
- [x] Commit 8bbc267 (signed with personal SSH key — set as repo-local `user.signingkey`, registered on AlexJF) + push to `origin`.
- [x] Draft PR DataDog/pup#876 (from AlexJF:feat/callgraph-exploration).
- [ ] Include this PLAN.md and PROMPTS.md as collapsible sections in the PR description.

## Notes

- This file and PROMPTS.md are intentionally excluded from the commit (per user's global rule).
- No org-gating/feature-flag concept exists in pup either (consistent with the other two repos) —
  auth is just the existing `pupRouteAuthn`/`pupReadPermission` (Continuous Profiler read scope),
  already covered by the existing `continuous_profiler_read` OAuth2 scope added in PR #819.

## Update 2026-10-01 (resumed under PROF-16039)

- Fixed `--frame-filter` help text and EXAMPLES.md: prof-viz-java treats `frameFilter` as an
  anchored glob (`CallGraphExplorationResponseFactory.globToPattern`), not a substring, so the
  example now uses `*MyService*`. Staged with the rest.
- **Blocked on commit signing**: this repo signs with the DataDog-org SSH key
  (`~/.config/datadog/git/config`, `…AAAAIC5Z1…`), which is not loaded in the forwarded SSH
  agent (only the ddoghq signing key is). `workspaces:create-and-push-commit` skill is not
  installed. Not bypassing signing — waiting for the key to be available in the agent.
- Next once unblocked: commit, rebase onto `upstream/main` (67aea61, 35 commits ahead; only
  `src/main.rs` overlaps), re-run fmt/clippy/tests, push to `origin`, open draft PR. Then the
  PROF-16039 timeline branch `feat/timeline-exploration` stacked on this one.
- Follow-up commit fc8b744 `fix(profiling): require --time-hint for callgraph trace scoping`: the
  backend `TraceContext.timeHint` is `@NotEmpty`, so pup's `timeHint: null` made `--trace-id`
  always 400. Added shared `trace_context_json` helper (validates trace-id/span-id/time-hint
  together, converts `--time-hint` to epoch seconds) + `--time-hint` flag + tests (39 profiling
  tests pass). Same pre-existing bug in merged `explore flamegraph`/`profile-types list` left
  for a separate fix.
- Timeline exploration (PROF-16039) stacked on this branch: DataDog/pup#877.
Prompts
# Prompts log — PROF-16038 (pup leg)

1. "Lets work on https://datadoghq.atlassian.net/browse/PROF-16038"
2. Scope-check answer: "Same as previous work but adapting to changes done on iterations to the
   merged PRs (e.g. org gating removed)"
3. Plan-approval correction: "For the pup side, adhere to the pup repo settings, not my own.
   Also, I don't have write access to the main pup repo so we'll need to open a draft PR
   through my fork: https://github.com/AlexJF/pup"
4. After profiling-backend leg completed (draft PR #9070 opened): "go" — proceed to the
   dd-source and pup repos.
5. (Continuation after dd-source leg completed, PR #102914 opened) — resumed on the pup leg:
   researched `explore_flamegraph`'s implementation/tests, the callgraph/flamegraph request DTOs
   in profiling-backend, `docs/EXAMPLES.md`/`docs/COMMANDS.md`, and PR #819's actual template;
   implemented `explore_callgraph` in `src/commands/profiling.rs` and the matching CLI wiring in
   `src/main.rs`, plus docs and tests; ran `cargo fmt`/`build`/`clippy`/`test`/`audit`.
6. (Resumed under PROF-16039) "Can't we sign with my personal SSH key, open a PR against my fork
   AlexJF/pup and open a draft PR from there to upstream?"

🤖 Generated with Claude Code

AlexJF and others added 2 commits October 1, 2026 12:06
Add `pup profiling explore callgraph`, calling the new prof-gateway
/api/unstable/profiling/pup/explore/callgraph endpoint (PROF-16038).

- explore_callgraph in src/commands/profiling.rs, mirroring explore_flamegraph
  (query/trace/profile scoping, profile-id+event-id pairing validation)
- ProfilingExploreActions::Callgraph clap variant and dispatch in src/main.rs
- Tests for happy path, scoping/pairing validation, null optional fields,
  validation errors, and missing auth
- Call graph examples in docs/EXAMPLES.md and command listing in docs/COMMANDS.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend's traceContext requires traceId, spanId and timeHint (epoch
seconds) together and rejects requests with a null timeHint, so
`--trace-id` scoping could never succeed.

- Add a shared trace_context_json helper that validates the three flags
  together and converts --time-hint (any pup time format) to epoch seconds
- Add --time-hint to `pup profiling explore callgraph`
- Tests for the helper and for the callgraph traceContext request body

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant