Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class ProfilerStats
// Whether fast_copy_memory (safe_memcpy) is enabled; unset until the sampler starts
std::optional<bool> fast_copy_memory_enabled;

// User opted out of fast copy (env var or set_fast_copy(false)); static per process
std::optional<bool> fast_copy_memory_user_disabled;

// Whether safe_memcpy initialized at startup; static per process
std::optional<bool> fast_copy_memory_capable;

// Sticky: fell back to syscall copy (init failure, foreign handler, etc.)
std::optional<bool> fast_copy_memory_syscall_fallback;

// Number of copy_memory errors accumulated since the last profile reset (i.e. since the last upload)
size_t copy_memory_error_count = 0;

Expand Down Expand Up @@ -71,6 +80,15 @@ class ProfilerStats
void set_fast_copy_memory_enabled(bool enabled);
std::optional<bool> get_fast_copy_memory_enabled() const;

void set_fast_copy_memory_user_disabled(bool disabled);
std::optional<bool> get_fast_copy_memory_user_disabled() const;

void set_fast_copy_memory_capable(bool capable);
std::optional<bool> get_fast_copy_memory_capable() const;

void set_fast_copy_memory_syscall_fallback(bool fallback);
std::optional<bool> get_fast_copy_memory_syscall_fallback() const;

void add_copy_memory_error_count(size_t count);
size_t get_copy_memory_error_count() const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ append_to_string(std::string& s, size_t value)
s.append(buf, ptr);
}

void
append_optional_bool(std::string& s, const char* key, const std::optional<bool>& value)
{
if (!value.has_value()) {
return;
}
s += '"';
s += key;
s += "\": ";
s += *value ? "true" : "false";
s += ',';
}

} // namespace

void
Expand Down Expand Up @@ -51,7 +64,7 @@ Datadog::ProfilerStats::reset_state()
asyncio_task_count = std::nullopt;
greenlet_count = std::nullopt;
sample_capture_cpu_time_us = 0;
// fast_copy_memory_enabled is intentionally not reset: it reflects a static configuration
// fast_copy_memory_* static fields are intentionally not reset (see setters).
}

