aboutsummaryrefslogtreecommitdiff
path: root/src/perfetto_cmd/rate_limiter.cc
blob: 65e26e3f1f8c6db8668fd6c329d57130b1cdff02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/*
 * 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 <inttypes.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <algorithm>

#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