blob: c7442a22e5c01f248bb1f29242754d0c8d5c0fe0 [file]
// Copyright 2025 The Pigweed Authors
//
// 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
//
// https://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 "pw_metric/metric_walker.h"
#include <cinttypes>
#include <optional>
#include "pw_assert/check.h"
#include "pw_function/function.h"
#include "pw_log/log.h"
#include "pw_metric/metric.h"
#include "pw_result/result.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
namespace pw::metric {
// Private implementation of ScopedName. Exists to safely push/pop parent groups
// from the explicit stack during a metric walk.
struct ResumableMetricWalker::ScopedName {
ScopedName(Token name, ResumableMetricWalker& rhs) : walker(rhs) {
// Ensure the metric path depth doesn't exceed the allocated capacity. If
// this assert fails, the path_ vector's capacity should be increased.
PW_CHECK_INT_LT(walker.path_.size(),
walker.path_.capacity(),
"The path_ vector's capacity should be increased.");
walker.path_.push_back(name);
}
~ScopedName() { walker.path_.pop_back(); }
ResumableMetricWalker& walker;
};
ResumableMetricWalker::ResumableMetricWalker(UnaryMetricWriter& writer)
: writer_(writer) {}
Result<uint64_t> ResumableMetricWalker::Walk(const MetricList& metrics,
const GroupList& groups,
std::optional<uint64_t> cursor) {
start_cursor_ = cursor.value_or(0);
writing_phase_ = (start_cursor_ == 0);
next_cursor_ = 0;
Status walk_status = RecursiveWalkHelper(metrics, groups);
// If the client provided a cursor but the walk ended (for any reason)
// without finding it, the cursor is invalid. This check must happen BEFORE
// checking for other statuses like RESOURCE_EXHAUSTED.
if (start_cursor_ != 0 && !writing_phase_) {
return Status::NotFound();
}
if (walk_status.IsResourceExhausted()) {
// A page was filled. Before returning the cursor for the next page, check
// if progress is being made. If the next cursor is the same as the start
// cursor, it means the metric at that address is too large to fit into an
// empty response buffer. This would cause an infinite loop.
if (start_cursor_ != 0 && start_cursor_ == next_cursor_) {
PW_LOG_ERROR("Walker stalled: metric at cursor 0x%llx is too large.",
(unsigned long long)start_cursor_);
return Status::ResourceExhausted();
}
return walk_status;
}
PW_TRY(walk_status);
// The walk completed successfully.
return 0u;
}
Status ResumableMetricWalker::RecursiveWalkHelper(const MetricList& metrics,
const GroupList& groups) {
for (const auto& metric : metrics.list()) {
ScopedName scoped_name(metric.name(), *this);
if (!writing_phase_) {
if (static_cast<uint64_t>(reinterpret_cast<uintptr_t>(&metric)) ==
start_cursor_) {
writing_phase_ = true;
// Fall through to write the metric on this iteration.
} else {
continue; // Still searching for the cursor.
}
}
Status status = writer_.Write(metric, path_);
if (status.IsResourceExhausted()) {
// The page is full. The current metric could not be written.
// Its address becomes the cursor for the next request.
next_cursor_ =
static_cast<uint64_t>(reinterpret_cast<uintptr_t>(&metric));
return status;
}
PW_TRY(status);
}
for (const auto& group : groups.list()) {
ScopedName scoped_name(group.name(), *this);
PW_TRY(RecursiveWalkHelper(group.metrics(), group.children()));
}
return OkStatus();
}
} // namespace pw::metric