summaryrefslogtreecommitdiff
path: root/simpleperf/cmd_debug_unwind.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'simpleperf/cmd_debug_unwind.cpp')
-rw-r--r--simpleperf/cmd_debug_unwind.cpp859
1 files changed, 581 insertions, 278 deletions
diff --git a/simpleperf/cmd_debug_unwind.cpp b/simpleperf/cmd_debug_unwind.cpp
index b5df2da3..9370a35e 100644
--- a/simpleperf/cmd_debug_unwind.cpp
+++ b/simpleperf/cmd_debug_unwind.cpp
@@ -14,10 +14,13 @@
* limitations under the License.
*/
+#include <stdio.h>
+
#include <algorithm>
#include <memory>
#include <string>
#include <unordered_map>
+#include <unordered_set>
#include <vector>
#include <android-base/file.h>
@@ -26,20 +29,18 @@
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
-#include "CallChainJoiner.h"
+#include "JITDebugReader.h"
+#include "OfflineUnwinder.h"
#include "command.h"
#include "environment.h"
-#include "OfflineUnwinder.h"
#include "perf_regs.h"
#include "record_file.h"
+#include "report_utils.h"
#include "thread_tree.h"
#include "utils.h"
-#include "workload.h"
-using namespace simpleperf;
-
-// Cache size used by CallChainJoiner to cache call chains in memory.
-constexpr size_t DEFAULT_CALL_CHAIN_JOINER_CACHE_SIZE = 8 * 1024 * 1024;
+namespace simpleperf {
+namespace {
struct MemStat {
std::string vm_peak;
@@ -56,7 +57,7 @@ struct MemStat {
static bool GetMemStat(MemStat* stat) {
std::string s;
if (!android::base::ReadFileToString(android::base::StringPrintf("/proc/%d/status", getpid()),
- &s)) {
+ &s)) {
PLOG(ERROR) << "Failed to read process status";
return false;
}
@@ -75,337 +76,639 @@ static bool GetMemStat(MemStat* stat) {
return true;
}
-class DebugUnwindCommand : public Command {
- public:
- DebugUnwindCommand()
- : Command("debug-unwind", "Debug/test offline unwinding.",
- // clang-format off
-"Usage: simpleperf debug-unwind [options]\n"
-" Given a perf.data generated with \"-g --no-unwind\", it converts\n"
-" regs/stack data of samples into callchains, and write result into\n"
-" a new perf.data. The new perf.data can be passed to\n"
-" unwind_result_reporter.py to generate a text report.\n"
-"-i <file> The path of record file generated with \"-g --no-unwind\".\n"
-" Default is perf.data.\n"
-"-o <file> The path ot write new perf.data. Default is perf.data.debug.\n"
-"--symfs <dir> Look for files with symbols relative to this directory.\n"
-"--time time Only unwind samples recorded at selected time.\n"
- // clang-format on
- ),
- input_filename_("perf.data"),
- output_filename_("perf.data.debug"),
- offline_unwinder_(OfflineUnwinder::Create(true)),
- callchain_joiner_(DEFAULT_CALL_CHAIN_JOINER_CACHE_SIZE, 1, true),
- selected_time_(0) {
- }
-
- bool Run(const std::vector<std::string>& args);
+struct UnwindingStat {
+ // For testing unwinding performance
+ uint64_t unwinding_sample_count = 0u;
+ uint64_t total_unwinding_time_in_ns = 0u;
+ uint64_t max_unwinding_time_in_ns = 0u;
- private:
- bool ParseOptions(const std::vector<std::string>& args);
- bool UnwindRecordFile();
- bool ProcessRecord(Record* record);
- void CollectHitFileInfo(const SampleRecord& r, const std::vector<uint64_t>& ips);
- bool JoinCallChains();
- bool WriteFeatureSections();
- void PrintStat();
-
- struct Stat {
- // For testing unwinding performance.
- uint64_t unwinding_sample_count = 0u;
- uint64_t total_unwinding_time_in_ns = 0u;
- uint64_t max_unwinding_time_in_ns = 0u;
-
- // For memory consumption.
- MemStat mem_before_unwinding;
- MemStat mem_after_unwinding;
- };
+ // For memory consumption
+ MemStat mem_before_unwinding;
+ MemStat mem_after_unwinding;
- std::string input_filename_;
- std::string output_filename_;
- std::unique_ptr<RecordFileReader> reader_;
- std::unique_ptr<RecordFileWriter> writer_;
- ThreadTree thread_tree_;
- std::unique_ptr<OfflineUnwinder> offline_unwinder_;
- CallChainJoiner callchain_joiner_;
- Stat stat_;
- uint64_t selected_time_;
-};
+ void AddUnwindingResult(const UnwindingResult& result) {
+ unwinding_sample_count++;
+ total_unwinding_time_in_ns += result.used_time;
+ max_unwinding_time_in_ns = std::max(max_unwinding_time_in_ns, result.used_time);
+ }
-bool DebugUnwindCommand::Run(const std::vector<std::string>& args) {
- // 1. Parse options.
- if (!ParseOptions(args)) {
- return false;
+ void Dump(FILE* fp) {
+ if (unwinding_sample_count == 0) {
+ return;
+ }
+ fprintf(fp, "unwinding_sample_count: %" PRIu64 "\n", unwinding_sample_count);
+ fprintf(fp, "average_unwinding_time: %.3f us\n",
+ total_unwinding_time_in_ns / 1e3 / unwinding_sample_count);
+ fprintf(fp, "max_unwinding_time: %.3f us\n", max_unwinding_time_in_ns / 1e3);
+
+ if (!mem_before_unwinding.vm_peak.empty()) {
+ fprintf(fp, "memory_change_VmPeak: %s -> %s\n", mem_before_unwinding.vm_peak.c_str(),
+ mem_after_unwinding.vm_peak.c_str());
+ fprintf(fp, "memory_change_VmSize: %s -> %s\n", mem_before_unwinding.vm_size.c_str(),
+ mem_after_unwinding.vm_size.c_str());
+ fprintf(fp, "memory_change_VmHwM: %s -> %s\n", mem_before_unwinding.vm_hwm.c_str(),
+ mem_after_unwinding.vm_hwm.c_str());
+ fprintf(fp, "memory_change_VmRSS: %s -> %s\n", mem_before_unwinding.vm_rss.c_str(),
+ mem_after_unwinding.vm_rss.c_str());
+ }
}
- ScopedTempFiles scoped_temp_files(android::base::Dirname(output_filename_));
+};
- // 2. Read input perf.data, and generate new perf.data.
- if (!UnwindRecordFile()) {
- return false;
+class RecordFileProcessor {
+ public:
+ RecordFileProcessor(const std::string& output_filename, bool output_binary_mode)
+ : output_filename_(output_filename),
+ output_binary_mode_(output_binary_mode),
+ unwinder_(OfflineUnwinder::Create(true)),
+ callchain_report_builder_(thread_tree_) {}
+
+ virtual ~RecordFileProcessor() {
+ if (out_fp_ != nullptr && out_fp_ != stdout) {
+ fclose(out_fp_);
+ }
}
- // 3. Show stat of unwinding.
- PrintStat();
- return true;
-}
+ bool ProcessFile(const std::string& input_filename) {
+ // 1. Check input file.
+ record_filename_ = input_filename;
+ reader_ = RecordFileReader::CreateInstance(record_filename_);
+ if (!reader_) {
+ return false;
+ }
+ std::string record_cmd = android::base::Join(reader_->ReadCmdlineFeature(), " ");
+ if (record_cmd.find("-g") == std::string::npos &&
+ record_cmd.find("--call-graph dwarf") == std::string::npos) {
+ LOG(ERROR) << "file isn't recorded with dwarf call graph: " << record_filename_;
+ return false;
+ }
+ if (!CheckRecordCmd(record_cmd)) {
+ return false;
+ }
-bool DebugUnwindCommand::ParseOptions(const std::vector<std::string>& args) {
- for (size_t i = 0; i < args.size(); ++i) {
- if (args[i] == "-i") {
- if (!NextArgumentOrError(args, &i)) {
- return false;
- }
- input_filename_ = args[i];
- } else if (args[i] == "-o") {
- if (!NextArgumentOrError(args, &i)) {
- return false;
- }
- output_filename_ = args[i];
- } else if (args[i] == "--symfs") {
- if (!NextArgumentOrError(args, &i)) {
+ // 2. Load feature sections.
+ reader_->LoadBuildIdAndFileFeatures(thread_tree_);
+ ScopedCurrentArch scoped_arch(
+ GetArchType(reader_->ReadFeatureString(PerfFileFormat::FEAT_ARCH)));
+ unwinder_->LoadMetaInfo(reader_->GetMetaInfoFeature());
+ if (reader_->HasFeature(PerfFileFormat::FEAT_DEBUG_UNWIND) &&
+ reader_->HasFeature(PerfFileFormat::FEAT_DEBUG_UNWIND_FILE)) {
+ auto debug_unwind_feature = reader_->ReadDebugUnwindFeature();
+ if (!debug_unwind_feature.has_value()) {
return false;
}
- if (!Dso::SetSymFsDir(args[i])) {
- return false;
+ uint64_t offset =
+ reader_->FeatureSectionDescriptors().at(PerfFileFormat::FEAT_DEBUG_UNWIND_FILE).offset;
+ for (DebugUnwindFile& file : debug_unwind_feature.value()) {
+ auto& loc = debug_unwind_files_[file.path];
+ loc.offset = offset;
+ loc.size = file.size;
+ offset += file.size;
}
- } else if (args[i] == "--time") {
- if (!GetUintOption(args, &i, &selected_time_)) {
+ }
+ callchain_report_builder_.SetRemoveArtFrame(false);
+ callchain_report_builder_.SetConvertJITFrame(false);
+
+ // 3. Open output file.
+ if (output_filename_.empty()) {
+ out_fp_ = stdout;
+ } else {
+ out_fp_ = fopen(output_filename_.c_str(), output_binary_mode_ ? "web+" : "we+");
+ if (out_fp_ == nullptr) {
+ PLOG(ERROR) << "failed to write to " << output_filename_;
return false;
}
- } else {
- ReportUnknownOption(args, i);
- return false;
}
+
+ // 4. Process records.
+ return Process();
}
- return true;
+
+ protected:
+ struct DebugUnwindFileLocation {
+ uint64_t offset;
+ uint64_t size;
+ };
+
+ virtual bool CheckRecordCmd(const std::string& record_cmd) = 0;
+ virtual bool Process() = 0;
+
+ std::string record_filename_;
+ std::unique_ptr<RecordFileReader> reader_;
+ std::string output_filename_;
+ bool output_binary_mode_;
+ FILE* out_fp_ = nullptr;
+ ThreadTree thread_tree_;
+ std::unique_ptr<OfflineUnwinder> unwinder_;
+ // Files stored in DEBUG_UNWIND_FILE feature section in the recording file.
+ // Map from file path to offset in the recording file.
+ std::unordered_map<std::string, DebugUnwindFileLocation> debug_unwind_files_;
+ CallChainReportBuilder callchain_report_builder_;
+};
+
+static void DumpUnwindingResult(const UnwindingResult& result, FILE* fp) {
+ fprintf(fp, "unwinding_used_time: %.3f us\n", result.used_time / 1e3);
+ fprintf(fp, "unwinding_error_code: %" PRIu64 "\n", result.error_code);
+ fprintf(fp, "unwinding_error_addr: 0x%" PRIx64 "\n", result.error_addr);
+ fprintf(fp, "stack_start: 0x%" PRIx64 "\n", result.stack_start);
+ fprintf(fp, "stack_end: 0x%" PRIx64 "\n", result.stack_end);
}
-bool DebugUnwindCommand::UnwindRecordFile() {
- // 1. Check input file.
- reader_ = RecordFileReader::CreateInstance(input_filename_);
- if (!reader_) {
- return false;
- }
- reader_->LoadBuildIdAndFileFeatures(thread_tree_);
- std::string record_cmd = android::base::Join(reader_->ReadCmdlineFeature(), " ");
- if (record_cmd.find("--no-unwind") == std::string::npos ||
- (record_cmd.find("-g") == std::string::npos &&
- record_cmd.find("--call-graph dwarf") == std::string::npos)) {
- LOG(ERROR) << input_filename_ << " isn't recorded with \"-g --no-unwind\"";
- return false;
+class SampleUnwinder : public RecordFileProcessor {
+ public:
+ SampleUnwinder(const std::string& output_filename,
+ const std::unordered_set<uint64_t>& sample_times)
+ : RecordFileProcessor(output_filename, false), sample_times_(sample_times) {}
+
+ protected:
+ bool CheckRecordCmd(const std::string& record_cmd) override {
+ if (record_cmd.find("--no-unwind") == std::string::npos &&
+ record_cmd.find("--keep-failed-unwinding-debug-info") == std::string::npos) {
+ LOG(ERROR) << "file isn't record with --no-unwind or --keep-failed-unwinding-debug-info: "
+ << record_filename_;
+ return false;
+ }
+ return true;
}
- ScopedCurrentArch scoped_arch(GetArchType(reader_->ReadFeatureString(PerfFileFormat::FEAT_ARCH)));
- // 2. Copy attr section.
- writer_ = RecordFileWriter::CreateInstance(output_filename_);
- if (!writer_ || !writer_->WriteAttrSection(reader_->AttrSection())) {
- return false;
- }
+ bool Process() override {
+ recording_file_dso_ = Dso::CreateDso(DSO_ELF_FILE, record_filename_);
- // 3. Process records in data section.
- if (!GetMemStat(&stat_.mem_before_unwinding)) {
- return false;
+ if (!GetMemStat(&stat_.mem_before_unwinding)) {
+ return false;
+ }
+ if (!reader_->ReadDataSection(
+ [&](std::unique_ptr<Record> r) { return ProcessRecord(std::move(r)); })) {
+ return false;
+ }
+ if (!GetMemStat(&stat_.mem_after_unwinding)) {
+ return false;
+ }
+ stat_.Dump(out_fp_);
+ return true;
}
- auto callback = [this](std::unique_ptr<Record> record) {
- return ProcessRecord(record.get());
- };
- if (!reader_->ReadDataSection(callback)) {
- return false;
+
+ bool ProcessRecord(std::unique_ptr<Record> r) {
+ thread_tree_.Update(*r);
+ if (r->type() == SIMPLE_PERF_RECORD_UNWINDING_RESULT) {
+ last_unwinding_result_.reset(static_cast<UnwindingResultRecord*>(r.release()));
+ } else if (r->type() == PERF_RECORD_SAMPLE) {
+ if (sample_times_.empty() || sample_times_.count(r->Timestamp())) {
+ auto& sr = *static_cast<SampleRecord*>(r.get());
+ const PerfSampleStackUserType* stack = &sr.stack_user_data;
+ const PerfSampleRegsUserType* regs = &sr.regs_user_data;
+ if (last_unwinding_result_ && last_unwinding_result_->Timestamp() == sr.Timestamp()) {
+ stack = &last_unwinding_result_->stack_user_data;
+ regs = &last_unwinding_result_->regs_user_data;
+ }
+ if (stack->size > 0 || regs->reg_mask > 0) {
+ if (!UnwindRecord(sr, *regs, *stack)) {
+ return false;
+ }
+ }
+ }
+ last_unwinding_result_.reset();
+ }
+ return true;
}
- if (!JoinCallChains()) {
- return false;
+
+ bool UnwindRecord(const SampleRecord& r, const PerfSampleRegsUserType& regs,
+ const PerfSampleStackUserType& stack) {
+ ThreadEntry* thread = thread_tree_.FindThreadOrNew(r.tid_data.pid, r.tid_data.tid);
+ ThreadEntry thread_with_new_maps = CreateThreadWithUpdatedMaps(*thread);
+
+ RegSet reg_set(regs.abi, regs.reg_mask, regs.regs);
+ std::vector<uint64_t> ips;
+ std::vector<uint64_t> sps;
+ if (!unwinder_->UnwindCallChain(thread_with_new_maps, reg_set, stack.data, stack.size, &ips,
+ &sps)) {
+ return false;
+ }
+ stat_.AddUnwindingResult(unwinder_->GetUnwindingResult());
+
+ // Print unwinding result.
+ fprintf(out_fp_, "sample_time: %" PRIu64 "\n", r.Timestamp());
+ DumpUnwindingResult(unwinder_->GetUnwindingResult(), out_fp_);
+ std::vector<CallChainReportEntry> entries = callchain_report_builder_.Build(thread, ips, 0);
+ for (size_t i = 0; i < entries.size(); i++) {
+ size_t id = i + 1;
+ const auto& entry = entries[i];
+ fprintf(out_fp_, "ip_%zu: 0x%" PRIx64 "\n", id, entry.ip);
+ fprintf(out_fp_, "sp_%zu: 0x%" PRIx64 "\n", id, sps[i]);
+ fprintf(out_fp_, "map_%zu: [0x%" PRIx64 "-0x%" PRIx64 "], pgoff 0x%" PRIx64 "\n", id,
+ entry.map->start_addr, entry.map->get_end_addr(), entry.map->pgoff);
+ fprintf(out_fp_, "dso_%zu: %s\n", id, entry.map->dso->Path().c_str());
+ fprintf(out_fp_, "vaddr_in_file_%zu: 0x%" PRIx64 "\n", id, entry.vaddr_in_file);
+ fprintf(out_fp_, "symbol_%zu: %s\n", id, entry.symbol->DemangledName());
+ }
+ fprintf(out_fp_, "\n");
+ return true;
}
- if (!GetMemStat(&stat_.mem_after_unwinding)) {
- return false;
+
+ // To use files stored in DEBUG_UNWIND_FILE feature section, create maps mapping to them.
+ ThreadEntry CreateThreadWithUpdatedMaps(const ThreadEntry& thread) {
+ ThreadEntry new_thread = thread;
+ new_thread.maps.reset(new MapSet);
+ new_thread.maps->version = thread.maps->version;
+ for (auto& p : thread.maps->maps) {
+ const MapEntry* old_map = p.second;
+ MapEntry* map = nullptr;
+ const std::string& path = old_map->dso->Path();
+ if (auto it = debug_unwind_files_.find(path); it != debug_unwind_files_.end()) {
+ map_storage_.emplace_back(new MapEntry);
+ map = map_storage_.back().get();
+ *map = *old_map;
+ map->dso = recording_file_dso_.get();
+ if (JITDebugReader::IsPathInJITSymFile(old_map->dso->Path())) {
+ map->pgoff = it->second.offset;
+ } else {
+ map->pgoff += it->second.offset;
+ }
+ } else {
+ map = const_cast<MapEntry*>(p.second);
+ }
+ new_thread.maps->maps[p.first] = map;
+ }
+ return new_thread;
}
- // 4. Write feature sections.
- return WriteFeatureSections();
-}
+ private:
+ const std::unordered_set<uint64_t> sample_times_;
+ std::unique_ptr<Dso> recording_file_dso_;
+ std::vector<std::unique_ptr<MapEntry>> map_storage_;
+ UnwindingStat stat_;
+ std::unique_ptr<UnwindingResultRecord> last_unwinding_result_;
+};
-bool DebugUnwindCommand::ProcessRecord(Record* record) {
- if (record->type() == PERF_RECORD_SAMPLE) {
- auto& r = *static_cast<SampleRecord*>(record);
- if (selected_time_ != 0u && r.Timestamp() != selected_time_) {
- return true;
- }
- uint64_t need_type = PERF_SAMPLE_CALLCHAIN | PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER;
- if ((r.sample_type & need_type) == need_type && r.regs_user_data.reg_mask != 0 &&
- r.GetValidStackSize() > 0) {
- ThreadEntry* thread = thread_tree_.FindThreadOrNew(r.tid_data.pid, r.tid_data.tid);
- RegSet regs(r.regs_user_data.abi, r.regs_user_data.reg_mask, r.regs_user_data.regs);
- std::vector<uint64_t> ips;
- std::vector<uint64_t> sps;
- if (!offline_unwinder_->UnwindCallChain(*thread, regs, r.stack_user_data.data,
- r.GetValidStackSize(), &ips, &sps)) {
- return false;
- }
+class TestFileGenerator : public RecordFileProcessor {
+ public:
+ TestFileGenerator(const std::string& output_filename,
+ const std::unordered_set<uint64_t>& sample_times,
+ const std::unordered_set<std::string>& kept_binaries)
+ : RecordFileProcessor(output_filename, true),
+ sample_times_(sample_times),
+ kept_binaries_(kept_binaries) {}
+
+ protected:
+ bool CheckRecordCmd(const std::string&) override { return true; }
+
+ bool Process() override {
+ writer_.reset(new RecordFileWriter(output_filename_, out_fp_, false));
+ if (!writer_ || !writer_->WriteAttrSection(reader_->AttrSection())) {
+ return false;
+ }
+ if (!reader_->ReadDataSection(
+ [&](std::unique_ptr<Record> r) { return ProcessRecord(std::move(r)); })) {
+ return false;
+ }
+ return WriteFeatureSections();
+ }
- const UnwindingResult& unwinding_result = offline_unwinder_->GetUnwindingResult();
- stat_.unwinding_sample_count++;
- stat_.total_unwinding_time_in_ns += unwinding_result.used_time;
- stat_.max_unwinding_time_in_ns = std::max(stat_.max_unwinding_time_in_ns,
- unwinding_result.used_time);
- if (!writer_->WriteRecord(UnwindingResultRecord(r.time_data.time, unwinding_result))) {
- return false;
- }
- // We want to keep both reg/stack data and callchain of a sample. However, storing both
- // can exceed the size limit of a SampleRecord. So instead we store one sample with reg/stack
- // data and one sample with callchain.
- if (!writer_->WriteRecord(r)) {
- return false;
+ bool ProcessRecord(std::unique_ptr<Record> r) {
+ thread_tree_.Update(*r);
+ bool keep_record = false;
+ if (r->type() == SIMPLE_PERF_RECORD_UNWINDING_RESULT) {
+ keep_record = (sample_times_.count(r->Timestamp()) > 0);
+ } else if (r->type() == PERF_RECORD_SAMPLE) {
+ keep_record = (sample_times_.count(r->Timestamp()) > 0);
+ if (keep_record) {
+ // Dump maps needed to unwind this sample.
+ if (!WriteMapsForSample(*static_cast<SampleRecord*>(r.get()))) {
+ return false;
+ }
}
- r.ReplaceRegAndStackWithCallChain(ips);
- if (!callchain_joiner_.AddCallChain(r.tid_data.pid, r.tid_data.tid,
- CallChainJoiner::ORIGINAL_OFFLINE, ips, sps)) {
- return false;
+ }
+ if (keep_record) {
+ return writer_->WriteRecord(*r);
+ }
+ return true;
+ }
+
+ bool WriteMapsForSample(const SampleRecord& r) {
+ ThreadEntry* thread = thread_tree_.FindThread(r.tid_data.tid);
+ if (thread != nullptr && thread->maps) {
+ auto attr = reader_->AttrSection()[0].attr;
+ auto event_id = reader_->AttrSection()[0].ids[0];
+
+ size_t kernel_ip_count;
+ std::vector<uint64_t> ips = r.GetCallChain(&kernel_ip_count);
+ for (size_t i = kernel_ip_count; i < ips.size(); i++) {
+ const MapEntry* map = thread_tree_.FindMap(thread, ips[i], false);
+ if (!thread_tree_.IsUnknownDso(map->dso)) {
+ Mmap2Record map_record(*attr, false, r.tid_data.pid, r.tid_data.tid, map->start_addr,
+ map->len, map->pgoff, map->flags, map->dso->Path(), event_id,
+ r.Timestamp());
+ if (!writer_->WriteRecord(map_record)) {
+ return false;
+ }
+ }
}
- CollectHitFileInfo(r, ips);
}
- } else {
- thread_tree_.Update(*record);
+ return true;
}
- return writer_->WriteRecord(*record);
-}
-void DebugUnwindCommand::CollectHitFileInfo(const SampleRecord& r,
- const std::vector<uint64_t>& ips) {
- const ThreadEntry* thread = thread_tree_.FindThreadOrNew(r.tid_data.pid, r.tid_data.tid);
- for (auto ip : ips) {
- const MapEntry* map = thread_tree_.FindMap(thread, ip, false);
- Dso* dso = map->dso;
- if (!dso->HasDumpId() && dso->type() != DSO_UNKNOWN_FILE) {
- dso->CreateDumpId();
+ bool WriteFeatureSections() {
+ if (!writer_->BeginWriteFeatures(reader_->FeatureSectionDescriptors().size())) {
+ return false;
}
- const Symbol* symbol = thread_tree_.FindSymbol(map, ip, nullptr, &dso);
- if (!symbol->HasDumpId()) {
- dso->CreateSymbolDumpId(symbol);
+ std::unordered_set<int> feature_types_to_copy = {
+ PerfFileFormat::FEAT_ARCH, PerfFileFormat::FEAT_CMDLINE, PerfFileFormat::FEAT_META_INFO};
+ const size_t BUFFER_SIZE = 64 * 1024;
+ std::string buffer(BUFFER_SIZE, '\0');
+ for (const auto& p : reader_->FeatureSectionDescriptors()) {
+ auto feat_type = p.first;
+ if (feat_type == PerfFileFormat::FEAT_DEBUG_UNWIND) {
+ DebugUnwindFeature feature;
+ buffer.resize(BUFFER_SIZE);
+ for (const auto& file_p : debug_unwind_files_) {
+ if (kept_binaries_.count(file_p.first)) {
+ feature.resize(feature.size() + 1);
+ feature.back().path = file_p.first;
+ feature.back().size = file_p.second.size;
+ if (!CopyDebugUnwindFile(file_p.second, buffer)) {
+ return false;
+ }
+ }
+ }
+ if (!writer_->WriteDebugUnwindFeature(feature)) {
+ return false;
+ }
+ } else if (feat_type == PerfFileFormat::FEAT_FILE) {
+ size_t read_pos = 0;
+ FileFeature file_feature;
+ while (reader_->ReadFileFeature(read_pos, &file_feature)) {
+ if (kept_binaries_.count(file_feature.path) && !writer_->WriteFileFeature(file_feature)) {
+ return false;
+ }
+ }
+ } else if (feat_type == PerfFileFormat::FEAT_BUILD_ID) {
+ std::vector<BuildIdRecord> build_ids = reader_->ReadBuildIdFeature();
+ std::vector<BuildIdRecord> write_build_ids;
+ for (auto& build_id : build_ids) {
+ if (kept_binaries_.count(build_id.filename)) {
+ write_build_ids.emplace_back(std::move(build_id));
+ }
+ }
+ if (!writer_->WriteBuildIdFeature(write_build_ids)) {
+ return false;
+ }
+ } else if (feature_types_to_copy.count(feat_type)) {
+ if (!reader_->ReadFeatureSection(feat_type, &buffer) ||
+ !writer_->WriteFeature(feat_type, buffer.data(), buffer.size())) {
+ return false;
+ }
+ }
}
+ return writer_->EndWriteFeatures() && writer_->Close();
}
-}
-bool DebugUnwindCommand::JoinCallChains() {
- // 1. Prepare joined callchains.
- if (!callchain_joiner_.JoinCallChains()) {
- return false;
- }
- // 2. Move records from record_filename_ to a temporary file.
- if (!writer_->Close()) {
- return false;
+ bool CopyDebugUnwindFile(const DebugUnwindFileLocation& loc, std::string& buffer) {
+ uint64_t offset = loc.offset;
+ uint64_t left_size = loc.size;
+ while (left_size > 0) {
+ size_t nread = std::min<size_t>(left_size, buffer.size());
+ if (!reader_->ReadAtOffset(offset, buffer.data(), nread) ||
+ !writer_->WriteFeature(PerfFileFormat::FEAT_DEBUG_UNWIND_FILE, buffer.data(), nread)) {
+ return false;
+ }
+ offset += nread;
+ left_size -= nread;
+ }
+ return true;
}
- writer_.reset();
- std::unique_ptr<TemporaryFile> tmp_file = ScopedTempFiles::CreateTempFile();
- if (!Workload::RunCmd({"mv", output_filename_, tmp_file->path})) {
- return false;
+
+ private:
+ const std::unordered_set<uint64_t> sample_times_;
+ const std::unordered_set<std::string> kept_binaries_;
+ std::unique_ptr<RecordFileWriter> writer_;
+};
+
+class ReportGenerator : public RecordFileProcessor {
+ public:
+ ReportGenerator(const std::string& output_filename)
+ : RecordFileProcessor(output_filename, false) {}
+
+ protected:
+ bool CheckRecordCmd(const std::string& record_cmd) override {
+ if (record_cmd.find("--keep-failed-unwinding-debug-info") == std::string::npos &&
+ record_cmd.find("--keep-failed-unwinding-result") == std::string::npos) {
+ LOG(ERROR) << "file isn't record with --keep-failed-unwinding-debug-info or "
+ << "--keep-failed-unwinding-result: " << record_filename_;
+ return false;
+ }
+ return true;
}
- // 3. Read records from the temporary file, and write records with joined call chains back
- // to record_filename_.
- std::unique_ptr<RecordFileReader> reader = RecordFileReader::CreateInstance(tmp_file->path);
- if (!reader) {
- return false;
+ bool Process() override {
+ if (!reader_->ReadDataSection(
+ [&](std::unique_ptr<Record> r) { return ProcessRecord(std::move(r)); })) {
+ return false;
+ }
+ return true;
}
- writer_ = RecordFileWriter::CreateInstance(output_filename_);
- if (!writer_ || !writer_->WriteAttrSection(reader->AttrSection())) {
- return false;
+
+ private:
+ bool ProcessRecord(std::unique_ptr<Record> r) {
+ thread_tree_.Update(*r);
+ if (r->type() == SIMPLE_PERF_RECORD_UNWINDING_RESULT) {
+ last_unwinding_result_.reset(static_cast<UnwindingResultRecord*>(r.release()));
+ } else if (r->type() == PERF_RECORD_SAMPLE) {
+ if (last_unwinding_result_) {
+ ReportUnwindingResult(*static_cast<SampleRecord*>(r.get()), *last_unwinding_result_);
+ last_unwinding_result_.reset();
+ }
+ }
+ return true;
}
- auto record_callback = [&](std::unique_ptr<Record> r) {
- if (r->type() != PERF_RECORD_SAMPLE) {
- return writer_->WriteRecord(*r);
+ void ReportUnwindingResult(const SampleRecord& sr, const UnwindingResultRecord& unwinding_r) {
+ ThreadEntry* thread = thread_tree_.FindThreadOrNew(sr.tid_data.pid, sr.tid_data.tid);
+ size_t kernel_ip_count;
+ std::vector<uint64_t> ips = sr.GetCallChain(&kernel_ip_count);
+ if (kernel_ip_count != 0) {
+ ips.erase(ips.begin(), ips.begin() + kernel_ip_count);
}
- SampleRecord& sr = *static_cast<SampleRecord*>(r.get());
- if (!sr.HasUserCallChain()) {
- return writer_->WriteRecord(sr);
+
+ fprintf(out_fp_, "sample_time: %" PRIu64 "\n", sr.Timestamp());
+ DumpUnwindingResult(unwinding_r.unwinding_result, out_fp_);
+ // Print callchain.
+ std::vector<CallChainReportEntry> entries = callchain_report_builder_.Build(thread, ips, 0);
+ for (size_t i = 0; i < entries.size(); i++) {
+ size_t id = i + 1;
+ const auto& entry = entries[i];
+ fprintf(out_fp_, "ip_%zu: 0x%" PRIx64 "\n", id, entry.ip);
+ if (i < unwinding_r.callchain.length) {
+ fprintf(out_fp_, "unwinding_ip_%zu: 0x%" PRIx64 "\n", id, unwinding_r.callchain.ips[i]);
+ fprintf(out_fp_, "unwinding_sp_%zu: 0x%" PRIx64 "\n", id, unwinding_r.callchain.sps[i]);
+ }
+ fprintf(out_fp_, "map_%zu: [0x%" PRIx64 "-0x%" PRIx64 "], pgoff 0x%" PRIx64 "\n", id,
+ entry.map->start_addr, entry.map->get_end_addr(), entry.map->pgoff);
+ fprintf(out_fp_, "dso_%zu: %s\n", id, entry.map->dso->Path().c_str());
+ fprintf(out_fp_, "vaddr_in_file_%zu: 0x%" PRIx64 "\n", id, entry.vaddr_in_file);
+ fprintf(out_fp_, "symbol_%zu: %s\n", id, entry.symbol->DemangledName());
}
- pid_t pid;
- pid_t tid;
- CallChainJoiner::ChainType type;
- std::vector<uint64_t> ips;
- std::vector<uint64_t> sps;
- do {
- if (!callchain_joiner_.GetNextCallChain(pid, tid, type, ips, sps)) {
- return false;
+ // Print regs.
+ uint64_t stack_addr = 0;
+ if (unwinding_r.regs_user_data.reg_nr > 0) {
+ auto& reg_data = unwinding_r.regs_user_data;
+ RegSet regs(reg_data.abi, reg_data.reg_mask, reg_data.regs);
+ uint64_t value;
+ if (regs.GetSpRegValue(&value)) {
+ stack_addr = value;
+ for (size_t i = 0; i < 64; i++) {
+ if (regs.GetRegValue(i, &value)) {
+ fprintf(out_fp_, "reg_%s: 0x%" PRIx64 "\n", GetRegName(i, regs.arch).c_str(), value);
+ }
+ }
}
- if (!writer_->WriteRecord(CallChainRecord(pid, tid, type, sr.Timestamp(), ips, sps))) {
- return false;
+ }
+ // Print stack.
+ if (unwinding_r.stack_user_data.size > 0) {
+ auto& stack = unwinding_r.stack_user_data;
+ const char* p = stack.data;
+ const char* end = stack.data + stack.size;
+ uint64_t value;
+ while (p + 8 <= end) {
+ fprintf(out_fp_, "stack_%" PRIx64 ":", stack_addr);
+ for (size_t i = 0; i < 4 && p + 8 <= end; ++i) {
+ MoveFromBinaryFormat(value, p);
+ fprintf(out_fp_, " %016" PRIx64, value);
+ }
+ fprintf(out_fp_, "\n");
+ stack_addr += 32;
}
- } while (type != CallChainJoiner::JOINED_OFFLINE);
- CHECK_EQ(pid, static_cast<pid_t>(sr.tid_data.pid));
- CHECK_EQ(tid, static_cast<pid_t>(sr.tid_data.tid));
- sr.UpdateUserCallChain(ips);
- return writer_->WriteRecord(sr);
- };
- return reader->ReadDataSection(record_callback);
-}
-
-bool DebugUnwindCommand::WriteFeatureSections() {
- // Add debug_unwind info in META_INFO section, and add symbol info in FILE section.
- const std::map<int, PerfFileFormat::SectionDesc>& features = reader_->FeatureSectionDescriptors();
- size_t new_feature_count = features.size();
- for (int feature : {PerfFileFormat::FEAT_FILE, PerfFileFormat::FEAT_META_INFO}) {
- if (features.find(feature) == features.end()) {
- new_feature_count++;
+ fprintf(out_fp_, "\n");
}
}
- if (!writer_->BeginWriteFeatures(new_feature_count)) {
+
+ std::unique_ptr<UnwindingResultRecord> last_unwinding_result_;
+};
+
+class DebugUnwindCommand : public Command {
+ public:
+ DebugUnwindCommand()
+ : Command(
+ "debug-unwind", "Debug/test offline unwinding.",
+ // clang-format off
+"Usage: simpleperf debug-unwind [options]\n"
+"--generate-report Generate a failed unwinding report.\n"
+"--generate-test-file Generate a test file with only one sample.\n"
+"-i <file> Input recording file. Default is perf.data.\n"
+"-o <file> Output file. Default is stdout.\n"
+"--keep-binaries-in-test-file binary1,binary2... Keep binaries in test file.\n"
+"--sample-time time1,time2... Only process samples recorded at selected times.\n"
+"--symfs <dir> Look for files with symbols relative to this directory.\n"
+"--unwind-sample Unwind samples.\n"
+"\n"
+"Examples:\n"
+"1. Unwind a sample.\n"
+"$ simpleperf debug-unwind -i perf.data --unwind-sample --sample-time 626970493946976\n"
+" perf.data should be generated with \"--no-unwind\" or \"--keep-failed-unwinding-debug-info\".\n"
+"2. Generate a test file.\n"
+"$ simpleperf debug-unwind -i perf.data --generate-test-file -o test.data --sample-time \\\n"
+" 626970493946976 --keep-binaries-in-test-file perf.data_jit_app_cache:255984-259968\n"
+"3. Generate a failed unwinding report.\n"
+"$ simpleperf debug-unwind -i perf.data --generate-report -o report.txt\n"
+" perf.data should be generated with \"--keep-failed-unwinding-debug-info\" or \\\n"
+" \"--keep-failed-unwinding-result\".\n"
+"\n"
+ // clang-format on
+ ) {}
+
+ bool Run(const std::vector<std::string>& args);
+
+ private:
+ bool ParseOptions(const std::vector<std::string>& args);
+
+ std::string input_filename_ = "perf.data";
+ std::string output_filename_;
+ bool unwind_sample_ = false;
+ bool generate_report_ = false;
+ bool generate_test_file_;
+ std::unordered_set<std::string> kept_binaries_in_test_file_;
+ std::unordered_set<uint64_t> sample_times_;
+};
+
+bool DebugUnwindCommand::Run(const std::vector<std::string>& args) {
+ // 1. Parse options.
+ if (!ParseOptions(args)) {
return false;
}
- auto it = features.begin();
- // Copy all feature sections except FEAT_FILE and FEAT_META_INFO, which require special handling.
- while (it != features.end() && it->first < PerfFileFormat::FEAT_FILE) {
- std::vector<char> data;
- if (!reader_->ReadFeatureSection(it->first, &data) || !writer_->WriteFeature(it->first, data)) {
- return false;
- }
- ++it;
+ // 2. Distribute sub commands.
+ if (unwind_sample_) {
+ SampleUnwinder sample_unwinder(output_filename_, sample_times_);
+ return sample_unwinder.ProcessFile(input_filename_);
}
- // Write a new file section.
- if (it != features.end() && it->first == PerfFileFormat::FEAT_FILE) {
- ++it;
+ if (generate_test_file_) {
+ TestFileGenerator test_file_generator(output_filename_, sample_times_,
+ kept_binaries_in_test_file_);
+ return test_file_generator.ProcessFile(input_filename_);
}
- if (!writer_->WriteFileFeatures(thread_tree_.GetAllDsos())) {
+ if (generate_report_) {
+ ReportGenerator report_generator(output_filename_);
+ return report_generator.ProcessFile(input_filename_);
+ }
+ return true;
+}
+
+bool DebugUnwindCommand::ParseOptions(const std::vector<std::string>& args) {
+ const OptionFormatMap option_formats = {
+ {"--generate-report", {OptionValueType::NONE, OptionType::SINGLE}},
+ {"--generate-test-file", {OptionValueType::NONE, OptionType::SINGLE}},
+ {"-i", {OptionValueType::STRING, OptionType::SINGLE}},
+ {"--keep-binaries-in-test-file", {OptionValueType::STRING, OptionType::MULTIPLE}},
+ {"-o", {OptionValueType::STRING, OptionType::SINGLE}},
+ {"--sample-time", {OptionValueType::STRING, OptionType::MULTIPLE}},
+ {"--symfs", {OptionValueType::STRING, OptionType::MULTIPLE}},
+ {"--unwind-sample", {OptionValueType::NONE, OptionType::SINGLE}},
+ };
+ OptionValueMap options;
+ std::vector<std::pair<OptionName, OptionValue>> ordered_options;
+ if (!PreprocessOptions(args, option_formats, &options, &ordered_options)) {
return false;
}
- // Write meta_info section.
- std::unordered_map<std::string, std::string> info_map;
- if (it != features.end() && it->first == PerfFileFormat::FEAT_META_INFO) {
- info_map = reader_->GetMetaInfoFeature();
- ++it;
+ generate_report_ = options.PullBoolValue("--generate-report");
+ generate_test_file_ = options.PullBoolValue("--generate-test-file");
+ options.PullStringValue("-i", &input_filename_);
+ for (auto& value : options.PullValues("--keep-binaries-in-test-file")) {
+ std::vector<std::string> binaries = android::base::Split(*value.str_value, ",");
+ kept_binaries_in_test_file_.insert(binaries.begin(), binaries.end());
}
- info_map["debug_unwind"] = "true";
- info_map["debug_unwind_mem_before"] = stat_.mem_before_unwinding.ToString();
- info_map["debug_unwind_mem_after"] = stat_.mem_after_unwinding.ToString();
- if (!writer_->WriteMetaInfoFeature(info_map)) {
- return false;
+ options.PullStringValue("-o", &output_filename_);
+ for (auto& value : options.PullValues("--sample-time")) {
+ auto times = ParseUintVector<uint64_t>(*value.str_value);
+ if (!times) {
+ return false;
+ }
+ sample_times_.insert(times.value().begin(), times.value().end());
}
- CHECK(it == features.end());
- return writer_->EndWriteFeatures() && writer_->Close();
-}
+ if (auto value = options.PullValue("--symfs"); value) {
+ if (!Dso::SetSymFsDir(*value->str_value)) {
+ return false;
+ }
+ }
+ unwind_sample_ = options.PullBoolValue("--unwind-sample");
+ CHECK(options.values.empty());
-void DebugUnwindCommand::PrintStat() {
- printf("Unwinding sample count: %" PRIu64 "\n", stat_.unwinding_sample_count);
- if (stat_.unwinding_sample_count > 0u) {
- printf("Average unwinding time: %f us\n", static_cast<double>(stat_.total_unwinding_time_in_ns)
- / 1000 / stat_.unwinding_sample_count);
- printf("Max unwinding time: %f us\n", static_cast<double>(stat_.max_unwinding_time_in_ns)
- / 1000);
+ if (generate_test_file_) {
+ if (output_filename_.empty()) {
+ LOG(ERROR) << "no output path for generated test file";
+ return false;
+ }
+ if (sample_times_.empty()) {
+ LOG(ERROR) << "no samples are selected via --sample-time";
+ return false;
+ }
}
- printf("Memory change:\n");
- PrintIndented(1, "VmPeak: %s -> %s\n", stat_.mem_before_unwinding.vm_peak.c_str(),
- stat_.mem_after_unwinding.vm_peak.c_str());
- PrintIndented(1, "VmSize: %s -> %s\n", stat_.mem_before_unwinding.vm_size.c_str(),
- stat_.mem_after_unwinding.vm_size.c_str());
- PrintIndented(1, "VmHWM: %s -> %s\n", stat_.mem_before_unwinding.vm_hwm.c_str(),
- stat_.mem_after_unwinding.vm_hwm.c_str());
- PrintIndented(1, "VmRSS: %s -> %s\n", stat_.mem_before_unwinding.vm_rss.c_str(),
- stat_.mem_after_unwinding.vm_rss.c_str());
- callchain_joiner_.DumpStat();
- printf("Please use debug_unwind_reporter.py to get a report in details.\n");
+
+ return true;
}
+} // namespace
+
void RegisterDebugUnwindCommand() {
RegisterCommand("debug-unwind",
- []{ return std::unique_ptr<Command>(new DebugUnwindCommand()); });
+ [] { return std::unique_ptr<Command>(new DebugUnwindCommand()); });
}
+
+} // namespace simpleperf