void
Expand All @@ -66,6 +79,42 @@ Datadog::ProfilerStats::get_fast_copy_memory_enabled() const
return fast_copy_memory_enabled;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_user_disabled(bool disabled)
{
fast_copy_memory_user_disabled = disabled;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_user_disabled() const
{
return fast_copy_memory_user_disabled;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_capable(bool capable)
{
fast_copy_memory_capable = capable;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_capable() const
{
return fast_copy_memory_capable;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_syscall_fallback(bool fallback)
{
fast_copy_memory_syscall_fallback = fallback;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_syscall_fallback() const
{
return fast_copy_memory_syscall_fallback;
}

void
Datadog::ProfilerStats::add_copy_memory_error_count(size_t count)
{
Expand Down Expand Up @@ -206,6 +255,11 @@ Datadog::ProfilerStats::get_internal_metadata_json()
internal_metadata_json += ",";
}

append_optional_bool(internal_metadata_json, "fast_copy_memory_user_disabled", fast_copy_memory_user_disabled);
append_optional_bool(internal_metadata_json, "fast_copy_memory_capable", fast_copy_memory_capable);
append_optional_bool(
internal_metadata_json, "fast_copy_memory_syscall_fallback", fast_copy_memory_syscall_fallback);

auto maybe_heap_tracker_count = get_heap_tracker_size();
if (maybe_heap_tracker_count) {
internal_metadata_json += R"("heap_tracker_count": )";
Expand Down
15 changes: 13 additions & 2 deletions ddtrace/internal/datadog/profiling/stack/echion/echion/vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,19 @@ inline kern_return_t (*safe_copy)(vm_map_read_t,
// Whether safe_copy is currently set to the memcpy-based wrapper.
inline bool fast_copy_active = false;

// Whether init_segv_catcher succeeded at constructor time. Persists even if
// fast_copy_active is later toggled off by set_fast_copy_enabled.
// User opted out via _DD_PROFILING_STACK_FAST_COPY or set_fast_copy(false).
inline bool fast_copy_user_disabled = false;

// Sticky: fell back to syscall copy (init failure, foreign handler, warmup miss).
inline bool fast_copy_syscall_fallback = false;

inline void
mark_fast_copy_syscall_fallback()
{
fast_copy_syscall_fallback = true;
}

// Set at init; survives toggling fast_copy_active.
inline bool safe_memcpy_initialized = false;

#if defined PL_LINUX
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/src/echion/vm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ init_safe_copy()
// Honor the fast-copy opt-out: when disabled via env var, skip installing
// the SIGSEGV/SIGBUS handlers and alt stack entirely.
if (fast_copy_env_disabled()) {
fast_copy_user_disabled = true;
if (process_vm_readv_available) {
safe_copy = process_vm_readv;
} else {
Expand All @@ -60,6 +61,7 @@ init_safe_copy()
fprintf(stderr, "Failed to initialize segv catcher. Trying process_vm_readv.\n");
if (process_vm_readv_available) {
safe_copy = process_vm_readv;
mark_fast_copy_syscall_fallback();
} else {
fprintf(stderr, "Failed to initialize safe copy interface\n");
failed_safe_copy = true;
Expand All @@ -72,6 +74,7 @@ init_safe_copy()
{
// Honor the fast-copy opt-out: skip installing signal handlers when disabled.
if (fast_copy_env_disabled()) {
fast_copy_user_disabled = true;
return;
}

Expand All @@ -84,6 +87,7 @@ init_safe_copy()

// std::cerr might not be fully initialized at constructor time.
fprintf(stderr, "Failed to initialize segv catcher. Using mach_vm_read_overwrite instead.\n");
mark_fast_copy_syscall_fallback();
}
#endif // PL_DARWIN

Expand All @@ -99,6 +103,7 @@ set_fast_copy_enabled(bool enabled)
}
fprintf(stderr,
"Warning: fast copy requested but safe_memcpy was not initialized; falling back to process_vm_readv\n");
mark_fast_copy_syscall_fallback();

// Fall through to process_vm_readv.
}
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/src/sampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ Sampler::sampling_thread(const uint64_t seq_num)
// syscall copy (already active from warmup) for the life of
// the process.
handler_fallback_done = true;
mark_fast_copy_syscall_fallback();
std::cerr << "ddtrace stack profiler: another component owns the SIGSEGV/SIGBUS "
"handler; keeping the syscall-based memory copy to avoid crashing."
<< std::endl;
Expand All @@ -304,6 +305,7 @@ Sampler::sampling_thread(const uint64_t seq_num)
// degrade sample quality (e.g. on asyncio workloads). We still prefer
// it over the alternative, which is crashing under a foreign handler.
handler_fallback_done = true;
mark_fast_copy_syscall_fallback();
std::cerr << "ddtrace stack profiler: SIGSEGV/SIGBUS handler was taken over by another "
"component; falling back to syscall-based memory copy to avoid crashing."
<< std::endl;
Expand Down Expand Up @@ -446,6 +448,9 @@ Sampler::sampling_thread(const uint64_t seq_num)
borrow.stats().increment_sampling_event_count();
borrow.stats().set_string_table_count(echion->string_table().size());
borrow.stats().set_string_table_ephemeral_count(echion->string_table().ephemeral_size());
borrow.stats().set_fast_copy_memory_user_disabled(fast_copy_user_disabled);
borrow.stats().set_fast_copy_memory_capable(safe_memcpy_initialized);
borrow.stats().set_fast_copy_memory_syscall_fallback(fast_copy_syscall_fallback);
Comment thread
vlad-scherbich marked this conversation as resolved.
Outdated
borrow.stats().set_fast_copy_memory_enabled(fast_copy_active);
borrow.stats().set_asyncio_task_count(echion->asyncio_task_count());
borrow.stats().set_greenlet_count(greenlet_count);
Expand Down
6 changes: 5 additions & 1 deletion ddtrace/internal/datadog/profiling/stack/src/stack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,11 @@ stack_set_fast_copy(PyObject* Py_UNUSED(self), PyObject* args)
return NULL;
}

set_fast_copy_enabled(static_cast<bool>(enabled));
const bool want = static_cast<bool>(enabled);
if (!want) {
fast_copy_user_disabled = true;
}
set_fast_copy_enabled(want);

Py_RETURN_NONE;
}
Expand Down
23 changes: 23 additions & 0 deletions tests/profiling/collector/test_copy_memory_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def test_copy_memory_error_count_present():
metadata = json.load(fp)
assert "copy_memory_error_count" in metadata, f"Missing copy_memory_error_count in {f}: {metadata}"
assert metadata["copy_memory_error_count"] >= 0, f"copy_memory_error_count must be non-negative: {metadata}"
assert "fast_copy_memory_user_disabled" in metadata, (
f"Missing fast_copy_memory_user_disabled in {f}: {metadata}"
)
assert "fast_copy_memory_capable" in metadata, f"Missing fast_copy_memory_capable in {f}: {metadata}"
assert "fast_copy_memory_syscall_fallback" in metadata, (
f"Missing fast_copy_memory_syscall_fallback in {f}: {metadata}"
)


@pytest.mark.subprocess(
Expand Down Expand Up @@ -70,6 +77,8 @@ def test_fast_copy_memory_disabled():
assert metadata["fast_copy_memory_enabled"] is False, (
f"Expected fast_copy_memory_enabled=false when _DD_PROFILING_STACK_FAST_COPY=false: {metadata}"
)
assert metadata["fast_copy_memory_user_disabled"] is True, metadata
assert metadata["fast_copy_memory_syscall_fallback"] is False, metadata


@pytest.mark.subprocess(
Expand All @@ -82,6 +91,9 @@ def test_fast_copy_memory_disabled():
)
def test_fast_copy_memory_enabled() -> None:
"""Sampler runs on the syscall copy during warmup, then upgrades to safe_memcpy (PROF-14568)."""
import glob
import json
import os
import time

# Underscore-prefixed, so only on the _stack submodule (`import *` skips it).
Expand Down Expand Up @@ -111,5 +123,16 @@ def test_fast_copy_memory_enabled() -> None:

p.stop()

output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid())
files = sorted(glob.glob(output_filename + ".*.internal_metadata.json"))
assert files, "Expected at least one internal_metadata.json file"

with open(files[-1]) as fp:
metadata = json.load(fp)
assert metadata["fast_copy_memory_user_disabled"] is False, metadata
assert metadata["fast_copy_memory_capable"] is True, metadata
assert metadata["fast_copy_memory_syscall_fallback"] is False, metadata
assert metadata["fast_copy_memory_enabled"] is True, metadata

assert saw_warmup, "Expected the sampler to run on the syscall copy during the warmup window"
assert saw_upgrade, "Expected the sampler to upgrade to safe_memcpy after warmup"
Loading