/* * 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