diff options
Diffstat (limited to 'simpleperf/cmd_debug_unwind.cpp')
-rw-r--r-- | simpleperf/cmd_debug_unwind.cpp | 859 |
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 |