blob: 994a02b952aa3e73b6fb88343aebd8e42c32618d [file] [log] [blame]
// 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