| // Copyright 2020 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/lib/metrics_buffer/metrics_buffer.h" | |
| #include <fidl/fuchsia.io/cpp/hlcpp_conversion.h> | |
| #include <inttypes.h> | |
| #include <lib/async-loop/default.h> | |
| #include <lib/async-loop/loop.h> | |
| #include <lib/async/cpp/task.h> | |
| #include <zircon/assert.h> | |
| #include <zircon/types.h> | |
| #include <memory> | |
| #include <mutex> | |
| #include <utility> | |
| #include "fidl/fuchsia.io/cpp/markers.h" | |
| #include "lib/fidl/cpp/wire/internal/transport_channel.h" | |
| #include "lib/sync/completion.h" | |
| #include "log.h" | |
| #include "src/lib/metrics_buffer/metrics_impl.h" | |
| namespace cobalt { | |
| namespace { | |
| template <class T> | |
| constexpr bool sfinae_false_v = false; | |
| // This is essentially the same code as cobalt buckets_config BucketIndex. | |
| uint32_t BucketIndex(std::vector<int64_t>& floors, int64_t val) { | |
| // 0 is the underflow bucket. | |
| if (val < floors[0]) { | |
| return 0; | |
| } | |
| // TODO(b/278918086): Maybe switch to binary search? | |
| for (uint32_t i = 1; i < floors.size(); i++) { | |
| if (val >= floors[i - 1] && val < floors[i]) { | |
| return i; | |
| } | |
| } | |
| // floors_.size() is the overflow bucket. | |
| return static_cast<uint32_t>(floors.size()); | |
| } | |
| } // namespace | |
| // static | |
| std::shared_ptr<MetricsBuffer> MetricsBuffer::Create(uint32_t project_id) { | |
| return std::shared_ptr<MetricsBuffer>(new MetricsBuffer(project_id)); | |
| } | |
| // static | |
| std::shared_ptr<MetricsBuffer> MetricsBuffer::Create( | |
| uint32_t project_id, std::shared_ptr<sys::ServiceDirectory> service_directory) { | |
| return std::shared_ptr<MetricsBuffer>(new MetricsBuffer(project_id, service_directory)); | |
| } | |
| MetricsBuffer::MetricsBuffer(uint32_t project_id) : project_id_(project_id) { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!loop_ && !cobalt_logger_); | |
| } | |
| MetricsBuffer::MetricsBuffer(uint32_t project_id, | |
| std::shared_ptr<sys::ServiceDirectory> service_directory) | |
| : project_id_(project_id) { | |
| SetServiceDirectory(service_directory); | |
| } | |
| MetricsBuffer::~MetricsBuffer() { SetServiceDirectory(nullptr); } | |
| void MetricsBuffer::SetServiceDirectory(std::shared_ptr<sys::ServiceDirectory> service_directory) { | |
| LOG(INFO, "SetServiceDirectory is called"); | |
| std::unique_ptr<cobalt::MetricsImpl> logger_to_delete_outside_lock; | |
| std::unique_ptr<async::Loop> loop_to_stop_outside_lock; | |
| { // scope lock | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| if (cobalt_logger_) { | |
| LOG(INFO, "cobalt_logger already exists"); | |
| ZX_DEBUG_ASSERT(loop_); | |
| // Clean these up after we've released lock_, to avoid potential deadlock waiting on a thread | |
| // that may be trying to get lock_. | |
| loop_to_stop_outside_lock = std::move(loop_); | |
| logger_to_delete_outside_lock = std::move(cobalt_logger_); | |
| } | |
| ZX_DEBUG_ASSERT(!loop_ && !cobalt_logger_); | |
| if (service_directory) { | |
| LOG(INFO, "Creating new cobalt_logger"); | |
| std::unique_ptr<cobalt::MetricsImpl> new_logger; | |
| fidl::ClientEnd<fuchsia_io::Directory> directory = | |
| fidl::HLCPPToNatural(service_directory->CloneChannel()); | |
| auto loop = std::make_unique<async::Loop>(&kAsyncLoopConfigNoAttachToCurrentThread); | |
| zx_status_t status = loop->StartThread("MetricsBuffer"); | |
| if (status != ZX_OK) { | |
| LOG(WARN, "MetricsBuffer::SetServiceDirectory() thread creation failed."); | |
| // ~loop | |
| // ~service_directory | |
| return; | |
| } | |
| sync_completion_t finished; | |
| // Must create fuchsia_metrics::MetricEventLogger on same dispatcher that it'll use. | |
| async::PostTask(loop->dispatcher(), [this, &loop, &directory, &new_logger, &finished] { | |
| // MetricsImpl will internally use the directory to reconnect as needed, should we ever lose | |
| // connection. | |
| new_logger = std::make_unique<cobalt::MetricsImpl>(loop->dispatcher(), std::move(directory), | |
| project_id_); | |
| sync_completion_signal(&finished); | |
| }); | |
| sync_completion_wait(&finished, ZX_TIME_INFINITE); | |
| loop_ = std::move(loop); | |
| cobalt_logger_ = std::move(new_logger); | |
| ZX_DEBUG_ASSERT(!!loop_ && !!cobalt_logger_); | |
| if (!pending_counts_.empty()) { | |
| LOG(INFO, "MetricsBuffer::SetServiceDirectory() flushing counts soon."); | |
| TryPostFlushCountsLocked(); | |
| } | |
| } | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| } // ~lock | |
| ZX_DEBUG_ASSERT(!!loop_to_stop_outside_lock == !!logger_to_delete_outside_lock); | |
| if (loop_to_stop_outside_lock) { | |
| // Need to delete the old MetricsImpl with the same dispatcher that created the old MetricsImpl. | |
| sync_completion_t finished; | |
| async::PostTask(loop_to_stop_outside_lock->dispatcher(), | |
| [&logger_to_delete_outside_lock, &finished] { | |
| logger_to_delete_outside_lock = nullptr; | |
| sync_completion_signal(&finished); | |
| }); | |
| ZX_ASSERT(ZX_OK == sync_completion_wait(&finished, ZX_TIME_INFINITE)); | |
| ZX_DEBUG_ASSERT(!logger_to_delete_outside_lock); | |
| loop_to_stop_outside_lock->Quit(); | |
| loop_to_stop_outside_lock->JoinThreads(); | |
| loop_to_stop_outside_lock->Shutdown(); | |
| // Delete here for clarity. | |
| loop_to_stop_outside_lock = nullptr; | |
| } | |
| } | |
| void MetricsBuffer::SetMinLoggingPeriod(zx::duration min_logging_period) { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(last_flushed_ == zx::time::infinite_past()); | |
| min_logging_period_ = min_logging_period; | |
| } | |
| void MetricsBuffer::LogEventCount(uint32_t metric_id, std::vector<uint32_t> dimension_values, | |
| uint32_t count) { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| bool was_empty = pending_counts_.empty(); | |
| MetricKey key(metric_id, std::move(dimension_values)); | |
| pending_counts_[key] += count; | |
| if (was_empty) { | |
| // We don't try to process locally, because if we're logging infrequently then the optimization | |
| // wouldn't matter, and if we're logging frequently then we need to post in order to delay | |
| // anyway. So we opt to keep the code simpler and always post even if the deadline is in the | |
| // past. | |
| TryPostFlushCountsLocked(); | |
| } | |
| } | |
| void MetricsBuffer::LogEvent(uint32_t metric_id, std::vector<uint32_t> dimension_values) { | |
| LogEventCount(metric_id, std::move(dimension_values), 1); | |
| } | |
| void MetricsBuffer::LogString(uint32_t metric_id, std::vector<uint32_t> dimension_values, | |
| std::string string_value) { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| bool was_empty = pending_strings_.empty(); | |
| MetricKey key(metric_id, std::move(dimension_values)); | |
| // By design, this emplace can fail if a matching string is already in the set. | |
| pending_strings_[key].emplace(std::move(string_value)); | |
| if (was_empty) { | |
| TryPostFlushCountsLocked(); | |
| } | |
| } | |
| void MetricsBuffer::LogHistogramValue(const HistogramInfo& histogram_info, | |
| std::vector<int64_t>& floors, | |
| std::vector<uint32_t> dimension_values, int64_t value) { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| bool was_empty = pending_histograms_.empty(); | |
| MetricKey key(histogram_info.metric_id, std::move(dimension_values)); | |
| uint32_t bucket_index = BucketIndex(floors, value); | |
| pending_histograms_[key][bucket_index]++; | |
| if (was_empty) { | |
| TryPostFlushCountsLocked(); | |
| } | |
| } | |
| void MetricsBuffer::FlushPendingEventCounts() { | |
| std::lock_guard<std::mutex> lock(lock_); | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| if (!cobalt_logger_) { | |
| // In some testing scenarios, we may not have access to a real MetricEventLoggerFactory, and we | |
| // can end up here if SetServiceDirectory() hit an error while (or shortly after) switching from | |
| // an old loop_ and cobalt_logger_ to a new loop_ and cobalt_logger_. | |
| // | |
| // If later we get a new cobalt_logger_ from a new SetServiceDirectory(), this method will run | |
| // again. | |
| return; | |
| } | |
| last_flushed_ = zx::clock::get_monotonic(); | |
| constexpr uint32_t kMaxBatchSize = 64; | |
| std::vector<fuchsia_metrics::MetricEvent> batch; | |
| PendingCounts snapped_pending_event_counts; | |
| snapped_pending_event_counts.swap(pending_counts_); | |
| for (auto& [key, count] : snapped_pending_event_counts) { | |
| batch.emplace_back(key.metric_id(), key.dimension_values(), | |
| fuchsia_metrics::MetricEventPayload::WithCount(count)); | |
| ZX_DEBUG_ASSERT(batch.size() <= kMaxBatchSize); | |
| if (batch.size() == kMaxBatchSize) { | |
| cobalt_logger_->LogMetricEvents(std::move(batch)); | |
| batch.clear(); | |
| } | |
| } | |
| PendingStrings snapped_pending_strings; | |
| snapped_pending_strings.swap(pending_strings_); | |
| for (auto& [key, strings] : snapped_pending_strings) { | |
| for (auto& string : strings) { | |
| batch.emplace_back(key.metric_id(), key.dimension_values(), | |
| fuchsia_metrics::MetricEventPayload::WithStringValue(string)); | |
| ZX_DEBUG_ASSERT(batch.size() <= kMaxBatchSize); | |
| if (batch.size() == kMaxBatchSize) { | |
| cobalt_logger_->LogMetricEvents(std::move(batch)); | |
| batch.clear(); | |
| } | |
| } | |
| } | |
| PendingHistograms snapped_pending_histograms; | |
| snapped_pending_histograms.swap(pending_histograms_); | |
| for (auto& [histogram_key, pending_buckets] : snapped_pending_histograms) { | |
| std::vector<fuchsia_metrics::HistogramBucket> buckets; | |
| auto bucket_iter = pending_buckets.begin(); | |
| while (bucket_iter != pending_buckets.end()) { | |
| auto [bucket_index, tally] = *bucket_iter; | |
| bucket_iter++; | |
| fuchsia_metrics::HistogramBucket bucket; | |
| bucket.index() = bucket_index; | |
| bucket.count() = tally; | |
| buckets.emplace_back(std::move(bucket)); | |
| if (buckets.size() == fuchsia_metrics::kMaxHistogramBuckets || | |
| bucket_iter == pending_buckets.end()) { | |
| batch.emplace_back(histogram_key.metric_id(), histogram_key.dimension_values(), | |
| fuchsia_metrics::MetricEventPayload::WithHistogram(std::move(buckets))); | |
| buckets.clear(); | |
| ZX_DEBUG_ASSERT(batch.size() <= kMaxBatchSize); | |
| if (batch.size() == kMaxBatchSize) { | |
| cobalt_logger_->LogMetricEvents(std::move(batch)); | |
| batch.clear(); | |
| } | |
| } | |
| } | |
| } | |
| ZX_DEBUG_ASSERT(batch.size() < kMaxBatchSize); | |
| if (!batch.empty()) { | |
| cobalt_logger_->LogMetricEvents(std::move(batch)); | |
| batch.clear(); | |
| } | |
| } | |
| void MetricsBuffer::TryPostFlushCountsLocked() { | |
| ZX_DEBUG_ASSERT(!!loop_ == !!cobalt_logger_); | |
| if (cobalt_logger_) { | |
| ZX_DEBUG_ASSERT(loop_); | |
| async::PostTaskForTime( | |
| loop_->dispatcher(), [this] { FlushPendingEventCounts(); }, | |
| last_flushed_ + min_logging_period_); | |
| } | |
| } | |
| MetricsBuffer::MetricKey::MetricKey(uint32_t metric_id, std::vector<uint32_t> dimension_values) | |
| : metric_id_(metric_id), dimension_values_(std::move(dimension_values)) {} | |
| uint32_t MetricsBuffer::MetricKey::metric_id() const { return metric_id_; } | |
| const std::vector<uint32_t>& MetricsBuffer::MetricKey::dimension_values() const { | |
| return dimension_values_; | |
| } | |
| size_t MetricsBuffer::MetricKeyHash::operator()(const MetricKey& key) const noexcept { | |
| // Rely on size_t being unsigned so it'll wrap without being undefined behavior. | |
| size_t hash = hash_uint32_(key.metric_id()); | |
| for (auto value : key.dimension_values()) { | |
| hash += hash_uint32_(value); | |
| } | |
| return hash; | |
| } | |
| bool MetricsBuffer::MetricKeyEqual::operator()(const MetricKey& lhs, | |
| const MetricKey& rhs) const noexcept { | |
| if (lhs.metric_id() != rhs.metric_id()) { | |
| return false; | |
| } | |
| if (lhs.dimension_values().size() != rhs.dimension_values().size()) { | |
| return false; | |
| } | |
| size_t size = lhs.dimension_values().size(); | |
| for (uint32_t i = 0; i < size; ++i) { | |
| if (lhs.dimension_values()[i] != rhs.dimension_values()[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| MetricBuffer MetricsBuffer::CreateMetricBuffer(uint32_t metric_id) { | |
| return MetricBuffer(shared_from_this(), metric_id); | |
| } | |
| StringMetricBuffer MetricsBuffer::CreateStringMetricBuffer(uint32_t metric_id) { | |
| return StringMetricBuffer(shared_from_this(), metric_id); | |
| } | |
| HistogramMetricBuffer MetricsBuffer::CreateHistogramMetricBuffer(HistogramInfo histogram_info) { | |
| return HistogramMetricBuffer(shared_from_this(), std::move(histogram_info)); | |
| } | |
| MetricBuffer::MetricBuffer(std::shared_ptr<MetricsBuffer> parent, uint32_t metric_id) | |
| : parent_(parent), metric_id_(metric_id) {} | |
| void MetricBuffer::LogEvent(std::vector<uint32_t> dimension_values) { | |
| parent_->LogEvent(metric_id_, std::move(dimension_values)); | |
| } | |
| void MetricBuffer::LogEventCount(std::vector<uint32_t> dimension_values, uint32_t count) { | |
| parent_->LogEventCount(metric_id_, std::move(dimension_values), count); | |
| } | |
| StringMetricBuffer::StringMetricBuffer(std::shared_ptr<MetricsBuffer> parent, uint32_t metric_id) | |
| : parent_(parent), metric_id_(metric_id) {} | |
| void StringMetricBuffer::LogString(std::vector<uint32_t> dimension_values, | |
| std::string string_value) { | |
| parent_->LogString(metric_id_, std::move(dimension_values), std::move(string_value)); | |
| } | |
| HistogramMetricBuffer::HistogramMetricBuffer(std::shared_ptr<MetricsBuffer> parent, | |
| HistogramInfo histogram_info) | |
| : parent_(parent), histogram_info_(std::move(histogram_info)) { | |
| std::visit( | |
| [this](auto&& info) { | |
| using InfoT = std::decay_t<decltype(info)>; | |
| if constexpr (std::is_same_v<InfoT, LinearIntegerBuckets>) { | |
| floors_.resize(info.num_buckets + 1); | |
| for (int64_t i = 0; i < info.num_buckets + 1; i++) { | |
| floors_[i] = info.floor + i * info.step_size; | |
| } | |
| } else if constexpr (std::is_same_v<InfoT, ExponentialIntegerBuckets>) { | |
| floors_.resize(info.num_buckets + 1); | |
| floors_[0] = info.floor; | |
| int64_t offset = info.initial_step; | |
| for (uint32_t i = 1; i < info.num_buckets + 1; i++) { | |
| floors_[i] = info.floor + offset; | |
| offset *= info.step_multiplier; | |
| } | |
| } else { | |
| static_assert(sfinae_false_v<InfoT>, "non-exhaustive"); | |
| } | |
| }, | |
| histogram_info_.buckets); | |
| } | |
| void HistogramMetricBuffer::LogValue(std::vector<uint32_t> dimension_values, int64_t value) { | |
| parent_->LogHistogramValue(histogram_info_, floors_, std::move(dimension_values), value); | |
| } | |
| } // namespace cobalt |