diff --git a/BenchmarkTests/Runner/Scenarios/Profiling/ProfilingScenario.swift b/BenchmarkTests/Runner/Scenarios/Profiling/ProfilingScenario.swift index 8461027a05..ad1aac922c 100644 --- a/BenchmarkTests/Runner/Scenarios/Profiling/ProfilingScenario.swift +++ b/BenchmarkTests/Runner/Scenarios/Profiling/ProfilingScenario.swift @@ -32,6 +32,12 @@ struct ProfilingScenario: Scenario { RUMMonitor.shared().addAttribute(forKey: "scenario", value: "ContinuousProfiling") - Profiling.enable(with: .init(applicationLaunchSampleRate: .maxSampleRate, continuousSampleRate: .maxSampleRate)) + Profiling.enable( + with: .init( + applicationLaunchSampleRate: .maxSampleRate, + continuousSampleRate: .maxSampleRate, + featureFlags: [.cpuTimeSamples: true] + ) + ) } } diff --git a/DatadogProfiling/Mach/dd_pprof.cpp b/DatadogProfiling/Mach/dd_pprof.cpp index 1a4b23fbde..4fa7fa60e9 100644 --- a/DatadogProfiling/Mach/dd_pprof.cpp +++ b/DatadogProfiling/Mach/dd_pprof.cpp @@ -17,8 +17,12 @@ extern "C" { dd_pprof_t* dd_pprof_create(uint64_t sampling_interval_ns) { + return dd_pprof_create_with_cpu_time(sampling_interval_ns, false); +} + +dd_pprof_t* dd_pprof_create_with_cpu_time(uint64_t sampling_interval_ns, bool record_cpu_time) { try { - auto* profiler = new dd::profiler::profile(sampling_interval_ns); + auto* profiler = new dd::profiler::profile(sampling_interval_ns, record_cpu_time); return reinterpret_cast(profiler); } catch (...) { return nullptr; diff --git a/DatadogProfiling/Mach/dd_profiler.cpp b/DatadogProfiling/Mach/dd_profiler.cpp index b79c02a0ed..aad146f436 100644 --- a/DatadogProfiling/Mach/dd_profiler.cpp +++ b/DatadogProfiling/Mach/dd_profiler.cpp @@ -155,6 +155,23 @@ static double read_profiling_sample_rate() { return sample_rate; } +static bool read_profiling_record_cpu_time() { + CFStringRef suiteName = CFSTR(DD_PROFILING_USER_DEFAULTS_SUITE_NAME); + CFStringRef key = CFSTR(DD_PROFILING_RECORD_CPU_TIME_KEY); + CFPropertyListRef value = CFPreferencesCopyAppValue(key, suiteName); + + bool result = false; + + if (value) { + if (CFGetTypeID(value) == CFBooleanGetTypeID()) { + result = CFBooleanGetValue((CFBooleanRef)value); + } + CFRelease(value); + } + + return result; +} + /** * Deletes the DatadogProfiling defaults from the `UserDefaults` * to be re-evaluated during `Profiling.enable()`. @@ -163,9 +180,11 @@ void dd_delete_profiling_defaults() { CFStringRef suiteName = CFSTR(DD_PROFILING_USER_DEFAULTS_SUITE_NAME); CFStringRef isEnabledKey = CFSTR(DD_PROFILING_IS_ENABLED_KEY); CFStringRef sampleRateKey = CFSTR(DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY); + CFStringRef recordCPUTimeKey = CFSTR(DD_PROFILING_RECORD_CPU_TIME_KEY); CFPreferencesSetValue(isEnabledKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost); CFPreferencesSetValue(sampleRateKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost); + CFPreferencesSetValue(recordCPUTimeKey, NULL, suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost); CFPreferencesSynchronize(suiteName, kCFPreferencesCurrentUser, kCFPreferencesAnyHost); } @@ -353,7 +372,9 @@ class dd_profiler { if (profiler) return true; - profile = new (std::nothrow) dd::profiler::profile(sampling_interval_ns); + record_cpu_time = read_profiling_record_cpu_time(); + + profile = new (std::nothrow) dd::profiler::profile(sampling_interval_ns, record_cpu_time); if (!profile) { status = DD_PROFILER_STATUS_ALLOCATION_FAILED; return false; @@ -362,6 +383,7 @@ class dd_profiler { sampling_config_t config = SAMPLING_CONFIG_DEFAULT; config.sampling_interval_nanos = sampling_interval_ns; + config.record_cpu_time = record_cpu_time ? 1 : 0; profiler = new (std::nothrow) mach_sampling_profiler(&config, callback, this, hard_limit_bytes); if (!profiler) { @@ -383,6 +405,7 @@ class dd_profiler { uint64_t hard_limit_bytes = DD_PROFILER_DEFAULT_HARD_LIMIT_BYTES; uint64_t sampling_interval_ns = SAMPLING_CONFIG_DEFAULT_INTERVAL_NANOS; int64_t server_time_offset_ns = 0; + bool record_cpu_time = false; /** * Mutex protecting the profile pointer. diff --git a/DatadogProfiling/Mach/include/dd_pprof.h b/DatadogProfiling/Mach/include/dd_pprof.h index b9426a2170..b55858a338 100644 --- a/DatadogProfiling/Mach/include/dd_pprof.h +++ b/DatadogProfiling/Mach/include/dd_pprof.h @@ -42,6 +42,15 @@ typedef struct profile dd_pprof_t; */ dd_pprof_t* dd_pprof_create(uint64_t sampling_interval_ns); +/** + * Create a new pprof profile aggregator with optional CPU-time sample values. + * + * @param sampling_interval_ns The sampling interval in nanoseconds + * @param record_cpu_time Whether samples should include CPU time as a second value + * @return Pointer to the created profile, or NULL on failure + */ +dd_pprof_t* dd_pprof_create_with_cpu_time(uint64_t sampling_interval_ns, bool record_cpu_time); + /** * Destroy a pprof profile aggregator and free all associated memory * diff --git a/DatadogProfiling/Mach/include/dd_profiler.h b/DatadogProfiling/Mach/include/dd_profiler.h index 77e8b939f7..abda1e875a 100644 --- a/DatadogProfiling/Mach/include/dd_profiler.h +++ b/DatadogProfiling/Mach/include/dd_profiler.h @@ -52,6 +52,8 @@ typedef struct stack_trace { uint64_t timestamp; /** Actual sampling interval in nanoseconds for this sample */ uint64_t sampling_interval_nanos; + /** CPU time consumed by this thread since the previous sample */ + uint64_t cpu_time_nanos; /** The stack frames array */ stack_frame_t* frames; /** Number of frames in the trace */ @@ -74,6 +76,8 @@ typedef struct sampling_config { uint32_t max_thread_count; // default: 100 /** QoS class for the sampling thread */ qos_class_t qos_class; + /** Whether samples should include a CPU-time value */ + uint8_t record_cpu_time; } sampling_config_t; /** @@ -100,7 +104,8 @@ static const sampling_config_t SAMPLING_CONFIG_DEFAULT = { SAMPLING_CONFIG_DEFAULT_BUFFER_SIZE, // max_buffer_size SAMPLING_CONFIG_DEFAULT_STACK_DEPTH, // max_stack_depth SAMPLING_CONFIG_DEFAULT_THREAD_COUNT, // max_thread_count - QOS_CLASS_USER_INTERACTIVE // qos_class + QOS_CLASS_USER_INTERACTIVE, // qos_class + 0 // record_cpu_time }; /** @@ -122,6 +127,7 @@ typedef void (*stack_trace_callback_t)(stack_trace_t* traces, size_t count, void #define DD_PROFILING_USER_DEFAULTS_SUITE_NAME "com.datadoghq.ios-sdk.profiling" #define DD_PROFILING_IS_ENABLED_KEY "is_profiling_enabled" #define DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY "profiling_app_launch_sample_rate" +#define DD_PROFILING_RECORD_CPU_TIME_KEY "profiling_record_cpu_time" #ifdef __cplusplus diff --git a/DatadogProfiling/Mach/include/mach_sampling_profiler.h b/DatadogProfiling/Mach/include/mach_sampling_profiler.h index a471170739..b55a6a892c 100644 --- a/DatadogProfiling/Mach/include/mach_sampling_profiler.h +++ b/DatadogProfiling/Mach/include/mach_sampling_profiler.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #ifdef __cplusplus @@ -165,13 +166,23 @@ class mach_sampling_profiler { * @param thread The thread to sample * @param interval_nanos The actual sampling interval in nanoseconds for this sample */ - void sample_thread(thread_t thread, uint64_t interval_nanos); + void sample_thread(thread_t thread, uint64_t interval_nanos, uint64_t cpu_time_nanos); /** * @brief Returns true when the thread is owned by the profiler itself. */ bool is_profiler_internal_thread(thread_t thread) const; + /** + * @brief Returns CPU time consumed since the previous observation for this thread. + */ + uint64_t thread_cpu_time_delta_nanos(thread_t thread); + + /** + * @brief Removes CPU-time state for threads no longer present in the task. + */ + void prune_thread_cpu_time_state(const thread_t* threads, mach_msg_type_number_t count); + private: /** * @brief Static entry point for the sampling thread @@ -184,6 +195,7 @@ class mach_sampling_profiler { std::mutex state_mutex; /// Indicates whether `sampling_thread` currently refers to a live session thread. std::atomic has_sampling_thread{false}; + std::unordered_map previous_thread_cpu_time_nanos; }; } // namespace dd::profiler diff --git a/DatadogProfiling/Mach/include/profile.h b/DatadogProfiling/Mach/include/profile.h index 7115342641..462c596d4e 100644 --- a/DatadogProfiling/Mach/include/profile.h +++ b/DatadogProfiling/Mach/include/profile.h @@ -147,7 +147,7 @@ class profile { * @brief Construct a new profile aggregator * @param sampling_interval_ns Sampling interval in nanoseconds */ - explicit profile(uint64_t sampling_interval_ns); + explicit profile(uint64_t sampling_interval_ns, bool record_cpu_time = false); ~profile() = default; profile(const profile&) = delete; @@ -189,10 +189,16 @@ class profile { /** @brief Get cached string ID for "wall-time" */ uint32_t wall_time_str_id() const { return _wall_time_str_id; } + + /** @brief Get cached string ID for "cpu-time" */ + uint32_t cpu_time_str_id() const { return _cpu_time_str_id; } /** @brief Get cached string ID for "nanoseconds" */ uint32_t nanoseconds_str_id() const { return _nanoseconds_str_id; } + /** @brief Whether samples include a CPU-time value in addition to wall-time */ + bool cpu_time_enabled() const { return _record_cpu_time; } + /** @brief Number of labels exported for the sample */ size_t label_count(const sample_t& sample) const { return sample.labels.size() + 1; } @@ -238,12 +244,18 @@ class profile { /** @brief Profile sampling interval in nanoseconds */ uint64_t _sampling_interval_ns; + + /** @brief Whether samples include CPU time as a second value */ + bool _record_cpu_time; /** @brief Cached string ID for empty string */ uint32_t _empty_str_id; /** @brief Cached string ID for "wall-time" */ uint32_t _wall_time_str_id; + + /** @brief Cached string ID for "cpu-time" */ + uint32_t _cpu_time_str_id; /** @brief Cached string ID for "nanoseconds" */ uint32_t _nanoseconds_str_id; diff --git a/DatadogProfiling/Mach/mach_sampling_profiler.cpp b/DatadogProfiling/Mach/mach_sampling_profiler.cpp index d81be50328..03708d6d5d 100644 --- a/DatadogProfiling/Mach/mach_sampling_profiler.cpp +++ b/DatadogProfiling/Mach/mach_sampling_profiler.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -160,11 +161,32 @@ bool stack_trace_init(stack_trace_t* trace, uint32_t max_depth, uint64_t interva trace->thread_name = nullptr; trace->timestamp = 0; trace->sampling_interval_nanos = interval_nanos; + trace->cpu_time_nanos = 0; trace->frame_count = 0; trace->frames = (stack_frame_t*)malloc(max_depth * sizeof(stack_frame_t)); return trace->frames != nullptr; } +static bool thread_cpu_time_nanos(thread_t thread, uint64_t* cpu_time_nanos) { + if (!cpu_time_nanos) return false; + + thread_basic_info_data_t info{}; + mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT; + if (thread_info(thread, THREAD_BASIC_INFO, reinterpret_cast(&info), &count) != KERN_SUCCESS) { + return false; + } + + const uint64_t user_time_nanos = + (static_cast(info.user_time.seconds) * 1000000000ULL) + + (static_cast(info.user_time.microseconds) * 1000ULL); + const uint64_t system_time_nanos = + (static_cast(info.system_time.seconds) * 1000000000ULL) + + (static_cast(info.system_time.microseconds) * 1000ULL); + + *cpu_time_nanos = user_time_nanos + system_time_nanos; + return true; +} + /** * Destroys a stack trace, freeing the thread name and frames array. * @@ -396,6 +418,7 @@ bool mach_sampling_profiler::start_sampling() { // Clear any leftover data from previous runs sample_buffer.clear(); + previous_thread_cpu_time_nanos.clear(); if (sample_buffer.capacity() < config.max_buffer_size) { sample_buffer.reserve(config.max_buffer_size); } @@ -491,9 +514,10 @@ bool mach_sampling_profiler::is_profiler_internal_thread(thread_t thread) const * @param thread The thread to sample * @param interval_nanos The actual sampling interval in nanoseconds for this sample */ -void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_nanos) { +void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_nanos, uint64_t cpu_time_nanos) { stack_trace_t trace; if (!stack_trace_init(&trace, config.max_stack_depth, interval_nanos)) return; + trace.cpu_time_nanos = cpu_time_nanos; // Get thread info stack_trace_get_thread_info(&trace, thread); @@ -519,6 +543,53 @@ void mach_sampling_profiler::sample_thread(thread_t thread, uint64_t interval_na } } +uint64_t mach_sampling_profiler::thread_cpu_time_delta_nanos(thread_t thread) { + if (!config.record_cpu_time) { + return 0; + } + + uint64_t current_cpu_time_nanos = 0; + if (!thread_cpu_time_nanos(thread, ¤t_cpu_time_nanos)) { + return 0; + } + + auto result = previous_thread_cpu_time_nanos.emplace(thread, current_cpu_time_nanos); + if (result.second) { + return 0; + } + + const uint64_t previous_cpu_time_nanos = result.first->second; + result.first->second = current_cpu_time_nanos; + + if (current_cpu_time_nanos < previous_cpu_time_nanos) { + return 0; + } + + return current_cpu_time_nanos - previous_cpu_time_nanos; +} + +void mach_sampling_profiler::prune_thread_cpu_time_state(const thread_t* threads, mach_msg_type_number_t count) { + if (!config.record_cpu_time || previous_thread_cpu_time_nanos.empty()) { + return; + } + + for (auto it = previous_thread_cpu_time_nanos.begin(); it != previous_thread_cpu_time_nanos.end();) { + bool is_live_thread = false; + for (mach_msg_type_number_t i = 0; i < count; i++) { + if (threads[i] == it->first) { + is_live_thread = true; + break; + } + } + + if (is_live_thread) { + ++it; + } else { + it = previous_thread_cpu_time_nanos.erase(it); + } + } +} + /** * Main sampling loop that collects stack traces from threads. */ @@ -535,7 +606,8 @@ void mach_sampling_profiler::main() { } if (config.profile_current_thread_only) { - sample_thread(pthread_mach_thread_np(target_thread), interval_nanos); + const thread_t thread = pthread_mach_thread_np(target_thread); + sample_thread(thread, interval_nanos, thread_cpu_time_delta_nanos(thread)); if (sample_buffer.size() >= config.max_buffer_size) { worker->enqueue_active_buffer(sample_buffer); } @@ -557,13 +629,15 @@ void mach_sampling_profiler::main() { // Skip profiler-owned threads to avoid self-noise in customer profiles. if (is_profiler_internal_thread(threads[i])) continue; - sample_thread(threads[i], interval_nanos); + sample_thread(threads[i], interval_nanos, thread_cpu_time_delta_nanos(threads[i])); if (sample_buffer.size() >= config.max_buffer_size) { worker->enqueue_active_buffer(sample_buffer); } } + prune_thread_cpu_time_state(threads, count); + // Clean up thread references for (mach_msg_type_number_t i = 0; i < count; i++) { mach_port_deallocate(mach_task_self(), threads[i]); diff --git a/DatadogProfiling/Mach/profile.cpp b/DatadogProfiling/Mach/profile.cpp index b96d1b2440..179271b552 100644 --- a/DatadogProfiling/Mach/profile.cpp +++ b/DatadogProfiling/Mach/profile.cpp @@ -81,8 +81,9 @@ std::string uuid_string(const uuid_t uuid) { * * @param sampling_interval_ns Sampling interval in nanoseconds */ -profile::profile(uint64_t sampling_interval_ns) +profile::profile(uint64_t sampling_interval_ns, bool record_cpu_time) : _sampling_interval_ns(sampling_interval_ns) + , _record_cpu_time(record_cpu_time) , _epoch_offset(uptime_epoch_offset()) , _server_time_offset_ns(0) , _start_timestamp(0) @@ -95,6 +96,7 @@ profile::profile(uint64_t sampling_interval_ns) // Pre-intern common strings for performance _empty_str_id = intern_string(""); _wall_time_str_id = intern_string("wall-time"); + _cpu_time_str_id = _record_cpu_time ? intern_string("cpu-time") : 0; _nanoseconds_str_id = intern_string("nanoseconds"); _end_timestamp_ns_str_id = intern_string("end_timestamp_ns"); _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 sample.timestamp_uptime_ns = trace.timestamp; sample.labels = std::move(labels); sample.values = {static_cast(trace.sampling_interval_nanos)}; + if (_record_cpu_time) { + sample.values.push_back(static_cast(trace.cpu_time_nanos)); + } _samples.push_back(std::move(sample)); diff --git a/DatadogProfiling/Mach/profile_pprof_packer.cpp b/DatadogProfiling/Mach/profile_pprof_packer.cpp index 97d5ac29c8..89ea42d393 100644 --- a/DatadogProfiling/Mach/profile_pprof_packer.cpp +++ b/DatadogProfiling/Mach/profile_pprof_packer.cpp @@ -74,7 +74,7 @@ namespace dd::profiler { void perftools_profiles_profile_add_strings(const std::vector& strings, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator); /** @brief Set sample type definitions (e.g., "cpu"/"nanoseconds", "wall"/"nanoseconds") */ -void perftools_profiles_profile_set_sample_type(int64_t type, int64_t unit, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator); +void perftools_profiles_profile_set_sample_type(const profile& prof, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator); /** @brief Set period type and value for sampling interval */ void perftools_profiles_profile_set_period(int64_t type, int64_t unit, int64_t period, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator); @@ -117,7 +117,7 @@ size_t profile_pprof_pack(const profile& prof, uint8_t** data) { // Convert each component of the profile to protobuf format perftools_profiles_profile_add_strings(prof.strings(), pprof, &profile_allocator); - perftools_profiles_profile_set_sample_type(prof.wall_time_str_id(), prof.nanoseconds_str_id(), pprof, &profile_allocator); + perftools_profiles_profile_set_sample_type(prof, pprof, &profile_allocator); perftools_profiles_profile_set_period(prof.wall_time_str_id(), prof.nanoseconds_str_id(), static_cast(prof.sampling_interval_ns()), pprof, &profile_allocator); perftools_profiles_profile_add_mappings(prof.mappings(), pprof, &profile_allocator); perftools_profiles_profile_add_locations(prof.locations(), pprof, &profile_allocator); @@ -174,20 +174,26 @@ void perftools_profiles_profile_add_strings(const std::vector& stri } } -void perftools_profiles_profile_set_sample_type(int64_t type, int64_t unit, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator) { - // Create wall-time sample types - pprof->n_sample_type = 1; +void perftools_profiles_profile_set_sample_type(const profile& prof, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator) { + pprof->n_sample_type = prof.cpu_time_enabled() ? 2 : 1; pprof->sample_type = static_cast( pb_alloc(allocator, pprof->n_sample_type * sizeof(Perftools__Profiles__ValueType*)) ); - - auto* sample_type_0 = static_cast( - pb_alloc(allocator, sizeof(Perftools__Profiles__ValueType)) - ); - perftools__profiles__value_type__init(sample_type_0); - sample_type_0->type = type; - sample_type_0->unit = unit; - pprof->sample_type[0] = sample_type_0; + + auto add_sample_type = [&](size_t index, int64_t type, int64_t unit) { + auto* sample_type = static_cast( + pb_alloc(allocator, sizeof(Perftools__Profiles__ValueType)) + ); + perftools__profiles__value_type__init(sample_type); + sample_type->type = type; + sample_type->unit = unit; + pprof->sample_type[index] = sample_type; + }; + + add_sample_type(0, prof.wall_time_str_id(), prof.nanoseconds_str_id()); + if (prof.cpu_time_enabled()) { + add_sample_type(1, prof.cpu_time_str_id(), prof.nanoseconds_str_id()); + } } void perftools_profiles_profile_set_period(int64_t type, int64_t unit, int64_t period, Perftools__Profiles__Profile* pprof, ProtobufCAllocator* allocator) { diff --git a/DatadogProfiling/Sources/ProfilerFeature.swift b/DatadogProfiling/Sources/ProfilerFeature.swift index c51417a14e..d56e24bc42 100644 --- a/DatadogProfiling/Sources/ProfilerFeature.swift +++ b/DatadogProfiling/Sources/ProfilerFeature.swift @@ -51,6 +51,12 @@ internal final class ProfilerFeature: DatadogRemoteFeature { let continuousSampleRate = configuration.debugSDK ? .maxSampleRate : configuration.continuousSampleRate self.profilingSamplerProvider = ProfilingSamplerProvider(continuousSampleRate: continuousSampleRate) + let cpuTimeSamplesEnabled = configuration.featureFlags[.cpuTimeSamples] + Self.setProfilingEnabled(in: userDefaults) + Self.setCPUTimeSamplesEnabled(cpuTimeSamplesEnabled, in: userDefaults) + let sampleRate = configuration.debugSDK ? .maxSampleRate : configuration.applicationLaunchSampleRate + Self.setAppLaunch(sampleRate: sampleRate, in: userDefaults) + var messageReceivers: [FeatureMessageReceiver] = [ ProfilingContextMessageReceiver(profilingSamplerProvider: profilingSamplerProvider), AppLaunchProfiler( @@ -70,17 +76,13 @@ internal final class ProfilerFeature: DatadogRemoteFeature { } self.messageReceiver = CombinedFeatureMessageReceiver(messageReceivers) - - setProfilingEnabled(in: userDefaults) - let sampleRate = configuration.debugSDK ? .maxSampleRate : configuration.applicationLaunchSampleRate - setAppLaunch(sampleRate: sampleRate, in: userDefaults) } - private func setProfilingEnabled(in userDefaults: UserDefaults) { //swiftlint:disable:this required_reason_api_name + private static func setProfilingEnabled(in userDefaults: UserDefaults) { //swiftlint:disable:this required_reason_api_name userDefaults.setValue(true, forKey: DD_PROFILING_IS_ENABLED_KEY) } - private func setAppLaunch(sampleRate: SampleRate, in userDefaults: UserDefaults) { //swiftlint:disable:this required_reason_api_name + private static func setAppLaunch(sampleRate: SampleRate, in userDefaults: UserDefaults) { //swiftlint:disable:this required_reason_api_name let previousSampleRate = userDefaults.value(forKey: DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY) as? SampleRate // Profiling will use the lowest sample rate @@ -89,6 +91,10 @@ internal final class ProfilerFeature: DatadogRemoteFeature { userDefaults.setValue(sampleRate, forKey: DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY) } } + + private static func setCPUTimeSamplesEnabled(_ enabled: Bool, in userDefaults: UserDefaults) { //swiftlint:disable:this required_reason_api_name + userDefaults.setValue(enabled, forKey: DD_PROFILING_RECORD_CPU_TIME_KEY) + } } #endif diff --git a/DatadogProfiling/Sources/ProfilingConfiguration.swift b/DatadogProfiling/Sources/ProfilingConfiguration.swift index 4d23a07a86..20ba7f774e 100644 --- a/DatadogProfiling/Sources/ProfilingConfiguration.swift +++ b/DatadogProfiling/Sources/ProfilingConfiguration.swift @@ -30,6 +30,9 @@ extension Profiling { /// Default: `5.0`. public var continuousSampleRate: SampleRate + /// Feature flags to preview features in Profiling. + public var featureFlags: FeatureFlags + // MARK: - Internal internal var debugSDK: Bool = ProcessInfo.processInfo.arguments.contains(LaunchArguments.Debug) @@ -42,13 +45,41 @@ extension Profiling { public init( customEndpoint: URL? = nil, applicationLaunchSampleRate: SampleRate = 5, - continuousSampleRate: SampleRate = 5 + continuousSampleRate: SampleRate = 5, + featureFlags: FeatureFlags = .defaults ) { self.customEndpoint = customEndpoint self.applicationLaunchSampleRate = applicationLaunchSampleRate self.continuousSampleRate = continuousSampleRate + self.featureFlags = featureFlags } } } +extension Profiling.Configuration { + public typealias FeatureFlags = [FeatureFlag: Bool] + + /// Feature flags available in Profiling. + public enum FeatureFlag: String { + /// Adds CPU-time sample values alongside wall-time sample values. + case cpuTimeSamples + } +} + +extension Profiling.Configuration.FeatureFlags { + /// The default feature flags applied to Profiling configuration. + public static var defaults: Self { + [ + .cpuTimeSamples: false, + ] + } + + /// Accesses a feature flag value. + /// + /// Returns false by default. + public subscript(flag: Key) -> Bool { + self[flag, default: false] + } +} + #endif diff --git a/DatadogProfiling/Tests/DDProfilerTests.swift b/DatadogProfiling/Tests/DDProfilerTests.swift index ff7435f4a4..7e08103b58 100644 --- a/DatadogProfiling/Tests/DDProfilerTests.swift +++ b/DatadogProfiling/Tests/DDProfilerTests.swift @@ -19,12 +19,14 @@ final class DDProfilerTests: XCTestCase { // `tearDown` leaves `g_dd_profiler` nil; without this, only the first test would match the // static constructor's state. Recreate with 0% sample rate so `auto_start` leaves `NOT_STARTED`. dd_profiler_destroy() + dd_delete_profiling_defaults() dd_profiler_start_testing(0, false, 5.seconds.dd.toInt64Nanoseconds, 0) } override func tearDown() { dd_profiler_stop() dd_profiler_destroy() + dd_delete_profiling_defaults() super.tearDown() } @@ -146,6 +148,36 @@ final class DDProfilerTests: XCTestCase { ) } + func testDDProfiler_withCPUTimingEnabled_serializesDualSampleValues() throws { + dd_profiler_destroy() + let userDefaults = try XCTUnwrap(UserDefaults(suiteName: DD_PROFILING_USER_DEFAULTS_SUITE_NAME)) + userDefaults.setValue(true, forKey: DD_PROFILING_RECORD_CPU_TIME_KEY) + + XCTAssertEqual(dd_profiler_start(), 1) + XCTAssertEqual(dd_profiler_get_status(), DD_PROFILER_STATUS_RUNNING) + + for i in 0..<10_000 { + _ = sqrt(Double(i)) + if i % 500 == 0 { + Thread.sleep(forTimeInterval: 0.002) + } + } + + let profile = try XCTUnwrap(dd_profiler_flush_and_get_profile()) + defer { dd_pprof_destroy(profile) } + + var data: UnsafeMutablePointer? + let size = dd_pprof_serialize(profile, &data) + defer { dd_pprof_free_serialized_data(data) } + + let unpackedProfile = try XCTUnwrap(perftools__profiles__profile__unpack(nil, size, data)) + defer { perftools__profiles__profile__free_unpacked(unpackedProfile, nil) } + + XCTAssertEqual(unpackedProfile.pointee.n_sample_type, 2) + let sample = try XCTUnwrap(unpackedProfile.pointee.sample[0]) + XCTAssertEqual(sample.pointee.n_value, 2) + } + func testDDProfiler_startTesting_withPrewarming_doesNotStart() { dd_profiler_start_testing(100, true, 5.seconds.dd.toInt64Nanoseconds, 0) // prewarming = true XCTAssertEqual(dd_profiler_get_status(), DD_PROFILER_STATUS_PREWARMED, "Profiler should not start when prewarming is active") diff --git a/DatadogProfiling/Tests/ProfileCxxTests.swift b/DatadogProfiling/Tests/ProfileCxxTests.swift index e8cd9bd10d..69e72ab0e3 100644 --- a/DatadogProfiling/Tests/ProfileCxxTests.swift +++ b/DatadogProfiling/Tests/ProfileCxxTests.swift @@ -373,6 +373,46 @@ final class ProfileCxxTests: XCTestCase { XCTAssertEqual(unpackedProfile.pointee.n_sample_type, 1, "Should have one sample type") } + func testProfileAggregation_withCPUTimingEnabled_serializesWallAndCPUValues() throws { + // Given + let profile = try XCTUnwrap(dd_pprof_create_with_cpu_time(10_000_000, true)) + defer { dd_pprof_destroy(profile) } + + let trace = UnsafeMutablePointer.allocate(capacity: 1) + trace.pointee = .mockWith( + tid: 1, + addresses: [0x100001000, 0x100002000], + samplingIntervalNanos: 10_000_000, + cpuTimeNanos: 4_000_000 + ) + defer { dd_free(trace) } + + // When + dd_pprof_add_samples(profile, trace, 1) + + var data: UnsafeMutablePointer? + let size = dd_pprof_serialize(profile, &data) + defer { dd_pprof_free_serialized_data(data) } + + // Then + let unpackedProfile = try XCTUnwrap(perftools__profiles__profile__unpack(nil, size, data)) + defer { perftools__profiles__profile__free_unpacked(unpackedProfile, nil) } + + XCTAssertEqual(unpackedProfile.pointee.n_sample_type, 2, "CPU timing should add a second sample type") + + let wallSampleType = try XCTUnwrap(unpackedProfile.pointee.sample_type[0]) + let cpuSampleType = try XCTUnwrap(unpackedProfile.pointee.sample_type[1]) + let wallType = try XCTUnwrap(unpackedProfile.pointee.string_table[Int(wallSampleType.pointee.type)]) + let cpuType = try XCTUnwrap(unpackedProfile.pointee.string_table[Int(cpuSampleType.pointee.type)]) + XCTAssertEqual(String(cString: wallType), "wall-time") + XCTAssertEqual(String(cString: cpuType), "cpu-time") + + let sample = try XCTUnwrap(unpackedProfile.pointee.sample[0]) + XCTAssertEqual(sample.pointee.n_value, 2) + XCTAssertEqual(sample.pointee.value[0], 10_000_000) + XCTAssertEqual(sample.pointee.value[1], 4_000_000) + } + func testProfileAggregation_withMissingImageCache_fallsBackToBinaryLookup() throws { // Given let profile = try XCTUnwrap(dd_pprof_create(10_000_000)) diff --git a/DatadogProfiling/Tests/ProfileMocks.swift b/DatadogProfiling/Tests/ProfileMocks.swift index 88d4c3d828..6c53b0a800 100644 --- a/DatadogProfiling/Tests/ProfileMocks.swift +++ b/DatadogProfiling/Tests/ProfileMocks.swift @@ -34,6 +34,7 @@ extension stack_trace_t { threadName: StaticString = "TestThread", timestamp: UInt64? = nil, samplingIntervalNanos: UInt64 = 10_000_000, + cpuTimeNanos: UInt64 = 0, binaryImage: binary_image_t = .mockAny() ) -> stack_trace_t { let frameCount = UInt32(addresses.count) @@ -56,6 +57,7 @@ extension stack_trace_t { thread_name: UnsafeRawPointer(threadName.utf8Start).assumingMemoryBound(to: CChar.self), timestamp: timestamp ?? UInt64(Date().timeIntervalSince1970 * 1_000_000_000), sampling_interval_nanos: samplingIntervalNanos, + cpu_time_nanos: cpuTimeNanos, frames: frames, frame_count: frameCount ) diff --git a/DatadogProfiling/Tests/ProfilerFeatureTests.swift b/DatadogProfiling/Tests/ProfilerFeatureTests.swift index f79c4edf76..6c0bda0355 100644 --- a/DatadogProfiling/Tests/ProfilerFeatureTests.swift +++ b/DatadogProfiling/Tests/ProfilerFeatureTests.swift @@ -67,6 +67,37 @@ final class ProfilerFeatureTests: XCTestCase { XCTAssertEqual(userDefaults.value(forKey: DD_PROFILING_APP_LAUNCH_SAMPLE_RATE_KEY) as? SampleRate, newSampleRate) } + func testInit_setsCPUTimingFeatureFlagValue() { + // Given + XCTAssertNil(userDefaults.value(forKey: DD_PROFILING_RECORD_CPU_TIME_KEY)) + + // When + _ = ProfilerFeature( + core: core, + configuration: .init(featureFlags: [.cpuTimeSamples: true]), + requestBuilder: requestBuilder, + telemetryController: telemetryController, + userDefaults: userDefaults + ) + + // Then + XCTAssertEqual(userDefaults.value(forKey: DD_PROFILING_RECORD_CPU_TIME_KEY) as? Bool, true) + } + + func testInit_setsCPUTimingFeatureFlagToFalseByDefault() { + // When + _ = ProfilerFeature( + core: core, + configuration: .init(), + requestBuilder: requestBuilder, + telemetryController: telemetryController, + userDefaults: userDefaults + ) + + // Then + XCTAssertEqual(userDefaults.value(forKey: DD_PROFILING_RECORD_CPU_TIME_KEY) as? Bool, false) + } + func testInit_overridesPreviousSampleRate_whenNewSampleRateIsLower() { // Given let previousSampleRate: SampleRate = 80 diff --git a/DatadogProfiling/Tests/ProfilingConfigurationTests.swift b/DatadogProfiling/Tests/ProfilingConfigurationTests.swift index ee7b195602..14d97c750b 100644 --- a/DatadogProfiling/Tests/ProfilingConfigurationTests.swift +++ b/DatadogProfiling/Tests/ProfilingConfigurationTests.swift @@ -18,6 +18,20 @@ final class ProfilingConfigurationTests: XCTestCase { // Then XCTAssertEqual(config.customEndpoint, endpoint) XCTAssertEqual(config.applicationLaunchSampleRate, 5) + XCTAssertEqual(config.continuousSampleRate, 5) + XCTAssertFalse(config.featureFlags[.cpuTimeSamples]) + } + + func testConfigurationWithCPUTimingFeatureFlag() { + // When + let config = Profiling.Configuration( + featureFlags: [ + .cpuTimeSamples: true + ] + ) + + // Then + XCTAssertTrue(config.featureFlags[.cpuTimeSamples]) } }