1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "src/trace_processor/dynamic/experimental_annotated_stack_generator.h"
18
19 #include "perfetto/ext/base/optional.h"
20 #include "src/trace_processor/storage/trace_storage.h"
21 #include "src/trace_processor/tables/profiler_tables.h"
22 #include "src/trace_processor/types/trace_processor_context.h"
23
24 #include "perfetto/ext/base/string_utils.h"
25
26 namespace perfetto {
27 namespace trace_processor {
28
29 namespace {
30
31 enum class MapType {
32 kArtInterp,
33 kArtJit,
34 kArtAot,
35 kNativeLibart,
36 kNativeOther,
37 kOther
38 };
39
40 // Mapping examples:
41 // /system/lib64/libc.so
42 // /system/framework/framework.jar
43 // /memfd:jit-cache (deleted)
44 // [vdso]
45 // TODO(rsavitski): consider moving this to a hidden column on
46 // stack_profile_mapping, once this logic is sufficiently stable.
ClassifyMap(NullTermStringView map)47 MapType ClassifyMap(NullTermStringView map) {
48 if (map.empty())
49 return MapType::kOther;
50
51 // Primary mapping where modern ART puts jitted code.
52 // TODO(rsavitski): look into /memfd:jit-zygote-cache.
53 if (!strncmp(map.c_str(), "/memfd:jit-cache", 16))
54 return MapType::kArtJit;
55
56 size_t last_slash_pos = map.rfind('/');
57 if (last_slash_pos != NullTermStringView::npos) {
58 if (!strncmp(map.c_str() + last_slash_pos, "/libart.so", 10))
59 return MapType::kNativeLibart;
60 if (!strncmp(map.c_str() + last_slash_pos, "/libartd.so", 11))
61 return MapType::kNativeLibart;
62 }
63
64 size_t extension_pos = map.rfind('.');
65 if (extension_pos != NullTermStringView::npos) {
66 if (!strncmp(map.c_str() + extension_pos, ".so", 3))
67 return MapType::kNativeOther;
68 // dex with verification speedup info, produced by dex2oat
69 if (!strncmp(map.c_str() + extension_pos, ".vdex", 5))
70 return MapType::kArtInterp;
71 // possibly uncompressed dex in a jar archive
72 if (!strncmp(map.c_str() + extension_pos, ".jar", 4))
73 return MapType::kArtInterp;
74 // ahead of time compiled ELFs
75 if (!strncmp(map.c_str() + extension_pos, ".oat", 4))
76 return MapType::kArtAot;
77 // older/alternative name for .oat
78 if (!strncmp(map.c_str() + extension_pos, ".odex", 5))
79 return MapType::kArtAot;
80 }
81 return MapType::kOther;
82 }
83
GetConstraintColumnIndex(TraceProcessorContext * context)84 uint32_t GetConstraintColumnIndex(TraceProcessorContext* context) {
85 // The dynamic table adds two columns on top of the callsite table. Last
86 // column is the hidden constrain (i.e. input arg) column.
87 return context->storage->stack_profile_callsite_table().GetColumnCount() + 1;
88 }
89
90 } // namespace
91
TableName()92 std::string ExperimentalAnnotatedStackGenerator::TableName() {
93 return "experimental_annotated_callstack";
94 }
95
CreateSchema()96 Table::Schema ExperimentalAnnotatedStackGenerator::CreateSchema() {
97 auto schema = tables::StackProfileCallsiteTable::Schema();
98 schema.columns.push_back(Table::Schema::Column{
99 "annotation", SqlValue::Type::kString, /* is_id = */ false,
100 /* is_sorted = */ false, /* is_hidden = */ false});
101 schema.columns.push_back(Table::Schema::Column{
102 "start_id", SqlValue::Type::kLong, /* is_id = */ false,
103 /* is_sorted = */ false, /* is_hidden = */ true});
104 return schema;
105 }
106
ValidateConstraints(const QueryConstraints & qc)107 util::Status ExperimentalAnnotatedStackGenerator::ValidateConstraints(
108 const QueryConstraints& qc) {
109 const auto& cs = qc.constraints();
110 int column = static_cast<int>(GetConstraintColumnIndex(context_));
111
112 auto id_fn = [column](const QueryConstraints::Constraint& c) {
113 return c.column == column && c.op == SQLITE_INDEX_CONSTRAINT_EQ;
114 };
115 bool has_id_cs = std::find_if(cs.begin(), cs.end(), id_fn) != cs.end();
116 return has_id_cs ? util::OkStatus()
117 : util::ErrStatus("Failed to find required constraints");
118 }
119
ComputeTable(const std::vector<Constraint> & cs,const std::vector<Order> &)120 std::unique_ptr<Table> ExperimentalAnnotatedStackGenerator::ComputeTable(
121 const std::vector<Constraint>& cs,
122 const std::vector<Order>&) {
123 const auto& cs_table = context_->storage->stack_profile_callsite_table();
124 const auto& f_table = context_->storage->stack_profile_frame_table();
125 const auto& m_table = context_->storage->stack_profile_mapping_table();
126
127 // Input (id of the callsite leaf) is the constraint on the hidden |start_id|
128 // column.
129 uint32_t constraint_col = GetConstraintColumnIndex(context_);
130 auto constraint_it =
131 std::find_if(cs.begin(), cs.end(), [constraint_col](const Constraint& c) {
132 return c.col_idx == constraint_col && c.op == FilterOp::kEq;
133 });
134 PERFETTO_DCHECK(constraint_it != cs.end());
135
136 auto start_id = static_cast<uint32_t>(constraint_it->value.AsLong());
137 base::Optional<uint32_t> start_row =
138 cs_table.id().IndexOf(CallsiteId(start_id));
139 if (!start_row)
140 return nullptr;
141
142 // Iteratively walk the parent_id chain to construct the list of callstack
143 // entries, each pointing at a frame.
144 std::vector<uint32_t> cs_rows;
145 cs_rows.push_back(*start_row);
146 base::Optional<CallsiteId> maybe_parent_id = cs_table.parent_id()[*start_row];
147 while (maybe_parent_id) {
148 uint32_t parent_row = cs_table.id().IndexOf(*maybe_parent_id).value();
149 cs_rows.push_back(parent_row);
150 maybe_parent_id = cs_table.parent_id()[parent_row];
151 }
152
153 // Walk the callsites root-to-leaf, annotating:
154 // * managed frames with their execution state (interpreted/jit/aot)
155 // * common ART frames, which are usually not relevant
156 //
157 // This is not a per-frame decision, because we do not want to filter out ART
158 // frames immediately after a JNI transition (such frames are often relevant).
159 //
160 // As a consequence of the logic being based on a root-to-leaf walk, a given
161 // callsite will always have the same annotation, as the parent path is always
162 // the same, and children callsites do not affect their parents' annotations.
163 //
164 // This could also be implemented as a hidden column on the callsite table
165 // (populated at import time), but we want to be more flexible for now.
166 StringId art_jni_trampoline =
167 context_->storage->InternString("art_jni_trampoline");
168
169 StringId common_frame = context_->storage->InternString("common-frame");
170 StringId art_interp = context_->storage->InternString("interp");
171 StringId art_jit = context_->storage->InternString("jit");
172 StringId art_aot = context_->storage->InternString("aot");
173
174 // Annotation FSM states:
175 // * kInitial: default, native-only callstacks never leave this state.
176 // * kEraseLibart: we've seen a managed frame, and will now "erase" (i.e. tag
177 // as a common-frame) frames belonging to the ART runtime.
178 // * kKeepNext: we've seen a special JNI trampoline for managed->native
179 // transition, keep the immediate child (even if it is in ART),
180 // and then go back to kEraseLibart.
181 // Regardless of the state, managed frames get annotated with their execution
182 // mode, based on the mapping.
183 enum class State { kInitial, kEraseLibart, kKeepNext };
184 State annotation_state = State::kInitial;
185
186 std::vector<StringPool::Id> annotations_reversed;
187 for (auto it = cs_rows.rbegin(); it != cs_rows.rend(); ++it) {
188 FrameId frame_id = cs_table.frame_id()[*it];
189 uint32_t frame_row = f_table.id().IndexOf(frame_id).value();
190
191 MappingId map_id = f_table.mapping()[frame_row];
192 uint32_t map_row = m_table.id().IndexOf(map_id).value();
193
194 // Keep immediate callee of a JNI trampoline, but keep tagging all
195 // successive libart frames as common.
196 if (annotation_state == State::kKeepNext) {
197 annotations_reversed.push_back(kNullStringId);
198 annotation_state = State::kEraseLibart;
199 continue;
200 }
201
202 // Special-case "art_jni_trampoline" frames, keeping their immediate callee
203 // even if it is in libart, as it could be a native implementation of a
204 // managed method. Example for "java.lang.reflect.Method.Invoke":
205 // art_jni_trampoline
206 // art::Method_invoke(_JNIEnv*, _jobject*, _jobject*, _jobjectArray*)
207 //
208 // Simpleperf also relies on this frame name, so it should be fairly stable.
209 // TODO(rsavitski): consider detecting standard JNI upcall entrypoints -
210 // _JNIEnv::Call*. These are sometimes inlined into other DSOs, so erasing
211 // only the libart frames does not clean up all of the JNI-related frames.
212 StringId fname_id = f_table.name()[frame_row];
213 if (fname_id == art_jni_trampoline) {
214 annotations_reversed.push_back(common_frame);
215 annotation_state = State::kKeepNext;
216 continue;
217 }
218
219 NullTermStringView map_view =
220 context_->storage->GetString(m_table.name()[map_row]);
221 MapType map_type = ClassifyMap(map_view);
222
223 // Annotate managed frames.
224 if (map_type == MapType::kArtInterp || //
225 map_type == MapType::kArtJit || //
226 map_type == MapType::kArtAot) {
227 if (map_type == MapType::kArtInterp)
228 annotations_reversed.push_back(art_interp);
229 else if (map_type == MapType::kArtJit)
230 annotations_reversed.push_back(art_jit);
231 else if (map_type == MapType::kArtAot)
232 annotations_reversed.push_back(art_aot);
233
234 // Now know to be in a managed callstack - erase subsequent ART frames.
235 if (annotation_state == State::kInitial)
236 annotation_state = State::kEraseLibart;
237 continue;
238 }
239
240 if (annotation_state == State::kEraseLibart &&
241 map_type == MapType::kNativeLibart) {
242 annotations_reversed.push_back(common_frame);
243 continue;
244 }
245
246 annotations_reversed.push_back(kNullStringId);
247 }
248
249 // Build the dynamic table.
250 auto base_rowmap = RowMap(std::move(cs_rows));
251
252 PERFETTO_DCHECK(base_rowmap.size() == annotations_reversed.size());
253 std::unique_ptr<NullableVector<StringPool::Id>> annotation_vals(
254 new NullableVector<StringPool::Id>());
255 for (auto it = annotations_reversed.rbegin();
256 it != annotations_reversed.rend(); ++it) {
257 annotation_vals->Append(*it);
258 }
259
260 // Hidden column - always the input, i.e. the callsite leaf.
261 std::unique_ptr<NullableVector<uint32_t>> start_id_vals(
262 new NullableVector<uint32_t>());
263 for (uint32_t i = 0; i < base_rowmap.size(); i++)
264 start_id_vals->Append(start_id);
265
266 return std::unique_ptr<Table>(new Table(
267 cs_table.Apply(std::move(base_rowmap))
268 .ExtendWithColumn("annotation", std::move(annotation_vals),
269 TypedColumn<StringPool::Id>::default_flags())
270 .ExtendWithColumn("start_id", std::move(start_id_vals),
271 TypedColumn<uint32_t>::default_flags() |
272 TypedColumn<uint32_t>::kHidden)));
273 }
274
EstimateRowCount()275 uint32_t ExperimentalAnnotatedStackGenerator::EstimateRowCount() {
276 return 1;
277 }
278
279 } // namespace trace_processor
280 } // namespace perfetto
281