Skip to content

perf(profiling): bound task stack collection#19085

Draft
taegyunkim wants to merge 2 commits into
taegyunkim/prof-14213-propagate-max-framesfrom
taegyunkim/prof-14213-task-stack-frame-budget
Draft

perf(profiling): bound task stack collection#19085
taegyunkim wants to merge 2 commits into
taegyunkim/prof-14213-propagate-max-framesfrom
taegyunkim/prof-14213-task-stack-frame-budget

Conversation

@taegyunkim

@taegyunkim taegyunkim commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

Bounds asyncio stack collection and applies an explicit task-priority frame budget after logical stack stitching.

  • materializes at most DD_PROFILING_MAX_FRAMES physical thread frames
  • materializes at most DD_PROFILING_MAX_FRAMES coroutine frames per task and stops assembling a logical task chain once its frame budget is full
  • gives logical task frames first claim on the final reporting budget
  • fills remaining capacity with synchronous leaf frames, followed by lower-priority event-loop roots
  • emits an inline synchronous-frame omission marker when physical frames are removed between retained logical segments
  • counts injected native locations while selecting frames, preventing physical context from displacing task context in the exporter
  • conservatively reports task context alone when an on-CPU asyncio boundary lies beyond the bounded physical stack
  • scales the Echion frame cache to 4 * max_frames, bounded between 256 and 1,024 entries

This is stacked only on #19073, which propagates the configured limit into Echion and applies it to plain thread stacks. Merged #19076 supplies the 600-location backend and configuration limit on main.

JIRA: PROF-14213

Testing

  • Added deterministic low-limit regressions covering:
    • task-only output when task frames fill the budget
    • preservation of task context when a deep synchronous stack hides the asyncio boundary
    • use of remaining capacity for synchronous context and an inline omission marker
  • Verified both new task-priority regressions fail on the parent implementation and pass with this change.
  • Recreated the Python 3.14 base environment to force a clean native extension build.
  • Passed 11 targeted tests in normal and fast-copy configurations, 22 test executions total:
    • three low-limit task frame-budget regressions
    • native frame-limit and cache scaling
    • plain stack truncation
    • asyncio basic and task labels
    • deep gather stitching
    • recursive on-CPU coroutine and task stitching
    • on-CPU and off-CPU asyncio wall-time stacks
  • After fixing clang-tidy findings, reran the three low-limit regressions in normal and fast-copy configurations, 6 test executions passed.
  • Reworked the deep synchronous-stack regression to use an explicitly created task, avoiding uvloop's wrapper-only top-level task, and verified all three regressions with uvloop on Python 3.9 and 3.14.
  • Confirmed the revised deep-stack regression fails on the parent implementation and passes with this change.
  • Ran the complete profiling clang-tidy analysis with clang-tidy 18, including fuzz harnesses.
  • Ran scripts/lint checks, C formatting, spelling, and git diff --check. Broad checks reported only existing repository warnings.
  • scripts/run-benchmarks --list found no benchmark suite matching the changed Echion files.

Risks

Low DD_PROFILING_MAX_FRAMES values now intentionally prefer logical task ancestry over physical synchronous frames. This changes which portion of a mixed stack is reported, but preserves a bounded and logically meaningful task trace instead of allowing a deep physical stack to hide task context.

If the asyncio boundary is outside the bounded physical candidates, the sampler does not guess at overlap or concatenate potentially duplicated segments. It reports the bounded logical task stack alone.

The smaller frame cache can increase cache churn in workloads with many distinct code locations. It retains a 256-entry floor and scales with configured stack depth to balance reuse and memory.

Additional Notes

Task selection and render ordering are intentionally separate. Task frames receive budgeting priority, while retained segments are still rendered in logical leaf-to-root order.

The uppercase MAX_STACK_DISCOVERY_DEPTH constant remains a hard internal safety bound. The configured reporting and materialization budget is stored separately in stack_max_frames.

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codeowners resolved as

