| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. |
| |
| #include "src/media/audio/audio_core/reporter.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <queue> |
| |
| #include "src/media/audio/audio_core/audio_driver.h" |
| #include "src/media/audio/audio_core/media_metrics_registry.cb.h" |
| |
| namespace media::audio { |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Singletons |
| |
| namespace { |
| static std::mutex singleton_mutex; |
| static Reporter* const singleton_nop = new Reporter(); |
| static Reporter* singleton_real; |
| |
| class TokenBucket { |
| public: |
| TokenBucket(zx::duration period, uint64_t tokens_per_period) |
| : period_(period), |
| tokens_per_period_(tokens_per_period), |
| start_time_(zx::clock::get_monotonic()), |
| tokens_(tokens_per_period) {} |
| |
| bool Acquire() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| auto now = zx::clock::get_monotonic(); |
| if (now - start_time_ >= period_) { |
| start_time_ = now; |
| tokens_ = tokens_per_period_; |
| } |
| if (tokens_ == 0) { |
| return false; |
| } |
| --tokens_; |
| return true; |
| } |
| |
| private: |
| const zx::duration period_; |
| const uint64_t tokens_per_period_; |
| |
| std::mutex mutex_; |
| zx::time start_time_ FXL_GUARDED_BY(mutex_); |
| uint64_t tokens_ FXL_GUARDED_BY(mutex_); |
| }; |
| |
| // To avoid overloading cobalt, throttle cobalt RPCs. See fxbug.dev/67416. |
| // In a typical worst case, we might expect about 1 RPC every 10ms, or 6000 RPCs per minute. |
| // Throttle to 30 per minute. |
| static TokenBucket* const cobalt_token_bucket = new TokenBucket(zx::min(1), 30); |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // No-op implementations, used before the Reporter has been initialized |
| |
| namespace { |
| class OutputDeviceNop : public Reporter::OutputDevice { |
| public: |
| void Destroy() override {} |
| |
| void StartSession(zx::time start_time) override {} |
| void StopSession(zx::time stop_time) override {} |
| |
| void SetDriverInfo(const AudioDriver& driver) override {} |
| void SetGainInfo(const fuchsia::media::AudioGainInfo& gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) override {} |
| void DeviceUnderflow(zx::time start_time, zx::time end_time) override {} |
| void PipelineUnderflow(zx::time start_time, zx::time end_time) override {} |
| }; |
| |
| class InputDeviceNop : public Reporter::InputDevice { |
| public: |
| void Destroy() override {} |
| |
| void StartSession(zx::time start_time) override {} |
| void StopSession(zx::time stop_time) override {} |
| |
| void SetDriverInfo(const AudioDriver& driver) override {} |
| void SetGainInfo(const fuchsia::media::AudioGainInfo& gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) override {} |
| }; |
| |
| class RendererNop : public Reporter::Renderer { |
| public: |
| void Destroy() override {} |
| |
| void StartSession(zx::time start_time) override {} |
| void StopSession(zx::time stop_time) override {} |
| |
| void SetUsage(RenderUsage usage) override {} |
| void SetFormat(const Format& format) override {} |
| void SetGain(float gain_db) override {} |
| void SetGainWithRamp(float gain_db, zx::duration duration, |
| fuchsia::media::audio::RampType ramp_type) override {} |
| void SetFinalGain(float gain_db) override {} |
| void SetMute(bool muted) override {} |
| void SetMinLeadTime(zx::duration min_lead_time) override {} |
| void SetPtsContinuityThreshold(float threshold_seconds) override {} |
| |
| void AddPayloadBuffer(uint32_t buffer_id, uint64_t size) override {} |
| void RemovePayloadBuffer(uint32_t buffer_id) override {} |
| void SendPacket(const fuchsia::media::StreamPacket& packet) override {} |
| void Underflow(zx::time start_time, zx::time end_time) override {} |
| }; |
| |
| class CapturerNop : public Reporter::Capturer { |
| public: |
| void Destroy() override {} |
| |
| void StartSession(zx::time start_time) override {} |
| void StopSession(zx::time stop_time) override {} |
| |
| void SetUsage(CaptureUsage usage) override {} |
| void SetFormat(const Format& format) override {} |
| void SetGain(float gain_db) override {} |
| void SetGainWithRamp(float gain_db, zx::duration duration, |
| fuchsia::media::audio::RampType ramp_type) override {} |
| void SetMute(bool muted) override {} |
| void SetMinFenceTime(zx::duration min_fence_time) override {} |
| |
| void AddPayloadBuffer(uint32_t buffer_id, uint64_t size) override {} |
| void SendPacket(const fuchsia::media::StreamPacket& packet) override {} |
| void Overflow(zx::time start_time, zx::time end_time) override {} |
| }; |
| |
| class VolumeControlNop : public Reporter::VolumeControl { |
| public: |
| void Destroy() override {} |
| |
| void SetVolumeMute(float volume, bool mute) override {} |
| void AddBinding(std::string name) override {} |
| }; |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Real implementations of reporting objects. |
| |
| class Reporter::OverflowUnderflowTracker { |
| public: |
| // Trackers begin in a "stopped" state and must move to a "started" state |
| // before metrics can be reported. The Start/Stop events are intended to |
| // mirror higher-level Play/Pause or Record/Stop events. If a session is not |
| // stopped explicitly, it's stopped automatically by our destructor. |
| void StartSession(zx::time start_time); |
| void StopSession(zx::time stop_time); |
| |
| // Report an event with the given start and end times. |
| void Report(zx::time start_time, zx::time end_time); |
| |
| struct Args { |
| uint32_t component; |
| std::string event_name; |
| inspect::Node& parent_node; |
| Reporter::Impl& impl; |
| bool is_underflow; |
| uint32_t cobalt_component_id; |
| }; |
| OverflowUnderflowTracker(Args args); |
| ~OverflowUnderflowTracker(); |
| |
| private: |
| void RestartSession(); |
| zx::duration ComputeDurationOfAllSessions(); |
| void LogCobaltDuration(uint32_t metric_id, std::vector<uint32_t> event_codes, zx::duration d); |
| |
| std::mutex mutex_; |
| |
| enum class State { Stopped, Started }; |
| State state_ FXL_GUARDED_BY(mutex_); |
| |
| // Ideally we'd record final cobalt metrics when the component exits, however we can't |
| // be notified of component exit until we've switched to Components v2. In the interim, |
| // we automatically restart sessions every hour. Inspect metrics don't have this limitation |
| // and can use the "real" session times. |
| static constexpr auto kMaxSessionDuration = zx::hour(1); |
| async::TaskClosureMethod<OverflowUnderflowTracker, &OverflowUnderflowTracker::RestartSession> |
| restart_session_timer_ FXL_GUARDED_BY(mutex_){this}; |
| |
| zx::time last_event_time_ FXL_GUARDED_BY(mutex_); // for cobalt |
| zx::time session_start_time_ FXL_GUARDED_BY(mutex_); // for cobalt |
| zx::time session_real_start_time_ FXL_GUARDED_BY(mutex_); // for inspect |
| zx::duration past_sessions_duration_ FXL_GUARDED_BY(mutex_); // for inspect |
| |
| inspect::Node node_; |
| inspect::UintProperty event_count_; |
| inspect::UintProperty event_duration_; |
| inspect::UintProperty session_count_; |
| inspect::LazyNode total_duration_; |
| |
| Reporter::Impl& impl_; |
| const uint32_t cobalt_component_id_; |
| const uint32_t cobalt_event_duration_metric_id_; |
| const uint32_t cobalt_time_since_last_event_or_session_start_metric_id_; |
| }; |
| |
| class Reporter::ObjectTracker { |
| public: |
| ObjectTracker(Reporter::Impl& impl, AudioObjectsCreatedMetricDimensionObjectType object_type) |
| : impl_(impl), object_type_(object_type) {} |
| |
| void SetFormat(const Format& f) { format_ = f; } |
| |
| // Marks the object "enabled", which triggers a cobalt metric increment. |
| void Enable() { |
| // Ignore when cobalt is disabled. |
| auto& logger = impl_.cobalt_logger; |
| if (!logger) { |
| return; |
| } |
| |
| // Log exactly once. Don't bother throttling: if we're creating objects quickly enough |
| // to overload cobalt with these RPCs, we'll hit many other problems first. |
| if (enabled_ || !format_) { |
| return; |
| } |
| enabled_ = true; |
| |
| AudioObjectsCreatedMetricDimensionSampleFormat sample_format; |
| switch (format_->sample_format()) { |
| case fuchsia::media::AudioSampleFormat::UNSIGNED_8: |
| sample_format = AudioObjectsCreatedMetricDimensionSampleFormat::Uint8; |
| break; |
| case fuchsia::media::AudioSampleFormat::SIGNED_16: |
| sample_format = AudioObjectsCreatedMetricDimensionSampleFormat::Int16; |
| break; |
| case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32: |
| sample_format = AudioObjectsCreatedMetricDimensionSampleFormat::Int24; |
| break; |
| case fuchsia::media::AudioSampleFormat::FLOAT: |
| sample_format = AudioObjectsCreatedMetricDimensionSampleFormat::Float32; |
| break; |
| default: |
| sample_format = AudioObjectsCreatedMetricDimensionSampleFormat::Other; |
| break; |
| } |
| AudioObjectsCreatedMetricDimensionChannels channels; |
| switch (format_->channels()) { |
| case 1: |
| channels = AudioObjectsCreatedMetricDimensionChannels::Chan1; |
| break; |
| case 2: |
| channels = AudioObjectsCreatedMetricDimensionChannels::Chan2; |
| break; |
| case 4: |
| channels = AudioObjectsCreatedMetricDimensionChannels::Chan4; |
| break; |
| default: |
| channels = AudioObjectsCreatedMetricDimensionChannels::Other; |
| break; |
| } |
| AudioObjectsCreatedMetricDimensionFrameRate frame_rate; |
| switch (format_->frames_per_second()) { |
| case 16000: |
| frame_rate = AudioObjectsCreatedMetricDimensionFrameRate::Rate16000; |
| break; |
| case 44100: |
| frame_rate = AudioObjectsCreatedMetricDimensionFrameRate::Rate44100; |
| break; |
| case 48000: |
| frame_rate = AudioObjectsCreatedMetricDimensionFrameRate::Rate48000; |
| break; |
| case 96000: |
| frame_rate = AudioObjectsCreatedMetricDimensionFrameRate::Rate96000; |
| break; |
| default: |
| frame_rate = AudioObjectsCreatedMetricDimensionFrameRate::Other; |
| break; |
| } |
| auto e = fuchsia::cobalt::CobaltEvent{ |
| .metric_id = kAudioObjectsCreatedMetricId, |
| .event_codes = {object_type_, sample_format, channels, frame_rate}, |
| .payload = |
| fuchsia::cobalt::EventPayload::WithEventCount(fuchsia::cobalt::CountEvent{.count = 1}), |
| }; |
| logger->LogCobaltEvent(std::move(e)); |
| } |
| |
| private: |
| Reporter::Impl& impl_; |
| AudioObjectsCreatedMetricDimensionObjectType object_type_; |
| std::optional<Format> format_; |
| bool enabled_ = false; |
| }; |
| |
| class FormatInfo { |
| public: |
| FormatInfo(inspect::Node& parent_node, const std::string& name) |
| : node_(parent_node.CreateChild(name)), |
| sample_format_(node_.CreateString("sample format", "unknown")), |
| channels_(node_.CreateUint("channels", 0)), |
| frames_per_second_(node_.CreateUint("frames per second", 0)) {} |
| |
| void Set(const Format& f) { |
| switch (f.sample_format()) { |
| case fuchsia::media::AudioSampleFormat::UNSIGNED_8: |
| sample_format_.Set("UNSIGNED_8"); |
| break; |
| case fuchsia::media::AudioSampleFormat::SIGNED_16: |
| sample_format_.Set("SIGNED_16"); |
| break; |
| case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32: |
| sample_format_.Set("SIGNED_24_IN_32"); |
| break; |
| case fuchsia::media::AudioSampleFormat::FLOAT: |
| sample_format_.Set("FLOAT"); |
| break; |
| default: |
| sample_format_.Set("unknown"); |
| FX_LOGS(ERROR) << "Unhandled sample stream_type type " |
| << fidl::ToUnderlying(f.sample_format()); |
| break; |
| } |
| channels_.Set(f.channels()); |
| frames_per_second_.Set(f.frames_per_second()); |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::StringProperty sample_format_; |
| inspect::UintProperty channels_; |
| inspect::UintProperty frames_per_second_; |
| }; |
| |
| class Reporter::DeviceDriverInfo { |
| public: |
| DeviceDriverInfo(inspect::Node& parent_node, ObjectTracker&& object_tracker) |
| : node_(parent_node.CreateChild("driver")), |
| name_(node_.CreateString("name", "unknown")), |
| total_delay_(node_.CreateUint("external delay + fifo delay (ns)", 0)), |
| external_delay_(node_.CreateUint("external delay (ns)", 0)), |
| fifo_delay_(node_.CreateUint("fifo delay (ns)", 0)), |
| fifo_depth_(node_.CreateUint("fifo depth in frames", 0)), |
| format_(parent_node, "format"), |
| object_tracker_(std::move(object_tracker)) {} |
| |
| void Set(const AudioDriver& d) { |
| name_.Set(d.manufacturer_name() + ' ' + d.product_name()); |
| total_delay_.Set((d.external_delay() + d.fifo_depth_duration()).get()); |
| external_delay_.Set(d.external_delay().get()); |
| fifo_delay_.Set(d.fifo_depth_duration().get()); |
| fifo_depth_.Set(d.fifo_depth_frames()); |
| if (auto f = d.GetFormat(); f.has_value()) { |
| format_.Set(*f); |
| object_tracker_.SetFormat(*f); |
| object_tracker_.Enable(); |
| } |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::StringProperty name_; |
| inspect::UintProperty total_delay_; |
| inspect::UintProperty external_delay_; |
| inspect::UintProperty fifo_delay_; |
| inspect::UintProperty fifo_depth_; |
| FormatInfo format_; |
| ObjectTracker object_tracker_; |
| }; |
| |
| class DeviceGainInfo { |
| public: |
| DeviceGainInfo(inspect::Node& node) |
| : gain_db_(node.CreateDouble("gain db", 0.0)), |
| muted_(node.CreateBool("muted", false)), |
| agc_supported_(node.CreateBool("agc supported", false)), |
| agc_enabled_(node.CreateBool("agc enabled", false)) {} |
| |
| void Set(const fuchsia::media::AudioGainInfo& gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) { |
| if ((set_flags & fuchsia::media::AudioGainValidFlags::GAIN_VALID) == |
| fuchsia::media::AudioGainValidFlags::GAIN_VALID) { |
| gain_db_.Set(gain_info.gain_db); |
| } |
| |
| if ((set_flags & fuchsia::media::AudioGainValidFlags::MUTE_VALID) == |
| fuchsia::media::AudioGainValidFlags::MUTE_VALID) { |
| muted_.Set((gain_info.flags & fuchsia::media::AudioGainInfoFlags::MUTE) == |
| fuchsia::media::AudioGainInfoFlags::MUTE); |
| } |
| |
| if ((set_flags & fuchsia::media::AudioGainValidFlags::AGC_VALID) == |
| fuchsia::media::AudioGainValidFlags::AGC_VALID) { |
| agc_supported_.Set((gain_info.flags & fuchsia::media::AudioGainInfoFlags::AGC_SUPPORTED) == |
| fuchsia::media::AudioGainInfoFlags::AGC_SUPPORTED); |
| agc_enabled_.Set((gain_info.flags & fuchsia::media::AudioGainInfoFlags::AGC_ENABLED) == |
| fuchsia::media::AudioGainInfoFlags::AGC_ENABLED); |
| } |
| } |
| |
| private: |
| inspect::DoubleProperty gain_db_; |
| inspect::BoolProperty muted_; |
| inspect::BoolProperty agc_supported_; |
| inspect::BoolProperty agc_enabled_; |
| }; |
| |
| class Reporter::ThermalStateTransition { |
| public: |
| ThermalStateTransition(inspect::Node& parent, std::string name, uint32_t state) |
| : node_(parent.CreateChild(name)), |
| state_(node_.CreateString("state", state ? std::to_string(state) : "normal")), |
| active_(node_.CreateBool("active", true)), |
| duration_(node_.CreateLazyValues( |
| "ThermalStateTransitionDuration", |
| [this] { |
| std::lock_guard<std::mutex> lock(mutex_); |
| inspect::Inspector i; |
| i.GetRoot().CreateUint( |
| "duration (ns)", |
| (alive_ ? zx::clock::get_monotonic() - start_time_ : past_duration_).get(), &i); |
| return fit::make_ok_promise(std::move(i)); |
| })), |
| start_time_(zx::clock::get_monotonic()) {} |
| |
| void Destroy() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| past_duration_ = zx::clock::get_monotonic() - start_time_; |
| alive_ = false; |
| active_.Set(false); |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::StringProperty state_; |
| inspect::BoolProperty active_; |
| inspect::LazyNode duration_; |
| |
| const zx::time start_time_; |
| |
| std::mutex mutex_; |
| bool alive_ FXL_GUARDED_BY(mutex_) = true; |
| zx::duration past_duration_ FXL_GUARDED_BY(mutex_); |
| }; |
| |
| class Reporter::ThermalStateTracker { |
| public: |
| ThermalStateTracker(Reporter::Impl& impl); |
| |
| void SetNumThermalStates(size_t num); |
| void SetThermalState(uint32_t state); |
| |
| private: |
| using CobaltStateTransition = AudioThermalStateTransitionsMetricDimensionStateTransition; |
| static constexpr std::array kCobaltStateTransitions = { |
| CobaltStateTransition::Normal, CobaltStateTransition::State1, CobaltStateTransition::State2}; |
| // Ideally we'd record final cobalt metrics when the component exits, however we can't |
| // be notified of component exit until we've switched to Components v2. In the interim, |
| // we automatically restart sessions every 5 min. |
| static constexpr auto kCobaltDataCollectionInterval = zx::min(5); |
| |
| struct State { |
| // Total duration this state has been active, not counting |
| // the current activation if this is the current state. |
| zx::duration past_duration; |
| // Set if this is the current thermal state. |
| std::optional<zx::time> current_activation_time; |
| std::optional<zx::time> interval_start_time; // Cobalt |
| // Running total of transitions to State. |
| uint32_t total_transitions; |
| |
| inspect::Node node; |
| inspect::LazyNode duration; |
| |
| void Activate(); |
| void Deactivate(); |
| }; |
| |
| void LogCobaltStateTransition(State& state, CobaltStateTransition event) FXL_REQUIRE(mutex_); |
| void LogCobaltStateDuration(State& old_state, State& new_state) FXL_REQUIRE(mutex_); |
| void ResetInterval(); |
| |
| Reporter::Impl& impl_; |
| inspect::Node root_; |
| inspect::UintProperty num_thermal_states_; |
| |
| inspect::Node transitions_node_; |
| Container<ThermalStateTransition, kThermalStatesToCache> thermal_state_transitions_; |
| |
| std::mutex mutex_; |
| uint32_t active_state_ FXL_GUARDED_BY(mutex_); |
| std::unordered_map<uint32_t, State> states_ FXL_GUARDED_BY(mutex_); |
| async::TaskClosureMethod<ThermalStateTracker, &ThermalStateTracker::ResetInterval> |
| restart_interval_timer_ FXL_GUARDED_BY(mutex_){this}; |
| |
| uint64_t next_thermal_transition_name_ FXL_GUARDED_BY(mutex_) = 0; |
| Container<ThermalStateTransition, kThermalStatesToCache>::Ptr last_transition_ |
| FXL_GUARDED_BY(mutex_); |
| }; |
| |
| namespace { |
| std::string UsageBehaviorToString(fuchsia::media::Behavior behavior) { |
| switch (behavior) { |
| case fuchsia::media::Behavior::NONE: |
| return "NONE"; |
| case fuchsia::media::Behavior::DUCK: |
| return "DUCK"; |
| case fuchsia::media::Behavior::MUTE: |
| return "MUTE"; |
| default: |
| FX_CHECK(false) << "Invalid fuchsia::media::Behavior: " << static_cast<int>(behavior); |
| } |
| } |
| } // namespace |
| |
| class Reporter::ActiveUsagePolicy { |
| public: |
| ActiveUsagePolicy(inspect::Node& parent, std::string name, |
| const std::vector<fuchsia::media::Usage>& active_usages, |
| const AudioAdmin::RendererPolicies& render_usage_behaviors, |
| const AudioAdmin::CapturerPolicies& capture_usage_behaviors) |
| : node_(parent.CreateChild(name)), active_(node_.CreateBool("active", true)) { |
| for (auto& active_usage : active_usages) { |
| if (active_usage.is_render_usage()) { |
| auto usage = StreamUsage::WithRenderUsage(active_usage.render_usage()).ToString(); |
| auto behavior = UsageBehaviorToString( |
| render_usage_behaviors[static_cast<int>(active_usage.render_usage())]); |
| renderer_policies_.emplace(node_.CreateString(usage, behavior)); |
| } else { |
| auto usage = StreamUsage::WithCaptureUsage(active_usage.capture_usage()).ToString(); |
| auto behavior = UsageBehaviorToString( |
| capture_usage_behaviors[static_cast<int>(active_usage.capture_usage())]); |
| capturer_policies_.emplace(node_.CreateString(usage, behavior)); |
| } |
| } |
| } |
| |
| void Destroy() { active_.Set(false); } |
| |
| private: |
| inspect::Node node_; |
| inspect::BoolProperty active_; |
| |
| std::queue<inspect::StringProperty> renderer_policies_; |
| std::queue<inspect::StringProperty> capturer_policies_; |
| }; |
| |
| class Reporter::ActiveUsagePolicyTracker { |
| public: |
| explicit ActiveUsagePolicyTracker(Reporter::Impl& impl) |
| : node_(impl.inspector->root().CreateChild("active usage policies")), |
| none_gain_(node_.CreateDouble("none gain db", 0.0)), |
| duck_gain_(node_.CreateDouble("duck gain db", 0.0)), |
| mute_gain_(node_.CreateDouble("mute gain db", 0.0)), |
| last_policy_(active_usage_policies_.New(new ActiveUsagePolicy( |
| node_, std::to_string(++next_active_usage_policy_name_), |
| std::vector<fuchsia::media::Usage>(), AudioAdmin::RendererPolicies(), |
| AudioAdmin::CapturerPolicies()))) {} |
| |
| void SetAudioPolicyBehaviorGain(AudioAdmin::BehaviorGain behavior_gain) { |
| none_gain_.Set(behavior_gain.none_gain_db); |
| duck_gain_.Set(behavior_gain.duck_gain_db); |
| mute_gain_.Set(behavior_gain.mute_gain_db); |
| } |
| |
| void UpdateActiveUsagePolicy(const std::vector<fuchsia::media::Usage>& active_usages, |
| const AudioAdmin::RendererPolicies& renderer_policies, |
| const AudioAdmin::CapturerPolicies& capturer_policies) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| last_policy_ = active_usage_policies_.New( |
| new ActiveUsagePolicy(node_, std::to_string(++next_active_usage_policy_name_), |
| active_usages, renderer_policies, capturer_policies)); |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::DoubleProperty none_gain_; |
| inspect::DoubleProperty duck_gain_; |
| inspect::DoubleProperty mute_gain_; |
| |
| Container<ActiveUsagePolicy, kActiveUsagePoliciesToCache> active_usage_policies_; |
| |
| std::mutex mutex_; |
| uint64_t next_active_usage_policy_name_ FXL_GUARDED_BY(mutex_) = 0; |
| Container<ActiveUsagePolicy, kActiveUsagePoliciesToCache>::Ptr last_policy_ |
| FXL_GUARDED_BY(mutex_); |
| }; |
| |
| class Reporter::VolumeSetting { |
| public: |
| explicit VolumeSetting(inspect::Node& parent, std::string name, float volume, bool mute) |
| : node_(parent.CreateChild(name)), |
| active_(node_.CreateBool("active", true)), |
| volume_(node_.CreateDouble("volume", volume)), |
| mute_(node_.CreateBool("mute", mute)) {} |
| |
| void Destroy() { active_.Set(false); } |
| |
| private: |
| inspect::Node node_; |
| inspect::BoolProperty active_; |
| inspect::DoubleProperty volume_; |
| inspect::BoolProperty mute_; |
| }; |
| |
| class Reporter::VolumeControlImpl : public Reporter::VolumeControl { |
| public: |
| VolumeControlImpl(Reporter::Impl& impl) |
| : node_(impl.volume_controls_node.CreateChild(impl.NextVolumeControlName())), |
| volume_settings_node_(node_.CreateChild("volume settings")), |
| name_(node_.CreateString("name", "unknown - no clients")), |
| client_count_(node_.CreateUint("client count", 0)), |
| last_volume_setting_(volume_settings_.New(new VolumeSetting( |
| volume_settings_node_, std::to_string(++next_volume_setting_name_), 0.0, false))) {} |
| |
| void Destroy() override {} |
| |
| void SetVolumeMute(float volume, bool mute) override { |
| std::lock_guard<std::mutex> lock(mutex_); |
| last_volume_setting_ = volume_settings_.New(new VolumeSetting( |
| volume_settings_node_, std::to_string(++next_volume_setting_name_), volume, mute)); |
| } |
| |
| void AddBinding(std::string name) override { |
| name_.Set(name); |
| client_count_.Add(1); |
| } |
| |
| private: |
| static constexpr size_t kVolumeSettingsToCache = 10; |
| |
| inspect::Node node_; |
| inspect::Node volume_settings_node_; |
| inspect::StringProperty name_; |
| inspect::UintProperty client_count_; |
| |
| Container<VolumeSetting, kVolumeSettingsToCache> volume_settings_; |
| |
| std::mutex mutex_; |
| uint64_t next_volume_setting_name_ FXL_GUARDED_BY(mutex_) = 0; |
| Container<VolumeSetting, kVolumeSettingsToCache>::Ptr last_volume_setting_ FXL_GUARDED_BY(mutex_); |
| }; |
| |
| class Reporter::OutputDeviceImpl : public Reporter::OutputDevice { |
| public: |
| OutputDeviceImpl(Reporter::Impl& impl, const std::string& name, const std::string& thread_name) |
| : node_(impl.outputs_node.CreateChild(name)), |
| thread_name_(node_.CreateString("mixer thread name", thread_name)), |
| driver_info_( |
| node_, ObjectTracker(impl, AudioObjectsCreatedMetricDimensionObjectType::OutputDevice)), |
| gain_info_(node_), |
| device_underflows_( |
| std::make_unique<OverflowUnderflowTracker>(OverflowUnderflowTracker::Args{ |
| .event_name = "device underflows", |
| .parent_node = node_, |
| .impl = impl, |
| .is_underflow = true, |
| .cobalt_component_id = AudioSessionDurationMetricDimensionComponent::OutputDevice, |
| })), |
| pipeline_underflows_( |
| std::make_unique<OverflowUnderflowTracker>(OverflowUnderflowTracker::Args{ |
| .event_name = "pipeline underflows", |
| .parent_node = node_, |
| .impl = impl, |
| .is_underflow = true, |
| .cobalt_component_id = AudioSessionDurationMetricDimensionComponent::OutputPipeline, |
| })) { |
| time_since_death_ = node_.CreateLazyValues("OutputDeviceTimeSinceDeath", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint( |
| "time since death (ns)", |
| time_of_death_ ? (zx::clock::get_monotonic() - time_of_death_.value()).get() : 0, &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| |
| void Destroy() override { time_of_death_ = zx::clock::get_monotonic(); } |
| |
| void StartSession(zx::time start_time) override { |
| device_underflows_->StartSession(start_time); |
| pipeline_underflows_->StartSession(start_time); |
| } |
| |
| void StopSession(zx::time stop_time) override { |
| device_underflows_->StopSession(stop_time); |
| pipeline_underflows_->StopSession(stop_time); |
| } |
| |
| void SetDriverInfo(const AudioDriver& driver) override { driver_info_.Set(driver); } |
| |
| void SetGainInfo(const fuchsia::media::AudioGainInfo& gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) override { |
| gain_info_.Set(gain_info, set_flags); |
| } |
| |
| void DeviceUnderflow(zx::time start_time, zx::time end_time) override { |
| device_underflows_->Report(start_time, end_time); |
| } |
| |
| void PipelineUnderflow(zx::time start_time, zx::time end_time) override { |
| pipeline_underflows_->Report(start_time, end_time); |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::LazyNode time_since_death_; |
| inspect::StringProperty thread_name_; |
| DeviceDriverInfo driver_info_; |
| DeviceGainInfo gain_info_; |
| std::unique_ptr<OverflowUnderflowTracker> device_underflows_; |
| std::unique_ptr<OverflowUnderflowTracker> pipeline_underflows_; |
| std::optional<zx::time> time_of_death_; |
| }; |
| |
| class Reporter::InputDeviceImpl : public Reporter::InputDevice { |
| public: |
| InputDeviceImpl(Reporter::Impl& impl, const std::string& name, const std::string& thread_name) |
| : node_(impl.inputs_node.CreateChild(name)), |
| thread_name_(node_.CreateString("mixer thread name", thread_name)), |
| driver_info_( |
| node_, ObjectTracker(impl, AudioObjectsCreatedMetricDimensionObjectType::InputDevice)), |
| gain_info_(node_) { |
| time_since_death_ = node_.CreateLazyValues("InputDeviceTimeSinceDeath", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint( |
| "time since death (ns)", |
| time_of_death_ ? (zx::clock::get_monotonic() - time_of_death_.value()).get() : 0, &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| |
| void Destroy() override { time_of_death_ = zx::clock::get_monotonic(); } |
| |
| void StartSession(zx::time start_time) override {} |
| void StopSession(zx::time stop_time) override {} |
| |
| void SetDriverInfo(const AudioDriver& driver) override { driver_info_.Set(driver); } |
| |
| void SetGainInfo(const fuchsia::media::AudioGainInfo& gain_info, |
| fuchsia::media::AudioGainValidFlags set_flags) override { |
| gain_info_.Set(gain_info, set_flags); |
| } |
| |
| private: |
| inspect::Node node_; |
| inspect::LazyNode time_since_death_; |
| inspect::StringProperty thread_name_; |
| DeviceDriverInfo driver_info_; |
| DeviceGainInfo gain_info_; |
| std::optional<zx::time> time_of_death_; |
| }; |
| |
| class Reporter::ClientPort { |
| public: |
| ClientPort(inspect::Node& node, ObjectTracker&& object_tracker) |
| : object_tracker_(std::move(object_tracker)), |
| format_(node, "format"), |
| payload_buffers_node_(node.CreateChild("payload buffers")), |
| gain_db_(node.CreateDouble("gain db", 0.0)), |
| muted_(node.CreateBool("muted", false)), |
| set_gain_with_ramp_calls_(node.CreateUint("calls to SetGainWithRamp", 0)) {} |
| |
| void SetFormat(const Format& format) { |
| object_tracker_.SetFormat(format); |
| format_.Set(format); |
| } |
| |
| void SetGain(float gain_db) { gain_db_.Set(gain_db); } |
| void SetGainWithRamp(float gain_db, zx::duration duration, |
| fuchsia::media::audio::RampType ramp_type) { |
| set_gain_with_ramp_calls_.Add(1); |
| } |
| void SetMute(bool muted) { muted_.Set(muted); } |
| |
| void AddPayloadBuffer(uint32_t buffer_id, uint64_t size) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| payload_buffers_.emplace( |
| buffer_id, |
| PayloadBuffer(payload_buffers_node_.CreateChild(std::to_string(buffer_id)), size)); |
| } |
| |
| void RemovePayloadBuffer(uint32_t buffer_id) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| payload_buffers_.erase(buffer_id); |
| } |
| |
| void SendPacket(const fuchsia::media::StreamPacket& packet) { |
| object_tracker_.Enable(); |
| std::lock_guard<std::mutex> lock(mutex_); |
| auto b = payload_buffers_.find(packet.payload_buffer_id); |
| if (b == payload_buffers_.end()) { |
| FX_LOGS(ERROR) << "Specified payload buffer not found"; |
| return; |
| } |
| b->second.packets_.Add(1); |
| } |
| |
| private: |
| ObjectTracker object_tracker_; |
| FormatInfo format_; |
| inspect::Node payload_buffers_node_; |
| |
| struct PayloadBuffer { |
| PayloadBuffer(inspect::Node node, uint64_t size) |
| : node_(std::move(node)), |
| size_(node_.CreateUint("size", size)), |
| packets_(node_.CreateUint("packets", 0)) {} |
| |
| inspect::Node node_; |
| inspect::UintProperty size_; |
| inspect::UintProperty packets_; |
| }; |
| std::mutex mutex_; |
| std::unordered_map<uint32_t, PayloadBuffer> payload_buffers_ FXL_GUARDED_BY(mutex_); |
| |
| inspect::DoubleProperty gain_db_; |
| inspect::BoolProperty muted_; |
| // Just counting these for now. |
| inspect::UintProperty set_gain_with_ramp_calls_; |
| }; |
| |
| class Reporter::RendererImpl : public Reporter::Renderer { |
| public: |
| RendererImpl(Reporter::Impl& impl) |
| : node_(impl.renderers_node.CreateChild(impl.NextRendererName())), |
| client_port_(node_, |
| ObjectTracker(impl, AudioObjectsCreatedMetricDimensionObjectType::Renderer)), |
| min_lead_time_ns_(node_.CreateUint("min lead time (ns)", 0)), |
| pts_continuity_threshold_seconds_(node_.CreateDouble("pts continuity threshold (s)", 0.0)), |
| final_stream_gain_(node_.CreateDouble("final stream gain (post-volume) dbfs", 0.0)), |
| usage_(node_.CreateString("usage", "default")), |
| underflows_(std::make_unique<OverflowUnderflowTracker>(OverflowUnderflowTracker::Args{ |
| .event_name = "underflows", |
| .parent_node = node_, |
| .impl = impl, |
| .is_underflow = true, |
| .cobalt_component_id = AudioSessionDurationMetricDimensionComponent::Renderer, |
| })) { |
| time_since_death_ = node_.CreateLazyValues("RendererTimeSinceDeath", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint( |
| "time since death (ns)", |
| time_of_death_ ? (zx::clock::get_monotonic() - time_of_death_.value()).get() : 0, &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| |
| void Destroy() override { time_of_death_ = zx::clock::get_monotonic(); } |
| |
| void StartSession(zx::time start_time) override { underflows_->StartSession(start_time); } |
| void StopSession(zx::time stop_time) override { underflows_->StopSession(stop_time); } |
| |
| void SetUsage(RenderUsage usage) override { usage_.Set(RenderUsageToString(usage)); } |
| void SetFormat(const Format& format) override { client_port_.SetFormat(format); } |
| void SetGain(float gain_db) override { client_port_.SetGain(gain_db); } |
| void SetGainWithRamp(float gain_db, zx::duration duration, |
| fuchsia::media::audio::RampType ramp_type) override { |
| client_port_.SetGainWithRamp(gain_db, duration, ramp_type); |
| } |
| void SetFinalGain(float gain_db) override { final_stream_gain_.Set(gain_db); } |
| void SetMute(bool muted) override { client_port_.SetMute(muted); } |
| |
| void SetMinLeadTime(zx::duration min_lead_time) override { |
| min_lead_time_ns_.Set(min_lead_time.to_nsecs()); |
| } |
| void SetPtsContinuityThreshold(float threshold_seconds) override { |
| pts_continuity_threshold_seconds_.Set(threshold_seconds); |
| } |
| |
| void AddPayloadBuffer(uint32_t buffer_id, uint64_t size) override { |
| client_port_.AddPayloadBuffer(buffer_id, size); |
| } |
| void RemovePayloadBuffer(uint32_t buffer_id) override { |
| client_port_.RemovePayloadBuffer(buffer_id); |
| } |
| void SendPacket(const fuchsia::media::StreamPacket& packet) override { |
| client_port_.SendPacket(packet); |
| } |
| void Underflow(zx::time start_time, zx::time end_time) override { |
| underflows_->Report(start_time, end_time); |
| } |
| |
| private: |
| inspect::Node node_; |
| ClientPort client_port_; |
| inspect::LazyNode time_since_death_; |
| inspect::UintProperty min_lead_time_ns_; |
| inspect::DoubleProperty pts_continuity_threshold_seconds_; |
| inspect::DoubleProperty final_stream_gain_; |
| inspect::StringProperty usage_; |
| std::unique_ptr<OverflowUnderflowTracker> underflows_; |
| std::optional<zx::time> time_of_death_; |
| }; |
| |
| class Reporter::CapturerImpl : public Reporter::Capturer { |
| public: |
| CapturerImpl(Reporter::Impl& impl, const std::string& thread_name) |
| : node_(impl.capturers_node.CreateChild(impl.NextCapturerName())), |
| client_port_(node_, |
| ObjectTracker(impl, AudioObjectsCreatedMetricDimensionObjectType::Capturer)), |
| min_fence_time_ns_(node_.CreateUint("min fence time (ns)", 0)), |
| usage_(node_.CreateString("usage", "default")), |
| thread_name_(node_.CreateString("mixer thread name", thread_name)), |
| overflows_(std::make_unique<OverflowUnderflowTracker>(OverflowUnderflowTracker::Args{ |
| .event_name = "overflows", |
| .parent_node = node_, |
| .impl = impl, |
| .is_underflow = false, |
| .cobalt_component_id = AudioSessionDurationMetricDimensionComponent::Capturer, |
| })) { |
| time_since_death_ = node_.CreateLazyValues("CapturerTimeSinceDeath", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint( |
| "time since death (ns)", |
| time_of_death_ ? (zx::clock::get_monotonic() - time_of_death_.value()).get() : 0, &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| |
| void Destroy() override { time_of_death_ = zx::clock::get_monotonic(); } |
| |
| void StartSession(zx::time start_time) override { overflows_->StartSession(start_time); } |
| void StopSession(zx::time stop_time) override { overflows_->StopSession(stop_time); } |
| |
| void SetUsage(CaptureUsage usage) override { usage_.Set(CaptureUsageToString(usage)); } |
| void SetFormat(const Format& format) override { client_port_.SetFormat(format); } |
| void SetGain(float gain_db) override { client_port_.SetGain(gain_db); } |
| void SetGainWithRamp(float gain_db, zx::duration duration, |
| fuchsia::media::audio::RampType ramp_type) override { |
| client_port_.SetGainWithRamp(gain_db, duration, ramp_type); |
| } |
| void SetMute(bool muted) override { client_port_.SetMute(muted); } |
| |
| void SetMinFenceTime(zx::duration min_fence_time) override { |
| min_fence_time_ns_.Set(min_fence_time.to_nsecs()); |
| } |
| |
| void AddPayloadBuffer(uint32_t buffer_id, uint64_t size) override { |
| client_port_.AddPayloadBuffer(buffer_id, size); |
| } |
| void SendPacket(const fuchsia::media::StreamPacket& packet) override { |
| client_port_.SendPacket(packet); |
| } |
| void Overflow(zx::time start_time, zx::time end_time) override { |
| overflows_->Report(start_time, end_time); |
| } |
| |
| private: |
| inspect::Node node_; |
| ClientPort client_port_; |
| inspect::LazyNode time_since_death_; |
| inspect::UintProperty min_fence_time_ns_; |
| inspect::StringProperty usage_; |
| inspect::StringProperty thread_name_; |
| std::unique_ptr<OverflowUnderflowTracker> overflows_; |
| std::optional<zx::time> time_of_death_; |
| }; |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Reporter implementation |
| |
| // static |
| Reporter& Reporter::Singleton() { |
| std::lock_guard<std::mutex> lock(singleton_mutex); |
| if (singleton_real) { |
| return *singleton_real; |
| } |
| FX_LOGS_FIRST_N(INFO, 1) |
| << "Creating reporting objects before the Reporter singleton has been initialized"; |
| return *singleton_nop; |
| } |
| |
| // static |
| void Reporter::InitializeSingleton(sys::ComponentContext& component_context, |
| ThreadingModel& threading_model) { |
| std::lock_guard<std::mutex> lock(singleton_mutex); |
| if (singleton_real) { |
| FX_LOGS(ERROR) << "Reporter::Singleton double initialized"; |
| return; |
| } |
| singleton_real = new Reporter(component_context, threading_model); |
| } |
| |
| Reporter::Reporter(sys::ComponentContext& component_context, ThreadingModel& threading_model) |
| : impl_(std::make_unique<Impl>(component_context, threading_model)) { |
| // This lock isn't necessary, but the lock analysis can't tell that. |
| std::lock_guard<std::mutex> lock(mutex_); |
| InitInspect(); |
| InitCobalt(); |
| } |
| |
| void Reporter::InitInspect() { |
| impl_->inspector = std::make_unique<sys::ComponentInspector>(&impl_->component_context); |
| inspect::Node& root_node = impl_->inspector->root(); |
| |
| impl_->failed_to_open_device_count = root_node.CreateUint("count of failures to open device", 0); |
| impl_->failed_to_obtain_fdio_service_channel_count = |
| root_node.CreateUint("count of failures to obtain device fdio service channel", 0); |
| impl_->failed_to_obtain_stream_channel_count = |
| root_node.CreateUint("count of failures to obtain device stream channel", 0); |
| impl_->failed_to_start_device_count = |
| root_node.CreateUint("count of failures to start a device", 0); |
| impl_->mixer_clock_skew_discontinuities = |
| root_node.CreateLinearIntHistogram("mixer clock skew discontinuities (error in ns)", |
| ZX_MSEC(-100), // floor |
| ZX_MSEC(2), // step size |
| 100); // buckets range from -100ms to +100ms |
| |
| impl_->outputs_node = root_node.CreateChild("output devices"); |
| impl_->inputs_node = root_node.CreateChild("input devices"); |
| impl_->renderers_node = root_node.CreateChild("renderers"); |
| impl_->capturers_node = root_node.CreateChild("capturers"); |
| impl_->volume_controls_node = root_node.CreateChild("volume controls"); |
| |
| impl_->thermal_state_tracker = std::make_unique<ThermalStateTracker>(*impl_); |
| impl_->active_usage_policy_tracker = std::make_unique<ActiveUsagePolicyTracker>(*impl_); |
| } |
| |
| void Reporter::InitCobalt() { |
| impl_->cobalt_logger = ::cobalt::NewCobaltLoggerFromProjectId( |
| impl_->threading_model.FidlDomain().dispatcher(), impl_->component_context.svc(), kProjectId); |
| } |
| |
| Reporter::Container<Reporter::OutputDevice, Reporter::kObjectsToCache>::Ptr |
| Reporter::CreateOutputDevice(const std::string& name, const std::string& thread_name) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return outputs_.New( |
| impl_ ? static_cast<OutputDevice*>(new OutputDeviceImpl(*impl_, name, thread_name)) |
| : static_cast<OutputDevice*>(new OutputDeviceNop)); |
| } |
| |
| Reporter::Container<Reporter::InputDevice, Reporter::kObjectsToCache>::Ptr |
| Reporter::CreateInputDevice(const std::string& name, const std::string& thread_name) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return inputs_.New(impl_ |
| ? static_cast<InputDevice*>(new InputDeviceImpl(*impl_, name, thread_name)) |
| : static_cast<InputDevice*>(new InputDeviceNop)); |
| } |
| |
| Reporter::Container<Reporter::Renderer, Reporter::kObjectsToCache>::Ptr Reporter::CreateRenderer() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return renderers_.New(impl_ ? static_cast<Renderer*>(new RendererImpl(*impl_)) |
| : static_cast<Renderer*>(new RendererNop)); |
| } |
| |
| Reporter::Container<Reporter::Capturer, Reporter::kObjectsToCache>::Ptr Reporter::CreateCapturer( |
| const std::string& thread_name) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return capturers_.New(impl_ ? static_cast<Capturer*>(new CapturerImpl(*impl_, thread_name)) |
| : static_cast<Capturer*>(new CapturerNop)); |
| } |
| |
| Reporter::Container<Reporter::VolumeControl, Reporter::kVolumeControlsToCache>::Ptr |
| Reporter::CreateVolumeControl() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return volume_controls_.New(impl_ ? static_cast<VolumeControl*>(new VolumeControlImpl(*impl_)) |
| : static_cast<VolumeControl*>(new VolumeControlNop())); |
| } |
| |
| void Reporter::FailedToOpenDevice(const std::string& name, bool is_input, int err) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->failed_to_open_device_count.Add(1); |
| } |
| |
| void Reporter::FailedToObtainFdioServiceChannel(const std::string& name, bool is_input, |
| zx_status_t status) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->failed_to_obtain_fdio_service_channel_count.Add(1); |
| } |
| |
| void Reporter::FailedToObtainStreamChannel(const std::string& name, bool is_input, |
| zx_status_t status) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->failed_to_obtain_stream_channel_count.Add(1); |
| } |
| |
| void Reporter::FailedToStartDevice(const std::string& name) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->failed_to_start_device_count.Add(1); |
| } |
| |
| void Reporter::MixerClockSkewDiscontinuity(zx::duration clock_error) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->mixer_clock_skew_discontinuities.Insert(clock_error.get()); |
| } |
| |
| void Reporter::SetNumThermalStates(size_t num) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->thermal_state_tracker->SetNumThermalStates(num); |
| } |
| |
| void Reporter::SetThermalState(uint32_t state) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->thermal_state_tracker->SetThermalState(state); |
| } |
| |
| void Reporter::SetAudioPolicyBehaviorGain(AudioAdmin::BehaviorGain behavior_gain) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->active_usage_policy_tracker->SetAudioPolicyBehaviorGain(behavior_gain); |
| } |
| |
| void Reporter::UpdateActiveUsagePolicy(const std::vector<fuchsia::media::Usage>& active_usages, |
| const AudioAdmin::RendererPolicies& renderer_policies, |
| const AudioAdmin::CapturerPolicies& capturer_policies) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (!impl_) { |
| return; |
| } |
| impl_->active_usage_policy_tracker->UpdateActiveUsagePolicy(active_usages, renderer_policies, |
| capturer_policies); |
| } |
| |
| Reporter::Impl::Impl(sys::ComponentContext& cc, ThreadingModel& tm) |
| : component_context(cc), threading_model(tm) {} |
| |
| Reporter::Impl::~Impl() {} |
| |
| ////////////////////////////////////////////////////////////////////////////////// |
| // OverflowUnderflowTracker implementation |
| |
| namespace { |
| constexpr auto kEventSessionStart = 0; |
| constexpr auto kEventOverflowUnderflow = 1; |
| static_assert(AudioTimeSinceLastOverflowOrSessionStartMetricDimensionLastEvent::SessionStart == 0); |
| static_assert(AudioTimeSinceLastUnderflowOrSessionStartMetricDimensionLastEvent::SessionStart == 0); |
| static_assert(AudioTimeSinceLastOverflowOrSessionStartMetricDimensionLastEvent::Overflow == 1); |
| static_assert(AudioTimeSinceLastUnderflowOrSessionStartMetricDimensionLastEvent::Underflow == 1); |
| } // namespace |
| |
| Reporter::OverflowUnderflowTracker::OverflowUnderflowTracker(Args args) |
| : state_(State::Stopped), |
| impl_(args.impl), |
| cobalt_component_id_(args.cobalt_component_id), |
| cobalt_event_duration_metric_id_(args.is_underflow ? kAudioUnderflowDurationMetricId |
| : kAudioOverflowDurationMetricId), |
| cobalt_time_since_last_event_or_session_start_metric_id_( |
| args.is_underflow ? kAudioTimeSinceLastUnderflowOrSessionStartMetricId |
| : kAudioTimeSinceLastOverflowOrSessionStartMetricId) { |
| node_ = args.parent_node.CreateChild(args.event_name); |
| event_count_ = node_.CreateUint("count", 0); |
| event_duration_ = node_.CreateUint("duration (ns)", 0); |
| session_count_ = node_.CreateUint("session count", 0); |
| total_duration_ = node_.CreateLazyValues("@wrapper", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint("total duration of all parent sessions (ns)", |
| ComputeDurationOfAllSessions().get(), &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| |
| Reporter::OverflowUnderflowTracker::~OverflowUnderflowTracker() { |
| bool started; |
| { |
| // Technically a lock is not required here, but the lock analysis can't tell that. |
| std::lock_guard<std::mutex> lock(mutex_); |
| started = (state_ == State::Started); |
| } |
| if (started) { |
| StopSession(zx::clock::get_monotonic()); |
| } |
| } |
| |
| void Reporter::OverflowUnderflowTracker::StartSession(zx::time start_time) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (state_ == State::Started) { |
| FX_LOGS(ERROR) << "StartSession called on session that is already started"; |
| return; |
| } |
| |
| session_count_.Add(1); |
| |
| state_ = State::Started; |
| last_event_time_ = start_time; |
| session_start_time_ = start_time; |
| session_real_start_time_ = start_time; |
| restart_session_timer_.PostDelayed(impl_.threading_model.IoDomain().dispatcher(), |
| kMaxSessionDuration); |
| } |
| |
| void Reporter::OverflowUnderflowTracker::StopSession(zx::time stop_time) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (state_ == State::Stopped) { |
| FX_LOGS(ERROR) << "StopSession called on session that is already stopped"; |
| return; |
| } |
| |
| LogCobaltDuration(kAudioSessionDurationMetricId, {cobalt_component_id_}, |
| stop_time - session_start_time_); |
| LogCobaltDuration(cobalt_time_since_last_event_or_session_start_metric_id_, |
| {cobalt_component_id_, kEventSessionStart}, stop_time - last_event_time_); |
| |
| state_ = State::Stopped; |
| past_sessions_duration_ += stop_time - session_real_start_time_; |
| restart_session_timer_.Cancel(); |
| } |
| |
| void Reporter::OverflowUnderflowTracker::RestartSession() { |
| auto stop_time = zx::clock::get_monotonic(); |
| |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (state_ == State::Stopped) { |
| return; // must have been stopped concurrently |
| } |
| |
| LogCobaltDuration(kAudioSessionDurationMetricId, {cobalt_component_id_}, |
| stop_time - session_start_time_); |
| LogCobaltDuration(cobalt_time_since_last_event_or_session_start_metric_id_, |
| {cobalt_component_id_, kEventSessionStart}, stop_time - last_event_time_); |
| |
| last_event_time_ = stop_time; |
| session_start_time_ = stop_time; |
| restart_session_timer_.PostDelayed(impl_.threading_model.IoDomain().dispatcher(), |
| kMaxSessionDuration); |
| } |
| |
| zx::duration Reporter::OverflowUnderflowTracker::ComputeDurationOfAllSessions() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| auto dt = past_sessions_duration_; |
| if (state_ == State::Started) { |
| dt += zx::clock::get_monotonic() - session_real_start_time_; |
| } |
| return dt; |
| } |
| |
| void Reporter::OverflowUnderflowTracker::Report(zx::time start_time, zx::time end_time) { |
| if (end_time < start_time) { |
| FX_LOGS(ERROR) << "Reported overflow/underflow with negative duration: " << start_time.get() |
| << " to " << end_time.get(); |
| } |
| |
| std::lock_guard<std::mutex> lock(mutex_); |
| |
| auto event_duration = end_time - start_time; |
| event_count_.Add(1); |
| event_duration_.Add(event_duration.get()); |
| |
| LogCobaltDuration(cobalt_event_duration_metric_id_, {cobalt_component_id_}, event_duration); |
| |
| if (state_ != State::Started) { |
| // This can happen because reporting can race with session boundaries. For example: |
| // If the mixer detects a renderer underflow as the client concurrently pauses the |
| // renderer, the Report and StopSession calls will race. |
| FX_LOGS_FIRST_N(INFO, 20) << "Overflow/Underflow event arrived when the session is stopped"; |
| return; |
| } |
| |
| LogCobaltDuration(cobalt_time_since_last_event_or_session_start_metric_id_, |
| {cobalt_component_id_, kEventOverflowUnderflow}, start_time - last_event_time_); |
| last_event_time_ = end_time; |
| } |
| |
| void Reporter::OverflowUnderflowTracker::LogCobaltDuration(uint32_t metric_id, |
| std::vector<uint32_t> event_codes, |
| zx::duration d) { |
| auto& logger = impl_.cobalt_logger; |
| if (!logger || !cobalt_token_bucket->Acquire()) { |
| return; |
| } |
| |
| auto e = fuchsia::cobalt::CobaltEvent{ |
| .metric_id = metric_id, |
| .event_codes = event_codes, |
| .payload = fuchsia::cobalt::EventPayload::WithElapsedMicros(d.get()), |
| }; |
| logger->LogCobaltEvent(std::move(e)); |
| } |
| |
| ////////////////////////////////////////////////////////////////////////////////// |
| // ThermalStateTracker implementation |
| |
| Reporter::ThermalStateTracker::ThermalStateTracker(Reporter::Impl& impl) |
| : impl_(impl), |
| root_(impl.inspector->root().CreateChild("thermal state")), |
| num_thermal_states_(root_.CreateUint("num thermal states", 1)), |
| transitions_node_(impl.inspector->root().CreateChild("thermal state transitions")), |
| active_state_(0), |
| last_transition_(thermal_state_transitions_.New(new ThermalStateTransition( |
| transitions_node_, std::to_string(++next_thermal_transition_name_), active_state_))) { |
| states_[active_state_] = State({.node = root_.CreateChild("normal")}); |
| states_[active_state_].Activate(); |
| LogCobaltStateTransition(states_[active_state_], kCobaltStateTransitions[active_state_]); |
| LogCobaltStateDuration(states_[active_state_], states_[active_state_]); |
| } |
| |
| void Reporter::ThermalStateTracker::SetNumThermalStates(size_t num) { |
| num_thermal_states_.Set(num); |
| } |
| |
| void Reporter::ThermalStateTracker::SetThermalState(uint32_t state) { |
| std::lock_guard<std::mutex> lock(mutex_); |
| if (active_state_ == state) { |
| return; |
| } |
| states_[active_state_].Deactivate(); |
| |
| if (states_.find(state) == states_.end()) { |
| states_[state] = State({.node = root_.CreateChild(state ? std::to_string(state) : "normal")}); |
| } |
| states_[state].Activate(); |
| last_transition_ = thermal_state_transitions_.New(new ThermalStateTransition( |
| transitions_node_, std::to_string(++next_thermal_transition_name_), state)); |
| LogCobaltStateTransition(states_[state], kCobaltStateTransitions[state]); |
| LogCobaltStateDuration(states_[active_state_], states_[state]); |
| |
| // Set |active_state_| after all activation steps are complete. |
| active_state_ = state; |
| } |
| |
| void Reporter::ThermalStateTracker::State::Activate() { |
| current_activation_time = zx::clock::get_monotonic(); |
| |
| // If state has never been activated, set duration LazyNode. |
| if (!past_duration.get()) { |
| duration = node.CreateLazyValues("TotalThermalStateDuration", [this] { |
| inspect::Inspector i; |
| i.GetRoot().CreateUint("total duration (ns)", |
| (current_activation_time ? past_duration + zx::clock::get_monotonic() - |
| current_activation_time.value() |
| : past_duration) |
| .get(), |
| &i); |
| return fit::make_ok_promise(std::move(i)); |
| }); |
| } |
| } |
| |
| void Reporter::ThermalStateTracker::State::Deactivate() { |
| past_duration += (zx::clock::get_monotonic() - current_activation_time.value()); |
| current_activation_time = std::nullopt; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| // Thermal State Cobalt Metrics |
| // |
| // `LogCobaltStateTransition` |
| // A per-device histogram is generated for each thermal state. Transitions into |state| are |
| // counted by marking "1" in each bucket up to N total transitions since boot. |
| // |
| // For example, if a device has N transitions to state 2 since boot, the state 2 histogram |
| // will be: |
| // |
| // value: [1] ... [1] [0] [0] ... |
| // bucket: 0 ... N N+1 N+2 ... |
| // |
| // For each thermal state, Cobalt will aggregate the per-device histograms into one for all |
| // devices, such that each bucket counts the number of devices that have reached that bucket's |
| // thermal state count. Post-processing in the custom dashboard will reduce these histograms to |
| // give an accurate representation of the total number of devices that have reached each thermal |
| // state transition count. |
| // |
| // `LogCobaltStateDuration` |
| // In addition, the total time spent in |old_state| is recorded (in ns). |
| //////////////////////////////////////////////////////////////////////////////////////////////// |
| void Reporter::ThermalStateTracker::LogCobaltStateTransition(State& state, |
| CobaltStateTransition event) { |
| auto& logger = impl_.cobalt_logger; |
| if (!logger) { |
| return; |
| } |
| |
| if (auto transition_count = ++state.total_transitions; transition_count > 50) { |
| FX_LOGS_FIRST_N(INFO, 10) |
| << "Cobalt logging of audio_thermal_state_transitions has exceeded maximum of 50: " << event |
| << " = " << transition_count << "transitions"; |
| } else if (cobalt_token_bucket->Acquire()) { |
| std::vector<fuchsia::cobalt::HistogramBucket> histogram{ |
| {.index = transition_count, .count = 1}}; |
| logger->LogIntHistogram(kAudioThermalStateTransitionsMetricId, event, "" /*component*/, |
| histogram); |
| } |
| } |
| |
| void Reporter::ThermalStateTracker::LogCobaltStateDuration(State& old_state, State& new_state) { |
| auto& logger = impl_.cobalt_logger; |
| if (!logger) { |
| return; |
| } |
| |
| auto now = zx::clock::get_monotonic(); |
| |
| // Record duration of |old_state|, if interval has started. |
| if (auto start_time = old_state.interval_start_time; |
| start_time && cobalt_token_bucket->Acquire()) { |
| auto e = fuchsia::cobalt::CobaltEvent{ |
| .metric_id = kAudioThermalStateDurationMetricId, |
| .event_codes = {active_state_}, |
| .payload = |
| fuchsia::cobalt::EventPayload::WithElapsedMicros((now - start_time.value()).get()), |
| }; |
| logger->LogCobaltEvent(std::move(e)); |
| } |
| |
| // End |old_state| and start |new_state| logging interval. |
| old_state.interval_start_time = std::nullopt; |
| new_state.interval_start_time = now; |
| restart_interval_timer_.PostDelayed(impl_.threading_model.IoDomain().dispatcher(), |
| kCobaltDataCollectionInterval); |
| } |
| |
| void Reporter::ThermalStateTracker::ResetInterval() { |
| std::lock_guard<std::mutex> lock(mutex_); |
| LogCobaltStateDuration(states_[active_state_], states_[active_state_]); |
| } |
| |
| } // namespace media::audio |