| /* |
| * Copyright 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at: |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "metrics_collector.h" |
| |
| #include <bluetooth/log.h> |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "common/metrics.h" |
| |
| namespace bluetooth::le_audio { |
| |
| using bluetooth::le_audio::ConnectionState; |
| using bluetooth::le_audio::types::LeAudioContextType; |
| |
| const static metrics::ClockTimePoint kInvalidTimePoint{}; |
| |
| MetricsCollector* MetricsCollector::instance = nullptr; |
| |
| inline int64_t get_timedelta_nanos(const metrics::ClockTimePoint& t1, |
| const metrics::ClockTimePoint& t2) { |
| if (t1 == kInvalidTimePoint || t2 == kInvalidTimePoint) { |
| return -1; |
| } |
| return std::abs( |
| std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t2).count()); |
| } |
| |
| const static std::unordered_map<LeAudioContextType, LeAudioMetricsContextType> |
| kContextTypeTable = { |
| {LeAudioContextType::UNINITIALIZED, LeAudioMetricsContextType::INVALID}, |
| {LeAudioContextType::UNSPECIFIED, |
| LeAudioMetricsContextType::UNSPECIFIED}, |
| {LeAudioContextType::CONVERSATIONAL, |
| LeAudioMetricsContextType::COMMUNICATION}, |
| {LeAudioContextType::MEDIA, LeAudioMetricsContextType::MEDIA}, |
| {LeAudioContextType::GAME, LeAudioMetricsContextType::GAME}, |
| {LeAudioContextType::INSTRUCTIONAL, |
| LeAudioMetricsContextType::INSTRUCTIONAL}, |
| {LeAudioContextType::VOICEASSISTANTS, |
| LeAudioMetricsContextType::MAN_MACHINE}, |
| {LeAudioContextType::LIVE, LeAudioMetricsContextType::LIVE}, |
| {LeAudioContextType::SOUNDEFFECTS, |
| LeAudioMetricsContextType::ATTENTION_SEEKING}, |
| {LeAudioContextType::NOTIFICATIONS, |
| LeAudioMetricsContextType::ATTENTION_SEEKING}, |
| {LeAudioContextType::RINGTONE, LeAudioMetricsContextType::RINGTONE}, |
| {LeAudioContextType::ALERTS, |
| LeAudioMetricsContextType::IMMEDIATE_ALERT}, |
| {LeAudioContextType::EMERGENCYALARM, |
| LeAudioMetricsContextType::EMERGENCY_ALERT}, |
| {LeAudioContextType::RFU, LeAudioMetricsContextType::RFU}, |
| }; |
| |
| inline int32_t to_atom_context_type(const LeAudioContextType stack_type) { |
| auto it = kContextTypeTable.find(stack_type); |
| if (it != kContextTypeTable.end()) { |
| return static_cast<int32_t>(it->second); |
| } |
| return static_cast<int32_t>(LeAudioMetricsContextType::INVALID); |
| } |
| |
| class DeviceMetrics { |
| public: |
| RawAddress address_; |
| metrics::ClockTimePoint connecting_timepoint_ = kInvalidTimePoint; |
| metrics::ClockTimePoint connected_timepoint_ = kInvalidTimePoint; |
| metrics::ClockTimePoint disconnected_timepoint_ = kInvalidTimePoint; |
| int32_t connection_status_ = 0; |
| int32_t disconnection_status_ = 0; |
| |
| DeviceMetrics(const RawAddress& address) : address_(address) {} |
| |
| void AddStateChangedEvent(ConnectionState state, ConnectionStatus status) { |
| switch (state) { |
| case ConnectionState::CONNECTING: |
| connecting_timepoint_ = std::chrono::high_resolution_clock::now(); |
| break; |
| case ConnectionState::CONNECTED: |
| connected_timepoint_ = std::chrono::high_resolution_clock::now(); |
| connection_status_ = static_cast<int32_t>(status); |
| break; |
| case ConnectionState::DISCONNECTED: |
| disconnected_timepoint_ = std::chrono::high_resolution_clock::now(); |
| disconnection_status_ = static_cast<int32_t>(status); |
| break; |
| case ConnectionState::DISCONNECTING: |
| // Ignore |
| break; |
| } |
| } |
| }; |
| |
| class GroupMetricsImpl : public GroupMetrics { |
| private: |
| static constexpr int32_t kInvalidGroupId = -1; |
| int32_t group_id_; |
| int32_t group_size_; |
| std::vector<std::unique_ptr<DeviceMetrics>> device_metrics_; |
| std::unordered_map<RawAddress, DeviceMetrics*> opened_devices_; |
| metrics::ClockTimePoint beginning_timepoint_; |
| std::vector<int64_t> streaming_offset_nanos_; |
| std::vector<int64_t> streaming_duration_nanos_; |
| std::vector<int32_t> streaming_context_type_; |
| |
| public: |
| GroupMetricsImpl() : group_id_(kInvalidGroupId), group_size_(0) { |
| beginning_timepoint_ = std::chrono::high_resolution_clock::now(); |
| } |
| GroupMetricsImpl(int32_t group_id, int32_t group_size) |
| : group_id_(group_id), group_size_(group_size) { |
| beginning_timepoint_ = std::chrono::high_resolution_clock::now(); |
| } |
| |
| void AddStateChangedEvent(const RawAddress& address, |
| bluetooth::le_audio::ConnectionState state, |
| ConnectionStatus status) override { |
| auto it = opened_devices_.find(address); |
| if (it == opened_devices_.end()) { |
| device_metrics_.push_back(std::make_unique<DeviceMetrics>(address)); |
| it = opened_devices_.insert(std::begin(opened_devices_), |
| {address, device_metrics_.back().get()}); |
| } |
| it->second->AddStateChangedEvent(state, status); |
| if (state == bluetooth::le_audio::ConnectionState::DISCONNECTED || |
| (state == bluetooth::le_audio::ConnectionState::CONNECTED && |
| status != ConnectionStatus::SUCCESS)) { |
| opened_devices_.erase(it); |
| } |
| } |
| |
| void AddStreamStartedEvent( |
| bluetooth::le_audio::types::LeAudioContextType context_type) override { |
| int32_t atom_context_type = to_atom_context_type(context_type); |
| // Make sure events aligned |
| if (streaming_offset_nanos_.size() - streaming_duration_nanos_.size() != |
| 0) { |
| // Allow type switching |
| if (!streaming_context_type_.empty() && |
| streaming_context_type_.back() != atom_context_type) { |
| AddStreamEndedEvent(); |
| } else { |
| return; |
| } |
| } |
| streaming_offset_nanos_.push_back(get_timedelta_nanos( |
| std::chrono::high_resolution_clock::now(), beginning_timepoint_)); |
| streaming_context_type_.push_back(atom_context_type); |
| } |
| |
| void AddStreamEndedEvent() override { |
| // Make sure events aligned |
| if (streaming_offset_nanos_.size() - streaming_duration_nanos_.size() != |
| 1) { |
| return; |
| } |
| streaming_duration_nanos_.push_back( |
| get_timedelta_nanos(std::chrono::high_resolution_clock::now(), |
| beginning_timepoint_) - |
| streaming_offset_nanos_.back()); |
| } |
| |
| void SetGroupSize(int32_t group_size) override { group_size_ = group_size; } |
| |
| bool IsClosed() override { return opened_devices_.empty(); } |
| |
| void WriteStats() override { |
| int64_t connection_duration_nanos = get_timedelta_nanos( |
| beginning_timepoint_, std::chrono::high_resolution_clock::now()); |
| |
| int len = device_metrics_.size(); |
| std::vector<int64_t> device_connecting_offset_nanos(len); |
| std::vector<int64_t> device_connected_offset_nanos(len); |
| std::vector<int64_t> device_connection_duration_nanos(len); |
| std::vector<int32_t> device_connection_statuses(len); |
| std::vector<int32_t> device_disconnection_statuses(len); |
| std::vector<RawAddress> device_address(len); |
| |
| while (streaming_duration_nanos_.size() < streaming_offset_nanos_.size()) { |
| AddStreamEndedEvent(); |
| } |
| |
| for (int i = 0; i < len; i++) { |
| auto device_metric = device_metrics_[i].get(); |
| device_connecting_offset_nanos[i] = get_timedelta_nanos( |
| device_metric->connecting_timepoint_, beginning_timepoint_); |
| device_connected_offset_nanos[i] = get_timedelta_nanos( |
| device_metric->connected_timepoint_, beginning_timepoint_); |
| device_connection_duration_nanos[i] = |
| get_timedelta_nanos(device_metric->disconnected_timepoint_, |
| device_metric->connected_timepoint_); |
| device_connection_statuses[i] = device_metric->connection_status_; |
| device_disconnection_statuses[i] = device_metric->disconnection_status_; |
| device_address[i] = device_metric->address_; |
| } |
| |
| bluetooth::common::LogLeAudioConnectionSessionReported( |
| group_size_, group_id_, connection_duration_nanos, |
| device_connecting_offset_nanos, device_connected_offset_nanos, |
| device_connection_duration_nanos, device_connection_statuses, |
| device_disconnection_statuses, device_address, streaming_offset_nanos_, |
| streaming_duration_nanos_, streaming_context_type_); |
| } |
| |
| void Flush() { |
| for (auto& p : opened_devices_) { |
| p.second->AddStateChangedEvent( |
| bluetooth::le_audio::ConnectionState::DISCONNECTED, |
| ConnectionStatus::SUCCESS); |
| } |
| WriteStats(); |
| } |
| }; |
| |
| /* Metrics Colloctor */ |
| |
| MetricsCollector* MetricsCollector::Get() { |
| if (MetricsCollector::instance == nullptr) { |
| MetricsCollector::instance = new MetricsCollector(); |
| } |
| return MetricsCollector::instance; |
| } |
| |
| void MetricsCollector::OnGroupSizeUpdate(int32_t group_id, int32_t group_size) { |
| group_size_table_[group_id] = group_size; |
| auto it = opened_groups_.find(group_id); |
| if (it != opened_groups_.end()) { |
| it->second->SetGroupSize(group_size); |
| } |
| } |
| |
| void MetricsCollector::OnConnectionStateChanged( |
| int32_t group_id, const RawAddress& address, |
| bluetooth::le_audio::ConnectionState state, ConnectionStatus status) { |
| if (address.IsEmpty() || group_id <= 0) { |
| return; |
| } |
| auto it = opened_groups_.find(group_id); |
| if (it == opened_groups_.end()) { |
| it = opened_groups_.insert( |
| std::begin(opened_groups_), |
| {group_id, std::make_unique<GroupMetricsImpl>( |
| group_id, group_size_table_[group_id])}); |
| } |
| it->second->AddStateChangedEvent(address, state, status); |
| |
| if (it->second->IsClosed()) { |
| it->second->WriteStats(); |
| opened_groups_.erase(it); |
| } |
| } |
| |
| void MetricsCollector::OnStreamStarted( |
| int32_t group_id, |
| bluetooth::le_audio::types::LeAudioContextType context_type) { |
| if (group_id <= 0) return; |
| auto it = opened_groups_.find(group_id); |
| if (it != opened_groups_.end()) { |
| it->second->AddStreamStartedEvent(context_type); |
| } |
| } |
| |
| void MetricsCollector::OnStreamEnded(int32_t group_id) { |
| if (group_id <= 0) return; |
| auto it = opened_groups_.find(group_id); |
| if (it != opened_groups_.end()) { |
| it->second->AddStreamEndedEvent(); |
| } |
| } |
| |
| void MetricsCollector::OnBroadcastStateChanged(bool started) { |
| if (started) { |
| broadcast_beginning_timepoint_ = std::chrono::high_resolution_clock::now(); |
| } else { |
| auto broadcast_ending_timepoint_ = |
| std::chrono::high_resolution_clock::now(); |
| bluetooth::common::LogLeAudioBroadcastSessionReported(get_timedelta_nanos( |
| broadcast_beginning_timepoint_, broadcast_ending_timepoint_)); |
| broadcast_beginning_timepoint_ = kInvalidTimePoint; |
| } |
| } |
| |
| void MetricsCollector::Flush() { |
| log::info(""); |
| for (auto& p : opened_groups_) { |
| p.second->Flush(); |
| } |
| opened_groups_.clear(); |
| } |
| |
| } // namespace bluetooth::le_audio |