Skip to content

Commit cffe63f

Browse files
authored
fix(profiling): align frame limit with backend location cap (#19076)
## 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>
1 parent 2c95256 commit cffe63f

4 files changed

Lines changed: 44 additions & 5 deletions

File tree

ddtrace/internal/datadog/profiling/dd_wrapper/include/constants.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
// is for ddtrace/settings/profiling.py:ProfilingConfig.max_frames, but should conform
88
constexpr unsigned int g_default_max_nframes = 64;
99

10-
// Maximum number of frames admissible in the Profiling backend. If a user exceeds this number, then
11-
// their stacks may be silently truncated, which is unfortunate.
12-
constexpr unsigned int g_backend_max_nframes = 512;
10+
// Maximum number of locations admissible in the Profiling backend. Reserve one location for the
11+
// synthetic "<N frames omitted>" frame emitted when a stack is truncated.
12+
// Keep in sync with BACKEND_MAX_LOCATIONS and MAX_FRAMES in ddtrace/internal/settings/profiling.py.
13+
constexpr unsigned int g_backend_max_nlocations = 600;
14+
constexpr unsigned int g_backend_max_nframes = g_backend_max_nlocations - 1;
1315

1416
// Default value for the max number of samples to keep in the StaticSamplePool
1517
constexpr size_t g_default_sample_pool_capacity = 4;

ddtrace/internal/settings/profiling.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ def _enrich_tags(tags: dict[str, str]) -> dict[str, str]:
118118
return tags
119119

120120

121+
# The profiling backend accepts at most this many locations per sample.
122+
BACKEND_MAX_LOCATIONS = 600
123+
# Reserve one location for the synthetic "<N frames omitted>" frame emitted when
124+
# a stack is truncated. Keep these constants in sync with g_backend_max_nlocations
125+
# and g_backend_max_nframes in constants.hpp.
126+
MAX_FRAMES = BACKEND_MAX_LOCATIONS - 1
127+
128+
129+
def _clamp_max_frames(value: str) -> int:
130+
return min(int(value), MAX_FRAMES)
131+
132+
121133
class ProfilingConfig(DDConfig):
122134
__prefix__ = "dd.profiling"
123135

@@ -198,9 +210,12 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
198210
int,
199211
"max_frames",
200212
default=64,
201-
validator=validators.range(0, t.cast(int, float("inf"))),
213+
parser=_clamp_max_frames,
214+
validator=validators.range(0, MAX_FRAMES),
202215
help_type="Integer",
203-
help="The maximum number of frames to capture in stack execution tracing",
216+
help="The maximum number of frames to capture in stack execution tracing. Values above "
217+
f"{MAX_FRAMES} are clamped so that truncated stacks, including the omitted-frame indicator, stay within "
218+
f"the profiling backend's {BACKEND_MAX_LOCATIONS}-location limit.",
204219
)
205220

206221
ignore_profiler = DDConfig.v(
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
profiling: Fixes inconsistent handling of ``DD_PROFILING_MAX_FRAMES`` by clamping it before stack collection so samples stay within the backend's 600-location limit.

tests/profiling/test_profiling_config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77
from ddtrace.internal.settings.profiling import ProfilingConfig
88

99

10+
class TestMaxFramesConfig:
11+
def test_default(self) -> None:
12+
assert ProfilingConfig().max_frames == 64
13+
14+
@pytest.mark.parametrize("value", (600, 601, 10_000))
15+
def test_clamps_to_backend_limit(self, monkeypatch: pytest.MonkeyPatch, value: int) -> None:
16+
monkeypatch.setenv("DD_PROFILING_MAX_FRAMES", str(value))
17+
18+
# One of the backend's 600 locations is reserved for the omitted-frame indicator.
19+
assert ProfilingConfig().max_frames == 599
20+
21+
@pytest.mark.parametrize("value", (512, 599))
22+
def test_preserves_value_within_backend_limit(self, monkeypatch: pytest.MonkeyPatch, value: int) -> None:
23+
monkeypatch.setenv("DD_PROFILING_MAX_FRAMES", str(value))
24+
25+
assert ProfilingConfig().max_frames == value
26+
27+
1028
class TestAdaptiveSamplingConfig:
1129
def test_defaults(self) -> None:
1230
config = ProfilingConfig()

0 commit comments

Comments
 (0)