| /* |
| * 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/util/annotated_callsites.h" |
| |
| #include <iostream> |
| #include <optional> |
| |
| #include "src/trace_processor/tables/profiler_tables_py.h" |
| #include "src/trace_processor/types/trace_processor_context.h" |
| |
| namespace perfetto { |
| namespace trace_processor { |
| |
| AnnotatedCallsites::AnnotatedCallsites(const TraceProcessorContext* context) |
| : context_(*context), |
| // String to identify trampoline frames. If the string does not exist in |
| // TraceProcessor's StringPool (nullopt) then there will be no trampoline |
| // frames in the trace so there is no point in adding it to the pool to do |
| // all comparisons, instead we initialize the member to std::nullopt and |
| // the string comparisons will all fail. |
| art_jni_trampoline_( |
| context->storage->string_pool().GetId("art_jni_trampoline")) {} |
| |
| AnnotatedCallsites::State AnnotatedCallsites::GetState( |
| std::optional<CallsiteId> id) { |
| if (!id) { |
| return State::kInitial; |
| } |
| auto it = states_.find(*id); |
| if (it != states_.end()) { |
| return it->second; |
| } |
| |
| State state = |
| Get(*context_.storage->stack_profile_callsite_table().FindById(*id)) |
| .first; |
| states_.emplace(*id, state); |
| return state; |
| } |
| |
| std::pair<AnnotatedCallsites::State, CallsiteAnnotation> |
| AnnotatedCallsites::Get( |
| const tables::StackProfileCallsiteTable::ConstRowReference& callsite) { |
| State state = GetState(callsite.parent_id()); |
| |
| // Keep immediate callee of a JNI trampoline, but keep tagging all |
| // successive libart frames as common. |
| if (state == State::kKeepNext) { |
| return {State::kEraseLibart, CallsiteAnnotation::kNone}; |
| } |
| |
| // 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. |
| auto frame = *context_.storage->stack_profile_frame_table().FindById( |
| callsite.frame_id()); |
| // art_jni_trampoline_ could be std::nullopt if the string does not exist in |
| // the StringPool, but that also means no frame will ever have that name. |
| if (art_jni_trampoline_.has_value() && |
| frame.name() == art_jni_trampoline_.value()) { |
| return {State::kKeepNext, CallsiteAnnotation::kCommonFrame}; |
| } |
| |
| MapType map_type = GetMapType(frame.mapping()); |
| |
| // Annotate managed frames. |
| if (map_type == MapType::kArtInterp || // |
| map_type == MapType::kArtJit || // |
| map_type == MapType::kArtAot) { |
| // Now know to be in a managed callstack - erase subsequent ART frames. |
| if (state == State::kInitial) { |
| state = State::kEraseLibart; |
| } |
| |
| if (map_type == MapType::kArtInterp) |
| return {state, CallsiteAnnotation::kArtInterpreted}; |
| if (map_type == MapType::kArtJit) |
| return {state, CallsiteAnnotation::kArtJit}; |
| if (map_type == MapType::kArtAot) |
| return {state, CallsiteAnnotation::kArtAot}; |
| } |
| |
| // 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 (state == State::kEraseLibart && map_type == MapType::kNativeLibart) { |
| NullTermStringView fname = context_.storage->GetString(frame.name()); |
| if (fname.StartsWith("nterp_") || fname.StartsWith("Nterp") || |
| fname.StartsWith("ExecuteNterp") || |
| fname.StartsWith("ExecuteSwitchImpl") || |
| fname.StartsWith("_ZN3art11interpreter")) { |
| return {state, CallsiteAnnotation::kCommonFrameInterp}; |
| } |
| return {state, CallsiteAnnotation::kCommonFrame}; |
| } |
| |
| return {state, CallsiteAnnotation::kNone}; |
| } |
| |
| AnnotatedCallsites::MapType AnnotatedCallsites::GetMapType(MappingId id) { |
| auto it = map_types_.find(id); |
| if (it != map_types_.end()) { |
| return it->second; |
| } |
| |
| return map_types_ |
| .emplace(id, ClassifyMap(context_.storage->GetString( |
| context_.storage->stack_profile_mapping_table() |
| .FindById(id) |
| ->name()))) |
| .first->second; |
| } |
| |
| AnnotatedCallsites::MapType AnnotatedCallsites::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 trace_processor |
| } // namespace perfetto |