| /* |
| * Copyright (C) 2018 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 "src/perfetto_cmd/rate_limiter.h" |
| |
| #include <fcntl.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| |
| #include <algorithm> |
| #include <cinttypes> |
| |
| #include "perfetto/base/logging.h" |
| #include "perfetto/ext/base/file_utils.h" |
| #include "perfetto/ext/base/scoped_file.h" |
| #include "perfetto/ext/base/utils.h" |
| #include "src/perfetto_cmd/perfetto_cmd.h" |
| |
| namespace perfetto { |
| namespace { |
| |
| // Every 24 hours we reset how much we've uploaded. |
| const uint64_t kMaxUploadResetPeriodInSeconds = 60 * 60 * 24; |
| |
| // Maximum of 10mb every 24h. |
| const uint64_t kMaxUploadInBytes = 1024 * 1024 * 10; |
| |
| // Keep track of last 10 observed sessions. |
| const uint64_t kMaxSessionsInHistory = 10; |
| |
| } // namespace |
| |
| using PerSessionState = gen::PerfettoCmdState::PerSessionState; |
| |
| RateLimiter::RateLimiter() = default; |
| RateLimiter::~RateLimiter() = default; |
| |
| RateLimiter::ShouldTraceResponse RateLimiter::ShouldTrace(const Args& args) { |
| uint64_t now_in_s = static_cast<uint64_t>(args.current_time.count()); |
| |
| // Not uploading? |
| // -> We can just trace. |
| if (!args.is_uploading) |
| return ShouldTraceResponse::kOkToTrace; |
| |
| // If we're tracing a user build we should only trace if the override in |
| // the config is set: |
| if (args.is_user_build && !args.allow_user_build_tracing) { |
| PERFETTO_ELOG( |
| "Guardrail: allow_user_build_tracing must be set to trace on user " |
| "builds"); |
| return ShouldTraceResponse::kNotAllowedOnUserBuild; |
| } |
| |
| // The state file is gone. |
| // Maybe we're tracing for the first time or maybe something went wrong the |
| // last time we tried to save the state. Either way reinitialize the state |
| // file. |
| if (!StateFileExists()) { |
| // We can't write the empty state file? |
| // -> Give up. |
| if (!ClearState()) { |
| PERFETTO_ELOG("Guardrail: failed to initialize guardrail state."); |
| return ShouldTraceResponse::kFailedToInitState; |
| } |
| } |
| |
| bool loaded_state = LoadState(&state_); |
| |
| // Failed to load the state? |
| // Current time is before either saved times? |
| // Last saved trace time is before first saved trace time? |
| // -> Try to save a clean state but don't trace. |
| if (!loaded_state || now_in_s < state_.first_trace_timestamp() || |
| now_in_s < state_.last_trace_timestamp() || |
| state_.last_trace_timestamp() < state_.first_trace_timestamp()) { |
| ClearState(); |
| PERFETTO_ELOG("Guardrail: state invalid, clearing it."); |
| if (!args.ignore_guardrails) |
| return ShouldTraceResponse::kInvalidState; |
| } |
| |
| // First trace was more than 24h ago? Reset state. |
| if ((now_in_s - state_.first_trace_timestamp()) > |
| kMaxUploadResetPeriodInSeconds) { |
| state_.set_first_trace_timestamp(0); |
| state_.set_last_trace_timestamp(0); |
| state_.set_total_bytes_uploaded(0); |
| return ShouldTraceResponse::kOkToTrace; |
| } |
| |
| uint64_t max_upload_guardrail = kMaxUploadInBytes; |
| if (args.max_upload_bytes_override > 0) { |
| if (args.unique_session_name.empty()) { |
| PERFETTO_ELOG( |
| "Ignoring max_upload_per_day_bytes override as unique_session_name " |
| "not set"); |
| } else { |
| max_upload_guardrail = args.max_upload_bytes_override; |
| } |
| } |
| |
| uint64_t uploaded_so_far = state_.total_bytes_uploaded(); |
| if (!args.unique_session_name.empty()) { |
| uploaded_so_far = 0; |
| for (const auto& session_state : state_.session_state()) { |
| if (session_state.session_name() == args.unique_session_name) { |
| uploaded_so_far = session_state.total_bytes_uploaded(); |
| break; |
| } |
| } |
| } |
| |
| if (uploaded_so_far > max_upload_guardrail) { |
| PERFETTO_ELOG("Guardrail: Uploaded %" PRIu64 |
| " in the last 24h. Limit is %" PRIu64 ".", |
| uploaded_so_far, max_upload_guardrail); |
| if (!args.ignore_guardrails) |
| return ShouldTraceResponse::kHitUploadLimit; |
| } |
| |
| return ShouldTraceResponse::kOkToTrace; |
| } |
| |
| bool RateLimiter::OnTraceDone(const Args& args, bool success, uint64_t bytes) { |
| uint64_t now_in_s = static_cast<uint64_t>(args.current_time.count()); |
| |
| // Failed to upload? Don't update the state. |
| if (!success) |
| return false; |
| |
| if (!args.is_uploading) |
| return true; |
| |
| // If the first trace timestamp is 0 (either because this is the |
| // first time or because it was reset for being more than 24h ago). |
| // -> We update it to the time of this trace. |
| if (state_.first_trace_timestamp() == 0) |
| state_.set_first_trace_timestamp(now_in_s); |
| // Always updated the last trace timestamp. |
| state_.set_last_trace_timestamp(now_in_s); |
| |
| if (args.unique_session_name.empty()) { |
| // Add the amount we uploaded to the running total. |
| state_.set_total_bytes_uploaded(state_.total_bytes_uploaded() + bytes); |
| } else { |
| PerSessionState* target_session = nullptr; |
| for (PerSessionState& session : *state_.mutable_session_state()) { |
| if (session.session_name() == args.unique_session_name) { |
| target_session = &session; |
| break; |
| } |
| } |
| if (!target_session) { |
| target_session = state_.add_session_state(); |
| target_session->set_session_name(args.unique_session_name); |
| } |
| target_session->set_total_bytes_uploaded( |
| target_session->total_bytes_uploaded() + bytes); |
| target_session->set_last_trace_timestamp(now_in_s); |
| } |
| |
| if (!SaveState(state_)) { |
| PERFETTO_ELOG("Failed to save state."); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| std::string RateLimiter::GetStateFilePath() const { |
| return std::string(kStateDir) + "/.guardraildata"; |
| } |
| |
| bool RateLimiter::StateFileExists() { |
| struct stat out; |
| return stat(GetStateFilePath().c_str(), &out) != -1; |
| } |
| |
| bool RateLimiter::ClearState() { |
| gen::PerfettoCmdState zero{}; |
| zero.set_total_bytes_uploaded(0); |
| zero.set_last_trace_timestamp(0); |
| zero.set_first_trace_timestamp(0); |
| bool success = SaveState(zero); |
| if (!success && StateFileExists()) |
| remove(GetStateFilePath().c_str()); |
| return success; |
| } |
| |
| bool RateLimiter::LoadState(gen::PerfettoCmdState* state) { |
| base::ScopedFile in_fd(base::OpenFile(GetStateFilePath(), O_RDONLY)); |
| if (!in_fd) |
| return false; |
| std::string s; |
| base::ReadFileDescriptor(in_fd.get(), &s); |
| if (s.empty()) |
| return false; |
| return state->ParseFromString(s); |
| } |
| |
| bool RateLimiter::SaveState(const gen::PerfettoCmdState& input_state) { |
| gen::PerfettoCmdState state = input_state; |
| |
| // Keep only the N most recent per session states so the file doesn't |
| // grow indefinitely: |
| std::vector<PerSessionState>* sessions = state.mutable_session_state(); |
| std::sort(sessions->begin(), sessions->end(), |
| [](const PerSessionState& a, const PerSessionState& b) { |
| return a.last_trace_timestamp() > b.last_trace_timestamp(); |
| }); |
| if (sessions->size() > kMaxSessionsInHistory) { |
| sessions->resize(kMaxSessionsInHistory); |
| } |
| |
| // Rationale for 0666: the cmdline client can be executed under two |
| // different Unix UIDs: shell and statsd. If we run one after the |
| // other and the file has 0600 permissions, then the 2nd run won't |
| // be able to read the file and will clear it, aborting the trace. |
| // SELinux still prevents that anything other than the perfetto |
| // executable can change the guardrail file. |
| std::vector<uint8_t> buf = state.SerializeAsArray(); |
| base::ScopedFile out_fd( |
| base::OpenFile(GetStateFilePath(), O_WRONLY | O_CREAT | O_TRUNC, 0666)); |
| if (!out_fd) |
| return false; |
| ssize_t written = base::WriteAll(out_fd.get(), buf.data(), buf.size()); |
| return written >= 0 && static_cast<size_t>(written) == buf.size(); |
| } |
| |
| } // namespace perfetto |