blob: 736af92d14efbfa9d7ce371fd7c03e70727a2a65 [file] [log] [blame]
/*
* Copyright (C) 2019 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/sqlite/db_sqlite_table.h"
#include <sqlite3.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <memory>
#include <numeric>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "perfetto/base/compiler.h"
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/small_vector.h"
#include "perfetto/ext/base/status_or.h"
#include "perfetto/ext/base/string_splitter.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/public/compiler.h"
#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/containers/row_map.h"
#include "src/trace_processor/db/column/types.h"
#include "src/trace_processor/db/runtime_table.h"
#include "src/trace_processor/db/table.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/static_table_function.h"
#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
#include "src/trace_processor/sqlite/sqlite_utils.h"
#include "src/trace_processor/tp_metatrace.h"
#include "src/trace_processor/util/regex.h"
#include "protos/perfetto/trace_processor/metatrace_categories.pbzero.h"
namespace perfetto::trace_processor {
namespace {
// TODO(b/341230981): Enable after it's fixed.
static constexpr bool kEnableDistinct = false;
std::optional<FilterOp> SqliteOpToFilterOp(int sqlite_op) {
switch (sqlite_op) {
case SQLITE_INDEX_CONSTRAINT_EQ:
return FilterOp::kEq;
case SQLITE_INDEX_CONSTRAINT_GT:
return FilterOp::kGt;
case SQLITE_INDEX_CONSTRAINT_LT:
return FilterOp::kLt;
case SQLITE_INDEX_CONSTRAINT_NE:
return FilterOp::kNe;
case SQLITE_INDEX_CONSTRAINT_GE:
return FilterOp::kGe;
case SQLITE_INDEX_CONSTRAINT_LE:
return FilterOp::kLe;
case SQLITE_INDEX_CONSTRAINT_ISNULL:
return FilterOp::kIsNull;
case SQLITE_INDEX_CONSTRAINT_ISNOTNULL:
return FilterOp::kIsNotNull;
case SQLITE_INDEX_CONSTRAINT_GLOB:
return FilterOp::kGlob;
case SQLITE_INDEX_CONSTRAINT_REGEXP:
if constexpr (regex::IsRegexSupported()) {
return FilterOp::kRegex;
}
return std::nullopt;
case SQLITE_INDEX_CONSTRAINT_LIKE:
// TODO(lalitm): start supporting these constraints.
case SQLITE_INDEX_CONSTRAINT_LIMIT:
case SQLITE_INDEX_CONSTRAINT_OFFSET:
case SQLITE_INDEX_CONSTRAINT_IS:
case SQLITE_INDEX_CONSTRAINT_ISNOT:
return std::nullopt;
default:
PERFETTO_FATAL("Currently unsupported constraint");
}
}
class SafeStringWriter {
public:
void AppendString(const char* s) {
for (const char* c = s; *c; ++c) {
buffer_.emplace_back(*c);
}
}
void AppendString(const std::string& s) {
for (char c : s) {
buffer_.emplace_back(c);
}
}
base::StringView GetStringView() const {
return {buffer_.data(), buffer_.size()};
}
private:
base::SmallVector<char, 2048> buffer_;
};
std::string CreateTableStatementFromSchema(const Table::Schema& schema,
const char* table_name) {
std::string stmt = "CREATE TABLE x(";
for (const auto& col : schema.columns) {
std::string c =
col.name + " " + sqlite::utils::SqlValueTypeToString(col.type);
if (col.is_hidden) {
c += " HIDDEN";
}
stmt += c + ",";
}
auto it =
std::find_if(schema.columns.begin(), schema.columns.end(),
[](const Table::Schema::Column& c) { return c.is_id; });
if (it == schema.columns.end()) {
PERFETTO_FATAL(
"id column not found in %s. All tables need to contain an id column;",
table_name);
}
stmt += "PRIMARY KEY(" + it->name + ")";
stmt += ") WITHOUT ROWID;";
return stmt;
}
int SqliteValueToSqlValueChecked(SqlValue* sql_val,
sqlite3_value* value,
const Constraint& cs,
sqlite3_vtab* vtab) {
*sql_val = sqlite::utils::SqliteValueToSqlValue(value);
if constexpr (regex::IsRegexSupported()) {
if (cs.op == FilterOp::kRegex) {
if (cs.value.type != SqlValue::kString) {
return sqlite::utils::SetError(vtab, "Value has to be a string");
}
if (auto st = regex::Regex::Create(cs.value.AsString()); !st.ok()) {
return sqlite::utils::SetError(vtab, st.status().c_message());
}
}
}
return SQLITE_OK;
}
inline uint32_t ReadLetterAndInt(char letter, base::StringSplitter* splitter) {
PERFETTO_CHECK(splitter->Next());
PERFETTO_DCHECK(splitter->cur_token_size() >= 2);
PERFETTO_DCHECK(splitter->cur_token()[0] == letter);
return *base::CStringToUInt32(splitter->cur_token() + 1);
}
int ReadIdxStrAndUpdateCursor(DbSqliteModule::Cursor* cursor,
const char* idx_str,
sqlite3_value** argv) {
base::StringSplitter splitter(idx_str, ',');
uint32_t cs_count = ReadLetterAndInt('C', &splitter);
Query q;
q.constraints.resize(cs_count);
uint32_t c_offset = 0;
for (auto& cs : q.constraints) {
PERFETTO_CHECK(splitter.Next());
cs.col_idx = *base::CStringToUInt32(splitter.cur_token());
PERFETTO_CHECK(splitter.Next());
cs.op = static_cast<FilterOp>(*base::CStringToUInt32(splitter.cur_token()));
if (int ret = SqliteValueToSqlValueChecked(&cs.value, argv[c_offset++], cs,
cursor->pVtab);
ret != SQLITE_OK) {
return ret;
}
}
uint32_t ob_count = ReadLetterAndInt('O', &splitter);
q.orders.resize(ob_count);
for (auto& ob : q.orders) {
PERFETTO_CHECK(splitter.Next());
ob.col_idx = *base::CStringToUInt32(splitter.cur_token());
PERFETTO_CHECK(splitter.Next());
ob.desc = *base::CStringToUInt32(splitter.cur_token());
}
// DISTINCT
q.order_type =
static_cast<Query::OrderType>(ReadLetterAndInt('D', &splitter));
// LIMIT
if (ReadLetterAndInt('L', &splitter)) {
auto val_op = sqlite::utils::SqliteValueToSqlValue(argv[c_offset++]);
if (val_op.type != SqlValue::kLong) {
return sqlite::utils::SetError(cursor->pVtab,
"LIMIT value has to be an INT");
}
q.limit = val_op.AsLong();
}
// OFFSET
if (ReadLetterAndInt('O', &splitter)) {
auto val_op = sqlite::utils::SqliteValueToSqlValue(argv[c_offset++]);
if (val_op.type != SqlValue::kLong) {
return sqlite::utils::SetError(cursor->pVtab,
"OFFSET value has to be an INT");
}
q.offset = static_cast<uint32_t>(val_op.AsLong());
}
cursor->query = std::move(q);
return SQLITE_OK;
}
PERFETTO_ALWAYS_INLINE void TryCacheCreateSortedTable(
DbSqliteModule::Cursor* cursor,
const Table::Schema& schema,
bool is_same_idx) {
if (!is_same_idx) {
cursor->repeated_cache_count = 0;
return;
}
// Only try and create the cached table on exactly the third time we see
// this constraint set.
constexpr uint32_t kRepeatedThreshold = 3;
if (cursor->sorted_cache_table ||
cursor->repeated_cache_count++ != kRepeatedThreshold) {
return;
}
// If we have more than one constraint, we can't cache the table using
// this method.
if (cursor->query.constraints.size() != 1) {
return;
}
// If the constraing is not an equality constraint, there's little
// benefit to caching
const auto& c = cursor->query.constraints.front();
if (c.op != FilterOp::kEq) {
return;
}
// If the column is already sorted, we don't need to cache at all.
if (schema.columns[c.col_idx].is_sorted) {
return;
}
// Try again to get the result or start caching it.
cursor->sorted_cache_table =
cursor->upstream_table->Sort({Order{c.col_idx, false}});
}
void FilterAndSortMetatrace(const std::string& table_name,
const Table::Schema& schema,
DbSqliteModule::Cursor* cursor,
metatrace::Record* r) {
r->AddArg("Table", table_name);
for (const Constraint& c : cursor->query.constraints) {
SafeStringWriter writer;
writer.AppendString(schema.columns[c.col_idx].name);
writer.AppendString(" ");
switch (c.op) {
case FilterOp::kEq:
writer.AppendString("=");
break;
case FilterOp::kGe:
writer.AppendString(">=");
break;
case FilterOp::kGt:
writer.AppendString(">");
break;
case FilterOp::kLe:
writer.AppendString("<=");
break;
case FilterOp::kLt:
writer.AppendString("<");
break;
case FilterOp::kNe:
writer.AppendString("!=");
break;
case FilterOp::kIsNull:
writer.AppendString("IS");
break;
case FilterOp::kIsNotNull:
writer.AppendString("IS NOT");
break;
case FilterOp::kGlob:
writer.AppendString("GLOB");
break;
case FilterOp::kRegex:
writer.AppendString("REGEXP");
break;
}
writer.AppendString(" ");
switch (c.value.type) {
case SqlValue::kString:
writer.AppendString(c.value.AsString());
break;
case SqlValue::kBytes:
writer.AppendString("<bytes>");
break;
case SqlValue::kNull:
writer.AppendString("<null>");
break;
case SqlValue::kDouble: {
writer.AppendString(std::to_string(c.value.AsDouble()));
break;
}
case SqlValue::kLong: {
writer.AppendString(std::to_string(c.value.AsLong()));
break;
}
}
r->AddArg("Constraint", writer.GetStringView());
}
for (const auto& o : cursor->query.orders) {
SafeStringWriter writer;
writer.AppendString(schema.columns[o.col_idx].name);
if (o.desc)
writer.AppendString(" desc");
r->AddArg("Order by", writer.GetStringView());
}
}
} // namespace
int DbSqliteModule::Create(sqlite3* db,
void* ctx,
int argc,
const char* const* argv,
sqlite3_vtab** vtab,
char**) {
PERFETTO_CHECK(argc == 3);
auto* context = GetContext(ctx);
auto state = std::move(context->temporary_create_state);
PERFETTO_CHECK(state);
std::string sql = CreateTableStatementFromSchema(state->schema, argv[2]);
if (int ret = sqlite3_declare_vtab(db, sql.c_str()); ret != SQLITE_OK) {
return ret;
}
std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
res->state = context->manager.OnCreate(argv, std::move(state));
res->table_name = argv[2];
*vtab = res.release();
return SQLITE_OK;
}
int DbSqliteModule::Destroy(sqlite3_vtab* vtab) {
auto* t = GetVtab(vtab);
auto* s = sqlite::ModuleStateManager<DbSqliteModule>::GetState(t->state);
if (s->computation == TableComputation::kStatic) {
// SQLite does not read error messages returned from xDestroy so just pick
// the closest appropriate error code.
return SQLITE_READONLY;
}
std::unique_ptr<Vtab> tab(GetVtab(vtab));
sqlite::ModuleStateManager<DbSqliteModule>::OnDestroy(tab->state);
return SQLITE_OK;
}
int DbSqliteModule::Connect(sqlite3* db,
void* ctx,
int argc,
const char* const* argv,
sqlite3_vtab** vtab,
char**) {
PERFETTO_CHECK(argc == 3);
auto* context = GetContext(ctx);
std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
res->state = context->manager.OnConnect(argv);
res->table_name = argv[2];
auto* state =
sqlite::ModuleStateManager<DbSqliteModule>::GetState(res->state);
std::string sql = CreateTableStatementFromSchema(state->schema, argv[2]);
if (int ret = sqlite3_declare_vtab(db, sql.c_str()); ret != SQLITE_OK) {
// If the registration happens to fail, make sure to disconnect the state
// again.
sqlite::ModuleStateManager<DbSqliteModule>::OnDisconnect(res->state);
return ret;
}
*vtab = res.release();
return SQLITE_OK;
}
int DbSqliteModule::Disconnect(sqlite3_vtab* vtab) {
std::unique_ptr<Vtab> tab(GetVtab(vtab));
sqlite::ModuleStateManager<DbSqliteModule>::OnDisconnect(tab->state);
return SQLITE_OK;
}
int DbSqliteModule::BestIndex(sqlite3_vtab* vtab, sqlite3_index_info* info) {
auto* t = GetVtab(vtab);
auto* s = sqlite::ModuleStateManager<DbSqliteModule>::GetState(t->state);
uint32_t row_count;
int argv_index;
switch (s->computation) {
case TableComputation::kStatic:
row_count = s->static_table->row_count();
argv_index = 1;
break;
case TableComputation::kRuntime:
row_count = s->runtime_table->row_count();
argv_index = 1;
break;
case TableComputation::kTableFunction:
base::Status status = sqlite::utils::ValidateFunctionArguments(
info, static_cast<size_t>(s->argument_count),
[s](uint32_t i) { return s->schema.columns[i].is_hidden; });
if (!status.ok()) {
// TODO(lalitm): instead of returning SQLITE_CONSTRAINT which shows the
// user a very cryptic error message, consider instead SQLITE_OK but
// with a very high (~infinite) cost. If SQLite still chose the query
// plan after that, we can throw a proper error message in xFilter.
return SQLITE_CONSTRAINT;
}
row_count = s->static_table_function->EstimateRowCount();
argv_index = 1 + s->argument_count;
break;
}
std::vector<int> cs_idxes;
// Limit and offset are a nonstandard type of constraint. We can check if they
// are present in the query here, but we won't save them as standard
// constraints and only add them to `idx_str` later.
int limit = -1;
int offset = -1;
cs_idxes.reserve(static_cast<uint32_t>(info->nConstraint));
for (int i = 0; i < info->nConstraint; ++i) {
const auto& c = info->aConstraint[i];
if (!c.usable || info->aConstraintUsage[i].omit) {
continue;
}
if (std::optional<FilterOp> opt_op = SqliteOpToFilterOp(c.op); !opt_op) {
if (c.op == SQLITE_INDEX_CONSTRAINT_LIMIT) {
limit = i;
} else if (c.op == SQLITE_INDEX_CONSTRAINT_OFFSET) {
offset = i;
}
continue;
}
cs_idxes.push_back(i);
}
std::vector<int> ob_idxes(static_cast<uint32_t>(info->nOrderBy));
std::iota(ob_idxes.begin(), ob_idxes.end(), 0);
// Reorder constraints to consider the constraints on columns which are
// cheaper to filter first.
{
std::sort(cs_idxes.begin(), cs_idxes.end(), [s, info](int a, int b) {
auto a_idx = static_cast<uint32_t>(info->aConstraint[a].iColumn);
auto b_idx = static_cast<uint32_t>(info->aConstraint[b].iColumn);
const auto& a_col = s->schema.columns[a_idx];
const auto& b_col = s->schema.columns[b_idx];
// Id columns are always very cheap to filter on so try and get them
// first.
if (a_col.is_id || b_col.is_id)
return a_col.is_id && !b_col.is_id;
// Set id columns are always very cheap to filter on so try and get them
// second.
if (a_col.is_set_id || b_col.is_set_id)
return a_col.is_set_id && !b_col.is_set_id;
// Sorted columns are also quite cheap to filter so order them after
// any id/set id columns.
if (a_col.is_sorted || b_col.is_sorted)
return a_col.is_sorted && !b_col.is_sorted;
// TODO(lalitm): introduce more orderings here based on empirical data.
return false;
});
}
// Remove any order by constraints which also have an equality constraint.
{
auto p = [info, &cs_idxes](int o_idx) {
auto& o = info->aOrderBy[o_idx];
auto inner_p = [info, &o](int c_idx) {
auto& c = info->aConstraint[c_idx];
return c.iColumn == o.iColumn && sqlite::utils::IsOpEq(c.op);
};
return std::any_of(cs_idxes.begin(), cs_idxes.end(), inner_p);
};
ob_idxes.erase(std::remove_if(ob_idxes.begin(), ob_idxes.end(), p),
ob_idxes.end());
}
// Go through the order by constraints in reverse order and eliminate
// constraints until the first non-sorted column or the first order by in
// descending order.
{
auto p = [info, s](int o_idx) {
auto& o = info->aOrderBy[o_idx];
const auto& col = s->schema.columns[static_cast<uint32_t>(o.iColumn)];
return o.desc || !col.is_sorted;
};
auto first_non_sorted_it =
std::find_if(ob_idxes.rbegin(), ob_idxes.rend(), p);
auto pop_count = std::distance(ob_idxes.rbegin(), first_non_sorted_it);
ob_idxes.resize(ob_idxes.size() - static_cast<uint32_t>(pop_count));
}
// Create index string. It contains information query Trace Processor will
// have to run. It can be split into 3 segments: C (constraints), O (orders)
// and D (distinct). It can be directly mapped into `Query` type. The number
// after C and O signifies how many constraints/orders there are. The number
// after D maps to the Query::Distinct enum value.
// "C2,0,0,2,1,O1,0,1,D1,L0,O1" maps to:
// - "C2,0,0,2,1" - two constraints: kEq on first column and kNe on third
// column.
// - "O1,0,1" - one order by: descending on first column.
// - "D1" - kUnsorted distinct.
// - "L1" - LIMIT set. "L0" if no limit.
// - "O1" - OFFSET set. Can only be set if "L1".
// Constraints:
std::string idx_str = "C";
idx_str += std::to_string(cs_idxes.size());
for (int i : cs_idxes) {
const auto& c = info->aConstraint[i];
auto& o = info->aConstraintUsage[i];
o.omit = true;
o.argvIndex = argv_index++;
auto op = SqliteOpToFilterOp(c.op);
PERFETTO_DCHECK(op);
idx_str += ',';
idx_str += std::to_string(c.iColumn);
idx_str += ',';
idx_str += std::to_string(static_cast<uint32_t>(*op));
}
idx_str += ",";
// Orders:
idx_str += "O";
idx_str += std::to_string(ob_idxes.size());
for (int i : ob_idxes) {
idx_str += ',';
idx_str += std::to_string(info->aOrderBy[i].iColumn);
idx_str += ',';
idx_str += std::to_string(info->aOrderBy[i].desc);
}
idx_str += ",";
// Distinct:
idx_str += "D";
if (ob_idxes.size() == 1 && kEnableDistinct) {
switch (sqlite3_vtab_distinct(info)) {
case 0:
case 1:
idx_str += std::to_string(static_cast<int>(Query::OrderType::kSort));
break;
case 2:
idx_str +=
std::to_string(static_cast<int>(Query::OrderType::kDistinct));
break;
case 3:
idx_str += std::to_string(
static_cast<int>(Query::OrderType::kDistinctAndSort));
break;
default:
PERFETTO_FATAL("Invalid sqlite3_vtab_distinct result");
}
} else {
// TODO(mayzner): Remove this if condition after implementing multicolumn
// distinct.
idx_str += std::to_string(static_cast<int>(Query::OrderType::kSort));
}
idx_str += ",";
// LIMIT. Save as "L1" if limit is present and "L0" if not.
idx_str += "L";
if (limit == -1) {
idx_str += "0";
} else {
auto& o = info->aConstraintUsage[limit];
o.omit = true;
o.argvIndex = argv_index++;
idx_str += "1";
}
idx_str += ",";
// OFFSET. Save as "O1" if offset is present and "O0" if not.
idx_str += "O";
if (offset == -1) {
idx_str += "0";
} else {
auto& o = info->aConstraintUsage[offset];
o.omit = true;
o.argvIndex = argv_index++;
idx_str += "1";
}
info->idxStr = sqlite3_mprintf("%s", idx_str.c_str());
info->idxNum = t->best_index_num++;
info->needToFreeIdxStr = true;
// We can sort on any column correctly.
info->orderByConsumed = true;
auto cost_and_rows =
EstimateCost(s->schema, row_count, info, cs_idxes, ob_idxes);
info->estimatedCost = cost_and_rows.cost;
info->estimatedRows = cost_and_rows.rows;
return SQLITE_OK;
}
int DbSqliteModule::Open(sqlite3_vtab* tab, sqlite3_vtab_cursor** cursor) {
auto* t = GetVtab(tab);
auto* s = sqlite::ModuleStateManager<DbSqliteModule>::GetState(t->state);
std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
switch (s->computation) {
case TableComputation::kStatic:
c->upstream_table = s->static_table;
break;
case TableComputation::kRuntime:
c->upstream_table = s->runtime_table.get();
break;
case TableComputation::kTableFunction:
c->table_function_arguments.resize(
static_cast<size_t>(s->argument_count));
break;
}
*cursor = c.release();
return SQLITE_OK;
}
int DbSqliteModule::Close(sqlite3_vtab_cursor* cursor) {
std::unique_ptr<Cursor> c(GetCursor(cursor));
return SQLITE_OK;
}
int DbSqliteModule::Filter(sqlite3_vtab_cursor* cursor,
int idx_num,
const char* idx_str,
int,
sqlite3_value** argv) {
auto* c = GetCursor(cursor);
auto* t = GetVtab(cursor->pVtab);
auto* s = sqlite::ModuleStateManager<DbSqliteModule>::GetState(t->state);
// Clear out the iterator before filtering to ensure the destructor is run
// before the table's destructor.
c->iterator = std::nullopt;
size_t offset = c->table_function_arguments.size();
bool is_same_idx = idx_num == c->last_idx_num;
if (PERFETTO_LIKELY(is_same_idx)) {
for (auto& cs : c->query.constraints) {
if (int ret = SqliteValueToSqlValueChecked(&cs.value, argv[offset++], cs,
c->pVtab);
ret != SQLITE_OK) {
return ret;
}
}
} else {
if (int r = ReadIdxStrAndUpdateCursor(c, idx_str, argv + offset);
r != SQLITE_OK) {
return r;
}
c->last_idx_num = idx_num;
}
// Setup the upstream table based on the computation state.
switch (s->computation) {
case TableComputation::kStatic:
case TableComputation::kRuntime:
// Tries to create a sorted cached table which can be used to speed up
// filters below.
TryCacheCreateSortedTable(c, s->schema, is_same_idx);
break;
case TableComputation::kTableFunction: {
PERFETTO_TP_TRACE(
metatrace::Category::QUERY_DETAILED, "TABLE_FUNCTION_CALL",
[t](metatrace::Record* r) { r->AddArg("Name", t->table_name); });
for (uint32_t i = 0; i < c->table_function_arguments.size(); ++i) {
c->table_function_arguments[i] =
sqlite::utils::SqliteValueToSqlValue(argv[i]);
}
base::StatusOr<std::unique_ptr<Table>> table =
s->static_table_function->ComputeTable(c->table_function_arguments);
if (!table.ok()) {
base::StackString<1024> err("%s: %s", t->table_name.c_str(),
table.status().c_message());
return sqlite::utils::SetError(t, err.c_str());
}
c->dynamic_table = std::move(*table);
c->upstream_table = c->dynamic_table.get();
break;
}
}
PERFETTO_TP_TRACE(metatrace::Category::QUERY_DETAILED,
"DB_TABLE_FILTER_AND_SORT",
[s, t, c](metatrace::Record* r) {
FilterAndSortMetatrace(t->table_name, s->schema, c, r);
});
const auto* source_table =
c->sorted_cache_table ? &*c->sorted_cache_table : c->upstream_table;
RowMap filter_map = source_table->QueryToRowMap(c->query);
if (filter_map.IsRange() && filter_map.size() <= 1) {
// Currently, our criteria where we have a special fast path is if it's
// a single ranged row. We have this fast path for joins on id columns
// where we get repeated queries filtering down to a single row. The
// other path performs allocations when creating the new table as well
// as the iterator on the new table whereas this path only uses a single
// number and lives entirely on the stack.
// TODO(lalitm): investigate some other criteria where it is beneficial
// to have a fast path and expand to them.
c->mode = Cursor::Mode::kSingleRow;
c->single_row = filter_map.size() == 1
? std::make_optional(filter_map.Get(0))
: std::nullopt;
c->eof = !c->single_row.has_value();
} else {
c->mode = Cursor::Mode::kTable;
c->iterator = source_table->ApplyAndIterateRows(std::move(filter_map));
c->eof = !*c->iterator;
}
return SQLITE_OK;
}
int DbSqliteModule::Next(sqlite3_vtab_cursor* cursor) {
auto* c = GetCursor(cursor);
if (c->mode == Cursor::Mode::kSingleRow) {
c->eof = true;
} else {
c->eof = !++*c->iterator;
}
return SQLITE_OK;
}
int DbSqliteModule::Eof(sqlite3_vtab_cursor* cursor) {
return GetCursor(cursor)->eof;
}
int DbSqliteModule::Column(sqlite3_vtab_cursor* cursor,
sqlite3_context* ctx,
int N) {
Cursor* c = GetCursor(cursor);
auto idx = static_cast<uint32_t>(N);
const auto* source_table =
c->sorted_cache_table ? &*c->sorted_cache_table : c->upstream_table;
SqlValue value = c->mode == Cursor::Mode::kSingleRow
? source_table->columns()[idx].Get(*c->single_row)
: c->iterator->Get(idx);
// We can say kSqliteStatic for strings because all strings are expected
// to come from the string pool. Thus they will be valid for the lifetime
// of trace processor. Similarily, for bytes, we can also use
// kSqliteStatic because for our iterator will hold onto the pointer as
// long as we don't call Next(). However, that only happens when Next() is
// called on the Cursor itself, at which point SQLite no longer cares
// about the bytes pointer.
sqlite::utils::ReportSqlValue(ctx, value, sqlite::utils::kSqliteStatic,
sqlite::utils::kSqliteStatic);
return SQLITE_OK;
}
int DbSqliteModule::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
return SQLITE_ERROR;
}
DbSqliteModule::QueryCost DbSqliteModule::EstimateCost(
const Table::Schema& schema,
uint32_t row_count,
sqlite3_index_info* info,
const std::vector<int>& cs_idxes,
const std::vector<int>& ob_idxes) {
// Currently our cost estimation algorithm is quite simplistic but is good
// enough for the simplest cases.
// TODO(lalitm): replace hardcoded constants with either more heuristics
// based on the exact type of constraint or profiling the queries
// themselves.
// We estimate the fixed cost of set-up and tear-down of a query in terms of
// the number of rows scanned.
constexpr double kFixedQueryCost = 1000.0;
// Setup the variables for estimating the number of rows we will have at the
// end of filtering. Note that |current_row_count| should always be at least
// 1 unless we are absolutely certain that we will return no rows as
// otherwise SQLite can make some bad choices.
uint32_t current_row_count = row_count;
// If the table is empty, any constraint set only pays the fixed cost. Also
// we can return 0 as the row count as we are certain that we will return no
// rows.
if (current_row_count == 0) {
return QueryCost{kFixedQueryCost, 0};
}
// Setup the variables for estimating the cost of filtering.
double filter_cost = 0.0;
for (int i : cs_idxes) {
if (current_row_count < 2) {
break;
}
const auto& c = info->aConstraint[i];
PERFETTO_DCHECK(c.usable);
PERFETTO_DCHECK(info->aConstraintUsage[i].omit);
PERFETTO_DCHECK(info->aConstraintUsage[i].argvIndex > 0);
const auto& col_schema = schema.columns[static_cast<uint32_t>(c.iColumn)];
if (sqlite::utils::IsOpEq(c.op) && col_schema.is_id) {
// If we have an id equality constraint, we can very efficiently filter
// down to a single row in C++. However, if we're joining with another
// table, SQLite will do this once per row which can be extremely
// expensive because of all the virtual table (which is implemented
// using virtual function calls) machinery. Indicate this by saying that
// an entire filter call is ~10x the cost of iterating a single row.
filter_cost += 10;
current_row_count = 1;
} else if (sqlite::utils::IsOpEq(c.op)) {
// If there is only a single equality constraint, we have special logic
// to sort by that column and then binary search if we see the
// constraint set often. Model this by dividing by the log of the number
// of rows as a good approximation. Otherwise, we'll need to do a full
// table scan. Alternatively, if the column is sorted, we can use the
// same binary search logic so we have the same low cost (even
// better because we don't // have to sort at all).
filter_cost += cs_idxes.size() == 1 || col_schema.is_sorted
? log2(current_row_count)
: current_row_count;
// As an extremely rough heuristic, assume that an equalty constraint
// will cut down the number of rows by approximately double log of the
// number of rows.
double estimated_rows = current_row_count / (2 * log2(current_row_count));
current_row_count = std::max(static_cast<uint32_t>(estimated_rows), 1u);
} else if (col_schema.is_sorted &&
(sqlite::utils::IsOpLe(c.op) || sqlite::utils::IsOpLt(c.op) ||
sqlite::utils::IsOpGt(c.op) || sqlite::utils::IsOpGe(c.op))) {
// On a sorted column, if we see any partition constraints, we can do
// this filter very efficiently. Model this using the log of the number
// of rows as a good approximation.
filter_cost += log2(current_row_count);
// As an extremely rough heuristic, assume that an partition constraint
// will cut down the number of rows by approximately double log of the
// number of rows.
double estimated_rows = current_row_count / (2 * log2(current_row_count));
current_row_count = std::max(static_cast<uint32_t>(estimated_rows), 1u);
} else {
// Otherwise, we will need to do a full table scan and we estimate we
// will maybe (at best) halve the number of rows.
filter_cost += current_row_count;
current_row_count = std::max(current_row_count / 2u, 1u);
}
}
// Now, to figure out the cost of sorting, multiply the final row count
// by |qc.order_by().size()| * log(row count). This should act as a crude
// estimation of the cost.
double sort_cost =
static_cast<double>(static_cast<uint32_t>(ob_idxes.size()) *
current_row_count) *
log2(current_row_count);
// The cost of iterating rows is more expensive than just filtering the rows
// so multiply by an appropriate factor.
double iteration_cost = current_row_count * 2.0;
// To get the final cost, add up all the individual components.
double final_cost =
kFixedQueryCost + filter_cost + sort_cost + iteration_cost;
return QueryCost{final_cost, current_row_count};
}
DbSqliteModule::State::State(const Table* _table, Table::Schema _schema)
: State(TableComputation::kStatic, std::move(_schema)) {
static_table = _table;
}
DbSqliteModule::State::State(std::unique_ptr<RuntimeTable> _table)
: State(TableComputation::kRuntime, _table->schema()) {
runtime_table = std::move(_table);
}
DbSqliteModule::State::State(
std::unique_ptr<StaticTableFunction> _static_function)
: State(TableComputation::kTableFunction,
_static_function->CreateSchema()) {
static_table_function = std::move(_static_function);
for (const auto& c : schema.columns) {
argument_count += c.is_hidden;
}
}
DbSqliteModule::State::State(TableComputation _computation,
Table::Schema _schema)
: computation(_computation), schema(std::move(_schema)) {}
} // namespace perfetto::trace_processor