• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 #ifndef SRC_TRACE_PROCESSOR_DB_VIEW_H_
18 #define SRC_TRACE_PROCESSOR_DB_VIEW_H_
19 
20 #include <stdint.h>
21 
22 #include <iterator>
23 #include <memory>
24 #include <numeric>
25 #include <optional>
26 #include <string>
27 #include <vector>
28 
29 #include "perfetto/base/logging.h"
30 #include "perfetto/base/status.h"
31 #include "perfetto/ext/base/flat_hash_map.h"
32 #include "perfetto/ext/base/small_vector.h"
33 #include "perfetto/trace_processor/iterator.h"
34 #include "src/trace_processor/containers/bit_vector.h"
35 #include "src/trace_processor/containers/row_map.h"
36 #include "src/trace_processor/containers/string_pool.h"
37 #include "src/trace_processor/db/table.h"
38 
39 namespace perfetto {
40 namespace trace_processor {
41 
42 // Implementation of a "SQL view" on top of trace processor
43 // columnar tables.
44 //
45 // Supported operations are:
46 // 1) joining tables together by id
47 // 2) exporting columns with different names
48 //
49 // Note: unlike traditional SQL views, this class does *not*
50 // allow arbitrary joins. Instead, it only supports joins between
51 // tables on ids and only supports a single constraint per table.
52 //
53 // Concretely, suppose you have two tables A and B with A having
54 // a column named b_id containing references to rows in B. This
55 // class allows defining a view AB which contains the rows of A,
56 // transparently extended with the columns from B.
57 //
58 // We implement this specially in trace processor instead of doing
59 // this in SQL for a few reasons:
60 //  1) The views we write using this class are used in highly
61 //     performance sensitive contexts so avoiding the "virtual table"
62 //     overhead from SQLite makes a meaningful difference.
63 //  2) In trace processor, we have more knowledge of the semantics
64 //     of tables (i.e. ids are unique, sorted and non-null). While
65 //     we can expose knowledge of some of this context of SQLite,
66 //     it will never do as good a job of ordering joins as we can
67 //     do ourselves.
68 //  3) By looking at which columns are used, we can potentially skip
69 //     filtering/sorting some tables in the join which can massively
70 //     speed up queries. Because SQLite lacks the semantic knowledge
71 //     (see 2), it refuses to skip any join as rows could potentially
72 //     be filtered out (even though, we know they wouldn't be).
73 //
74 // Design doc: go/perfetto-cpp-views
75 class View {
76  public:
77   // Bitflags which can be set to modify how joins between tables are
78   // performed. Multiple flags can be set by bitwise-oring them together.
79   enum JoinFlag : uint32_t {
80     // Flag to be specified if the join has no special properties. That is
81     // the join is standard inner join.
82     kNoFlag = 0,
83 
84     // Indicates that the right hand side of the join is for a column containing
85     // strongly typed ids but the left side only contains serialized uint32_t.
86     // This means both sides will be type-checked based on serialized types
87     // rather than actual column types.
88     //
89     // This flag is not utilized by this class but any wrapping logic (e.g. the
90     // view macros) can make use of this to have very strict type checking
91     // except where this flag is specified.
92     //
93     // The motivation for this flag comes from thread/process table where we
94     // use uint32_t as ids for these tables: this is because this was standard
95     // convention before typechecked tables which we didn't change because it
96     // was a) too much effort to change b) made the code messier (as UniqueTid
97     // and UniquePid are used as indices into vectors in several places in trace
98     // processor).
99     kTypeCheckSerialized = 1 << 0,
100 
101     // Indicates that the right table's id column will contain every potential
102     // id which can appear in the left table.
103     //
104     // As a rule of thumb, this flag can be specified whenever the right table
105     // is a "root" table; it's possible that there are other cases but this
106     // would need case-by-case consideration.
107     kIdAlwaysPresent = 1 << 0,
108   };
109 
110   // References a new table which should be introduced into the view by joining
111   // it with an existing table.
112   //
113   // Note that all |const char*| varaibles below should be long lived string
114   // literals (generally coming from macro defintions).
115   struct JoinTable {
116     // The table which is being joined into this view. This table will be
117     // on the RHS of the join.
118     Table* table;
119 
120     // The name of this table; only used to allow referencing this table in
121     // later |Join| structs.
122     const char* table_name;
123 
124     // The name of the id column |table|.
125     // Note: in practice this will always be "id" but we allow specifiying it
126     // to allow generality.
127     const char* col;
128 
129     // A table previously introduced into this view which will act as the LHS of
130     // the join.
131     const char* prev_table_name;
132 
133     // The name of the column in the table given by |prev_table_name| which will
134     // contain ids for |table|.
135     const char* prev_col;
136 
137     // Set of bitwise-ORed flags modifying how the join should be perfomed. See
138     // |JoinFlag| enum for potential flags.
139     uint32_t join_flags;
140   };
141 
142   // Stores information about an output column for this view.
143   struct OutputColumn {
144     // The name of the column being exposed.
145     const char* col_name;
146 
147     // The name of the source table this column comes from.
148     const char* source_table_name;
149 
150     // The name of the column in the source table.
151     const char* source_col_name;
152   };
153 
154   View();
155 
156   View(const View&) noexcept = delete;
157   View& operator=(const View&) = delete;
158 
159   View(View&&) noexcept = default;
160   View& operator=(View&&) = default;
161 
162   virtual ~View();
163 
164   // Creates a new View from the given parameters.
165   base::Status Create(Table* root_table,
166                       const char* root_table_name,
167                       std::initializer_list<JoinTable> joins,
168                       std::initializer_list<OutputColumn> columns,
169                       View* view);
170 
171   Table Query(const std::vector<Constraint>& cs,
172               const std::vector<Order>& ob,
173               const BitVector& cols_used) const;
174 
175   uint32_t GetColumnCount() const;
176   uint32_t EstimateRowCount() const;
177 
schema()178   const Table::Schema& schema() const { return schema_; }
179 
180  protected:
181   // Constructor variant of Create, exposed for subclasses; any errors will
182   // simply be PERFETTO_FATAL-ed.
183   View(Table* root_table,
184        const char* root_table_name,
185        std::initializer_list<JoinTable> joins,
186        std::initializer_list<OutputColumn> columns);
187 
188  private:
189   // The tables participating in a view laid out in a tree structure.
190   //
191   // The parent represents the LHS of the join with each child being a separate
192   // table being joined on the RHS of the join. This structure allows enforces,
193   // at the type-system level, that each joined table has preceisely one join
194   // condition.
195   //
196   // Note, however, that the same table pointer *can* appear multiple times in
197   // different parts of the tree but only when the "name" of the table is also
198   // different (by having a different name we can disambiguate which column we
199   // need to choose when constructing the final output table).
200   struct TableNode {
201     /// The table for the root of this tree.
202     //
203     // For all except the root node, this table will always be on the right side
204     // of the join for its parent and the left side of the join for any nodes in
205     // |children|.
206     Table* table;
207 
208     // The index of the id column in |table|.
209     // In practice, this will always be zero (as id columns are implicitly the
210     // first column) but having this allows flexibility for the future.
211     std::optional<uint32_t> join_col_idx;
212 
213     // The index of the column in the parent table which is selecting the rows
214     // in |table|.
215     std::optional<uint32_t> parent_join_col_idx;
216 
217     // Set of bitwise-ORed flags modifying how the join should be perfomed. See
218     // |JoinFlag| struct for potential flags.
219     uint32_t join_flags;
220 
221     // The child tables participating in the join.
222     using Children = base::SmallVector<std::unique_ptr<TableNode>, 4>;
223     Children children;
224   };
225   using SourceColumn = std::pair<const TableNode*, uint32_t /* column_idx */>;
226 
227   // Helper class for performing the join algorithm.
228   //
229   // This is useful to split up the algorithm into functions without having to
230   // constantly pass the state data structures between functions.
231   class QueryHelper {
232    public:
233     // Contains transient state about a single table which is used while
234     // querying a view.
235     struct NodeState {
236       // "Input" parameters.
237       // The following are set by BuildNodeStateMap and used in the other
238       // functions.
239 
240       // The set of filter constraints on this table.
241       std::vector<Constraint> cs;
242 
243       // Whether any column from this table is used by SQLite or if this table
244       // is an ancestor of such a table.
245       bool is_used;
246 
247       // Whether joining this table with its parent can cause rows to be removed
248       // from the parent. This is true either if:
249       // 1) this table is filtered (i.e. |cs| is not empty).
250       // 2) this table does not have every id (i.e. it's not a root table)
251       // 3) this table is an ancestor of a table which |removes_parent_rows|.
252       bool removes_parent_rows;
253 
254       // "Output" parameters.
255       // These are modified throughout every function and will be incrementally
256       // refined to the final until used to build the output table in
257       // |BuildTable|.
258 
259       // The current output table. At the end of |FilterAndJoinRecursive|, this
260       // contains the table
261       Table output;
262 
263       // The current RowMap which needs to be applied to |output| to accurately
264       // join with the parent. Built by |FilterAndJoinRecursive| and applied
265       // recursively downwards in |ApplyRowMapRecursive|
266       RowMap parent_join_rm;
267     };
268     using NodeStateMap = base::FlatHashMap<const TableNode*, NodeState>;
269 
270     QueryHelper(TableNode* root_node,
271                 const std::vector<SourceColumn>&,
272                 const std::vector<Constraint>&,
273                 const BitVector& cols_used);
274 
275     // See definition of View::Query for information about these functions.
276     static NodeStateMap BuildNodeStateMap(TableNode* root_node,
277                                           const std::vector<SourceColumn>&,
278                                           const std::vector<Constraint>&,
279                                           const BitVector& cols_used);
280     void FilterAndJoinRecursive(TableNode* node);
ApplyRowMapRecursive(TableNode * root)281     void ApplyRowMapRecursive(TableNode* root) {
282       // To avoid the root node parent_rm emptying it, create a RowMap which
283       // will simply select all the rows.
284       auto& root_state = *state_.Find(root);
285       return ApplyRowMapRecursive(root,
286                                   RowMap(0, root_state.output.row_count()));
287     }
288     Table BuildTable(TableNode* root,
289                      const Table::Schema& schema,
290                      const std::vector<SourceColumn>&,
291                      const BitVector& cols_used);
292 
293    private:
294     void ApplyRowMapRecursive(TableNode* node, RowMap rm);
295 
296     NodeStateMap state_;
297   };
298 
299   View(std::unique_ptr<TableNode> root,
300        std::vector<SourceColumn>,
301        Table::Schema schema);
302 
303   // Implements a post-order DFS on the |TableNode| struct. Useful for
304   // compactly writing a tree traversal with a focus on what's happening.
305   template <typename Fn>
PostOrderDfs(TableNode * node,Fn fn)306   static void PostOrderDfs(TableNode* node, Fn fn) {
307     for (const auto& child : node->children) {
308       PostOrderDfs(child.get(), fn);
309     }
310     fn(node);
311   }
312 
313   std::unique_ptr<TableNode> root_node_;
314   std::vector<SourceColumn> source_col_by_output_idx_;
315   Table::Schema schema_;
316 };
317 
318 }  // namespace trace_processor
319 }  // namespace perfetto
320 
321 #endif  // SRC_TRACE_PROCESSOR_DB_VIEW_H_
322