blob: 84f9bbb9950ea8133ee73ee1dcf4128c799791d7 [file] [log] [blame]
// Copyright 2019 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 <inttypes.h>
#include <lib/debugdata/datasink.h>
#include <lib/fzl/vmo-mapper.h>
#include <sys/stat.h>
#include <zircon/status.h>
#include <cstddef>
#include <cstdio>
#include <forward_list>
#include <optional>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
#include <fbl/string.h>
#include <fbl/unique_fd.h>
#include "src/lib/fxl/strings/string_printf.h"
#include <profile/InstrProfData.inc>
namespace debugdata {
namespace {
constexpr char kProfileSink[] = "llvm-profile";
using IntPtrT = intptr_t;
enum ValueKind {
#define VALUE_PROF_KIND(Enumerator, Value, Descr) Enumerator = (Value),
#include <profile/InstrProfData.inc>
};
struct __llvm_profile_data {
#define INSTR_PROF_DATA(Type, LLVMType, Name, Initializer) Type Name;
#include <profile/InstrProfData.inc>
};
struct __llvm_profile_header {
#define INSTR_PROF_RAW_HEADER(Type, Name, Initializer) Type Name;
#include <profile/InstrProfData.inc>
};
std::error_code ReadFile(const fbl::unique_fd& fd, uint8_t* data, size_t size) {
auto* buf = data;
ssize_t count = size;
off_t off = 0;
while (count > 0) {
ssize_t len = pread(fd.get(), buf, count, off);
if (len <= 0) {
return std::error_code{errno, std::generic_category()};
}
buf += len;
count -= len;
off += len;
}
return std::error_code{};
}
std::error_code WriteFile(const fbl::unique_fd& fd, const uint8_t* data, size_t size) {
auto* buf = data;
ssize_t count = size;
off_t off = 0;
while (count > 0) {
ssize_t len = pwrite(fd.get(), buf, count, off);
if (len <= 0) {
return std::error_code{errno, std::generic_category()};
}
buf += len;
count -= len;
off += len;
}
return std::error_code{};
}
std::optional<std::string> GetVMOName(const zx::vmo& vmo) {
char name[ZX_MAX_NAME_LEN];
zx_status_t status = vmo.get_property(ZX_PROP_NAME, name, sizeof(name));
if (status != ZX_OK || name[0] == '\0') {
zx_info_handle_basic_t info;
status = vmo.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
if (status != ZX_OK) {
return {};
}
snprintf(name, sizeof(name), "unnamed.%" PRIu64, info.koid);
}
return name;
}
fbl::String JoinPath(std::string_view parent, std::string_view child) {
if (parent.empty()) {
return fbl::String(child);
}
if (child.empty()) {
return fbl::String(parent);
}
if (parent[parent.size() - 1] != '/' && child[0] != '/') {
return fbl::String::Concat({parent, "/", child});
}
if (parent[parent.size() - 1] == '/' && child[0] == '/') {
return fbl::String::Concat({parent, &child[1]});
}
return fbl::String::Concat({parent, child});
}
// Returns true if raw profiles |src| and |dst| are structurally compatible.
bool ProfilesCompatible(const uint8_t* dst, const uint8_t* src, size_t size) {
const __llvm_profile_header* src_header = reinterpret_cast<const __llvm_profile_header*>(src);
const __llvm_profile_header* dst_header = reinterpret_cast<const __llvm_profile_header*>(dst);
if (src_header->Magic != dst_header->Magic || src_header->Version != dst_header->Version ||
src_header->DataSize != dst_header->DataSize ||
src_header->CountersSize != dst_header->CountersSize ||
src_header->NamesSize != dst_header->NamesSize)
return false;
const __llvm_profile_data* src_data_start =
reinterpret_cast<const __llvm_profile_data*>(src + sizeof(*src_header));
const __llvm_profile_data* src_data_end = src_data_start + src_header->DataSize;
const __llvm_profile_data* dst_data_start =
reinterpret_cast<const __llvm_profile_data*>(dst + sizeof(*dst_header));
const __llvm_profile_data* dst_data_end = dst_data_start + dst_header->DataSize;
for (const __llvm_profile_data *src_data = src_data_start, *dst_data = dst_data_start;
src_data < src_data_end && dst_data < dst_data_end; ++src_data, ++dst_data) {
if (src_data->NameRef != dst_data->NameRef || src_data->FuncHash != dst_data->FuncHash ||
src_data->NumCounters != dst_data->NumCounters)
return false;
}
return true;
}
// Merges raw profiles |src| and |dst| into |dst|.
//
// Note that this function does not check whether the profiles are compatible.
uint8_t* MergeProfiles(uint8_t* dst, const uint8_t* src, size_t size) {
const __llvm_profile_header* src_header = reinterpret_cast<const __llvm_profile_header*>(src);
const __llvm_profile_data* src_data_start =
reinterpret_cast<const __llvm_profile_data*>(src + sizeof(*src_header));
const __llvm_profile_data* src_data_end = src_data_start + src_header->DataSize;
const uint64_t* src_counters_start = reinterpret_cast<const uint64_t*>(src_data_end);
__llvm_profile_header* dst_header = reinterpret_cast<__llvm_profile_header*>(dst);
__llvm_profile_data* dst_data_start =
reinterpret_cast<__llvm_profile_data*>(dst + sizeof(*dst_header));
__llvm_profile_data* dst_data_end = dst_data_start + dst_header->DataSize;
uint64_t* dst_counters_start = reinterpret_cast<uint64_t*>(dst_data_end);
const __llvm_profile_data* src_data = src_data_start;
__llvm_profile_data* dst_data = dst_data_start;
for (; src_data < src_data_end && dst_data < dst_data_end; src_data++, dst_data++) {
const uint64_t* src_counters =
src_counters_start + (src_data->CounterPtr - src_header->CountersDelta) / sizeof(uint64_t);
uint64_t* dst_counters =
dst_counters_start + (dst_data->CounterPtr - dst_header->CountersDelta) / sizeof(uint64_t);
for (unsigned i = 0; i < src_data->NumCounters; i++) {
dst_counters[i] += src_counters[i];
}
}
return dst;
}
// This function processes all raw profiles that were published via data sink
// in an efficient manner. Concretely, rather than writing each data sink into
// a separate file, it merges all profiles from the same binary into a single
// profile. First it groups all VMOs by name which uniquely identifies each
// binary. Then it merges together all VMOs for the same binary together with
// data that's already on the disk (if it exists). Finally it writes the data
// back to disk (or creates the file if necessary). This ensures that at the
// end, we have exactly one profile for each binary in total, and each profile
// is read and written at most once per call to ProcessProfiles.
std::optional<std::vector<DumpFile>> ProcessProfiles(const std::vector<zx::vmo>& data,
const fbl::unique_fd& data_sink_dir_fd,
DataSinkCallback& error_callback,
DataSinkCallback& warning_callback) {
zx_status_t status;
if (mkdirat(data_sink_dir_fd.get(), kProfileSink, 0777) != 0 && errno != EEXIST) {
error_callback(fxl::StringPrintf("FAILURE: cannot mkdir \"%s\" for data-sink: %s\n",
kProfileSink, strerror(errno)));
return {};
}
fbl::unique_fd sink_dir_fd{openat(data_sink_dir_fd.get(), kProfileSink, O_RDONLY | O_DIRECTORY)};
if (!sink_dir_fd) {
error_callback(fxl::StringPrintf("FAILURE: cannot open data-sink directory \"%s\": %s\n",
kProfileSink, strerror(errno)));
return {};
}
std::unordered_map<std::string, std::forward_list<std::reference_wrapper<const zx::vmo>>>
profiles;
std::vector<DumpFile> dump_files;
// Group data by profile name. The name is a hash computed from profile metadata and
// should be unique across all binaries (modulo hash collisions).
for (const auto& vmo : data) {
auto name = GetVMOName(vmo);
if (!name) {
error_callback(fxl::StringPrintf("FAILURE: Cannot get a name for the VMO\n"));
return {};
}
profiles[*name].push_front(std::cref(vmo));
}
for (auto& [name, vmos] : profiles) {
fbl::unique_fd fd{openat(sink_dir_fd.get(), name.c_str(), O_RDWR | O_CREAT, 0666)};
if (!fd) {
error_callback(fxl::StringPrintf("FAILURE: Cannot open data-sink file \"%s\": %s\n",
name.c_str(), strerror(errno)));
return {};
}
uint64_t buffer_size;
std::unique_ptr<uint8_t[]> buffer;
struct stat stat;
if (fstat(fd.get(), &stat) == -1) {
error_callback(fxl::StringPrintf("FAILURE: Cannot stat data-sink file \"%s\": %s\n",
name.c_str(), strerror(errno)));
return {};
}
if (auto file_size = static_cast<uint64_t>(stat.st_size); file_size > 0) {
// The file already exists, use it to initialize the buffer...
buffer_size = file_size;
buffer = std::make_unique<uint8_t[]>(buffer_size);
if (std::error_code ec = ReadFile(fd, buffer.get(), file_size); ec) {
error_callback(fxl::StringPrintf("FAILURE: Cannot read data from \"%s\": %s\n",
name.c_str(), strerror(ec.value())));
return {};
}
}
while (!vmos.empty()) {
// Merge all VMOs into the buffer.
const zx::vmo& vmo = vmos.front();
vmos.pop_front();
uint64_t vmo_size;
status = vmo.get_size(&vmo_size);
if (status != ZX_OK) {
error_callback(
fxl::StringPrintf("FAILURE: Cannot get size of VMO \"%s\" for data-sink \"%s\": %s\n",
name.c_str(), kProfileSink, zx_status_get_string(status)));
return {};
}
fzl::VmoMapper mapper;
if (vmo_size > 0) {
zx_status_t status = mapper.Map(vmo, 0, vmo_size, ZX_VM_PERM_READ);
if (status != ZX_OK) {
error_callback(
fxl::StringPrintf("FAILURE: Cannot map VMO \"%s\" for data-sink \"%s\": %s\n",
name.c_str(), kProfileSink, zx_status_get_string(status)));
return {};
}
} else {
warning_callback(
fxl::StringPrintf("WARNING: Empty VMO \"%s\" published for data-sink \"%s\"\n",
kProfileSink, name.c_str()));
continue;
}
if (likely(buffer)) {
if (buffer_size != vmo_size) {
error_callback(
fxl::StringPrintf("FAILURE: Mismatch between content sizes for \"%s\": %lu != %lu\n",
name.c_str(), buffer_size, vmo_size));
}
ZX_ASSERT(buffer_size == vmo_size);
// Ensure that profiles are structuraly compatible.
if (!ProfilesCompatible(buffer.get(), reinterpret_cast<const uint8_t*>(mapper.start()),
buffer_size)) {
warning_callback(fxl::StringPrintf("WARNING: Unable to merge profile data: %s\n",
"source profile file is not compatible"));
continue;
}
MergeProfiles(buffer.get(), reinterpret_cast<const uint8_t*>(mapper.start()), buffer_size);
} else {
// ...Otherwise use the first non-empty VMO in the list to initialize the buffer.
buffer_size = vmo_size;
buffer = std::make_unique<uint8_t[]>(buffer_size);
memcpy(buffer.get(), mapper.start(), buffer_size);
}
}
// Write the data back to the file.
if (std::error_code ec = WriteFile(fd, buffer.get(), buffer_size); ec) {
error_callback(fxl::StringPrintf("FAILURE: Cannot write data to \"%s\": %s\n", name.c_str(),
strerror(ec.value())));
return {};
}
dump_files.push_back(DumpFile{name, JoinPath(kProfileSink, name).c_str()});
}
return dump_files;
}
// Process all data sink dumps and write to the disk.
std::optional<DumpFile> ProcessDataSinkDump(const std::string& sink_name, const zx::vmo& file_data,
const fbl::unique_fd& data_sink_dir_fd,
DataSinkCallback& error_callback,
DataSinkCallback& warning_callback) {
zx_status_t status;
if (mkdirat(data_sink_dir_fd.get(), sink_name.c_str(), 0777) != 0 && errno != EEXIST) {
error_callback(fxl::StringPrintf("FAILURE: cannot mkdir \"%s\" for data-sink: %s\n",
sink_name.c_str(), strerror(errno)));
return {};
}
fbl::unique_fd sink_dir_fd{
openat(data_sink_dir_fd.get(), sink_name.c_str(), O_RDONLY | O_DIRECTORY)};
if (!sink_dir_fd) {
error_callback(fxl::StringPrintf("FAILURE: cannot open data-sink directory \"%s\": %s\n",
sink_name.c_str(), strerror(errno)));
return {};
}
auto name = GetVMOName(file_data);
if (!name) {
error_callback(fxl::StringPrintf("FAILURE: Cannot get a name for the VMO\n"));
return {};
}
uint64_t size;
status = file_data.get_size(&size);
if (status != ZX_OK) {
error_callback(
fxl::StringPrintf("FAILURE: Cannot get size of VMO \"%s\" for data-sink \"%s\": %s\n",
name->c_str(), sink_name.c_str(), zx_status_get_string(status)));
return {};
}
fzl::VmoMapper mapper;
if (size > 0) {
zx_status_t status = mapper.Map(file_data, 0, size, ZX_VM_PERM_READ);
if (status != ZX_OK) {
error_callback(fxl::StringPrintf("FAILURE: Cannot map VMO \"%s\" for data-sink \"%s\": %s\n",
name->c_str(), sink_name.c_str(),
zx_status_get_string(status)));
return {};
}
} else {
warning_callback(fxl::StringPrintf("WARNING: Empty VMO \"%s\" published for data-sink \"%s\"\n",
name->c_str(), sink_name.c_str()));
return {};
}
zx_info_handle_basic_t info;
status = file_data.get_info(ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
if (status != ZX_OK) {
error_callback(fxl::StringPrintf("FAILURE: Cannot get a basic info for VMO \"%s\": %s\n",
name->c_str(), zx_status_get_string(status)));
return {};
}
char filename[ZX_MAX_NAME_LEN];
snprintf(filename, sizeof(filename), "%s.%" PRIu64, sink_name.c_str(), info.koid);
fbl::unique_fd fd{openat(sink_dir_fd.get(), filename, O_WRONLY | O_CREAT | O_EXCL, 0666)};
if (!fd) {
error_callback(fxl::StringPrintf("FAILURE: Cannot open data-sink file \"%s\": %s\n", filename,
strerror(errno)));
return {};
}
if (std::error_code ec = WriteFile(fd, reinterpret_cast<uint8_t*>(mapper.start()), size); ec) {
error_callback(fxl::StringPrintf("FAILURE: Cannot write data to \"%s\": %s\n", filename,
strerror(ec.value())));
return {};
}
return DumpFile{*name, JoinPath(sink_name, filename).c_str()};
}
} // namespace
std::unordered_map<std::string, std::vector<DumpFile>> ProcessDebugData(
const fbl::unique_fd& data_sink_dir_fd,
std::unordered_map<std::string, std::vector<zx::vmo>> debug_data,
DataSinkCallback error_callback, DataSinkCallback warning_callback) {
std::unordered_map<std::string, std::vector<DumpFile>> data_sinks;
for (const auto& [sink_name, data] : debug_data) {
if (sink_name == kProfileSink) {
if (auto dump_files =
ProcessProfiles(data, data_sink_dir_fd, error_callback, warning_callback)) {
data_sinks.emplace(sink_name, std::move(*dump_files));
}
} else {
for (const auto& file_data : data) {
if (auto dump_file = ProcessDataSinkDump(sink_name, file_data, data_sink_dir_fd,
error_callback, warning_callback)) {
data_sinks[sink_name].push_back(*dump_file);
}
}
}
}
return data_sinks;
}
} // namespace debugdata