| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| #include <unordered_map> |
| #include <vector> |
| |
| #include <android-base/file.h> |
| #include <android-base/logging.h> |
| #include <android-base/parseint.h> |
| #include <android-base/stringprintf.h> |
| #include <android-base/strings.h> |
| |
| #include "CallChainJoiner.h" |
| #include "command.h" |
| #include "environment.h" |
| #include "OfflineUnwinder.h" |
| #include "perf_regs.h" |
| #include "record_file.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; |
| |
| struct MemStat { |
| std::string vm_peak; |
| std::string vm_size; |
| std::string vm_hwm; |
| std::string vm_rss; |
| |
| std::string ToString() const { |
| return android::base::StringPrintf("VmPeak:%s;VmSize:%s;VmHWM:%s;VmRSS:%s", vm_peak.c_str(), |
| vm_size.c_str(), vm_hwm.c_str(), vm_rss.c_str()); |
| } |
| }; |
| |
| static bool GetMemStat(MemStat* stat) { |
| std::string s; |
| if (!android::base::ReadFileToString(android::base::StringPrintf("/proc/%d/status", getpid()), |
| &s)) { |
| PLOG(ERROR) << "Failed to read process status"; |
| return false; |
| } |
| std::vector<std::string> lines = android::base::Split(s, "\n"); |
| for (auto& line : lines) { |
| if (android::base::StartsWith(line, "VmPeak:")) { |
| stat->vm_peak = android::base::Trim(line.substr(strlen("VmPeak:"))); |
| } else if (android::base::StartsWith(line, "VmSize:")) { |
| stat->vm_size = android::base::Trim(line.substr(strlen("VmSize:"))); |
| } else if (android::base::StartsWith(line, "VmHWM:")) { |
| stat->vm_hwm = android::base::Trim(line.substr(strlen("VmHWM:"))); |
| } else if (android::base::StartsWith(line, "VmRSS:")) { |
| stat->vm_rss = android::base::Trim(line.substr(strlen("VmRSS:"))); |
| } |
| } |
| 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_(true), |
| callchain_joiner_(DEFAULT_CALL_CHAIN_JOINER_CACHE_SIZE, 1, true), |
| selected_time_(0) { |
| } |
| |
| bool Run(const std::vector<std::string>& args); |
| |
| 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; |
| }; |
| |
| std::string input_filename_; |
| std::string output_filename_; |
| std::unique_ptr<RecordFileReader> reader_; |
| std::unique_ptr<RecordFileWriter> writer_; |
| ThreadTree thread_tree_; |
| OfflineUnwinder offline_unwinder_; |
| CallChainJoiner callchain_joiner_; |
| Stat stat_; |
| uint64_t selected_time_; |
| }; |
| |
| bool DebugUnwindCommand::Run(const std::vector<std::string>& args) { |
| // 1. Parse options. |
| if (!ParseOptions(args)) { |
| return false; |
| } |
| ScopedTempFiles scoped_temp_files(android::base::Dirname(output_filename_)); |
| |
| // 2. Read input perf.data, and generate new perf.data. |
| if (!UnwindRecordFile()) { |
| return false; |
| } |
| |
| // 3. Show stat of unwinding. |
| PrintStat(); |
| return true; |
| } |
| |
| 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)) { |
| return false; |
| } |
| if (!Dso::SetSymFsDir(args[i])) { |
| return false; |
| } |
| } else if (args[i] == "--time") { |
| if (!GetUintOption(args, &i, &selected_time_)) { |
| return false; |
| } |
| } else { |
| ReportUnknownOption(args, i); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| 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; |
| } |
| 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; |
| } |
| |
| // 3. Process records in data section. |
| if (!GetMemStat(&stat_.mem_before_unwinding)) { |
| return false; |
| } |
| auto callback = [this](std::unique_ptr<Record> record) { |
| return ProcessRecord(record.get()); |
| }; |
| if (!reader_->ReadDataSection(callback)) { |
| return false; |
| } |
| if (!JoinCallChains()) { |
| return false; |
| } |
| if (!GetMemStat(&stat_.mem_after_unwinding)) { |
| return false; |
| } |
| |
| // 4. Write feature sections. |
| return WriteFeatureSections(); |
| } |
| |
| 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; |
| } |
| r.AdjustCallChainGeneratedByKernel(); |
| r.RemoveInvalidStackData(); |
| 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; |
| } |
| |
| 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; |
| } |
| r.ReplaceRegAndStackWithCallChain(ips); |
| if (!callchain_joiner_.AddCallChain(r.tid_data.pid, r.tid_data.tid, |
| CallChainJoiner::ORIGINAL_OFFLINE, ips, sps)) { |
| return false; |
| } |
| CollectHitFileInfo(r, ips); |
| } |
| } else { |
| thread_tree_.Update(*record); |
| } |
| 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(); |
| } |
| const Symbol* symbol = thread_tree_.FindSymbol(map, ip, nullptr, &dso); |
| if (!symbol->HasDumpId()) { |
| dso->CreateSymbolDumpId(symbol); |
| } |
| } |
| } |
| |
| 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; |
| } |
| writer_.reset(); |
| std::unique_ptr<TemporaryFile> tmp_file = ScopedTempFiles::CreateTempFile(); |
| if (!Workload::RunCmd({"mv", output_filename_, tmp_file->path})) { |
| return false; |
| } |
| |
| // 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; |
| } |
| writer_ = RecordFileWriter::CreateInstance(output_filename_); |
| if (!writer_ || !writer_->WriteAttrSection(reader->AttrSection())) { |
| return false; |
| } |
| |
| auto record_callback = [&](std::unique_ptr<Record> r) { |
| if (r->type() != PERF_RECORD_SAMPLE) { |
| return writer_->WriteRecord(*r); |
| } |
| SampleRecord& sr = *static_cast<SampleRecord*>(r.get()); |
| if (!sr.HasUserCallChain()) { |
| return writer_->WriteRecord(sr); |
| } |
| 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; |
| } |
| if (!writer_->WriteRecord(CallChainRecord(pid, tid, type, sr.Timestamp(), ips, sps))) { |
| return false; |
| } |
| } 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, false); |
| } |
| |
| 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++; |
| } |
| } |
| if (!writer_->BeginWriteFeatures(new_feature_count)) { |
| 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; |
| } |
| // Write a new file section. |
| if (it != features.end() && it->first == PerfFileFormat::FEAT_FILE) { |
| ++it; |
| } |
| if (!writer_->WriteFileFeatures(thread_tree_.GetAllDsos())) { |
| 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) { |
| if (!reader_->ReadMetaInfoFeature(&info_map)) { |
| return false; |
| } |
| ++it; |
| } |
| 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; |
| } |
| CHECK(it == features.end()); |
| return writer_->EndWriteFeatures() && writer_->Close(); |
| } |
| |
| 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); |
| } |
| 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"); |
| } |
| |
| void RegisterDebugUnwindCommand() { |
| RegisterCommand("debug-unwind", |
| []{ return std::unique_ptr<Command>(new DebugUnwindCommand()); }); |
| } |