aboutsummaryrefslogtreecommitdiff
path: root/src/trace_processor/prelude/table_functions/experimental_annotated_stack.cc
blob: 8198376585d90eb638496a890d87c912633851fb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
/*
 * 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