/*
* Copyright (c) 2016, Google Inc.
* All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "perf_data_converter.h"
#include <algorithm>
#include <deque>
#include <map>
#include <sstream>
#include <vector>
#include "int_compat.h"
#include "perf_data_handler.h"
#include "string_compat.h"
#include "builder.h"
#include "quipper/perf_data.pb.h"
#include "quipper/perf_parser.h"
#include "quipper/perf_reader.h"
namespace perftools {
namespace {
typedef perftools::profiles::Profile Profile;
typedef perftools::profiles::Builder ProfileBuilder;
typedef uint32 Pid;
enum ExecutionMode {
Unknown,
HostKernel,
HostUser,
GuestKernel,
GuestUser,
Hypervisor
};
const char* ExecModeString(ExecutionMode mode) {
switch (mode) {
case HostKernel:
return ExecutionModeHostKernel;
case HostUser:
return ExecutionModeHostUser;
case GuestKernel:
return ExecutionModeGuestKernel;
case GuestUser:
return ExecutionModeGuestUser;
case Hypervisor:
return ExecutionModeHypervisor;
default:
std::cerr << "Execution mode not handled: " << mode << std::endl;
return "";
}
}
ExecutionMode PerfExecMode(const PerfDataHandler::SampleContext& sample) {
if (sample.header.has_misc()) {
switch (sample.header.misc() & PERF_RECORD_MISC_CPUMODE_MASK) {
case PERF_RECORD_MISC_KERNEL:
return HostKernel;
case PERF_RECORD_MISC_USER:
return HostUser;
case PERF_RECORD_MISC_GUEST_KERNEL:
return GuestKernel;
case PERF_RECORD_MISC_GUEST_USER:
return GuestUser;
case PERF_RECORD_MISC_HYPERVISOR:
return Hypervisor;
}
}
return Unknown;
}
// Adds the string to the profile builder. If the UTF-8 library is included,
// this also ensures the string contains structurally valid UTF-8.
// In order to successfully unmarshal the proto in Go, all strings inserted into
// the profile string table must be valid UTF-8.
int64 UTF8StringId(const string& s, ProfileBuilder* builder) {
return builder->StringId(s.c_str());
}
// Returns the file name of the mapping as either the real file path if it's
// present or the string representation of the file path MD5 checksum prefix
// when the real file path was stripped from the data for privacy reasons.
string MappingFilename(const PerfDataHandler::Mapping* m) {
if (m->filename != nullptr && !m->filename->empty()) {
return *m->filename;
} else if (m->filename_md5_prefix != 0) {
std::stringstream filename;
filename << std::hex << m->filename_md5_prefix;
return filename.str();
}
return "";
}
// List of profile location IDs, currently used to represent a call stack.
typedef std::vector<uint64> LocationIdVector;
// It is sufficient to key the location and mapping maps by PID.
// However, when Samples include labels, it is necessary to key their maps
// not only by PID, but also by anything their labels may contain, since labels
// are also distinguishing features. This struct should contain everything
// required to uniquely identify a Sample: if two Samples you consider different
// end up with the same SampleKey, you should extend SampleKey til they don't.
//
// If any of these values are not used as labels, they should be set to 0.
struct SampleKey {
Pid pid = 0;
Pid tid = 0;
uint64 time_ns = 0;
ExecutionMode exec_mode = Unknown;
// The index of the sample's command in the profile's string table.
uint64 comm = 0;
LocationIdVector stack;
};
struct SampleKeyEqualityTester {
bool operator()(const SampleKey a, const SampleKey b) const {
return ((a.pid == b.pid) && (a.tid == b.tid) && (a.time_ns == b.time_ns) &&
(a.exec_mode == b.exec_mode) && (a.comm == b.comm) &&
(a.stack == b.stack));
}
};
struct SampleKeyHasher {
size_t operator()(const SampleKey k) const {
size_t hash = 0;
hash ^= std::hash<int32>()(k.pid);
hash ^= std::hash<int32>()(k.tid);
hash ^= std::hash<uint64>()(k.time_ns);
hash ^= std::hash<int>()(k.exec_mode);
hash ^= std::hash<uint64>()(k.comm);
for (const auto& id : k.stack) {
hash ^= std::hash<uint64>()(id);
}
return hash;
}
};
// While Locations and Mappings are per-address-space (=per-process), samples
// can be thread-specific. If the requested sample labels include PID and
// TID, we'll need to maintain separate profile sample objects for samples
// that are identical except for TID. Likewise, if the requested sample
// labels include timestamp_ns, then we'll need to have separate
// profile_proto::Samples for samples that are identical except for timestamp.
typedef std::unordered_map<SampleKey, perftools::profiles::Sample*,
SampleKeyHasher, SampleKeyEqualityTester> SampleMap;
// Map from a virtual address to a profile location ID. It only keys off the
// address, not also the mapping ID since the map / its portions are invalidated
// by Comm() and MMap() methods to force re-creation of those locations.
//
typedef std::map<uint64, uint64> LocationMap;
// Map from the handler mapping object to profile mapping ID. The mappings
// the handler creates are immutable and reasonably shared (as in no new mapping
// object is created per, say, each sample), so using the pointers is OK.
typedef std::unordered_map<const PerfDataHandler::Mapping*, uint64> MappingMap;
// Per-process (aggregated when no PID grouping requested) info.
// See docs on ProcessProfile in the header file for details on the fields.
class ProcessMeta {
public:
// Constructs the object for the specified PID.
explicit ProcessMeta(Pid pid) : pid_(pid) {}
// Updates the bounding time interval ranges per specified timestamp.
void UpdateTimestamps(int64 time_nsec) {
if (min_sample_time_ns_ == 0 || time_nsec < min_sample_time_ns_) {
min_sample_time_ns_ = time_nsec;
}
if (max_sample_time_ns_ == 0 || time_nsec > max_sample_time_ns_) {
max_sample_time_ns_ = time_nsec;
}
}
std::unique_ptr<ProcessProfile> makeProcessProfile(Profile* data) {
ProcessProfile* pp = new ProcessProfile();
pp->pid = pid_;
pp->data.Swap(data);
pp->min_sample_time_ns = min_sample_time_ns_;
pp->max_sample_time_ns = max_sample_time_ns_;
return std::unique_ptr<ProcessProfile>(pp);
}
private:
Pid pid_;
int64 min_sample_time_ns_ = 0;
int64 max_sample_time_ns_ = 0;
};
class PerfDataConverter : public PerfDataHandler {
public:
explicit PerfDataConverter(const quipper::PerfDataProto& perf_data,
uint32 sample_labels = kNoLabels,
uint32 options = kGroupByPids)
: perf_data_(perf_data),
sample_labels_(sample_labels),
options_(options) {}
PerfDataConverter(const PerfDataConverter&) = delete;
PerfDataConverter& operator=(const PerfDataConverter&) = delete;
virtual ~PerfDataConverter() {}
ProcessProfiles Profiles();
// Callbacks for PerfDataHandler
void Sample(const PerfDataHandler::SampleContext& sample) override;
void Comm(const CommContext& comm) override;
void MMap(const MMapContext& mmap) override;
private:
// Adds a new sample updating the event counters if such sample is not present
// in the profile initializing its metrics. Updates the metrics associated
// with the sample if the sample was added before.
void AddOrUpdateSample(const PerfDataHandler::SampleContext& context,
const Pid& pid, const SampleKey& sample_key,
ProfileBuilder* builder);
// Adds a new location to the profile if such location is not present in the
// profile, returning the ID of the location. It also adds the profile mapping
// corresponding to the specified handler mapping.
uint64 AddOrGetLocation(const Pid& pid, uint64 addr,
const PerfDataHandler::Mapping* mapping,
ProfileBuilder* builder);
// Adds a new mapping to the profile if such mapping is not present in the
// profile, returning the ID of the mapping. It returns 0 to indicate that the
// mapping was not added (only happens if smap == 0 currently).
uint64 AddOrGetMapping(const Pid& pid, const PerfDataHandler::Mapping* smap,
ProfileBuilder* builder);
// Returns whether pid labels were requested for inclusion in the
// profile.proto's Sample.Label field.
bool IncludePidLabels() const { return (sample_labels_ & kPidLabel); }
// Returns whether tid labels were requested for inclusion in the
// profile.proto's Sample.Label field.
bool IncludeTidLabels() const { return (sample_labels_ & kTidLabel); }
// Returns whether timestamp_ns labels were requested for inclusion in the
// profile.proto's Sample.Label field.
bool IncludeTimestampNsLabels() const {
return (sample_labels_ & kTimestampNsLabel);
}
// Returns whether execution_mode labels were requested for inclusion in the
// profile.proto's Sample.Label field.
bool IncludeExecutionModeLabels() const {
return (sample_labels_ & kExecutionModeLabel);
}
// Returns whether comm labels were requested for inclusion in the
// profile.proto's Sample.Label field.
bool IncludeCommLabels() const { return (sample_labels_ & kCommLabel); }
SampleKey MakeSampleKey(const PerfDataHandler::SampleContext& sample,
ProfileBuilder* builder);
ProfileBuilder* GetOrCreateBuilder(
const PerfDataHandler::SampleContext& sample);
const quipper::PerfDataProto& perf_data_;
// Using deque so that appends do not invalidate existing pointers.
std::deque<ProfileBuilder> builders_;
std::deque<ProcessMeta> process_metas_;
struct PerPidInfo {
ProfileBuilder* builder = nullptr;
ProcessMeta* process_meta = nullptr;
LocationMap location_map;
MappingMap mapping_map;
std::unordered_map<Pid, string> tid_to_comm_map;
SampleMap sample_map;
void clear() {
builder = nullptr;
process_meta = nullptr;
location_map.clear();
mapping_map.clear();
tid_to_comm_map.clear();
sample_map.clear();
}
};
std::unordered_map<Pid, PerPidInfo> per_pid_;
const uint32 sample_labels_;
const uint32 options_;
};
SampleKey PerfDataConverter::MakeSampleKey(
const PerfDataHandler::SampleContext& sample, ProfileBuilder* builder) {
SampleKey sample_key;
sample_key.pid = sample.sample.has_pid() ? sample.sample.pid() : 0;
sample_key.tid =
(IncludeTidLabels() && sample.sample.has_tid()) ? sample.sample.tid() : 0;
sample_key.time_ns =
(IncludeTimestampNsLabels() && sample.sample.has_sample_time_ns())
? sample.sample.sample_time_ns()
: 0;
if (IncludeExecutionModeLabels()) {
sample_key.exec_mode = PerfExecMode(sample);
}
if (IncludeCommLabels() && sample.sample.has_pid() &&
sample.sample.has_tid()) {
Pid pid = sample.sample.pid();
Pid tid = sample.sample.tid();
const string& comm = per_pid_[pid].tid_to_comm_map[tid];
if (!comm.empty()) {
sample_key.comm = UTF8StringId(comm, builder);
}
}
return sample_key;
}
ProfileBuilder* PerfDataConverter::GetOrCreateBuilder(
const PerfDataHandler::SampleContext& sample) {
Pid builder_pid = (options_ & kGroupByPids) ? sample.sample.pid() : 0;
auto& per_pid = per_pid_[builder_pid];
if (per_pid.builder == nullptr) {
builders_.push_back(ProfileBuilder());
per_pid.builder = &builders_.back();
process_metas_.push_back(ProcessMeta(builder_pid));
per_pid.process_meta = &process_metas_.back();
ProfileBuilder* builder = per_pid.builder;
Profile* profile = builder->mutable_profile();
int unknown_event_idx = 0;
for (int event_idx = 0; event_idx < perf_data_.file_attrs_size();
++event_idx) {
// Come up with an event name for this event. perf.data will usually
// contain an event_types section of the same cardinality as its
// file_attrs; in this case we can just use the name there. Otherwise
// we just give it an anonymous name.
string event_name = "";
if (perf_data_.file_attrs_size() == perf_data_.event_types_size()) {
const auto& event_type = perf_data_.event_types(event_idx);
if (event_type.has_name()) {
event_name = event_type.name() + "_";
}
}
if (event_name == "") {
event_name = "event_" + std::to_string(unknown_event_idx++) + "_";
}
auto sample_type = profile->add_sample_type();
sample_type->set_type(UTF8StringId(event_name + "sample", builder));
sample_type->set_unit(builder->StringId("count"));
sample_type = profile->add_sample_type();
sample_type->set_type(UTF8StringId(event_name + "event", builder));
sample_type->set_unit(builder->StringId("count"));
}
if (sample.main_mapping == nullptr) {
auto fake_main = profile->add_mapping();
fake_main->set_id(profile->mapping_size());
fake_main->set_memory_start(0);
fake_main->set_memory_limit(1);
} else {
AddOrGetMapping(sample.sample.pid(), sample.main_mapping, builder);
}
if (perf_data_.string_metadata().has_perf_version()) {
string perf_version =
"perf-version:" + perf_data_.string_metadata().perf_version().value();
profile->add_comment(UTF8StringId(perf_version, builder));
}
if (perf_data_.string_metadata().has_perf_command_line_whole()) {
string perf_command =
"perf-command:" +
perf_data_.string_metadata().perf_command_line_whole().value();
profile->add_comment(UTF8StringId(perf_command, builder));
}
} else {
Profile* profile = per_pid.builder->mutable_profile();
if ((options_ & kGroupByPids) && sample.main_mapping != nullptr &&
sample.main_mapping->filename != nullptr) {
const string& filename =
profile->string_table(profile->mapping(0).filename());
const string& sample_filename = MappingFilename(sample.main_mapping);
if (filename != sample_filename) {
if (options_ & kFailOnMainMappingMismatch) {
LOG(FATAL) << "main mapping mismatch: " << sample.sample.pid() << " "
<< filename << " " << sample_filename;
} else {
LOG(WARNING) << "main mapping mismatch: " << sample.sample.pid()
<< " " << filename << " " << sample_filename;
}
}
}
}
if (sample.sample.sample_time_ns()) {
per_pid.process_meta->UpdateTimestamps(sample.sample.sample_time_ns());
}
return per_pid.builder;
}
uint64 PerfDataConverter::AddOrGetMapping(const Pid& pid,
const PerfDataHandler::Mapping* smap,
ProfileBuilder* builder) {
if (builder == nullptr) {
std::cerr << "Cannot add mapping to null builder." << std::endl;
abort();
}
if (smap == nullptr) {
return 0;
}
MappingMap& mapmap = per_pid_[pid].mapping_map;
auto it = mapmap.find(smap);
if (it != mapmap.end()) {
return it->second;
}
Profile* profile = builder->mutable_profile();
auto mapping = profile->add_mapping();
uint64 mapping_id = profile->mapping_size();
mapping->set_id(mapping_id);
mapping->set_memory_start(smap->start);
mapping->set_memory_limit(smap->limit);
mapping->set_file_offset(smap->file_offset);
if (smap->build_id != nullptr && !smap->build_id->empty()) {
mapping->set_build_id(UTF8StringId(*smap->build_id, builder));
}
mapping->set_filename(UTF8StringId(MappingFilename(smap), builder));
if (mapping->memory_start() >= mapping->memory_limit()) {
std::cerr << "The start of the mapping must be strictly less than its"
<< "limit in file: " << mapping->filename() << std::endl
<< "Start: " << mapping->memory_start() << std::endl
<< "Limit: " << mapping->memory_limit() << std::endl;
abort();
}
mapmap.insert(std::make_pair(smap, mapping_id));
return mapping_id;
}
void PerfDataConverter::AddOrUpdateSample(
const PerfDataHandler::SampleContext& context, const Pid& pid,
const SampleKey& sample_key,
ProfileBuilder* builder) {
perftools::profiles::Sample* sample = per_pid_[pid].sample_map[sample_key];
if (sample == nullptr) {
Profile* profile = builder->mutable_profile();
sample = profile->add_sample();
per_pid_[pid].sample_map[sample_key] = sample;
for (const auto& location_id : sample_key.stack) {
sample->add_location_id(location_id);
}
// Emit any requested labels.
if (IncludePidLabels() && context.sample.has_pid()) {
auto* label = sample->add_label();
label->set_key(builder->StringId(PidLabelKey));
label->set_num(static_cast<int64>(context.sample.pid()));
}
if (IncludeTidLabels() && context.sample.has_tid()) {
auto* label = sample->add_label();
label->set_key(builder->StringId(TidLabelKey));
label->set_num(static_cast<int64>(context.sample.tid()));
}
if (IncludeCommLabels() && sample_key.comm != 0) {
auto* label = sample->add_label();
label->set_key(builder->StringId(CommLabelKey));
label->set_str(sample_key.comm);
}
if (IncludeTimestampNsLabels() && context.sample.has_sample_time_ns()) {
auto* label = sample->add_label();
label->set_key(builder->StringId(TimestampNsLabelKey));
int64 timestamp_ns_as_int64 =
static_cast<int64>(context.sample.sample_time_ns());
label->set_num(timestamp_ns_as_int64);
}
if (IncludeExecutionModeLabels() && sample_key.exec_mode != Unknown) {
auto* label = sample->add_label();
label->set_key(builder->StringId(ExecutionModeLabelKey));
label->set_str(builder->StringId(ExecModeString(sample_key.exec_mode)));
}
// Two values per collected event: the first is sample counts, the second is
// event counts (unsampled weight for each sample).
for (int event_id = 0; event_id < perf_data_.file_attrs_size();
++event_id) {
sample->add_value(0);
sample->add_value(0);
}
}
int64 weight = 1;
// If the sample has a period, use that in preference
if (context.sample.period() > 0) {
weight = context.sample.period();
} else if (context.file_attrs_index >= 0) {
uint64 period =
perf_data_.file_attrs(context.file_attrs_index).attr().sample_period();
if (period > 0) {
// If sampling used a fixed period, use that as the weight.
weight = period;
}
}
int event_index = context.file_attrs_index;
sample->set_value(2 * event_index, sample->value(2 * event_index) + 1);
sample->set_value(2 * event_index + 1,
sample->value(2 * event_index + 1) + weight);
}
uint64 PerfDataConverter::AddOrGetLocation(
const Pid& pid, uint64 addr, const PerfDataHandler::Mapping* mapping,
ProfileBuilder* builder) {
LocationMap& loc_map = per_pid_[pid].location_map;
auto loc_it = loc_map.find(addr);
if (loc_it != loc_map.end()) {
return loc_it->second;
}
Profile* profile = builder->mutable_profile();
perftools::profiles::Location* loc = profile->add_location();
uint64 loc_id = profile->location_size();
loc->set_id(loc_id);
loc->set_address(addr);
uint64 mapping_id = AddOrGetMapping(pid, mapping, builder);
if (mapping_id != 0) {
loc->set_mapping_id(mapping_id);
} else {
if (addr != 0) {
std::cerr << "Found unmapped address: " << addr << " in PID " << pid
<< std::endl;
abort();
}
}
loc_map[addr] = loc_id;
return loc_id;
}
void PerfDataConverter::Comm(const CommContext& comm) {
Pid pid = comm.comm->pid();
Pid tid = comm.comm->tid();
if (pid == tid) {
// pid==tid means an exec() happened, so clear everything from the
// existing pid.
per_pid_[pid].clear();
}
per_pid_[pid].tid_to_comm_map[tid] = comm.comm->comm();
}
// Invalidates the locations in location_map in the mmap event's range.
void PerfDataConverter::MMap(const MMapContext& mmap) {
LocationMap& loc_map = per_pid_[mmap.pid].location_map;
loc_map.erase(loc_map.lower_bound(mmap.mapping->start),
loc_map.lower_bound(mmap.mapping->limit));
}
void PerfDataConverter::Sample(const PerfDataHandler::SampleContext& sample) {
if (sample.file_attrs_index < 0 ||
sample.file_attrs_index >= perf_data_.file_attrs_size()) {
LOG(WARNING) << "out of bounds file_attrs_index: "
<< sample.file_attrs_index;
return;
}
Pid event_pid = sample.sample.pid();
ProfileBuilder *builder = GetOrCreateBuilder(sample);
SampleKey sample_key = MakeSampleKey(sample, builder);
uint64 ip = sample.sample_mapping != nullptr ? sample.sample.ip() : 0;
if (ip != 0) {
const auto start = sample.sample_mapping->start;
const auto limit = sample.sample_mapping->limit;
if (ip < start || ip >= limit) {
std::cerr << "IP is out of bound of mapping." << std::endl
<< "IP: " << ip << std::endl
<< "Start: " << start << std::endl
<< "Limit: " << limit << std::endl;
}
}
// Leaf at stack[0]
sample_key.stack.push_back(
AddOrGetLocation(event_pid, ip, sample.sample_mapping, builder));
// LBR callstacks include only user call chains. If this is an LBR sample,
// we get the kernel callstack from the sample's callchain, and the user
// callstack from the sample's branch_stack.
const bool lbr_sample = !sample.branch_stack.empty();
bool skipped_dup = false;
for (const auto& frame : sample.callchain) {
if (lbr_sample && frame.ip == PERF_CONTEXT_USER) {
break;
}
if (!skipped_dup && sample_key.stack.size() == 1 && frame.ip == ip) {
skipped_dup = true;
// Newer versions of perf_events include the IP at the leaf of
// the callchain.
continue;
}
if (frame.mapping == nullptr) {
continue;
}
uint64 frame_ip = frame.ip;
// Why <=? Because this is a return address, which should be
// preceded by a call (the "real" context.) If we're at the edge
// of the mapping, we're really off its edge.
if (frame_ip <= frame.mapping->start) {
continue;
}
// these aren't real callchain entries, just hints as to kernel/user
// addresses.
if (frame_ip >= PERF_CONTEXT_MAX) {
continue;
}
// subtract one so we point to the call instead of the return addr.
frame_ip--;
sample_key.stack.push_back(
AddOrGetLocation(event_pid, frame_ip, frame.mapping, builder));
}
for (const auto& frame : sample.branch_stack) {
// branch_stack entries are pairs of <from, to> locations corresponding to
// addresses of call instructions and target addresses of those calls.
// We need only the addresses of the function call instructions, stored in
// the 'from' field, to recover the call chains.
if (frame.from.mapping == nullptr) {
continue;
}
// An LBR entry includes the address of the call instruction, so we don't
// have to do any adjustments.
if (frame.from.ip < frame.from.mapping->start) {
continue;
}
sample_key.stack.push_back(AddOrGetLocation(event_pid, frame.from.ip,
frame.from.mapping, builder));
}
AddOrUpdateSample(sample, event_pid, sample_key, builder);
}
ProcessProfiles PerfDataConverter::Profiles() {
ProcessProfiles pps;
for (int i = 0; i < builders_.size(); i++) {
auto& b = builders_[i];
b.Finalize();
auto pp = process_metas_[i].makeProcessProfile(b.mutable_profile());
pps.push_back(std::move(pp));
}
return pps;
}
} // namespace
ProcessProfiles PerfDataProtoToProfiles(const quipper::PerfDataProto* perf_data,
const uint32 sample_labels,
const uint32 options) {
PerfDataConverter converter(*perf_data, sample_labels, options);
PerfDataHandler::Process(*perf_data, &converter);
return converter.Profiles();
}
ProcessProfiles RawPerfDataToProfiles(const void* raw, const int raw_size,
const std::map<string, string>& build_ids,
const uint32 sample_labels,
const uint32 options) {
quipper::PerfReader reader;
if (!reader.ReadFromPointer(reinterpret_cast<const char*>(raw), raw_size)) {
LOG(ERROR) << "Could not read input perf.data";
return ProcessProfiles();
}
reader.InjectBuildIDs(build_ids);
// Perf populates info about the kernel using multiple pathways,
// which don't actually all match up how they name kernel data; in
// particular, buildids are reported by a different name than the
// actual "mmap" info. Normalize these names so our ProcessProfiles
// will match kernel mappings to a buildid.
reader.LocalizeUsingFilenames({
{"[kernel.kallsyms]_text", "[kernel.kallsyms]"},
{"[kernel.kallsyms]_stext", "[kernel.kallsyms]"},
});
// Use PerfParser to modify reader's events to have magic done to them such
// as hugepage deduction and sorting events based on time, if timestamps are
// present.
quipper::PerfParserOptions opts;
opts.sort_events_by_time = true;
opts.deduce_huge_page_mappings = true;
opts.combine_mappings = true;
quipper::PerfParser parser(&reader, opts);
if (!parser.ParseRawEvents()) {
LOG(ERROR) << "Could not parse perf events.";
return ProcessProfiles();
}
return PerfDataProtoToProfiles(&reader.proto(), sample_labels, options);
}
} // namespace perftools