blob: b53e3cfe63b55b634c3337cc0f235e25a0addf26 [file] [log] [blame]
/*
* 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