Skip to content

Commit 348bb37

Browse files
committed
RUM-16039: Upgrade Mach profiler sampler to sample CPU-time
1 parent 562d076 commit 348bb37

16 files changed

Lines changed: 337 additions & 30 deletions

DatadogProfiling/Mach/dd_pprof.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
extern "C" {
1818

1919
dd_pprof_t* dd_pprof_create(uint64_t sampling_interval_ns) {
20+
return dd_pprof_create_with_cpu_time(sampling_interval_ns, false);
21+
}
22+
23+
dd_pprof_t* dd_pprof_create_with_cpu_time(uint64_t sampling_interval_ns, bool record_cpu_time) {
2024
try {
21-
auto* profiler = new dd::profiler::profile(sampling_interval_ns);
25+
auto* profiler = new dd::profiler::profile(sampling_interval_ns, record_cpu_time);
2226
return reinterpret_cast<dd_pprof_t*>(profiler);
2327
} catch (...) {
2428
return nullptr;

DatadogProfiling/Mach/dd_profiler.cpp

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,23 @@ static double read_profiling_sample_rate() {
137137
return sample_rate;
138138
}
139139

140+
static bool read_profiling_record_cpu_time() {
141+
CFStringRef suiteName = CFSTR(DD_PROFILING_USER_DEFAULTS_SUITE_NAME);
142+
CFStringRef key = CFSTR(DD_PROFILING_RECORD_CPU_TIME_KEY);
143+
CFPropertyListRef value = CFPreferencesCopyAppValue(key, suiteName);
144+
145+
bool result = false;
146+
147+
if (value) {
148+
if (CFGetTypeID(value) == CFBooleanGetTypeID()) {
149+
result = CFBooleanGetValue((CFBooleanRef)value);
150+
}
151+
CFRelease(value);
152+
}
153+
154+
return result;
155+
}
156+
140157
/**
141158
* Deletes the DatadogProfiling defaults from the `UserDefaults`
142159
* to be re-evaluated during `Profiling.enable()`.
@@ -145,9 +162,11 @@ void dd_delete_profiling_defaults() {
145162
CFStringRef suiteName = CFSTR(DD_PROFILING_USER_DEFAULTS_SUITE_NAME);
146163
CFStringRef isEnabledKey = CFSTR(DD_PROFILING_IS_ENABLED_KEY);
147164
CFStringRef sampleRateKey = CFSTR(DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY);
165+
CFStringRef recordCPUTimeKey = CFSTR(DD_PROFILING_RECORD_CPU_TIME_KEY);
148166

149167
CFPreferencesSetValue(isEnabledKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
150168
CFPreferencesSetValue(sampleRateKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
169+
CFPreferencesSetValue(recordCPUTimeKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
151170
CFPreferencesSynchronize(suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
152171
}
153172

@@ -265,7 +284,7 @@ class dd_profiler {
265284
profile* flush_and_get_profile() {
266285
profile_swap_context swap_context{
267286
this,
268-
new (std::nothrow) dd::profiler::profile(sampling_interval_ns),
287+
new (std::nothrow) dd::profiler::profile(sampling_interval_ns, record_cpu_time),
269288
nullptr
270289
};
271290

@@ -343,7 +362,9 @@ class dd_profiler {
343362

344363
if (profiler) return true;
345364

346-
profile = new (std::nothrow) dd::profiler::profile(sampling_interval_ns);
365+
record_cpu_time = read_profiling_record_cpu_time();
366+
367+
profile = new (std::nothrow) dd::profiler::profile(sampling_interval_ns, record_cpu_time);
347368
if (!profile) {
348369
status = DD_PROFILER_STATUS_ALLOCATION_FAILED;
349370
return false;
@@ -352,6 +373,7 @@ class dd_profiler {
352373

353374
sampling_config_t config = SAMPLING_CONFIG_DEFAULT;
354375
config.sampling_interval_nanos = sampling_interval_ns;
376+
config.record_cpu_time = record_cpu_time ? 1 : 0;
355377

356378
profiler = new (std::nothrow) mach_sampling_profiler(&config, callback, this, hard_limit_bytes);
357379
if (!profiler) {
@@ -373,6 +395,7 @@ class dd_profiler {
373395
uint64_t hard_limit_bytes = DD_PROFILER_DEFAULT_HARD_LIMIT_BYTES;
374396
uint64_t sampling_interval_ns = SAMPLING_CONFIG_DEFAULT_INTERVAL_NANOS;
375397
int64_t server_time_offset_ns = 0;
398+
bool record_cpu_time = false;
376399

377400
/**
378401
* Mutex protecting the profile pointer.

DatadogProfiling/Mach/include/dd_pprof.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ typedef struct profile dd_pprof_t;
4242
*/
4343
dd_pprof_t* dd_pprof_create(uint64_t sampling_interval_ns);
4444

45+
/**
46+
* Create a new pprof profile aggregator with optional CPU-time sample values.
47+
*
48+
* @param sampling_interval_ns The sampling interval in nanoseconds
49+
* @param record_cpu_time Whether samples should include CPU time as a second value
50+
* @return Pointer to the created profile, or NULL on failure
51+
*/
52+
dd_pprof_t* dd_pprof_create_with_cpu_time(uint64_t sampling_interval_ns, bool record_cpu_time);
53+
4554
/**
4655
* Destroy a pprof profile aggregator and free all associated memory
4756
*

DatadogProfiling/Mach/include/dd_profiler.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ typedef struct stack_trace {
5252
uint64_t timestamp;
5353
/** Actual sampling interval in nanoseconds for this sample */
5454
uint64_t sampling_interval_nanos;
55+
/** CPU time consumed by this thread since the previous sample */
56+
uint64_t cpu_time_nanos;
5557
/** The stack frames array */
5658
stack_frame_t* frames;
5759
/** Number of frames in the trace */
@@ -74,6 +76,8 @@ typedef struct sampling_config {
7476
uint32_t max_thread_count; // default: 100
7577
/** QoS class for the sampling thread */
7678
qos_class_t qos_class;
79+
/** Whether samples should include a CPU-time value */
80+
uint8_t record_cpu_time;
7781
} sampling_config_t;
7882

7983
/**
@@ -100,7 +104,8 @@ static const sampling_config_t SAMPLING_CONFIG_DEFAULT = {
100104
SAMPLING_CONFIG_DEFAULT_BUFFER_SIZE, // max_buffer_size
101105
SAMPLING_CONFIG_DEFAULT_STACK_DEPTH, // max_stack_depth
102106
SAMPLING_CONFIG_DEFAULT_THREAD_COUNT, // max_thread_count
103-
QOS_CLASS_USER_INTERACTIVE // qos_class
107+
QOS_CLASS_USER_INTERACTIVE, // qos_class
108+
0 // record_cpu_time
104109
};
105110

106111
/**
@@ -122,6 +127,7 @@ typedef void (*stack_trace_callback_t)(stack_trace_t* traces, size_t count, void
122127
#define DD_PROFILING_USER_DEFAULTS_SUITE_NAME "com.datadoghq.ios-sdk.profiling"
123128
#define DD_PROFILING_IS_ENABLED_KEY "is_profiling_enabled"
124129
#define DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY "profiling_app_launch_sample_rate"
130+
#define DD_PROFILING_RECORD_CPU_TIME_KEY "profiling_record_cpu_time"
125131

126132
#ifdef __cplusplus
127133

DatadogProfiling/Mach/include/mach_sampling_profiler.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <memory>
2121
#include <mutex>
2222
#include <pthread.h>
23+
#include <unordered_map>
2324
#include <vector>
2425

2526
#ifdef __cplusplus
@@ -162,13 +163,23 @@ class mach_sampling_profiler {
162163
* @param thread The thread to sample
163164
* @param interval_nanos The actual sampling interval in nanoseconds for this sample
164165
*/
165-
void sample_thread(thread_t thread, uint64_t interval_nanos);
166+
void sample_thread(thread_t thread, uint64_t interval_nanos, uint64_t cpu_time_nanos);
166167

167168
/**
168169
* @brief Returns true when the thread is owned by the profiler itself.
169170
*/
170171
bool is_profiler_internal_thread(thread_t thread) const;
171172

173+
/**
174+
* @brief Returns CPU time consumed since the previous observation for this thread.
175+
*/
176+
uint64_t thread_cpu_time_delta_nanos(thread_t thread);
177+
178+
/**
179+
* @brief Removes CPU-time state for threads no longer present in the task.
180+
*/
181+
void prune_thread_cpu_time_state(const thread_t* threads, mach_msg_type_number_t count);
182+
172183
private:
173184
/**
174185
* @brief Static entry point for the sampling thread
@@ -181,6 +192,7 @@ class mach_sampling_profiler {
181192
std::mutex state_mutex;
182193
/// Indicates whether `sampling_thread` currently refers to a live session thread.
183194
std::atomic<bool> has_sampling_thread{false};
195+
std::unordered_map<thread_t, uint64_t> previous_thread_cpu_time_nanos;
184196
};
185197

186198
} // namespace dd::profiler

DatadogProfiling/Mach/include/profile.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class profile {
147147
* @brief Construct a new profile aggregator
148148
* @param sampling_interval_ns Sampling interval in nanoseconds
149149
*/
150-
explicit profile(uint64_t sampling_interval_ns);
150+
explicit profile(uint64_t sampling_interval_ns, bool record_cpu_time = false);
151151
~profile() = default;
152152

153153
profile(const profile&) = delete;
@@ -189,10 +189,16 @@ class profile {
189189

190190
/** @brief Get cached string ID for "wall-time" */
191191
uint32_t wall_time_str_id() const { return _wall_time_str_id; }
192+
193+
/** @brief Get cached string ID for "cpu-time" */
194+
uint32_t cpu_time_str_id() const { return _cpu_time_str_id; }
192195

193196
/** @brief Get cached string ID for "nanoseconds" */
194197
uint32_t nanoseconds_str_id() const { return _nanoseconds_str_id; }
195198

199+
/** @brief Whether samples include a CPU-time value in addition to wall-time */
200+
bool cpu_time_enabled() const { return _record_cpu_time; }
201+
196202
/** @brief Number of labels exported for the sample */
197203
size_t label_count(const sample_t& sample) const { return sample.labels.size() + 1; }
198204

@@ -238,12 +244,18 @@ class profile {
238244

239245
/** @brief Profile sampling interval in nanoseconds */
240246
uint64_t _sampling_interval_ns;
247+
248+
/** @brief Whether samples include CPU time as a second value */
249+
bool _record_cpu_time;
241250

242251
/** @brief Cached string ID for empty string */
243252
uint32_t _empty_str_id;
244253

245254
/** @brief Cached string ID for "wall-time" */
246255
uint32_t _wall_time_str_id;
256+
257+
/** @brief Cached string ID for "cpu-time" */
258+
uint32_t _cpu_time_str_id;
247259

248260
/** @brief Cached string ID for "nanoseconds" */
249261
uint32_t _nanoseconds_str_id;

DatadogProfiling/Mach/mach_sampling_profiler.cpp

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <setjmp.h>
2020
#include <mach/thread_act.h>
2121
#include <mach/thread_status.h>
22+
#include <mach/thread_info.h>
2223
#include <mach/machine/thread_state.h>
2324
#include <new>
2425

@@ -159,11 +160,32 @@ bool stack_trace_init(stack_trace_t* trace, uint32_t max_depth, uint64_t interva
159160
trace->thread_name = nullptr;
160161
trace->timestamp = 0;
161162
trace->sampling_interval_nanos = interval_nanos;
163+
trace->cpu_time_nanos = 0;
162164
trace->frame_count = 0;
163165
trace->frames = (stack_frame_t*)malloc(max_depth * sizeof(stack_frame_t));
164166
return trace->frames != nullptr;
165167
}
166168

169+
static bool thread_cpu_time_nanos(thread_t thread, uint64_t* cpu_time_nanos) {
170+
if (!cpu_time_nanos) return false;
171+
172+
thread_basic_info_data_t info{};
173+
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
174+
if (thread_info(thread, THREAD_BASIC_INFO, reinterpret_cast<thread_info_t>(&info), &count) != KERN_SUCCESS) {
175+
return false;
176+
}
177+
178+
const uint64_t user_time_nanos =
179+
(static_cast<uint64_t>(info.user_time.seconds) * 1000000000ULL)
180+
+ (static_cast<uint64_t>(info.user_time.microseconds) * 1000ULL);
181+
const uint64_t system_time_nanos =
182+
(static_cast<uint64_t>(info.system_time.seconds) * 1000000000ULL)
183+
+ (static_cast<uint64_t>(info.system_time.microseconds) * 1000ULL);
184+
185+
*cpu_time_nanos = user_time_nanos + system_time_nanos;
186+
return true;
187+
}
188+
167189
/**
168190
* Destroys a stack trace, freeing the thread name and frames array.
169191
*
@@ -395,6 +417,7 @@ bool mach_sampling_profiler::start_sampling() {
395417

396418
// Clear any leftover data from previous runs
397419
sample_buffer.clear();
420+
previous_thread_cpu_time_nanos.clear();
398421
if (sample_buffer.capacity() < config.max_buffer_size) {
399422
sample_buffer.reserve(config.max_buffer_size);
400423
}
@@ -488,9 +511,10 @@ bool mach_sampling_profiler::is_profiler_internal_thread(thread_t thread) const
488511
* @param thread The thread to sample
489512
* @param interval_nanos The actual sampling interval in nanoseconds for this sample
490513
*/
491-
void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_nanos) {
514+
void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_nanos, uint64_t cpu_time_nanos) {
492515
stack_trace_t trace;
493516
if (!stack_trace_init(&trace, config.max_stack_depth, interval_nanos)) return;
517+
trace.cpu_time_nanos = cpu_time_nanos;
494518

495519
// Get thread info
496520
stack_trace_get_thread_info(&trace, thread);
@@ -516,6 +540,53 @@ void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_na
516540
}
517541
}
518542

543+
uint64_t mach_sampling_profiler::thread_cpu_time_delta_nanos(thread_t thread) {
544+
if (!config.record_cpu_time) {
545+
return 0;
546+
}
547+
548+
uint64_t current_cpu_time_nanos = 0;
549+
if (!thread_cpu_time_nanos(thread, &current_cpu_time_nanos)) {
550+
return 0;
551+
}
552+
553+
auto result = previous_thread_cpu_time_nanos.emplace(thread, current_cpu_time_nanos);
554+
if (result.second) {
555+
return 0;
556+
}
557+
558+
const uint64_t previous_cpu_time_nanos = result.first->second;
559+
result.first->second = current_cpu_time_nanos;
560+
561+
if (current_cpu_time_nanos < previous_cpu_time_nanos) {
562+
return 0;
563+
}
564+
565+
return current_cpu_time_nanos - previous_cpu_time_nanos;
566+
}
567+
568+
void mach_sampling_profiler::prune_thread_cpu_time_state(const thread_t* threads, mach_msg_type_number_t count) {
569+
if (!config.record_cpu_time || previous_thread_cpu_time_nanos.empty()) {
570+
return;
571+
}
572+
573+
for (auto it = previous_thread_cpu_time_nanos.begin(); it != previous_thread_cpu_time_nanos.end();) {
574+
bool is_live_thread = false;
575+
for (mach_msg_type_number_t i = 0; i < count; i++) {
576+
if (threads[i] == it->first) {
577+
is_live_thread = true;
578+
break;
579+
}
580+
}
581+
582+
if (is_live_thread) {
583+
++it;
584+
} else {
585+
it = previous_thread_cpu_time_nanos.erase(it);
586+
}
587+
}
588+
}
589+
519590
/**
520591
* Main sampling loop that collects stack traces from threads.
521592
*/
@@ -532,7 +603,8 @@ void mach_sampling_profiler::main() {
532603
}
533604

534605
if (config.profile_current_thread_only) {
535-
sample_thread(pthread_mach_thread_np(target_thread), interval_nanos);
606+
const thread_t thread = pthread_mach_thread_np(target_thread);
607+
sample_thread(thread, interval_nanos, thread_cpu_time_delta_nanos(thread));
536608
if (sample_buffer.size() >= config.max_buffer_size) {
537609
worker->enqueue_active_buffer(sample_buffer);
538610
}
@@ -554,13 +626,15 @@ void mach_sampling_profiler::main() {
554626
// Skip profiler-owned threads to avoid self-noise in customer profiles.
555627
if (is_profiler_internal_thread(threads[i])) continue;
556628

557-
sample_thread(threads[i], interval_nanos);
629+
sample_thread(threads[i], interval_nanos, thread_cpu_time_delta_nanos(threads[i]));
558630

559631
if (sample_buffer.size() >= config.max_buffer_size) {
560632
worker->enqueue_active_buffer(sample_buffer);
561633
}
562634
}
563635

636+
prune_thread_cpu_time_state(threads, count);
637+
564638
// Clean up thread references
565639
for (mach_msg_type_number_t i = 0; i < count; i++) {
566640
mach_port_deallocate(mach_task_self(), threads[i]);

DatadogProfiling/Mach/profile.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ std::string uuid_string(const uuid_t uuid) {
8181
*
8282
* @param sampling_interval_ns Sampling interval in nanoseconds
8383
*/
84-
profile::profile(uint64_t sampling_interval_ns)
84+
profile::profile(uint64_t sampling_interval_ns, bool record_cpu_time)
8585
: _sampling_interval_ns(sampling_interval_ns)
86+
, _record_cpu_time(record_cpu_time)
8687
, _epoch_offset(uptime_epoch_offset())
8788
, _server_time_offset_ns(0)
8889
, _start_timestamp(0)
@@ -95,6 +96,7 @@ profile::profile(uint64_t sampling_interval_ns)
9596
// Pre-intern common strings for performance
9697
_empty_str_id = intern_string("");
9798
_wall_time_str_id = intern_string("wall-time");
99+
_cpu_time_str_id = _record_cpu_time ? intern_string("cpu-time") : 0;
98100
_nanoseconds_str_id = intern_string("nanoseconds");
99101
_end_timestamp_ns_str_id = intern_string("end_timestamp_ns");
100102
_thread_id_str_id = intern_string("thread id");
@@ -176,6 +178,9 @@ void profile::add_samples(const stack_trace_t* traces, size_t count, binary_imag
176178
sample.timestamp_uptime_ns = trace.timestamp;
177179
sample.labels = std::move(labels);
178180
sample.values = {static_cast<int64_t>(trace.sampling_interval_nanos)};
181+
if (_record_cpu_time) {
182+
sample.values.push_back(static_cast<int64_t>(trace.cpu_time_nanos));
183+
}
179184

180185
_samples.push_back(std::move(sample));
181186

0 commit comments

Comments
 (0)