Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
20 changes: 15 additions & 5 deletions ddtrace/internal/datadog/profiling/stack/echion/echion/cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,38 @@

#pragma once

#include <algorithm>
#include <functional>
#include <list>
#include <memory>
#include <unordered_map>

#include <echion/errors.h>

#define CACHE_MAX_ENTRIES 2048

template<typename K, typename V>
class LRUCache
{
public:
LRUCache(size_t capacity)
: capacity(capacity)
: capacity_(std::max<size_t>(capacity, 1))
{
}

Result<std::reference_wrapper<V>> lookup(const K& k);

void store(const K& k, std::unique_ptr<V> v);

void set_capacity(size_t capacity)
{
capacity_ = std::max<size_t>(capacity, 1);
while (items.size() > capacity_) {
index.erase(items.back().first);
items.pop_back();
}
}

[[nodiscard]] size_t capacity() const { return capacity_; }

void clear()
{
items.clear();
Expand All @@ -44,7 +54,7 @@ class LRUCache
}

private:
size_t capacity;
size_t capacity_;
std::list<std::pair<K, std::unique_ptr<V>>> items;
std::unordered_map<K, typename std::list<std::pair<K, std::unique_ptr<V>>>::iterator> index;
};
Expand All @@ -54,7 +64,7 @@ void
LRUCache<K, V>::store(const K& k, std::unique_ptr<V> v)
{
// Check if cache is full
if (items.size() >= capacity) {
if (items.size() >= capacity_) {
index.erase(items.back().first);
items.pop_back();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,12 @@ class EchionSampler
void add_asyncio_task_count(size_t count) { asyncio_task_count_ += count; }
size_t asyncio_task_count() const { return asyncio_task_count_; }

void set_max_frames(size_t max_frames) { stack_max_frames_ = std::max<size_t>(max_frames, 1); }
void configure_frame_limits(size_t max_frames, size_t frame_cache_capacity)
{
stack_max_frames_ = std::max<size_t>(max_frames, 1);
frame_cache_.set_capacity(frame_cache_capacity);
}

[[nodiscard]] size_t stack_max_frames() const { return stack_max_frames_; }

unsigned int max_tasks_per_sample() const { return max_tasks_per_sample_; }
Expand All @@ -170,6 +175,7 @@ class EchionSampler

// Accessor for frame cache operations
LRUCache<uintptr_t, Frame>& frame_cache() { return frame_cache_; }
[[nodiscard]] size_t frame_cache_capacity() const { return frame_cache_.capacity(); }

void postfork_child()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ constexpr unsigned int g_default_max_threads_per_sample = 25;
// leaf tasks exceeds this, reservoir sampling is used. 0 means sample all tasks.
constexpr unsigned int g_default_max_tasks_per_sample = 50;

// Echion maintains a cache of frames--the size of this cache is specified up-front.
constexpr unsigned int g_default_echion_frame_cache_size = 1024;
// Echion retains recently rendered frames across stack walks. Scale the cache with
// the configured stack depth while bounding both churn and retained memory.
constexpr unsigned int g_min_echion_frame_cache_size = 256;
constexpr unsigned int g_max_echion_frame_cache_size = 1024;
constexpr unsigned int g_echion_frame_cache_size_multiplier = 4;
17 changes: 14 additions & 3 deletions ddtrace/internal/datadog/profiling/stack/src/sampler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ create_thread_with_stack(size_t stack_size, Sampler* sampler, uint64_t seq_num)

namespace {

size_t
calculate_frame_cache_capacity(size_t max_frames)
{
if (max_frames >= g_max_echion_frame_cache_size / g_echion_frame_cache_size_multiplier) {
return g_max_echion_frame_cache_size;
}

const size_t scaled = max_frames * g_echion_frame_cache_size_multiplier;
return std::max(scaled, static_cast<size_t>(g_min_echion_frame_cache_size));
}

// Returns the CPU time of the calling thread in microseconds, or 0 on error.
uint64_t
get_thread_cpu_time_us()
Expand Down Expand Up @@ -594,7 +605,7 @@ Sampler::set_max_frames(uint64_t value)

// Setting to 0 uses the default limit.
const size_t requested = value == 0 ? g_default_max_nframes : static_cast<size_t>(value);
echion->set_max_frames(requested);
echion->configure_frame_limits(requested, calculate_frame_cache_capacity(requested));
return true;
}

Expand All @@ -607,11 +618,11 @@ Sampler::max_frames() const
size_t
Sampler::frame_cache_capacity() const
{
return g_default_echion_frame_cache_size;
return echion->frame_cache_capacity();
}

Sampler::Sampler()
: echion{ std::make_unique<EchionSampler>(g_default_echion_frame_cache_size) }
: echion{ std::make_unique<EchionSampler>(calculate_frame_cache_capacity(g_default_max_nframes)) }
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ endfunction()
# Add the tests
dd_wrapper_add_test(test_thread_span_links test_thread_span_links.cpp)
dd_wrapper_add_test(test_origin_task_links test_origin_task_links.cpp)
dd_wrapper_add_test(test_cache test_cache.cpp)

function(configure_stack_internal_test name)
target_include_directories(${name} PRIVATE ../..)
Expand Down
38 changes: 38 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/test/test_cache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include "echion/cache.h"

#include <gtest/gtest.h>

#include <memory>

TEST(LRUCacheCapacity, ClampsCapacityToOne)
{
LRUCache<int, int> cache(0);

EXPECT_EQ(cache.capacity(), 1);
cache.set_capacity(0);
EXPECT_EQ(cache.capacity(), 1);
}

TEST(LRUCacheCapacity, ShrinkingEvictsLeastRecentlyUsedEntries)
{
LRUCache<int, int> cache(3);
cache.store(1, std::make_unique<int>(1));
cache.store(2, std::make_unique<int>(2));
cache.store(3, std::make_unique<int>(3));

ASSERT_TRUE(cache.lookup(1).has_value());
cache.set_capacity(2);

EXPECT_EQ(cache.capacity(), 2);
EXPECT_TRUE(cache.lookup(1).has_value());
EXPECT_FALSE(cache.lookup(2).has_value());
EXPECT_TRUE(cache.lookup(3).has_value());

cache.set_capacity(1);
EXPECT_TRUE(cache.lookup(3).has_value());
EXPECT_FALSE(cache.lookup(1).has_value());

cache.store(4, std::make_unique<int>(4));
EXPECT_FALSE(cache.lookup(3).has_value());
EXPECT_TRUE(cache.lookup(4).has_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
profiling: Reduces profiling memory usage by scaling frame caching with ``DD_PROFILING_MAX_FRAMES``.
17 changes: 10 additions & 7 deletions tests/profiling/collector/test_stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_collect_truncate() -> None:
ddup.config(env="test", service="test", version="0.0.0", max_nframes=64, output_filename=pprof_prefix)
ddup.start()
with stack.StackCollector():
assert _stack._get_frame_limits() == (max_nframes, 1024)
assert _stack._get_frame_limits() == (max_nframes, 256)
func1()
ddup.upload()

Expand All @@ -111,14 +111,17 @@ def test_collect_truncate() -> None:


@pytest.mark.subprocess
def test_native_frame_limit() -> None:
def test_native_frame_limit_scales_cache() -> None:
from ddtrace.internal.datadog.profiling.stack import _stack

_stack.set_max_frames(0)
assert _stack._get_frame_limits() == (64, 1024)
assert _stack._get_frame_limits() == (64, 256)

_stack.set_max_frames(65)
assert _stack._get_frame_limits() == (65, 1024)
assert _stack._get_frame_limits() == (65, 260)

_stack.set_max_frames(512)
assert _stack._get_frame_limits() == (512, 1024)

_stack.set_max_frames(10_000)
assert _stack._get_frame_limits() == (10_000, 1024)
Expand Down Expand Up @@ -203,7 +206,7 @@ def test_set_max_frames_after_fork_restart() -> None:
try:
stack.stop()
stack.set_max_frames(1)
assert _stack._get_frame_limits() == (1, 1024)
assert _stack._get_frame_limits() == (1, 256)
assert stack.start()
func1()
stack.stop()
Expand Down Expand Up @@ -273,7 +276,7 @@ async def outer() -> None:
await inner()

with stack.StackCollector(nframes=1):
assert _stack._get_frame_limits() == (1, 1024)
assert _stack._get_frame_limits() == (1, 256)
sync_outer()
asyncio.run(outer())

Expand Down Expand Up @@ -748,7 +751,7 @@ def _do_fib() -> None:
threads = []

with stack.StackCollector(nframes=1):
assert _stack._get_frame_limits() == (1, 1024)
assert _stack._get_frame_limits() == (1, 256)
for i in range(5):
t = threading.Thread(target=_do_fib, name=f"TestThread {i}")
t.start()
Expand Down
Loading