diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-12-16 00:26:26 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-12-16 00:26:26 +0000 |
commit | ff66dc20c5ffbdd204ea09e84530cd3482a1a40b (patch) | |
tree | 9832431580d7609800082d2e2ae4f0dfcf0b81f2 | |
parent | 0a6edc868a6df315a2c793df4d9e438d7e6c1050 (diff) | |
parent | b3a524ad9aaf48ab85112d62bd86144525735572 (diff) | |
download | extras-ff66dc20c5ffbdd204ea09e84530cd3482a1a40b.tar.gz |
Merge "Snap for 8005954 from 0e37740be7a9e531b6bc879cf264c9f6d164a8dd to sdk-release" into sdk-releaseplatform-tools-32.0.0
-rw-r--r-- | profcollectd/libprofcollectd/scheduler.rs | 24 | ||||
-rw-r--r-- | profcollectd/libprofcollectd/simpleperf_etm_trace_provider.rs | 42 | ||||
-rw-r--r-- | simpleperf/build_id.h | 14 | ||||
-rw-r--r-- | simpleperf/cmd_inject.cpp | 866 | ||||
-rw-r--r-- | simpleperf/cmd_inject_test.cpp | 23 | ||||
-rw-r--r-- | simpleperf/cmd_report.cpp | 6 |
6 files changed, 649 insertions, 326 deletions
diff --git a/profcollectd/libprofcollectd/scheduler.rs b/profcollectd/libprofcollectd/scheduler.rs index d083612e..31a495a5 100644 --- a/profcollectd/libprofcollectd/scheduler.rs +++ b/profcollectd/libprofcollectd/scheduler.rs @@ -110,18 +110,18 @@ impl Scheduler { /// Run if space usage is under limit. fn check_space_limit(path: &Path, config: &Config) -> Result<bool> { - let ret = dir_size(path)? <= config.max_trace_limit; - if !ret { + // Returns the size of a directory, non-recursive. + let dir_size = |path| -> Result<u64> { + fs::read_dir(path)?.try_fold(0, |acc, file| { + let metadata = file?.metadata()?; + let size = if metadata.is_file() { metadata.len() } else { 0 }; + Ok(acc + size) + }) + }; + + if dir_size(path)? > config.max_trace_limit { log::error!("trace storage exhausted."); + return Ok(false); } - Ok(ret) -} - -/// Returns the size of a directory, non-recursive. -fn dir_size(path: &Path) -> Result<u64> { - fs::read_dir(path)?.try_fold(0, |acc, file| { - let metadata = file?.metadata()?; - let size = if metadata.is_file() { metadata.len() } else { 0 }; - Ok(acc + size) - }) + Ok(true) } diff --git a/profcollectd/libprofcollectd/simpleperf_etm_trace_provider.rs b/profcollectd/libprofcollectd/simpleperf_etm_trace_provider.rs index b993007b..14fb7509 100644 --- a/profcollectd/libprofcollectd/simpleperf_etm_trace_provider.rs +++ b/profcollectd/libprofcollectd/simpleperf_etm_trace_provider.rs @@ -45,28 +45,32 @@ impl TraceProvider for SimpleperfEtmTraceProvider { } fn process(&self, trace_dir: &Path, profile_dir: &Path, binary_filter: &str) -> Result<()> { + let is_etm_extension = |file: &PathBuf| { + file.extension() + .and_then(|f| f.to_str()) + .filter(|ext| ext == &ETM_TRACEFILE_EXTENSION) + .is_some() + }; + + let process_trace_file = |trace_file: PathBuf| { + let mut profile_file = PathBuf::from(profile_dir); + profile_file.push( + trace_file + .file_name() + .ok_or_else(|| anyhow!("Malformed trace path: {}", trace_file.display()))?, + ); + profile_file.set_extension(ETM_PROFILE_EXTENSION); + simpleperf_profcollect::process(&trace_file, &profile_file, binary_filter); + remove_file(&trace_file)?; + Ok(()) + }; + read_dir(trace_dir)? .filter_map(|e| e.ok()) .map(|e| e.path()) - .filter(|e| { - e.is_file() - && e.extension() - .and_then(|f| f.to_str()) - .filter(|ext| ext == &ETM_TRACEFILE_EXTENSION) - .is_some() - }) - .try_for_each(|trace_file| -> Result<()> { - let mut profile_file = PathBuf::from(profile_dir); - profile_file.push( - trace_file - .file_name() - .ok_or_else(|| anyhow!("Malformed trace path: {}", trace_file.display()))?, - ); - profile_file.set_extension(ETM_PROFILE_EXTENSION); - simpleperf_profcollect::process(&trace_file, &profile_file, binary_filter); - remove_file(&trace_file)?; - Ok(()) - }) + .filter(|e| e.is_file()) + .filter(is_etm_extension) + .try_for_each(process_trace_file) } } diff --git a/simpleperf/build_id.h b/simpleperf/build_id.h index 05acc266..72ff2eaf 100644 --- a/simpleperf/build_id.h +++ b/simpleperf/build_id.h @@ -20,6 +20,8 @@ #include <android-base/stringprintf.h> #include <string.h> #include <algorithm> +#include <string> +#include <string_view> namespace simpleperf { @@ -90,4 +92,16 @@ inline std::ostream& operator<<(std::ostream& os, const BuildId& build_id) { } // namespace simpleperf +namespace std { + +template <> +struct hash<simpleperf::BuildId> { + size_t operator()(const simpleperf::BuildId& build_id) const noexcept { + return std::hash<std::string_view>()( + std::string_view(reinterpret_cast<const char*>(build_id.Data()), build_id.Size())); + } +}; + +} // namespace std + #endif // SIMPLE_PERF_BUILD_ID_H_ diff --git a/simpleperf/cmd_inject.cpp b/simpleperf/cmd_inject.cpp index bbc26028..4bb3bdcb 100644 --- a/simpleperf/cmd_inject.cpp +++ b/simpleperf/cmd_inject.cpp @@ -23,6 +23,7 @@ #include <string> #include <android-base/parseint.h> +#include <android-base/strings.h> #include "ETMDecoder.h" #include "cmd_inject_impl.h" @@ -57,6 +58,8 @@ std::vector<bool> ProtoStringToBranch(const std::string& s, size_t bit_size) { namespace { +constexpr const char* ETM_BRANCH_LIST_PROTO_MAGIC = "simpleperf:EtmBranchList"; + using AddrPair = std::pair<uint64_t, uint64_t>; struct AddrPairHash { @@ -73,14 +76,124 @@ enum class OutputFormat { BranchList, }; +// When processing binary info in an input file, the binaries are identified by their path. +// But this isn't sufficient when merging binary info from multiple input files. Because +// binaries for the same path may be changed between generating input files. So after processing +// each input file, we create BinaryKeys to identify binaries, which consider path, build_id and +// kernel_start_addr (for vmlinux). kernel_start_addr affects how addresses in BranchListBinaryInfo +// are interpreted for vmlinux. +struct BinaryKey { + std::string path; + BuildId build_id; + uint64_t kernel_start_addr = 0; + + BinaryKey() {} + + BinaryKey(const std::string& path, BuildId build_id) : path(path), build_id(build_id) {} + + BinaryKey(Dso* dso, uint64_t kernel_start_addr) : path(dso->Path()) { + build_id = Dso::FindExpectedBuildIdForPath(dso->Path()); + if (dso->type() == DSO_KERNEL) { + this->kernel_start_addr = kernel_start_addr; + } + } + + bool operator==(const BinaryKey& other) const { + return path == other.path && build_id == other.build_id && + kernel_start_addr == other.kernel_start_addr; + } +}; + +struct BinaryKeyHash { + size_t operator()(const BinaryKey& key) const noexcept { + size_t seed = 0; + HashCombine(seed, key.path); + HashCombine(seed, key.build_id); + if (key.kernel_start_addr != 0) { + HashCombine(seed, key.kernel_start_addr); + } + return seed; + } +}; + +static void OverflowSafeAdd(uint64_t& dest, uint64_t add) { + if (__builtin_add_overflow(dest, add, &dest)) { + dest = UINT64_MAX; + } +} + struct AutoFDOBinaryInfo { + uint64_t first_load_segment_addr = 0; std::unordered_map<AddrPair, uint64_t, AddrPairHash> range_count_map; std::unordered_map<AddrPair, uint64_t, AddrPairHash> branch_count_map; + + void AddInstrRange(const ETMInstrRange& instr_range) { + uint64_t total_count = instr_range.branch_taken_count; + OverflowSafeAdd(total_count, instr_range.branch_not_taken_count); + OverflowSafeAdd(range_count_map[AddrPair(instr_range.start_addr, instr_range.end_addr)], + total_count); + if (instr_range.branch_taken_count > 0) { + OverflowSafeAdd(branch_count_map[AddrPair(instr_range.end_addr, instr_range.branch_to_addr)], + instr_range.branch_taken_count); + } + } + + void Merge(const AutoFDOBinaryInfo& other) { + for (const auto& p : other.range_count_map) { + auto res = range_count_map.emplace(p.first, p.second); + if (!res.second) { + OverflowSafeAdd(res.first->second, p.second); + } + } + for (const auto& p : other.branch_count_map) { + auto res = branch_count_map.emplace(p.first, p.second); + if (!res.second) { + OverflowSafeAdd(res.first->second, p.second); + } + } + } }; -using BranchListBinaryInfo = +using UnorderedBranchMap = std::unordered_map<uint64_t, std::unordered_map<std::vector<bool>, uint64_t>>; +struct BranchListBinaryInfo { + DsoType dso_type; + UnorderedBranchMap branch_map; + + void Merge(const BranchListBinaryInfo& other) { + for (auto& other_p : other.branch_map) { + auto it = branch_map.find(other_p.first); + if (it == branch_map.end()) { + branch_map[other_p.first] = std::move(other_p.second); + } else { + auto& map2 = it->second; + for (auto& other_p2 : other_p.second) { + auto it2 = map2.find(other_p2.first); + if (it2 == map2.end()) { + map2[other_p2.first] = other_p2.second; + } else { + OverflowSafeAdd(it2->second, other_p2.second); + } + } + } + } + } + + BranchMap GetOrderedBranchMap() const { + BranchMap result; + for (const auto& p : branch_map) { + uint64_t addr = p.first; + const auto& b_map = p.second; + result[addr] = std::map<std::vector<bool>, uint64_t>(b_map.begin(), b_map.end()); + } + return result; + } +}; + +using AutoFDOBinaryCallback = std::function<void(const BinaryKey&, AutoFDOBinaryInfo&)>; +using BranchListBinaryCallback = std::function<void(const BinaryKey&, BranchListBinaryInfo&)>; + class ThreadTreeWithFilter : public ThreadTree { public: void ExcludePid(pid_t pid) { exclude_pid_ = pid; } @@ -97,148 +210,86 @@ class ThreadTreeWithFilter : public ThreadTree { std::optional<pid_t> exclude_pid_; }; -constexpr const char* ETM_BRANCH_LIST_PROTO_MAGIC = "simpleperf:EtmBranchList"; - -class InjectCommand : public Command { +class DsoFilter { public: - InjectCommand() - : Command("inject", "parse etm instruction tracing data", - // clang-format off -"Usage: simpleperf inject [options]\n" -"--binary binary_name Generate data only for binaries matching binary_name regex.\n" -"-i <file> Input file. Default is perf.data. Support below formats:\n" -" 1. perf.data generated by recording cs-etm event type.\n" -" 2. branch_list file generated by `inject --output branch-list`.\n" -"-o <file> output file. Default is perf_inject.data.\n" -"--output <format> Select output file format:\n" -" autofdo -- text format accepted by TextSampleReader\n" -" of AutoFDO\n" -" branch-list -- protobuf file in etm_branch_list.proto\n" -" Default is autofdo.\n" -"--dump-etm type1,type2,... Dump etm data. A type is one of raw, packet and element.\n" -"--exclude-perf Exclude trace data for the recording process.\n" -"--symdir <dir> Look for binaries in a directory recursively.\n" -"\n" -"Examples:\n" -"1. Generate autofdo text output.\n" -"$ simpleperf inject -i perf.data -o autofdo.txt --output autofdo\n" -"\n" -"2. Generate branch list proto, then convert to autofdo text.\n" -"$ simpleperf inject -i perf.data -o branch_list.data --output branch-list\n" -"$ simpleperf inject -i branch_list.data -o autofdo.txt --output autofdo\n" - // clang-format on - ), - output_fp_(nullptr, fclose) {} + DsoFilter(const std::regex& binary_name_regex) : binary_name_regex_(binary_name_regex) {} - bool Run(const std::vector<std::string>& args) override { - GOOGLE_PROTOBUF_VERIFY_VERSION; - // 1. Parse options. - if (!ParseOptions(args)) { - return false; - } - - // 2. Open output file. - const char* open_mode = (output_format_ == OutputFormat::AutoFDO) ? "w" : "wb"; - output_fp_.reset(fopen(output_filename_.c_str(), open_mode)); - if (!output_fp_) { - PLOG(ERROR) << "failed to write to " << output_filename_; - return false; - } - - // 3. Process input file. - if (!ProcessInputFile()) { - return false; - } - - // 4. Write output file. - if (!WriteOutput()) { - return false; + bool FilterDso(Dso* dso) { + auto lookup = dso_filter_cache_.find(dso); + if (lookup != dso_filter_cache_.end()) { + return lookup->second; } - output_fp_.reset(nullptr); - return true; + bool match = std::regex_search(dso->Path(), binary_name_regex_); + dso_filter_cache_.insert({dso, match}); + return match; } private: - bool ParseOptions(const std::vector<std::string>& args) { - const OptionFormatMap option_formats = { - {"--binary", {OptionValueType::STRING, OptionType::SINGLE}}, - {"--dump-etm", {OptionValueType::STRING, OptionType::SINGLE}}, - {"--exclude-perf", {OptionValueType::NONE, OptionType::SINGLE}}, - {"-i", {OptionValueType::STRING, OptionType::SINGLE}}, - {"-o", {OptionValueType::STRING, OptionType::SINGLE}}, - {"--output", {OptionValueType::STRING, OptionType::SINGLE}}, - {"--symdir", {OptionValueType::STRING, OptionType::MULTIPLE}}, - }; - OptionValueMap options; - std::vector<std::pair<OptionName, OptionValue>> ordered_options; - if (!PreprocessOptions(args, option_formats, &options, &ordered_options, nullptr)) { - return false; - } + std::regex binary_name_regex_; + std::unordered_map<Dso*, bool> dso_filter_cache_; +}; - if (auto value = options.PullValue("--binary"); value) { - binary_name_regex_ = *value->str_value; - } - if (auto value = options.PullValue("--dump-etm"); value) { - if (!ParseEtmDumpOption(*value->str_value, &etm_dump_option_)) { - return false; - } - } - exclude_perf_ = options.PullBoolValue("--exclude-perf"); - options.PullStringValue("-i", &input_filename_); - options.PullStringValue("-o", &output_filename_); - if (auto value = options.PullValue("--output"); value) { - const std::string& output = *value->str_value; - if (output == "autofdo") { - output_format_ = OutputFormat::AutoFDO; - } else if (output == "branch-list") { - output_format_ = OutputFormat::BranchList; - } else { - LOG(ERROR) << "unknown format in --output option: " << output; - return false; +static uint64_t GetFirstLoadSegmentVaddr(Dso* dso) { + ElfStatus status; + if (auto elf = ElfFile::Open(dso->GetDebugFilePath(), &status); elf) { + for (const auto& segment : elf->GetProgramHeader()) { + if (segment.is_load) { + return segment.vaddr; } } - if (auto value = options.PullValue("--symdir"); value) { - if (!Dso::AddSymbolDir(*value->str_value)) { - return false; - } - } - CHECK(options.values.empty()); - return true; } + return 0; +} - bool ProcessInputFile() { - if (IsPerfDataFile(input_filename_)) { - record_file_reader_ = RecordFileReader::CreateInstance(input_filename_); - if (!record_file_reader_) { +// Read perf.data, and generate AutoFDOBinaryInfo or BranchListBinaryInfo. +// To avoid resetting data, it only processes one input file per instance. +class PerfDataReader { + public: + PerfDataReader(const std::string& filename, bool exclude_perf, ETMDumpOption etm_dump_option, + const std::regex& binary_name_regex) + : filename_(filename), + exclude_perf_(exclude_perf), + etm_dump_option_(etm_dump_option), + dso_filter_(binary_name_regex) {} + + void SetCallback(const AutoFDOBinaryCallback& callback) { autofdo_callback_ = callback; } + void SetCallback(const BranchListBinaryCallback& callback) { branch_list_callback_ = callback; } + + bool Read() { + record_file_reader_ = RecordFileReader::CreateInstance(filename_); + if (!record_file_reader_) { + return false; + } + if (exclude_perf_) { + const auto& info_map = record_file_reader_->GetMetaInfoFeature(); + if (auto it = info_map.find("recording_process"); it == info_map.end()) { + LOG(ERROR) << filename_ << " doesn't support --exclude-perf"; return false; - } - if (exclude_perf_) { - const auto& info_map = record_file_reader_->GetMetaInfoFeature(); - if (auto it = info_map.find("recording_process"); it == info_map.end()) { - LOG(ERROR) << input_filename_ << " doesn't support --exclude-perf"; + } else { + int pid; + if (!android::base::ParseInt(it->second, &pid, 0)) { + LOG(ERROR) << "invalid recording_process " << it->second << " in " << filename_; return false; - } else { - int pid; - if (!android::base::ParseInt(it->second, &pid, 0)) { - LOG(ERROR) << "invalid recording_process " << it->second; - return false; - } - thread_tree_.ExcludePid(pid); } + thread_tree_.ExcludePid(pid); } - record_file_reader_->LoadBuildIdAndFileFeatures(thread_tree_); - if (!record_file_reader_->ReadDataSection( - [this](auto r) { return ProcessRecord(r.get()); })) { - return false; - } - if (etm_decoder_ && !etm_decoder_->FinishData()) { - return false; - } - return true; } - return ProcessBranchListFile(); + record_file_reader_->LoadBuildIdAndFileFeatures(thread_tree_); + if (!record_file_reader_->ReadDataSection([this](auto r) { return ProcessRecord(r.get()); })) { + return false; + } + if (etm_decoder_ && !etm_decoder_->FinishData()) { + return false; + } + if (autofdo_callback_) { + ProcessAutoFDOBinaryInfo(); + } else if (branch_list_callback_) { + ProcessBranchListBinaryInfo(); + } + return true; } + private: bool ProcessRecord(Record* r) { thread_tree_.Update(*r); if (r->type() == PERF_RECORD_AUXTRACE_INFO) { @@ -247,10 +298,10 @@ class InjectCommand : public Command { return false; } etm_decoder_->EnableDump(etm_dump_option_); - if (output_format_ == OutputFormat::AutoFDO) { + if (autofdo_callback_) { etm_decoder_->RegisterCallback( [this](const ETMInstrRange& range) { ProcessInstrRange(range); }); - } else if (output_format_ == OutputFormat::BranchList) { + } else if (branch_list_callback_) { etm_decoder_->RegisterCallback( [this](const ETMBranchList& branch) { ProcessBranchList(branch); }); } @@ -263,7 +314,7 @@ class InjectCommand : public Command { } if (!record_file_reader_->ReadAuxData(aux->Cpu(), aux->data->aux_offset, aux_data_buffer_.data(), aux_size)) { - LOG(ERROR) << "failed to read aux data"; + LOG(ERROR) << "failed to read aux data in " << filename_; return false; } return etm_decoder_->ProcessData(aux_data_buffer_.data(), aux_size, !aux->Unformatted(), @@ -278,104 +329,136 @@ class InjectCommand : public Command { return true; } - std::unordered_map<Dso*, bool> dso_filter_cache; - bool FilterDso(Dso* dso) { - auto lookup = dso_filter_cache.find(dso); - if (lookup != dso_filter_cache.end()) { - return lookup->second; - } - bool match = std::regex_search(dso->Path(), binary_name_regex_); - dso_filter_cache.insert({dso, match}); - return match; - } - void ProcessInstrRange(const ETMInstrRange& instr_range) { - if (!FilterDso(instr_range.dso)) { + if (!dso_filter_.FilterDso(instr_range.dso)) { return; } - auto& binary = autofdo_binary_map_[instr_range.dso]; - binary.range_count_map[AddrPair(instr_range.start_addr, instr_range.end_addr)] += - instr_range.branch_taken_count + instr_range.branch_not_taken_count; - if (instr_range.branch_taken_count > 0) { - binary.branch_count_map[AddrPair(instr_range.end_addr, instr_range.branch_to_addr)] += - instr_range.branch_taken_count; - } + autofdo_binary_map_[instr_range.dso].AddInstrRange(instr_range); } void ProcessBranchList(const ETMBranchList& branch_list) { - if (!FilterDso(branch_list.dso)) { + if (!dso_filter_.FilterDso(branch_list.dso)) { return; } - ++branch_list_binary_map_[branch_list.dso][branch_list.addr][branch_list.branch]; + auto& branch_map = branch_list_binary_map_[branch_list.dso].branch_map; + ++branch_map[branch_list.addr][branch_list.branch]; } - bool ProcessBranchListFile() { - if (output_format_ != OutputFormat::AutoFDO) { - LOG(ERROR) << "Only support autofdo output when given a branch list file."; - return false; + void ProcessAutoFDOBinaryInfo() { + for (auto& p : autofdo_binary_map_) { + Dso* dso = p.first; + AutoFDOBinaryInfo& binary = p.second; + binary.first_load_segment_addr = GetFirstLoadSegmentVaddr(dso); + autofdo_callback_(BinaryKey(dso, 0), binary); + } + } + + void ProcessBranchListBinaryInfo() { + for (auto& p : branch_list_binary_map_) { + Dso* dso = p.first; + BranchListBinaryInfo& binary = p.second; + binary.dso_type = dso->type(); + BinaryKey key(dso, 0); + if (binary.dso_type == DSO_KERNEL) { + if (kernel_map_start_addr_ == 0) { + LOG(WARNING) << "Can't convert kernel ip addresses without kernel start addr. So remove " + "branches for the kernel."; + continue; + } + if (dso->GetDebugFilePath() == dso->Path()) { + // vmlinux isn't available. We still use kernel ip addr. Put kernel start addr in proto + // for address conversion later. + key.kernel_start_addr = kernel_map_start_addr_; + } + } + branch_list_callback_(key, binary); } - // 1. Load EtmBranchList msg from proto file. - auto fd = FileHelper::OpenReadOnly(input_filename_); + } + + const std::string filename_; + bool exclude_perf_; + ETMDumpOption etm_dump_option_; + DsoFilter dso_filter_; + AutoFDOBinaryCallback autofdo_callback_; + BranchListBinaryCallback branch_list_callback_; + + std::vector<uint8_t> aux_data_buffer_; + std::unique_ptr<ETMDecoder> etm_decoder_; + std::unique_ptr<RecordFileReader> record_file_reader_; + ThreadTreeWithFilter thread_tree_; + uint64_t kernel_map_start_addr_ = 0; + // Store results for AutoFDO. + std::unordered_map<Dso*, AutoFDOBinaryInfo> autofdo_binary_map_; + // Store results for BranchList. + std::unordered_map<Dso*, BranchListBinaryInfo> branch_list_binary_map_; +}; + +// Read a protobuf file specified by etm_branch_list.proto, and generate BranchListBinaryInfo. +class BranchListReader { + public: + BranchListReader(const std::string& filename, const std::regex binary_name_regex) + : filename_(filename), binary_name_regex_(binary_name_regex) {} + + void SetCallback(const BranchListBinaryCallback& callback) { callback_ = callback; } + + bool Read() { + auto fd = FileHelper::OpenReadOnly(filename_); if (!fd.ok()) { - PLOG(ERROR) << "failed to open " << input_filename_; + PLOG(ERROR) << "failed to open " << filename_; return false; } + proto::ETMBranchList branch_list_proto; if (!branch_list_proto.ParseFromFileDescriptor(fd)) { - PLOG(ERROR) << "failed to read msg from " << input_filename_; + PLOG(ERROR) << "failed to read msg from " << filename_; return false; } if (branch_list_proto.magic() != ETM_BRANCH_LIST_PROTO_MAGIC) { - PLOG(ERROR) << "file not in format etm_branch_list.proto: " << input_filename_; + PLOG(ERROR) << "file not in format etm_branch_list.proto: " << filename_; return false; } - // 2. Build branch map for each binary, convert them to instr ranges. - auto callback = [this](const ETMInstrRange& range) { ProcessInstrRange(range); }; - auto check_build_id = [](Dso* dso, const BuildId& expected_build_id) { - if (expected_build_id.IsEmpty()) { - return true; - } - BuildId build_id; - return GetBuildIdFromDsoPath(dso->GetDebugFilePath(), &build_id) && - build_id == expected_build_id; - }; - for (size_t i = 0; i < branch_list_proto.binaries_size(); i++) { const auto& binary_proto = branch_list_proto.binaries(i); - BuildId build_id(binary_proto.build_id()); - std::optional<DsoType> dso_type = ToDsoType(binary_proto.type()); - if (!dso_type.has_value()) { - return false; - } - std::unique_ptr<Dso> dso = - Dso::CreateDsoWithBuildId(dso_type.value(), binary_proto.path(), build_id); - if (!dso || !FilterDso(dso.get()) || !check_build_id(dso.get(), build_id)) { + if (!std::regex_search(binary_proto.path(), binary_name_regex_)) { continue; } - // Dso is used in ETMInstrRange in post process, so need to extend its lifetime. - Dso* dso_p = dso.get(); - branch_list_dso_v_.emplace_back(dso.release()); - auto branch_map = BuildBranchMap(binary_proto); - - if (dso_p->type() == DSO_KERNEL) { - if (!ModifyBranchMapForKernel(binary_proto, dso_p, branch_map)) { - return false; - } + BinaryKey key(binary_proto.path(), BuildId(binary_proto.build_id())); + if (binary_proto.has_kernel_info()) { + key.kernel_start_addr = binary_proto.kernel_info().kernel_start_addr(); } - - if (auto result = ConvertBranchMapToInstrRanges(dso_p, branch_map, callback); !result.ok()) { - LOG(WARNING) << "failed to build instr ranges for binary " << dso_p->Path() << ": " - << result.error(); + BranchListBinaryInfo binary; + auto dso_type = ToDsoType(binary_proto.type()); + if (!dso_type) { + LOG(ERROR) << "invalid binary type in " << filename_; + return false; } + binary.dso_type = dso_type.value(); + binary.branch_map = BuildUnorderedBranchMap(binary_proto); + callback_(key, binary); } return true; } - BranchMap BuildBranchMap(const proto::ETMBranchList_Binary& binary_proto) { - BranchMap branch_map; + private: + std::optional<DsoType> ToDsoType(proto::ETMBranchList_Binary::BinaryType binary_type) { + switch (binary_type) { + case proto::ETMBranchList_Binary::ELF_FILE: + return DSO_ELF_FILE; + case proto::ETMBranchList_Binary::KERNEL: + return DSO_KERNEL; + case proto::ETMBranchList_Binary::KERNEL_MODULE: + return DSO_KERNEL_MODULE; + default: + LOG(ERROR) << "unexpected binary type " << binary_type; + return std::nullopt; + } + } + + UnorderedBranchMap BuildUnorderedBranchMap(const proto::ETMBranchList_Binary& binary_proto) { + UnorderedBranchMap branch_map; for (size_t i = 0; i < binary_proto.addrs_size(); i++) { const auto& addr_proto = binary_proto.addrs(i); auto& b_map = branch_map[addr_proto.addr()]; @@ -389,54 +472,106 @@ class InjectCommand : public Command { return branch_map; } - bool ModifyBranchMapForKernel(const proto::ETMBranchList_Binary& binary_proto, Dso* dso, - BranchMap& branch_map) { - if (!binary_proto.has_kernel_info()) { - LOG(ERROR) << "no kernel info"; - return false; + const std::string filename_; + const std::regex binary_name_regex_; + BranchListBinaryCallback callback_; +}; + +// Convert BranchListBinaryInfo into AutoFDOBinaryInfo. +class BranchListToAutoFDOConverter { + public: + std::unique_ptr<AutoFDOBinaryInfo> Convert(const BinaryKey& key, BranchListBinaryInfo& binary) { + BuildId build_id = key.build_id; + std::unique_ptr<Dso> dso = Dso::CreateDsoWithBuildId(binary.dso_type, key.path, build_id); + if (!dso || !CheckBuildId(dso.get(), key.build_id)) { + return nullptr; + } + std::unique_ptr<AutoFDOBinaryInfo> autofdo_binary(new AutoFDOBinaryInfo); + autofdo_binary->first_load_segment_addr = GetFirstLoadSegmentVaddr(dso.get()); + + if (dso->type() == DSO_KERNEL) { + ModifyBranchMapForKernel(dso.get(), key.kernel_start_addr, binary); + } + + auto process_instr_range = [&](const ETMInstrRange& range) { + CHECK_EQ(range.dso, dso.get()); + autofdo_binary->AddInstrRange(range); + }; + + auto result = + ConvertBranchMapToInstrRanges(dso.get(), binary.GetOrderedBranchMap(), process_instr_range); + if (!result.ok()) { + LOG(WARNING) << "failed to build instr ranges for binary " << dso->Path() << ": " + << result.error(); + return nullptr; } - uint64_t kernel_map_start_addr = binary_proto.kernel_info().kernel_start_addr(); - if (kernel_map_start_addr == 0) { + return autofdo_binary; + } + + private: + bool CheckBuildId(Dso* dso, const BuildId& expected_build_id) { + if (expected_build_id.IsEmpty()) { return true; } + BuildId build_id; + return GetBuildIdFromDsoPath(dso->GetDebugFilePath(), &build_id) && + build_id == expected_build_id; + } + + void ModifyBranchMapForKernel(Dso* dso, uint64_t kernel_start_addr, + BranchListBinaryInfo& binary) { + if (kernel_start_addr == 0) { + // vmlinux has been provided when generating branch lists. Addresses in branch lists are + // already vaddrs in vmlinux. + return; + } // Addresses are still kernel ip addrs in memory. Need to convert them to vaddrs in vmlinux. - BranchMap new_branch_map; - for (auto& p : branch_map) { - uint64_t vaddr_in_file = dso->IpToVaddrInFile(p.first, kernel_map_start_addr, 0); + UnorderedBranchMap new_branch_map; + for (auto& p : binary.branch_map) { + uint64_t vaddr_in_file = dso->IpToVaddrInFile(p.first, kernel_start_addr, 0); new_branch_map[vaddr_in_file] = std::move(p.second); } - branch_map = std::move(new_branch_map); - return true; + binary.branch_map = std::move(new_branch_map); } +}; - bool WriteOutput() { - if (output_format_ == OutputFormat::AutoFDO) { - GenerateInstrRange(); - return true; +// Write instruction ranges to a file in AutoFDO text format. +class AutoFDOWriter { + public: + void AddAutoFDOBinary(const BinaryKey& key, AutoFDOBinaryInfo& binary) { + auto it = binary_map_.find(key); + if (it == binary_map_.end()) { + binary_map_[key] = std::move(binary); + } else { + it->second.Merge(binary); } - CHECK(output_format_ == OutputFormat::BranchList); - return GenerateBranchList(); } - void GenerateInstrRange() { - // autofdo_binary_map is used to store instruction ranges, which can have a large amount. And it - // has a larger access time (instruction ranges * executed time). So it's better to use + bool Write(const std::string& output_filename) { + std::unique_ptr<FILE, decltype(&fclose)> output_fp(fopen(output_filename.c_str(), "w"), fclose); + if (!output_fp) { + PLOG(ERROR) << "failed to write to " << output_filename; + return false; + } + // autofdo_binary_map is used to store instruction ranges, which can have a large amount. And + // it has a larger access time (instruction ranges * executed time). So it's better to use // unorder_maps to speed up access time. But we also want a stable output here, to compare // output changes result from code changes. So generate a sorted output here. - std::vector<Dso*> dso_v; - for (auto& p : autofdo_binary_map_) { - dso_v.emplace_back(p.first); - } - std::sort(dso_v.begin(), dso_v.end(), [](Dso* d1, Dso* d2) { return d1->Path() < d2->Path(); }); - if (dso_v.size() > 1) { - fprintf(output_fp_.get(), + std::vector<BinaryKey> keys; + for (auto& p : binary_map_) { + keys.emplace_back(p.first); + } + std::sort(keys.begin(), keys.end(), + [](const BinaryKey& key1, const BinaryKey& key2) { return key1.path < key2.path; }); + if (keys.size() > 1) { + fprintf(output_fp.get(), "// Please split this file. AutoFDO only accepts profile for one binary.\n"); } - for (auto dso : dso_v) { - const AutoFDOBinaryInfo& binary = autofdo_binary_map_[dso]; + for (const auto& key : keys) { + const AutoFDOBinaryInfo& binary = binary_map_[key]; // AutoFDO text format needs file_offsets instead of virtual addrs in a binary. And it uses // below formula: vaddr = file_offset + GetFirstLoadSegmentVaddr(). - uint64_t first_load_segment_addr = GetFirstLoadSegmentVaddr(dso); + uint64_t first_load_segment_addr = binary.first_load_segment_addr; auto to_offset = [&](uint64_t vaddr) -> uint64_t { if (vaddr == 0) { @@ -449,76 +584,85 @@ class InjectCommand : public Command { // Write range_count_map. std::map<AddrPair, uint64_t> range_count_map(binary.range_count_map.begin(), binary.range_count_map.end()); - fprintf(output_fp_.get(), "%zu\n", range_count_map.size()); + fprintf(output_fp.get(), "%zu\n", range_count_map.size()); for (const auto& pair2 : range_count_map) { const AddrPair& addr_range = pair2.first; uint64_t count = pair2.second; - fprintf(output_fp_.get(), "%" PRIx64 "-%" PRIx64 ":%" PRIu64 "\n", + fprintf(output_fp.get(), "%" PRIx64 "-%" PRIx64 ":%" PRIu64 "\n", to_offset(addr_range.first), to_offset(addr_range.second), count); } // Write addr_count_map. - fprintf(output_fp_.get(), "0\n"); + fprintf(output_fp.get(), "0\n"); // Write branch_count_map. std::map<AddrPair, uint64_t> branch_count_map(binary.branch_count_map.begin(), binary.branch_count_map.end()); - fprintf(output_fp_.get(), "%zu\n", branch_count_map.size()); + fprintf(output_fp.get(), "%zu\n", branch_count_map.size()); for (const auto& pair2 : branch_count_map) { const AddrPair& branch = pair2.first; uint64_t count = pair2.second; - fprintf(output_fp_.get(), "%" PRIx64 "->%" PRIx64 ":%" PRIu64 "\n", to_offset(branch.first), + fprintf(output_fp.get(), "%" PRIx64 "->%" PRIx64 ":%" PRIu64 "\n", to_offset(branch.first), to_offset(branch.second), count); } // Write the binary path in comment. - fprintf(output_fp_.get(), "// %s\n\n", dso->Path().c_str()); + fprintf(output_fp.get(), "// %s\n\n", key.path.c_str()); } + return true; } - uint64_t GetFirstLoadSegmentVaddr(Dso* dso) { - ElfStatus status; - if (auto elf = ElfFile::Open(dso->GetDebugFilePath(), &status); elf) { - for (const auto& segment : elf->GetProgramHeader()) { - if (segment.is_load) { - return segment.vaddr; - } - } + private: + std::unordered_map<BinaryKey, AutoFDOBinaryInfo, BinaryKeyHash> binary_map_; +}; + +// Write branch lists to a protobuf file specified by etm_branch_list.proto. +class BranchListWriter { + public: + void AddBranchListBinary(const BinaryKey& key, BranchListBinaryInfo& binary) { + auto it = binary_map_.find(key); + if (it == binary_map_.end()) { + binary_map_[key] = std::move(binary); + } else { + it->second.Merge(binary); } - return 0; } - bool GenerateBranchList() { + bool Write(const std::string& output_filename) { // Don't produce empty output file. - if (branch_list_binary_map_.empty()) { + if (binary_map_.empty()) { LOG(INFO) << "Skip empty output file."; - output_fp_.reset(nullptr); - unlink(output_filename_.c_str()); + unlink(output_filename.c_str()); return true; } + std::unique_ptr<FILE, decltype(&fclose)> output_fp(fopen(output_filename.c_str(), "wb"), + fclose); + if (!output_fp) { + PLOG(ERROR) << "failed to write to " << output_filename; + return false; + } proto::ETMBranchList branch_list_proto; branch_list_proto.set_magic(ETM_BRANCH_LIST_PROTO_MAGIC); std::vector<char> branch_buf; - for (const auto& dso_p : branch_list_binary_map_) { - Dso* dso = dso_p.first; - auto& addr_map = dso_p.second; + for (const auto& p : binary_map_) { + const BinaryKey& key = p.first; + const BranchListBinaryInfo& binary = p.second; auto binary_proto = branch_list_proto.add_binaries(); - binary_proto->set_path(dso->Path()); - BuildId build_id = Dso::FindExpectedBuildIdForPath(dso->Path()); - if (!build_id.IsEmpty()) { - binary_proto->set_build_id(build_id.ToString().substr(2)); + binary_proto->set_path(key.path); + if (!key.build_id.IsEmpty()) { + binary_proto->set_build_id(key.build_id.ToString().substr(2)); } - auto opt_binary_type = ToProtoBinaryType(dso->type()); + auto opt_binary_type = ToProtoBinaryType(binary.dso_type); if (!opt_binary_type.has_value()) { return false; } binary_proto->set_type(opt_binary_type.value()); - for (const auto& addr_p : addr_map) { + for (const auto& addr_p : binary.branch_map) { auto addr_proto = binary_proto->add_addrs(); addr_proto->set_addr(addr_p.first); @@ -532,31 +676,18 @@ class InjectCommand : public Command { } } - if (dso->type() == DSO_KERNEL) { - if (kernel_map_start_addr_ == 0) { - LOG(WARNING) << "Can't convert kernel ip addresses without kernel start addr. So remove " - "branches for the kernel."; - branch_list_proto.mutable_binaries()->RemoveLast(); - continue; - } - if (dso->GetDebugFilePath() == dso->Path()) { - // vmlinux isn't available. We still use kernel ip addr. Put kernel start addr in proto - // for address conversion later. - binary_proto->mutable_kernel_info()->set_kernel_start_addr(kernel_map_start_addr_); - } else { - // vmlinux is available. We have converted kernel ip addr to vaddr in vmlinux. So no need - // to put kernel start addr in proto. - binary_proto->mutable_kernel_info()->set_kernel_start_addr(0); - } + if (binary.dso_type == DSO_KERNEL) { + binary_proto->mutable_kernel_info()->set_kernel_start_addr(key.kernel_start_addr); } } - if (!branch_list_proto.SerializeToFileDescriptor(fileno(output_fp_.get()))) { - PLOG(ERROR) << "failed to write to output file"; + if (!branch_list_proto.SerializeToFileDescriptor(fileno(output_fp.get()))) { + PLOG(ERROR) << "failed to write to " << output_filename; return false; } return true; } + private: std::optional<proto::ETMBranchList_Binary::BinaryType> ToProtoBinaryType(DsoType dso_type) { switch (dso_type) { case DSO_ELF_FILE: @@ -571,38 +702,189 @@ class InjectCommand : public Command { } } - std::optional<DsoType> ToDsoType(proto::ETMBranchList_Binary::BinaryType binary_type) { - switch (binary_type) { - case proto::ETMBranchList_Binary::ELF_FILE: - return DSO_ELF_FILE; - case proto::ETMBranchList_Binary::KERNEL: - return DSO_KERNEL; - case proto::ETMBranchList_Binary::KERNEL_MODULE: - return DSO_KERNEL_MODULE; - default: - LOG(ERROR) << "unexpected binary type " << binary_type; - return std::nullopt; + std::unordered_map<BinaryKey, BranchListBinaryInfo, BinaryKeyHash> binary_map_; +}; // namespace + +class InjectCommand : public Command { + public: + InjectCommand() + : Command("inject", "parse etm instruction tracing data", + // clang-format off +"Usage: simpleperf inject [options]\n" +"--binary binary_name Generate data only for binaries matching binary_name regex.\n" +"-i file1,file2,... Input files. Default is perf.data. Support below formats:\n" +" 1. perf.data generated by recording cs-etm event type.\n" +" 2. branch_list file generated by `inject --output branch-list`.\n" +" If a file name starts with @, it contains a list of input files.\n" +"-o <file> output file. Default is perf_inject.data.\n" +"--output <format> Select output file format:\n" +" autofdo -- text format accepted by TextSampleReader\n" +" of AutoFDO\n" +" branch-list -- protobuf file in etm_branch_list.proto\n" +" Default is autofdo.\n" +"--dump-etm type1,type2,... Dump etm data. A type is one of raw, packet and element.\n" +"--exclude-perf Exclude trace data for the recording process.\n" +"--symdir <dir> Look for binaries in a directory recursively.\n" +"\n" +"Examples:\n" +"1. Generate autofdo text output.\n" +"$ simpleperf inject -i perf.data -o autofdo.txt --output autofdo\n" +"\n" +"2. Generate branch list proto, then convert to autofdo text.\n" +"$ simpleperf inject -i perf.data -o branch_list.data --output branch-list\n" +"$ simpleperf inject -i branch_list.data -o autofdo.txt --output autofdo\n" + // clang-format on + ) {} + + bool Run(const std::vector<std::string>& args) override { + GOOGLE_PROTOBUF_VERIFY_VERSION; + if (!ParseOptions(args)) { + return false; + } + + for (const auto& filename : input_filenames_) { + if (!ProcessInputFile(filename)) { + return false; + } } + + return WriteOutput(); + } + + private: + bool ParseOptions(const std::vector<std::string>& args) { + const OptionFormatMap option_formats = { + {"--binary", {OptionValueType::STRING, OptionType::SINGLE}}, + {"--dump-etm", {OptionValueType::STRING, OptionType::SINGLE}}, + {"--exclude-perf", {OptionValueType::NONE, OptionType::SINGLE}}, + {"-i", {OptionValueType::STRING, OptionType::MULTIPLE}}, + {"-o", {OptionValueType::STRING, OptionType::SINGLE}}, + {"--output", {OptionValueType::STRING, OptionType::SINGLE}}, + {"--symdir", {OptionValueType::STRING, OptionType::MULTIPLE}}, + }; + OptionValueMap options; + std::vector<std::pair<OptionName, OptionValue>> ordered_options; + if (!PreprocessOptions(args, option_formats, &options, &ordered_options, nullptr)) { + return false; + } + + if (auto value = options.PullValue("--binary"); value) { + binary_name_regex_ = *value->str_value; + } + if (auto value = options.PullValue("--dump-etm"); value) { + if (!ParseEtmDumpOption(*value->str_value, &etm_dump_option_)) { + return false; + } + } + exclude_perf_ = options.PullBoolValue("--exclude-perf"); + + for (const OptionValue& value : options.PullValues("-i")) { + std::vector<std::string> files = android::base::Split(*value.str_value, ","); + for (std::string& file : files) { + if (android::base::StartsWith(file, "@")) { + if (!ReadFileList(file.substr(1), &input_filenames_)) { + return false; + } + } else { + input_filenames_.emplace_back(file); + } + } + } + if (input_filenames_.empty()) { + input_filenames_.emplace_back("perf.data"); + } + options.PullStringValue("-o", &output_filename_); + if (auto value = options.PullValue("--output"); value) { + const std::string& output = *value->str_value; + if (output == "autofdo") { + output_format_ = OutputFormat::AutoFDO; + } else if (output == "branch-list") { + output_format_ = OutputFormat::BranchList; + } else { + LOG(ERROR) << "unknown format in --output option: " << output; + return false; + } + } + if (auto value = options.PullValue("--symdir"); value) { + if (!Dso::AddSymbolDir(*value->str_value)) { + return false; + } + // Symbol dirs are cleaned when Dso count is decreased to zero, which can happen between + // processing input files. To make symbol dirs always available, create a placeholder dso to + // prevent cleaning from happening. + placeholder_dso_ = Dso::CreateDso(DSO_UNKNOWN_FILE, "unknown"); + } + CHECK(options.values.empty()); + return true; + } + + bool ReadFileList(const std::string& path, std::vector<std::string>* file_list) { + std::string data; + if (!android::base::ReadFileToString(path, &data)) { + PLOG(ERROR) << "failed to read " << path; + return false; + } + std::vector<std::string> tokens = android::base::Tokenize(data, " \t\n\r"); + file_list->insert(file_list->end(), tokens.begin(), tokens.end()); + return true; + } + + bool ProcessInputFile(const std::string& input_filename) { + if (IsPerfDataFile(input_filename)) { + PerfDataReader reader(input_filename, exclude_perf_, etm_dump_option_, binary_name_regex_); + if (output_format_ == OutputFormat::AutoFDO) { + reader.SetCallback([this](const BinaryKey& key, AutoFDOBinaryInfo& binary) { + autofdo_writer_.AddAutoFDOBinary(key, binary); + }); + } else if (output_format_ == OutputFormat::BranchList) { + reader.SetCallback([this](const BinaryKey& key, BranchListBinaryInfo& binary) { + branch_list_writer_.AddBranchListBinary(key, binary); + }); + } + return reader.Read(); + } + return ProcessBranchListFile(input_filename); + } + + bool ProcessBranchListFile(const std::string& input_filename) { + if (output_format_ != OutputFormat::AutoFDO) { + LOG(ERROR) << "Only support autofdo output when given a branch list file."; + return false; + } + BranchListToAutoFDOConverter converter; + auto callback = [&](const BinaryKey& key, BranchListBinaryInfo& binary) { + std::unique_ptr<AutoFDOBinaryInfo> autofdo_binary = converter.Convert(key, binary); + if (autofdo_binary) { + // Create new BinaryKey with kernel_start_addr = 0. Because AutoFDO output doesn't care + // kernel_start_addr. + autofdo_writer_.AddAutoFDOBinary(BinaryKey(key.path, key.build_id), *autofdo_binary); + } + }; + + BranchListReader reader(input_filename, binary_name_regex_); + reader.SetCallback(callback); + return reader.Read(); + } + + bool WriteOutput() { + if (output_format_ == OutputFormat::AutoFDO) { + return autofdo_writer_.Write(output_filename_); + } + + CHECK(output_format_ == OutputFormat::BranchList); + return branch_list_writer_.Write(output_filename_); } std::regex binary_name_regex_{""}; // Default to match everything. bool exclude_perf_ = false; - std::string input_filename_ = "perf.data"; + std::vector<std::string> input_filenames_; std::string output_filename_ = "perf_inject.data"; OutputFormat output_format_ = OutputFormat::AutoFDO; - ThreadTreeWithFilter thread_tree_; - std::unique_ptr<RecordFileReader> record_file_reader_; ETMDumpOption etm_dump_option_; - std::unique_ptr<ETMDecoder> etm_decoder_; - std::vector<uint8_t> aux_data_buffer_; - std::unique_ptr<FILE, decltype(&fclose)> output_fp_; - // Store results for AutoFDO. - std::unordered_map<Dso*, AutoFDOBinaryInfo> autofdo_binary_map_; - // Store results for BranchList. - std::unordered_map<Dso*, BranchListBinaryInfo> branch_list_binary_map_; - std::vector<std::unique_ptr<Dso>> branch_list_dso_v_; - uint64_t kernel_map_start_addr_ = 0; + std::unique_ptr<Dso> placeholder_dso_; + AutoFDOWriter autofdo_writer_; + BranchListWriter branch_list_writer_; }; } // namespace diff --git a/simpleperf/cmd_inject_test.cpp b/simpleperf/cmd_inject_test.cpp index daad83c9..4e7e8515 100644 --- a/simpleperf/cmd_inject_test.cpp +++ b/simpleperf/cmd_inject_test.cpp @@ -155,3 +155,26 @@ TEST(cmd_inject, unformatted_trace) { ASSERT_NE(data.find("etm_test_loop"), std::string::npos); CheckMatchingExpectedData(data); } + +TEST(cmd_inject, multiple_input_files) { + std::string data; + std::string perf_data = GetTestData(std::string("etm") + OS_PATH_SEPARATOR + "perf.data"); + std::string perf_with_unformatted_trace = + GetTestData(std::string("etm") + OS_PATH_SEPARATOR + "perf_with_unformatted_trace.data"); + + // Test input files separated by comma. + ASSERT_TRUE(RunInjectCmd({"-i", perf_with_unformatted_trace + "," + perf_data}, &data)); + ASSERT_NE(data.find("106c->1074:200"), std::string::npos); + + // Test input files from different -i options. + ASSERT_TRUE(RunInjectCmd({"-i", perf_with_unformatted_trace, "-i", perf_data}, &data)); + ASSERT_NE(data.find("106c->1074:200"), std::string::npos); + + // Test input files provided by input_file_list. + TemporaryFile tmpfile; + std::string input_file_list = perf_data + "\n" + perf_with_unformatted_trace + "\n"; + ASSERT_TRUE(android::base::WriteStringToFd(input_file_list, tmpfile.fd)); + close(tmpfile.release()); + ASSERT_TRUE(RunInjectCmd({"-i", std::string("@") + tmpfile.path}, &data)); + ASSERT_NE(data.find("106c->1074:200"), std::string::npos); +} diff --git a/simpleperf/cmd_report.cpp b/simpleperf/cmd_report.cpp index 04803838..522687cd 100644 --- a/simpleperf/cmd_report.cpp +++ b/simpleperf/cmd_report.cpp @@ -392,9 +392,9 @@ class ReportCommand : public Command { "--percent-limit <percent> Set min percentage in report entries and call graphs.\n" "--pids pid1,pid2,... Report only for selected pids.\n" "--raw-period Report period count instead of period percentage.\n" -"--sort key1,key2,... Select keys used to sort and print the report. The\n" -" appearance order of keys decides the order of keys used\n" -" to sort and print the report.\n" +"--sort key1,key2,... Select keys used to group samples into report entries. Samples having\n" +" the same key values are aggregated into one report entry. Each report\n" +" entry is printed in one row, having columns to show key values.\n" " Possible keys include:\n" " pid -- process id\n" " tid -- thread id\n" |