blob: 4ba1836302b957d54d7bd736e476d2145257d3dd [file]
/*
* Copyright (C) 2019 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/trace_processor/read_trace_internal.h"
#include <fcntl.h>
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/scoped_mmap.h"
#include "perfetto/ext/base/status_macros.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/trace_processor/trace_blob.h"
#include "perfetto/trace_processor/trace_blob_view.h"
#include "perfetto/trace_processor/trace_processor.h"
// URL fetching shells out to `curl` via base::Subprocess. Keep in sync with
// `_subprocess_supported` in src/base/BUILD.gn; elsewhere we error out cleanly.
#define PERFETTO_TP_HTTP_IMPORT_SUPPORTED() \
(PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX_BUT_NOT_QNX) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_MAC) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_FREEBSD) || \
PERFETTO_BUILDFLAG(PERFETTO_OS_WIN))
#if PERFETTO_TP_HTTP_IMPORT_SUPPORTED()
#include "perfetto/base/proc_utils.h"
#include "perfetto/ext/base/murmur_hash.h"
#include "perfetto/ext/base/pipe.h"
#include "perfetto/ext/base/subprocess.h"
#include "src/trace_processor/util/simple_json_parser.h"
#endif
namespace perfetto::trace_processor {
namespace {
// 1MB chunk size seems the best tradeoff on a MacBook Pro 2013 - i7 2.8 GHz.
constexpr size_t kChunkSize = 1024 * 1024;
bool IsUrl(const std::string& path) {
return base::StartsWith(path, "http://") ||
base::StartsWith(path, "https://");
}
// The decomposed components of a URL of the form:
// <scheme>://<host>[:<port>]/<path>[?<query>][#<fragment>]
struct ParsedUrl {
std::string_view scheme;
std::string_view host;
std::string_view path;
std::string_view query;
std::string_view fragment;
};
// Parses `url` into its components. Returns nullopt if there is no "://"
// separating the scheme from the authority. `url` must outlive the result, as
// the components are views into it.
std::optional<ParsedUrl> ParseUrl(std::string_view url) {
size_t scheme_end = url.find("://");
if (scheme_end == std::string_view::npos)
return std::nullopt;
ParsedUrl out;
out.scheme = url.substr(0, scheme_end);
std::string_view rest = url.substr(scheme_end + 3);
// The authority runs up to the first '/', '?' or '#'.
size_t authority_end = rest.find_first_of("/?#");
std::string_view authority = rest.substr(0, authority_end);
rest = authority_end == std::string_view::npos ? std::string_view()
: rest.substr(authority_end);
// Strip any "userinfo@" prefix and ":port" suffix to leave just the host.
if (size_t at = authority.find('@'); at != std::string_view::npos)
authority = authority.substr(at + 1);
out.host = authority.substr(0, authority.find(':'));
// The fragment is everything after the first '#'.
if (size_t frag = rest.find('#'); frag != std::string_view::npos) {
out.fragment = rest.substr(frag + 1);
rest = rest.substr(0, frag);
}
// The query is everything between '?' and the fragment.
if (size_t q = rest.find('?'); q != std::string_view::npos) {
out.query = rest.substr(q + 1);
rest = rest.substr(0, q);
}
out.path = rest;
return out;
}
// Returns the value of the `key` parameter in an "a=1&b=2" style query string,
// or nullopt if it is absent.
std::optional<std::string_view> GetQueryParam(std::string_view query,
std::string_view key) {
while (!query.empty()) {
size_t amp = query.find('&');
std::string_view pair = query.substr(0, amp);
size_t eq = pair.find('=');
if (pair.substr(0, eq) == key)
return eq == std::string_view::npos ? std::string_view()
: pair.substr(eq + 1);
if (amp == std::string_view::npos)
break;
query = query.substr(amp + 1);
}
return std::nullopt;
}
bool IsPermalinkHash(std::string_view s) {
if (s.size() != 40)
return false;
for (char c : s) {
if (!std::isxdigit(static_cast<unsigned char>(c)))
return false;
}
return true;
}
// If `url` is a Perfetto UI share link (ui.perfetto.dev/#!/?s=<hash>), returns
// the 40-char hex permalink hash. Returns nullopt for any other URL.
std::optional<std::string> GetPermalinkHash(const std::string& url) {
std::optional<ParsedUrl> parsed = ParseUrl(url);
if (!parsed || parsed->host != "ui.perfetto.dev")
return std::nullopt;
// The share id is the `s` query param. The Perfetto UI uses hash-based
// routing (#!/?s=<hash>), so it normally lives in the query string of the
// fragment, but accept it in the top-level query string too.
auto valid_hash =
[](std::optional<std::string_view> v) -> std::optional<std::string> {
if (v && IsPermalinkHash(*v))
return std::string(*v);
return std::nullopt;
};
if (auto h = valid_hash(GetQueryParam(parsed->query, "s")))
return h;
if (size_t q = parsed->fragment.find('?'); q != std::string_view::npos) {
if (auto h = valid_hash(GetQueryParam(parsed->fragment.substr(q + 1), "s")))
return h;
}
return std::nullopt;
}
#if PERFETTO_TP_HTTP_IMPORT_SUPPORTED()
// GCS bucket that backs ui.perfetto.dev share links. A permalink hash resolves
// to a small JSON object stored at <kPermalinkBucketUrl><hash>, which in turn
// references the underlying trace via its "traceUrl" field. Keep in sync with
// ui/src/base/gcs_uploader.ts and ui/src/frontend/permalink.ts.
constexpr char kPermalinkBucketUrl[] =
"https://storage.googleapis.com/perfetto-ui-data/";
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
constexpr char kPathSep = '\\';
#else
constexpr char kPathSep = '/';
#endif
// Returns the directory used to cache traces downloaded from URLs, creating it
// (and the intermediate directories) if necessary. Returns nullopt if no
// suitable location is available or the directory can't be created, in which
// case callers proceed without caching.
// POSIX: $XDG_CACHE_HOME/perfetto/tp-http-traces, else ~/.cache/...
// Windows: %LOCALAPPDATA%\perfetto\tp-http-traces
std::optional<std::string> GetTraceCacheDir() {
std::string dir;
auto append_and_mkdir = [&dir](std::initializer_list<const char*> parts) {
for (const char* p : parts) {
dir += kPathSep;
dir += p;
base::Mkdir(dir); // Best-effort; ignores "already exists".
}
};
#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
const char* root = getenv("LOCALAPPDATA");
if (!root || !*root)
return std::nullopt;
dir = root;
append_and_mkdir({"perfetto", "tp-http-traces"});
#else
if (const char* xdg = getenv("XDG_CACHE_HOME"); xdg && *xdg) {
dir = xdg;
append_and_mkdir({"perfetto", "tp-http-traces"});
} else if (const char* home = getenv("HOME"); home && *home) {
dir = home;
append_and_mkdir({".cache", "perfetto", "tp-http-traces"});
} else {
return std::nullopt;
}
#endif
if (!base::FileExists(dir))
return std::nullopt;
return dir;
}
// Returns the path of the cache entry for `url`, or nullopt if caching is
// unavailable. The file is named by the hash of the URL we actually download
// (for permalinks, the resolved content-addressed trace URL), so the same
// trace shared via different links maps to a single cache entry.
std::optional<std::string> CachePathForUrl(const std::string& url) {
std::optional<std::string> dir = GetTraceCacheDir();
if (!dir)
return std::nullopt;
base::StackString<24> name("%016" PRIx64,
base::MurmurHashValue(std::string_view(url)));
return *dir + kPathSep + name.c_str();
}
// Builds curl to fetch `url` with its stderr redirected to `stderr_wr`. The
// flags keep curl quiet: -L follows redirects, -f makes it fail (non-zero exit,
// empty body) on HTTP errors and -sS suppresses the progress meter while still
// emitting actual error messages on stderr (which we capture rather than let
// leak to the terminal).
//
// If `if_modified_since_file` is set, curl issues a conditional GET (-z), using
// that file's modification time as the If-Modified-Since value. The server then
// returns "304 Not Modified" with an empty body when the resource is unchanged,
// which lets us revalidate a cached copy without re-downloading it.
base::Subprocess MakeCurl(
const std::string& url,
const std::optional<std::string>& if_modified_since_file,
base::ScopedPlatformHandle stderr_wr) {
base::Subprocess curl;
curl.args.exec_cmd = {"curl", "-L", "-f", "-sS"};
if (if_modified_since_file) {
curl.args.exec_cmd.push_back("-z");
curl.args.exec_cmd.push_back(*if_modified_since_file);
}
curl.args.exec_cmd.push_back(url);
curl.args.stdin_mode = base::Subprocess::InputMode::kDevNull;
curl.args.stdout_mode = base::Subprocess::OutputMode::kBuffer;
curl.args.stderr_mode = base::Subprocess::OutputMode::kFd;
curl.args.out_fd = std::move(stderr_wr);
return curl;
}
// Turns a failed curl invocation into a Status, preferring curl's own (now
// captured) error message and falling back to the exit code.
base::Status CurlError(const std::string& url,
int returncode,
const std::string& stderr_text) {
std::string detail = base::TrimWhitespace(stderr_text);
if (!detail.empty())
return base::ErrStatus("Failed to download '%s': %s", url.c_str(),
detail.c_str());
return base::ErrStatus(
"Failed to download '%s': curl exited with code %d (is curl installed "
"and "
"the URL reachable?)",
url.c_str(), returncode);
}
// Fetches `url` in full into `out`. Used for small responses (permalink JSON).
base::Status CurlFetchAll(const std::string& url, std::string* out) {
base::Pipe err = base::Pipe::Create();
base::Subprocess curl = MakeCurl(url, std::nullopt, std::move(err.wr));
// Call() drains stdout into output() and waits for curl to exit; the write
// end of `err` is closed in the parent by Start(), so the read below sees
// EOF.
bool ok = curl.Call();
std::string stderr_text;
base::ReadPlatformHandle(*err.rd, &stderr_text);
if (!ok)
return CurlError(url, curl.returncode(), stderr_text);
*out = std::move(curl.output());
return base::OkStatus();
}
// Fetches `url` and streams the bytes into `tp` as they arrive, keeping at most
// one poll interval of data in memory at a time. If `cache_path` is set, the
// bytes are also written to a temp file and atomically renamed into place once
// the whole download has been parsed successfully. If `conditional` is true the
// request is a conditional GET against the existing `cache_path` (see
// MakeCurl): the server may answer "304 Not Modified" with an empty body, in
// which case nothing is streamed and `*bytes_streamed` is left at 0 so the
// caller knows to fall back to the cached copy. On return `*bytes_streamed`
// holds the number of bytes fed into `tp`.
base::Status CurlStreamInto(
TraceProcessor* tp,
const std::string& url,
const std::optional<std::string>& cache_path,
bool conditional,
const std::function<void(uint64_t parsed_size)>& progress_callback,
uint64_t* bytes_streamed) {
*bytes_streamed = 0;
// Tee the download into a per-pid temp file alongside the final cache entry
// so concurrent downloads of the same URL don't clobber each other. Caching
// is best-effort: any failure here just disables it for this download.
base::ScopedFile cache_fd;
std::string cache_tmp;
if (cache_path) {
cache_tmp = *cache_path + "." +
std::to_string(static_cast<uint64_t>(base::GetProcessId())) +
".tmp";
cache_fd = base::OpenFile(cache_tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (!cache_fd)
cache_tmp.clear();
}
// For a conditional request curl derives the If-Modified-Since time from the
// existing cache file's mtime.
std::optional<std::string> if_modified_since =
conditional ? cache_path : std::nullopt;
base::Pipe err = base::Pipe::Create();
base::Subprocess curl = MakeCurl(url, if_modified_since, std::move(err.wr));
curl.Start();
uint64_t bytes_read = 0;
base::Status parse_status;
bool curl_exited = false;
for (;;) {
// Wait() drains whatever stdout is currently available into output() and
// returns true once curl has exited and all output has been read.
bool done = curl.Wait(/*timeout_ms=*/100);
std::string& out = curl.output();
if (!out.empty()) {
if (cache_fd && base::WriteAll(*cache_fd, out.data(), out.size()) < 0)
cache_fd.reset(); // Stop caching, but keep streaming.
bytes_read += out.size();
TraceBlobView view(TraceBlob::CopyFrom(out.data(), out.size()));
out.clear();
parse_status = tp->Parse(std::move(view));
if (!parse_status.ok())
break;
if (progress_callback)
progress_callback(bytes_read);
}
if (done) {
curl_exited = true;
break;
}
}
// If we bailed out of the loop early (parse error) curl is still running and,
// now that we've stopped draining its stdout, will soon block on a full pipe.
// Kill it so it releases its end of the stderr pipe; otherwise the
// ReadPlatformHandle() below would deadlock waiting for an EOF that never
// comes.
if (!curl_exited)
curl.KillAndWaitForTermination();
// curl has exited; with -sS its stderr is at most a short error line, which
// is now fully buffered in the pipe and read here (the parent's write end was
// closed by Start(), so this sees EOF).
std::string stderr_text;
base::ReadPlatformHandle(*err.rd, &stderr_text);
bool curl_ok =
curl.status() == base::Subprocess::kTerminated && curl.returncode() == 0;
// Commit the cache entry only on a fully successful download+parse that
// actually produced bytes. A 304 Not Modified streams nothing, so committing
// here would clobber the up-to-date cache with an empty file.
if (!cache_tmp.empty()) {
cache_fd.reset(); // Close before renaming.
if (curl_ok && parse_status.ok() && bytes_read > 0 && cache_path &&
std::rename(cache_tmp.c_str(), cache_path->c_str()) == 0) {
// Committed.
} else {
remove(cache_tmp.c_str()); // 304, lost a race, or download failed.
}
}
RETURN_IF_ERROR(parse_status);
if (!curl_ok)
return CurlError(url, curl.returncode(), stderr_text);
*bytes_streamed = bytes_read;
return base::OkStatus();
}
// Resolves a Perfetto UI permalink hash to the underlying trace URL by fetching
// and parsing the permalink JSON object from GCS.
base::StatusOr<std::string> ResolvePermalink(const std::string& hash) {
std::string json;
RETURN_IF_ERROR(CurlFetchAll(kPermalinkBucketUrl + hash, &json));
std::optional<std::string> trace_url;
json::SimpleJsonParser parser(json);
RETURN_IF_ERROR(parser.Parse());
RETURN_IF_ERROR(
parser.ForEachField([&](std::string_view key) -> json::FieldResult {
if (key == "traceUrl") {
if (auto s = parser.GetString())
trace_url = std::string(*s);
return json::FieldResult::Handled{};
}
return json::FieldResult::Skip{};
}));
if (!trace_url || trace_url->empty()) {
return base::ErrStatus(
"Perfetto UI permalink '%s' does not reference a trace (it may only "
"contain saved UI state).",
hash.c_str());
}
return *trace_url;
}
// Reads the cached copy at `cache_path` into `tp` via the normal local-file
// path (mmap etc.).
base::Status ReadCachedTrace(
TraceProcessor* tp,
const std::string& url,
const std::string& cache_path,
const std::function<void(uint64_t parsed_size)>& progress_callback) {
RETURN_IF_ERROR(
ReadTraceUnfinalized(tp, cache_path.c_str(), progress_callback));
tp->SetCurrentTraceName(url);
return base::OkStatus();
}
base::Status ReadTraceFromUrl(
TraceProcessor* tp,
const std::string& url,
const std::optional<std::string>& permalink_hash,
bool use_cache,
const std::function<void(uint64_t parsed_size)>& progress_callback) {
std::string trace_url = url;
if (permalink_hash) {
PERFETTO_LOG("Resolving Perfetto UI permalink (hash: %s)",
permalink_hash->c_str());
ASSIGN_OR_RETURN(trace_url, ResolvePermalink(*permalink_hash));
}
std::optional<std::string> cache_path =
use_cache ? CachePathForUrl(trace_url) : std::nullopt;
bool cache_exists = cache_path && base::FileExists(*cache_path);
// Always ask the server whether our cached copy (if any) is still current via
// a conditional GET. We never serve a stale trace for a mutable URL, while a
// "304 Not Modified" still avoids re-downloading an unchanged one.
PERFETTO_LOG("Downloading trace from %s", trace_url.c_str());
uint64_t streamed = 0;
base::Status status =
CurlStreamInto(tp, trace_url, cache_path, /*conditional=*/cache_exists,
progress_callback, &streamed);
// The fetch failed (e.g. offline) but nothing was streamed into `tp` yet, so
// it's safe to fall back to a previously cached copy rather than fail
// outright.
if (!status.ok()) {
if (cache_exists && streamed == 0) {
PERFETTO_LOG("Download failed (%s); using cached trace for %s",
status.c_message(), trace_url.c_str());
return ReadCachedTrace(tp, url, *cache_path, progress_callback);
}
return status;
}
// A successful fetch that streamed no bytes is a 304 Not Modified: the cached
// copy is current, so parse that instead.
if (streamed == 0 && cache_exists) {
PERFETTO_LOG("Cached trace is up to date for %s", trace_url.c_str());
return ReadCachedTrace(tp, url, *cache_path, progress_callback);
}
tp->SetCurrentTraceName(url);
return base::OkStatus();
}
#else // !PERFETTO_TP_HTTP_IMPORT_SUPPORTED()
base::Status ReadTraceFromUrl(
TraceProcessor*,
const std::string& url,
const std::optional<std::string>&,
bool,
const std::function<void(uint64_t parsed_size)>&) {
return base::ErrStatus(
"Loading traces from a URL is not supported on this platform (url: %s)",
url.c_str());
}
#endif // PERFETTO_TP_HTTP_IMPORT_SUPPORTED()
base::Status ReadTraceUsingRead(
TraceProcessor* tp,
int fd,
uint64_t* file_size,
const std::function<void(uint64_t parsed_size)>& progress_callback) {
// Load the trace in chunks using ordinary read().
for (int i = 0;; i++) {
if (progress_callback && i % 128 == 0)
progress_callback(*file_size);
TraceBlob blob = TraceBlob::Allocate(kChunkSize);
auto rsize = base::Read(fd, blob.data(), blob.size());
if (rsize == 0)
break;
if (rsize < 0) {
return base::ErrStatus("Reading trace file failed (errno: %d, %s)", errno,
strerror(errno));
}
*file_size += static_cast<uint64_t>(rsize);
TraceBlobView blob_view(std::move(blob), 0, static_cast<size_t>(rsize));
RETURN_IF_ERROR(tp->Parse(std::move(blob_view)));
}
return base::OkStatus();
}
} // namespace
base::Status ReadTraceUnfinalized(
TraceProcessor* tp,
const char* filename,
const std::function<void(uint64_t parsed_size)>& progress_callback,
const ReadTraceArgs& args) {
// Handle http(s) URLs, but only the kind the caller has opted into. Perfetto
// UI share links and plain URLs are gated on separate flags; if the relevant
// one is not set we return a clear error rather than treating the URL as a
// (non-existent) local file path.
if (IsUrl(filename)) {
std::optional<std::string> permalink_hash = GetPermalinkHash(filename);
if (permalink_hash) {
if (!args.allow_perfetto_ui_links) {
return base::ErrStatus(
"Cannot load Perfetto UI share link '%s': loading share links is "
"not enabled (set ReadTraceArgs::allow_perfetto_ui_links).",
filename);
}
} else if (!args.allow_http) {
return base::ErrStatus(
"Cannot load '%s' from a URL: loading traces from URLs is not "
"enabled "
"(set ReadTraceArgs::allow_http).",
filename);
}
return ReadTraceFromUrl(tp, filename, permalink_hash, args.cache_downloads,
progress_callback);
}
uint64_t bytes_read = 0;
#if PERFETTO_HAS_MMAP()
char* no_mmap = getenv("TRACE_PROCESSOR_NO_MMAP");
bool use_mmap = !no_mmap || *no_mmap != '1';
if (use_mmap) {
base::ScopedMmap mapped = base::ReadMmapWholeFile(filename);
if (mapped.IsValid()) {
size_t length = mapped.length();
TraceBlobView whole_mmap(TraceBlob::FromMmap(std::move(mapped)));
// Parse the file in chunks so we get some status update on stdio.
static constexpr size_t kMmapChunkSize = 128ul * 1024 * 1024;
while (bytes_read < length) {
progress_callback(bytes_read);
const size_t bytes_read_z = static_cast<size_t>(bytes_read);
size_t slice_size = std::min(length - bytes_read_z, kMmapChunkSize);
TraceBlobView slice = whole_mmap.slice_off(bytes_read_z, slice_size);
RETURN_IF_ERROR(tp->Parse(std::move(slice)));
bytes_read += slice_size;
} // while (slices)
} // if (mapped.IsValid())
} // if (use_mmap)
if (bytes_read == 0)
PERFETTO_LOG("Cannot use mmap on this system. Falling back on read()");
#endif // PERFETTO_HAS_MMAP()
if (bytes_read == 0) {
base::ScopedFile fd(base::OpenFile(filename, O_RDONLY));
if (!fd)
return base::ErrStatus("Could not open trace file (path: %s)", filename);
RETURN_IF_ERROR(
ReadTraceUsingRead(tp, *fd, &bytes_read, progress_callback));
}
tp->SetCurrentTraceName(filename);
if (progress_callback)
progress_callback(bytes_read);
return base::OkStatus();
}
} // namespace perfetto::trace_processor