| /* |
| * Copyright (C) 2021 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 "src/trace_processor/prelude/table_functions/experimental_annotated_stack.h" |
| |
| #include <optional> |
| |
| #include "perfetto/ext/base/string_utils.h" |
| #include "src/trace_processor/sqlite/sqlite_utils.h" |
| #include "src/trace_processor/storage/trace_storage.h" |
| #include "src/trace_processor/tables/profiler_tables.h" |
| #include "src/trace_processor/types/trace_processor_context.h" |
| |
| namespace perfetto { |
| namespace trace_processor { |
| namespace tables { |
| |
| #define PERFETTO_TP_ANNOTATED_CALLSTACK_TABLE_DEF(NAME, PARENT, C) \ |
| NAME(ExperimentalAnnotatedCallstackTable, \ |
| "experimental_annotated_callstack") \ |
| PARENT(PERFETTO_TP_STACK_PROFILE_CALLSITE_DEF, C) \ |
| C(StringId, annotation) \ |
| C(tables::StackProfileCallsiteTable::Id, start_id, Column::Flag::kHidden) |
| |
| PERFETTO_TP_TABLE(PERFETTO_TP_ANNOTATED_CALLSTACK_TABLE_DEF); |
| |
| ExperimentalAnnotatedCallstackTable::~ExperimentalAnnotatedCallstackTable() = |
| default; |
| |
| } // namespace tables |
| |
| namespace { |
| |
| enum class MapType { |
| kArtInterp, |
| kArtJit, |
| kArtAot, |
| kNativeLibart, |
| kNativeOther, |
| kOther |
| }; |
| |
| // Mapping examples: |
| // /system/lib64/libc.so |
| // /system/framework/framework.jar |
| // /memfd:jit-cache (deleted) |
| // /data/dalvik-cache/arm64/<snip>.apk@classes.dex |
| // /data/app/<snip>/base.apk!libmonochrome_64.so |
| // [vdso] |
| // TODO(rsavitski): consider moving this to a hidden column on |
| // stack_profile_mapping. |
| MapType ClassifyMap(NullTermStringView map) { |
| if (map.empty()) |
| return MapType::kOther; |
| |
| // Primary mapping where modern ART puts jitted code. |
| // The Zygote's JIT region is inherited by all descendant apps, so it can |
| // still appear in their callstacks. |
| if (map.StartsWith("/memfd:jit-cache") || |
| map.StartsWith("/memfd:jit-zygote-cache")) { |
| return MapType::kArtJit; |
| } |
| |
| size_t last_slash_pos = map.rfind('/'); |
| if (last_slash_pos != NullTermStringView::npos) { |
| base::StringView suffix = map.substr(last_slash_pos); |
| if (suffix.StartsWith("/libart.so") || suffix.StartsWith("/libartd.so")) |
| return MapType::kNativeLibart; |
| } |
| size_t extension_pos = map.rfind('.'); |
| if (extension_pos != NullTermStringView::npos) { |
| base::StringView suffix = map.substr(extension_pos); |
| if (suffix.StartsWith(".so")) |
| return MapType::kNativeOther; |
| // unqualified dex |
| if (suffix.StartsWith(".dex")) |
| return MapType::kArtInterp; |
| // dex with verification speedup info, produced by dex2oat |
| if (suffix.StartsWith(".vdex")) |
| return MapType::kArtInterp; |
| // possibly uncompressed dex in a jar archive |
| if (suffix.StartsWith(".jar")) |
| return MapType::kArtInterp; |
| // android package (zip file), this can contain uncompressed dexes or |
| // native libraries that are mmap'd directly into the process. We rely on |
| // libunwindstack's MapInfo::GetFullName, which suffixes the mapping with |
| // "!lib.so" if it knows that the referenced piece of the archive is an |
| // uncompressed ELF file. So an unadorned ".apk" is assumed to be a dex |
| // file. |
| if (suffix.StartsWith(".apk")) |
| return MapType::kArtInterp; |
| // ahead of time compiled ELFs |
| if (suffix.StartsWith(".oat")) |
| return MapType::kArtAot; |
| // older/alternative name for .oat |
| if (suffix.StartsWith(".odex")) |
| return MapType::kArtAot; |
| } |
| return MapType::kOther; |
| } |
| |
| } // namespace |
| |
| std::string ExperimentalAnnotatedStack::TableName() { |
| return tables::ExperimentalAnnotatedCallstackTable::Name(); |
| } |
| |
| Table::Schema ExperimentalAnnotatedStack::CreateSchema() { |
| return tables::ExperimentalAnnotatedCallstackTable::ComputeStaticSchema(); |
| } |
| |
| base::Status ExperimentalAnnotatedStack::ValidateConstraints( |
| const QueryConstraints& qc) { |
| const auto& cs = qc.constraints(); |
| int column = static_cast<int>( |
| tables::ExperimentalAnnotatedCallstackTable::ColumnIndex::start_id); |
| |
| auto id_fn = [column](const QueryConstraints::Constraint& c) { |
| return c.column == column && sqlite_utils::IsOpEq(c.op); |
| }; |
| bool has_id_cs = std::find_if(cs.begin(), cs.end(), id_fn) != cs.end(); |
| return has_id_cs ? base::OkStatus() |
| : base::ErrStatus("Failed to find required constraints"); |
| } |
| |
| // TODO(carlscab): Replace annotation logic with |
| // src/trace_processor/util/annotated_callsites.h |
| base::Status ExperimentalAnnotatedStack::ComputeTable( |
| const std::vector<Constraint>& cs, |
| const std::vector<Order>&, |
| const BitVector&, |
| std::unique_ptr<Table>& table_return) { |
| using CallsiteTable = tables::StackProfileCallsiteTable; |
| |
| const auto& cs_table = context_->storage->stack_profile_callsite_table(); |
| const auto& f_table = context_->storage->stack_profile_frame_table(); |
| const auto& m_table = context_->storage->stack_profile_mapping_table(); |
| |
| // Input (id of the callsite leaf) is the constraint on the hidden |start_id| |
| // column. |
| using ColumnIndex = tables::ExperimentalAnnotatedCallstackTable::ColumnIndex; |
| auto constraint_it = |
| std::find_if(cs.begin(), cs.end(), [](const Constraint& c) { |
| return c.col_idx == ColumnIndex::start_id && c.op == FilterOp::kEq; |
| }); |
| PERFETTO_DCHECK(constraint_it != cs.end()); |
| if (constraint_it == cs.end() || |
| constraint_it->value.type != SqlValue::Type::kLong) { |
| return base::ErrStatus("invalid input callsite id"); |
| } |
| |
| CallsiteId start_id = |
| CallsiteId(static_cast<uint32_t>(constraint_it->value.AsLong())); |
| auto opt_start_ref = cs_table.FindById(start_id); |
| if (!opt_start_ref) { |
| return base::ErrStatus("callsite with id %" PRIu32 " not found", |
| start_id.value); |
| } |
| |
| // Iteratively walk the parent_id chain to construct the list of callstack |
| // entries, each pointing at a frame. |
| std::vector<CallsiteTable::RowNumber> cs_rows; |
| cs_rows.push_back(opt_start_ref->ToRowNumber()); |
| std::optional<CallsiteId> maybe_parent_id = opt_start_ref->parent_id(); |
| while (maybe_parent_id) { |
| auto parent_ref = *cs_table.FindById(*maybe_parent_id); |
| cs_rows.push_back(parent_ref.ToRowNumber()); |
| maybe_parent_id = parent_ref.parent_id(); |
| } |
| |
| // Walk the callsites root-to-leaf, annotating: |
| // * managed frames with their execution state (interpreted/jit/aot) |
| // * common ART frames, which are usually not relevant to |
| // visualisation/inspection |
| // |
| // This is not a per-frame decision, because we do not want to filter out ART |
| // frames immediately after a JNI transition (such frames are often relevant). |
| // |
| // As a consequence of the logic being based on a root-to-leaf walk, a given |
| // callsite will always have the same annotation, as the parent path is always |
| // the same, and children callsites do not affect their parents' annotations. |
| StringId art_jni_trampoline = |
| context_->storage->InternString("art_jni_trampoline"); |
| |
| StringId common_frame = context_->storage->InternString("common-frame"); |
| StringId common_frame_interp = |
| context_->storage->InternString("common-frame-interp"); |
| StringId art_interp = context_->storage->InternString("interp"); |
| StringId art_jit = context_->storage->InternString("jit"); |
| StringId art_aot = context_->storage->InternString("aot"); |
| |
| // Annotation FSM states: |
| // * kInitial: default, native-only callstacks never leave this state. |
| // * kEraseLibart: we've seen a managed frame, and will now "erase" (i.e. tag |
| // as a common-frame) frames belonging to the ART runtime. |
| // * kKeepNext: we've seen a special JNI trampoline for managed->native |
| // transition, keep the immediate child (even if it is in ART), |
| // and then go back to kEraseLibart. |
| // Regardless of the state, managed frames get annotated with their execution |
| // mode, based on the mapping. |
| enum class State { kInitial, kEraseLibart, kKeepNext }; |
| State annotation_state = State::kInitial; |
| |
| std::vector<StringPool::Id> annotations_reversed; |
| for (auto it = cs_rows.rbegin(); it != cs_rows.rend(); ++it) { |
| auto cs_ref = it->ToRowReference(cs_table); |
| auto frame_ref = *f_table.FindById(cs_ref.frame_id()); |
| auto map_ref = *m_table.FindById(frame_ref.mapping()); |
| |
| // Keep immediate callee of a JNI trampoline, but keep tagging all |
| // successive libart frames as common. |
| if (annotation_state == State::kKeepNext) { |
| annotations_reversed.push_back(kNullStringId); |
| annotation_state = State::kEraseLibart; |
| continue; |
| } |
| |
| // Special-case "art_jni_trampoline" frames, keeping their immediate callee |
| // even if it is in libart, as it could be a native implementation of a |
| // managed method. Example for "java.lang.reflect.Method.Invoke": |
| // art_jni_trampoline |
| // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*) |
| // |
| // Simpleperf also relies on this frame name, so it should be fairly stable. |
| // TODO(rsavitski): consider detecting standard JNI upcall entrypoints - |
| // _JNIEnv::Call*. These are sometimes inlined into other DSOs, so erasing |
| // only the libart frames does not clean up all of the JNI-related frames. |
| StringId fname_id = frame_ref.name(); |
| if (fname_id == art_jni_trampoline) { |
| annotations_reversed.push_back(common_frame); |
| annotation_state = State::kKeepNext; |
| continue; |
| } |
| |
| NullTermStringView map_view = context_->storage->GetString(map_ref.name()); |
| MapType map_type = ClassifyMap(map_view); |
| |
| // Annotate managed frames. |
| if (map_type == MapType::kArtInterp || // |
| map_type == MapType::kArtJit || // |
| map_type == MapType::kArtAot) { |
| if (map_type == MapType::kArtInterp) |
| annotations_reversed.push_back(art_interp); |
| else if (map_type == MapType::kArtJit) |
| annotations_reversed.push_back(art_jit); |
| else if (map_type == MapType::kArtAot) |
| annotations_reversed.push_back(art_aot); |
| |
| // Now know to be in a managed callstack - erase subsequent ART frames. |
| if (annotation_state == State::kInitial) |
| annotation_state = State::kEraseLibart; |
| continue; |
| } |
| |
| // Mixed callstack, tag libart frames as uninteresting (common-frame). |
| // Special case a subset of interpreter implementation frames as |
| // "common-frame-interp" using frame name prefixes. Those functions are |
| // actually executed, whereas the managed "interp" frames are synthesised as |
| // their caller by the unwinding library (based on the dex_pc virtual |
| // register restored using the libart's DWARF info). The heuristic covers |
| // the "nterp" and "switch" interpreter implementations. |
| // |
| // Example: |
| // <towards root> |
| // android.view.WindowLayout.computeFrames [interp] |
| // nterp_op_iget_object_slow_path [common-frame-interp] |
| // |
| // This annotation is helpful when trying to answer "what mode was the |
| // process in?" based on the leaf frame of the callstack. As we want to |
| // classify such cases as interpreted, even though the leaf frame is |
| // libart.so. |
| // |
| // For "switch" interpreter, we match any frame starting with |
| // "art::interpreter::" according to itanium mangling. |
| if (annotation_state == State::kEraseLibart && |
| map_type == MapType::kNativeLibart) { |
| NullTermStringView fname = context_->storage->GetString(fname_id); |
| if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") || |
| fname.StartsWith("ExecuteNterp") || |
| fname.StartsWith("ExecuteSwitchImpl") || |
| fname.StartsWith("_ZN3art11interpreter")) { |
| annotations_reversed.push_back(common_frame_interp); |
| continue; |
| } |
| annotations_reversed.push_back(common_frame); |
| continue; |
| } |
| |
| // default - no special annotation |
| annotations_reversed.push_back(kNullStringId); |
| } |
| |
| // Build the dynamic table. |
| PERFETTO_DCHECK(cs_rows.size() == annotations_reversed.size()); |
| ColumnStorage<StringPool::Id> annotation_vals; |
| for (auto it = annotations_reversed.rbegin(); |
| it != annotations_reversed.rend(); ++it) { |
| annotation_vals.Append(*it); |
| } |
| |
| // Hidden column - always the input, i.e. the callsite leaf. |
| ColumnStorage<uint32_t> start_id_vals; |
| for (uint32_t i = 0; i < cs_rows.size(); i++) |
| start_id_vals.Append(start_id.value); |
| |
| table_return = |
| tables::ExperimentalAnnotatedCallstackTable::SelectAndExtendParent( |
| cs_table, std::move(cs_rows), std::move(annotation_vals), |
| std::move(start_id_vals)); |
| return base::OkStatus(); |
| } |
| |
| uint32_t ExperimentalAnnotatedStack::EstimateRowCount() { |
| return 1; |
| } |
| |
| } // namespace trace_processor |
| } // namespace perfetto |