ddtrace/internal/datadog/profiling/stack/fuzz/fuzz_echion_stacks.cpp    @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/fuzz/fuzz_echion_task_unwind.cpp  @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/echion/threads.cc          @DataDog/profiling-python
tests/profiling/collector/test_asyncio_task_frame_budget.py             @DataDog/profiling-python

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jul 15, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 61 circular imports that already exist on the base branch and have not been changed by this PR.

Show existing cycles (showing 5 of 61 shortest)
ddtrace.internal.datastreams -> ddtrace.internal.datastreams.aiokafka -> ddtrace.internal.datastreams
ddtrace.internal.datastreams -> ddtrace.internal.datastreams.kafka -> ddtrace.internal.datastreams
ddtrace.internal.datastreams -> ddtrace.internal.datastreams.google_cloud_pubsub -> ddtrace.internal.datastreams
ddtrace.internal.datastreams -> ddtrace.internal.datastreams.botocore -> ddtrace.internal.datastreams
ddtrace.internal.datastreams -> ddtrace.internal.datastreams.kombu -> ddtrace.internal.datastreams

To see all cycles, download the cycles-base.json and cycles-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/cycles.py compare cycles-base.json cycles-pr.json

@datadog-datadog-us1-prod

This comment has been minimized.

@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-propagate-max-frames branch from 32b532a to ecdbdab Compare July 15, 2026 20:02
@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-task-stack-frame-budget branch 2 times, most recently from 3b50116 to e3d2e8a Compare July 16, 2026 01:00
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 16, 2026
## Description

Aligns profiling frame-limit configuration with the backend's [600-location limit](https://datadoghq.atlassian.net/wiki/spaces/PROF/pages/5307007274/Profiling+Libraries+Frame+Truncation+Behaviors).

- raises the native exporter cap from 512 to 599 captured frames
- reserves the 600th backend location for the synthetic `<N frames omitted>` indicator on truncated stacks
- clamps `DD_PROFILING_MAX_FRAMES` to 599 in `ddtrace.internal.settings.profiling`
- preserves configured values from 513 through 599 instead of reducing them to 512 in the native exporter
- ensures collectors receive the same effective configured limit

JIRA: [PROF-14213](https://datadoghq.atlassian.net/browse/PROF-14213?atlOrigin=eyJpIjoiNWFmMTNkMDg2MmJjNDVlZTlkN2JlNGZkMjg3ZWZiYzEiLCJwIjoiaiJ9)

## Testing

- Added configuration tests covering the default, values below the maximum captured-frame limit, the exact 599-frame limit, and values at or above the 600-location backend limit.
- Ran `TestMaxFramesConfig` on Python 3.9, all 6 tests passed. The editable install also rebuilt the native extension successfully.
- Completed a clean Python 3.14 native build for the initial change.
- Ran `scripts/lint checks`, targeted formatting, spelling, and `git diff --check`. Broad lint checks reported only existing repository warnings.

## Risks

Configurations between 513 and 599 can now retain more frames than before. This matches the backend contract but can slightly increase per-sample work for users who explicitly configure values in that range.

Values of 600 and above are now clamped to 599 at configuration load instead of reaching individual collectors unchanged. This leaves one location for the omitted-frame indicator so truncated samples remain within the backend's 600-location cap.

## Additional Notes

This PR is the base of stacked #19073 and #19085, which propagate the limit into Echion and bound task-aware stack collection.

[PROF-14213]: https://datadoghq.atlassian.net/browse/PROF-14213?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiaiJ9


Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-propagate-max-frames branch from 67ae55e to c0a7c8f Compare July 16, 2026 21:17
@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-task-stack-frame-budget branch from b2e94b5 to 29fa643 Compare July 16, 2026 21:17
@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-propagate-max-frames branch from c0a7c8f to dd6bece Compare July 16, 2026 21:45
@taegyunkim taegyunkim force-pushed the taegyunkim/prof-14213-task-stack-frame-budget branch from 29fa643 to 679beb6 Compare July 16, 2026 21:45
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