blob: 130434dd069f0a83bcacc47f9f8537ceeef0310c [file]
/*
* Copyright (C) 2025 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/dataframe/impl/query_plan.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <memory>
#include <optional>
#include <utility>
#include <variant>
#include <vector>
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/small_vector.h"
#include "perfetto/ext/base/status_macros.h"
#include "perfetto/ext/base/variant.h"
#include "perfetto/public/compiler.h"
#include "src/trace_processor/dataframe/impl/bytecode_core.h"
#include "src/trace_processor/dataframe/impl/bytecode_instructions.h"
#include "src/trace_processor/dataframe/impl/bytecode_registers.h"
#include "src/trace_processor/dataframe/impl/slab.h"
#include "src/trace_processor/dataframe/impl/types.h"
#include "src/trace_processor/dataframe/specs.h"
#include "src/trace_processor/dataframe/type_set.h"
#include "src/trace_processor/dataframe/types.h"
#include "src/trace_processor/util/regex.h"
namespace perfetto::trace_processor::dataframe::impl {
namespace {
// Calculates filter preference score for ordering filters.
// Lower scores are applied first for better efficiency.
uint32_t FilterPreference(const FilterSpec& fs, const impl::Column& col) {
enum AbsolutePreference : uint8_t {
kIdEq, // Most efficient: id equality check
kSetIdSortedEq, // Set id sorted equality check
kIdInequality, // Id inequality check
kNumericSortedEq, // Numeric sorted equality check
kNumericSortedInequality, // Numeric inequality check
kStringSortedEq, // String sorted equality check
kStringSortedInequality, // String inequality check
kLeastPreferred, // Least preferred
};
const auto& op = fs.op;
const auto& ct = col.storage.type();
const auto& n = col.null_storage.nullability();
if (n.Is<NonNull>() && ct.Is<Id>() && op.Is<Eq>()) {
return kIdEq;
}
if (n.Is<NonNull>() && ct.Is<Uint32>() && col.sort_state.Is<SetIdSorted>() &&
op.Is<Eq>()) {
return kSetIdSortedEq;
}
if (n.Is<NonNull>() && ct.Is<Id>() && op.IsAnyOf<InequalityOp>()) {
return kIdInequality;
}
if (n.Is<NonNull>() && col.sort_state.Is<Sorted>() &&
ct.IsAnyOf<IntegerOrDoubleType>() && op.Is<Eq>()) {
return kNumericSortedEq;
}
if (n.Is<NonNull>() && col.sort_state.Is<Sorted>() &&
ct.IsAnyOf<IntegerOrDoubleType>() && op.IsAnyOf<InequalityOp>()) {
return kNumericSortedInequality;
}
if (n.Is<NonNull>() && col.sort_state.Is<Sorted>() && ct.Is<String>() &&
op.Is<Eq>()) {
return kStringSortedEq;
}
if (n.Is<NonNull>() && col.sort_state.Is<Sorted>() && ct.Is<String>() &&
op.IsAnyOf<InequalityOp>()) {
return kStringSortedInequality;
}
return kLeastPreferred;
}
// Gets the appropriate bound modifier and range operation type
// for a given range operation.
std::pair<BoundModifier, EqualRangeLowerBoundUpperBound> GetSortedFilterArgs(
const RangeOp& op) {
switch (op.index()) {
case RangeOp::GetTypeIndex<Eq>():
return std::make_pair(BothBounds{}, EqualRange{});
case RangeOp::GetTypeIndex<Lt>():
return std::make_pair(EndBound{}, LowerBound{});
case RangeOp::GetTypeIndex<Le>():
return std::make_pair(EndBound{}, UpperBound{});
case RangeOp::GetTypeIndex<Gt>():
return std::make_pair(BeginBound{}, UpperBound{});
case RangeOp::GetTypeIndex<Ge>():
return std::make_pair(BeginBound{}, LowerBound{});
default:
PERFETTO_FATAL("Unreachable");
}
}
// Helper to get byte size of storage types for layout calculation.
// Returns 0 for Id type as it's handled specially.
uint8_t GetDataSize(StorageType type) {
switch (type.index()) {
case StorageType::GetTypeIndex<Id>():
case StorageType::GetTypeIndex<Uint32>():
case StorageType::GetTypeIndex<Int32>():
case StorageType::GetTypeIndex<String>():
return sizeof(uint32_t);
case StorageType::GetTypeIndex<Int64>():
return sizeof(int64_t);
case StorageType::GetTypeIndex<Double>():
return sizeof(double);
default:
PERFETTO_FATAL("Invalid storage type");
}
}
SparseNullCollapsedNullability NullabilityToSparseNullCollapsedNullability(
Nullability nullability) {
switch (nullability.index()) {
case Nullability::GetTypeIndex<NonNull>():
return NonNull{};
case Nullability::GetTypeIndex<DenseNull>():
return DenseNull{};
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<SparseNullWithPopcountUntilFinalization>():
return SparseNull{};
default:
PERFETTO_FATAL("Invalid nullability type");
}
}
struct BestIndex {
uint32_t best_index_idx;
std::vector<uint32_t> best_index_specs;
};
std::optional<BestIndex> GetBestIndexForFilterSpecs(
const QueryPlan::ExecutionParams& params,
const std::vector<FilterSpec>& all_specs,
const std::vector<uint8_t>& spec_already_handled,
const std::vector<Index>& indexes) {
// If we have very few rows, there's no point in using an index.
if (params.max_row_count <= 1) {
return std::nullopt;
}
uint32_t best_index_idx = std::numeric_limits<uint32_t>::max();
std::vector<uint32_t> best_index_specs;
for (uint32_t i = 0; i < indexes.size(); ++i) {
const Index& index = indexes[i];
std::vector<uint32_t> current_specs_for_this_index;
for (uint32_t column : index.columns()) {
bool found_spec_for_column = false;
for (uint32_t spec_idx = 0; spec_idx < all_specs.size(); ++spec_idx) {
if (spec_already_handled[spec_idx]) {
continue;
}
const FilterSpec& current_spec = all_specs[spec_idx];
if (current_spec.col == column && current_spec.op.Is<Eq>()) {
current_specs_for_this_index.push_back(spec_idx);
found_spec_for_column = true;
break;
}
}
if (!found_spec_for_column) {
break;
}
}
if (current_specs_for_this_index.size() > best_index_specs.size()) {
best_index_idx = i;
best_index_specs = std::move(current_specs_for_this_index);
}
}
if (best_index_idx == std::numeric_limits<uint32_t>::max()) {
return std::nullopt;
}
return BestIndex{best_index_idx, std::move(best_index_specs)};
}
} // namespace
QueryPlanBuilder::QueryPlanBuilder(
uint32_t row_count,
const std::vector<std::shared_ptr<Column>>& columns,
const std::vector<Index>& indexes)
: columns_(columns), indexes_(indexes) {
for (uint32_t i = 0; i < columns_.size(); ++i) {
column_states_.emplace_back();
}
// Setup the maximum and estimated row counts.
plan_.params.max_row_count = row_count;
plan_.params.estimated_row_count = row_count;
// Initialize with a range covering all rows
bytecode::reg::RwHandle<Range> range{plan_.params.register_count++};
{
using B = bytecode::InitRange;
auto& ir = AddOpcode<B>(UnchangedRowCount{});
ir.arg<B::size>() = row_count;
ir.arg<B::dest_register>() = range;
}
indices_reg_ = range;
}
base::Status QueryPlanBuilder::Filter(std::vector<FilterSpec>& specs) {
// Sort filters by efficiency (most selective/cheapest first)
std::stable_sort(specs.begin(), specs.end(),
[this](const FilterSpec& a, const FilterSpec& b) {
const auto& a_col = GetColumn(a.col);
const auto& b_col = GetColumn(b.col);
return FilterPreference(a, a_col) <
FilterPreference(b, b_col);
});
std::vector<uint8_t> specs_handled(specs.size(), false);
// Phase 1: Handle sorted constraints first
for (uint32_t i = 0; i < specs.size(); ++i) {
if (specs_handled[i]) {
continue;
}
FilterSpec& c = specs[i];
auto non_null_op = c.op.TryDowncast<NonNullOp>();
if (!non_null_op) {
continue;
}
const Column& col = GetColumn(c.col);
if (!TrySortedConstraint(c, col.storage.type(), *non_null_op)) {
continue;
}
specs_handled[i] = true;
}
// Phase 2: Handle constraints which can use an index.
std::optional<BestIndex> best_index =
GetBestIndexForFilterSpecs(plan_.params, specs, specs_handled, indexes_);
if (best_index) {
IndexConstraints(specs, specs_handled, best_index->best_index_idx,
best_index->best_index_specs);
}
// Phase 3: Handle all remaining constraints.
for (uint32_t i = 0; i < specs.size(); ++i) {
if (specs_handled[i]) {
continue;
}
FilterSpec& c = specs[i];
const Column& col = GetColumn(c.col);
StorageType ct = col.storage.type();
if (c.op.Is<In>()) {
bytecode::reg::RwHandle<CastFilterValueListResult> value{
plan_.params.register_count++};
{
using B = bytecode::CastFilterValueListBase;
auto& bc =
AddOpcode<B>(bytecode::Index<bytecode::CastFilterValueList>(ct),
UnchangedRowCount{});
bc.arg<B::fval_handle>() = {plan_.params.filter_value_count};
bc.arg<B::write_register>() = value;
bc.arg<B::op>() = Eq{};
c.value_index = plan_.params.filter_value_count++;
}
auto update = EnsureIndicesAreInSlab();
PruneNullIndices(c.col, update);
auto source = TranslateNonNullIndices(c.col, update, false);
{
using B = bytecode::InBase;
B& bc = AddOpcode<B>(bytecode::Index<bytecode::In>(col.storage.type()),
RowCountModifier{NonEqualityFilterRowCount{}});
bc.arg<B::col>() = c.col;
bc.arg<B::value_list_register>() = value;
bc.arg<B::source_register>() = source;
bc.arg<B::update_register>() = update;
}
MaybeReleaseScratchSpanRegister();
continue;
}
// Get the non-null operation (all our ops are non-null at this point)
auto non_null_op = c.op.TryDowncast<NonNullOp>();
if (!non_null_op) {
NullConstraint(*c.op.TryDowncast<NullOp>(), c);
continue;
}
// Handle non-string data types
if (const auto& n = ct.TryDowncast<NonStringType>(); n) {
if (auto op = c.op.TryDowncast<NonStringOp>(); op) {
NonStringConstraint(c, *n, *op, CastFilterValue(c, ct, *non_null_op));
} else {
SetGuaranteedToBeEmpty();
}
continue;
}
PERFETTO_CHECK(ct.Is<String>());
auto op = non_null_op->TryDowncast<StringOp>();
PERFETTO_CHECK(op);
RETURN_IF_ERROR(
StringConstraint(c, *op, CastFilterValue(c, ct, *non_null_op)));
}
return base::OkStatus();
}
void QueryPlanBuilder::Distinct(
const std::vector<DistinctSpec>& distinct_specs) {
if (distinct_specs.empty()) {
return;
}
std::vector<RowLayoutParams> row_layout_params;
row_layout_params.reserve(distinct_specs.size());
for (const auto& spec : distinct_specs) {
row_layout_params.push_back({spec.col, false});
}
uint16_t total_row_stride = CalculateRowLayoutStride(row_layout_params);
bytecode::reg::RwHandle<Span<uint32_t>> indices = EnsureIndicesAreInSlab();
auto buffer_reg =
CopyToRowLayout(total_row_stride, indices, {}, row_layout_params);
{
using B = bytecode::Distinct;
auto& bc = AddOpcode<B>(NonEqualityFilterRowCount{});
bc.arg<B::buffer_register>() = buffer_reg;
bc.arg<B::total_row_stride>() = total_row_stride;
bc.arg<B::indices_register>() = indices;
}
}
void QueryPlanBuilder::Sort(const std::vector<SortSpec>& sort_specs) {
if (sort_specs.empty()) {
return;
}
// Optimization: If there's a single sort constraint on a NonNull
// column that is already sorted accordingly, skip the sort operation.
if (sort_specs.size() == 1) {
const auto& single_spec = sort_specs[0];
const Column& col = GetColumn(single_spec.col);
if (col.null_storage.nullability().Is<NonNull>() &&
(col.sort_state.Is<Sorted>() || col.sort_state.Is<IdSorted>() ||
col.sort_state.Is<SetIdSorted>())) {
switch (single_spec.direction) {
case SortDirection::kAscending:
// The column is NonNull and already sorted as required.
return;
case SortDirection::kDescending:
// The column is NonNull and sorted in the reverse order. Just
// reverse the indices to get the correct order.
{
auto indices = EnsureIndicesAreInSlab();
using B = bytecode::Reverse;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::update_register>() = indices;
return;
}
}
}
}
// main_indices_span will be modified by the final sort operation.
// EnsureIndicesAreInSlab makes it an RwHandle.
bytecode::reg::RwHandle<Span<uint32_t>> indices = EnsureIndicesAreInSlab();
bool has_string_sort_keys = false;
for (const auto& spec : sort_specs) {
if (GetColumn(spec.col).storage.type().Is<String>()) {
has_string_sort_keys = true;
break;
}
}
using Map = bytecode::reg::StringIdToRankMap;
bytecode::reg::RwHandle<Map> string_rank_map;
if (has_string_sort_keys) {
string_rank_map =
bytecode::reg::RwHandle<Map>{plan_.params.register_count++};
{
using B = bytecode::InitRankMap;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::dest_register>() = string_rank_map;
}
// For each string column in the sort specification, collect its unique IDs.
// This involves preparing a temporary set of indices for that column which
// are non-null and translated to storage indices if originally sparse.
for (const auto& spec : sort_specs) {
const Column& col = GetColumn(spec.col);
if (!col.storage.type().Is<String>()) {
continue;
}
bytecode::reg::RwHandle<Span<uint32_t>> translated;
if (col.null_storage.nullability().Is<NonNull>()) {
// If the column is non-null, we can use the main indices directly.
translated = indices;
} else {
// Get a scratch register to prepare indices for this specific column.
// This ensures that the main_indices_span is not modified, allowing
// each string column to be processed independently from the original
// set of rows.
bytecode::reg::RwHandle<Span<uint32_t>> scratch =
GetOrCreateScratchSpanRegister(plan_.params.max_row_count);
// 1. Copy the current indices to our temporary scratch span.
{
auto& op = AddOpcode<bytecode::StrideCopy>(UnchangedRowCount{});
op.arg<bytecode::StrideCopy::source_register>() = indices;
op.arg<bytecode::StrideCopy::update_register>() = scratch;
op.arg<bytecode::StrideCopy::stride>() = 1;
}
// 2. Prune nulls from this temporary span in-place.
PruneNullIndices(spec.col, scratch);
// 3. Translate these non-null table indices to storage indices if
// necessary.
translated = TranslateNonNullIndices(spec.col, scratch, true);
PERFETTO_CHECK(translated.index == scratch.index);
}
// Collect IDs using the prepared (non-null, translated) indices.
{
using B = bytecode::CollectIdIntoRankMap;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::col>() = spec.col;
op.arg<B::source_register>() = translated;
op.arg<B::rank_map_register>() = string_rank_map;
}
// Maybe release the scratch register if we used one.
MaybeReleaseScratchSpanRegister();
}
// Finalize ranks in the map (sorts keys, updates map values to ranks).
// The argument name in the patch for FinalizeRanksInMap was
// 'update_register_register'.
{
using B = bytecode::FinalizeRanksInMap;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::update_register>() = string_rank_map;
}
}
std::vector<RowLayoutParams> row_layout_params;
row_layout_params.reserve(sort_specs.size());
for (const auto& spec : sort_specs) {
row_layout_params.push_back(
{spec.col, columns_[spec.col]->storage.type().Is<String>(),
spec.direction == SortDirection::kDescending});
}
uint16_t total_row_stride = CalculateRowLayoutStride(row_layout_params);
auto buffer_reg = CopyToRowLayout(total_row_stride, indices, string_rank_map,
row_layout_params);
{
using B = bytecode::SortRowLayout;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::buffer_register>() = buffer_reg;
op.arg<B::total_row_stride>() = total_row_stride;
op.arg<B::indices_register>() = indices;
}
}
void QueryPlanBuilder::MinMax(const SortSpec& sort_spec) {
uint32_t col_idx = sort_spec.col;
const auto& col = GetColumn(col_idx);
StorageType storage_type = col.storage.type();
MinMaxOp mmop = sort_spec.direction == SortDirection::kAscending
? MinMaxOp(MinOp{})
: MinMaxOp(MaxOp{});
auto indices = EnsureIndicesAreInSlab();
using B = bytecode::FindMinMaxIndexBase;
auto& op = AddOpcode<B>(
bytecode::Index<bytecode::FindMinMaxIndex>(storage_type, mmop),
OneRowCount{});
op.arg<B::update_register>() = indices;
op.arg<B::col>() = col_idx;
}
void QueryPlanBuilder::Output(const LimitSpec& limit, uint64_t cols_used) {
// Structure to track column and offset pairs
struct ColAndOffset {
uint32_t col;
uint32_t offset;
};
base::SmallVector<ColAndOffset, 24> null_cols;
plan_.params.output_per_row = 1;
for (uint32_t i = 0; i < columns_.size(); ++i) {
plan_.col_to_output_offset.emplace_back();
}
// Process each column that will be used in the output
for (uint32_t i = 0; i < columns_.size(); ++i) {
// Any column with index >= 64 uses the 64th bit in cols_used.
uint64_t mask = 1ULL << std::min(i, 63u);
if ((cols_used & mask) == 0) {
continue;
}
const auto& col = GetColumn(i);
switch (col.null_storage.nullability().index()) {
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<SparseNullWithPopcountUntilFinalization>():
case Nullability::GetTypeIndex<DenseNull>(): {
uint32_t offset = plan_.params.output_per_row++;
null_cols.emplace_back(ColAndOffset{i, offset});
plan_.col_to_output_offset[i] = offset;
break;
}
case Nullability::GetTypeIndex<NonNull>():
// For non-null columns, we can directly use the indices
plan_.col_to_output_offset[i] = 0;
break;
default:
PERFETTO_FATAL("Unreachable");
}
}
auto in_memory_indices = EnsureIndicesAreInSlab();
if (limit.limit || limit.offset) {
auto o = limit.offset.value_or(0);
auto l = limit.limit.value_or(std::numeric_limits<uint32_t>::max());
using B = bytecode::LimitOffsetIndices;
auto& bc = AddOpcode<B>(LimitOffsetRowCount{l, o});
bc.arg<B::offset_value>() = o;
bc.arg<B::limit_value>() = l;
bc.arg<B::update_register>() = in_memory_indices;
}
bytecode::reg::RwHandle<Span<uint32_t>> storage_update_register;
if (plan_.params.output_per_row > 1) {
bytecode::reg::RwHandle<Slab<uint32_t>> slab_register{
plan_.params.register_count++};
storage_update_register =
bytecode::reg::RwHandle<Span<uint32_t>>{plan_.params.register_count++};
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::size>() =
plan_.params.max_row_count * plan_.params.output_per_row;
bc.arg<B::dest_slab_register>() = slab_register;
bc.arg<B::dest_span_register>() = storage_update_register;
}
{
using B = bytecode::StrideCopy;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::source_register>() = in_memory_indices;
bc.arg<B::update_register>() = storage_update_register;
bc.arg<B::stride>() = plan_.params.output_per_row;
}
for (auto [col, offset] : null_cols) {
const auto& c = GetColumn(col);
switch (c.null_storage.nullability().index()) {
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<
SparseNullWithPopcountUntilFinalization>(): {
using B = bytecode::StrideTranslateAndCopySparseNullIndices;
auto reg = PrefixPopcountRegisterFor(col);
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::update_register>() = storage_update_register;
bc.arg<B::popcount_register>() = {reg};
bc.arg<B::col>() = col;
bc.arg<B::offset>() = offset;
bc.arg<B::stride>() = plan_.params.output_per_row;
break;
}
case Nullability::GetTypeIndex<DenseNull>(): {
using B = bytecode::StrideCopyDenseNullIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::update_register>() = storage_update_register;
bc.arg<B::col>() = col;
bc.arg<B::offset>() = offset;
bc.arg<B::stride>() = plan_.params.output_per_row;
break;
}
case Nullability::GetTypeIndex<NonNull>():
default:
PERFETTO_FATAL("Unreachable");
}
}
} else {
PERFETTO_CHECK(null_cols.empty());
storage_update_register = in_memory_indices;
}
plan_.params.output_register = storage_update_register;
}
QueryPlan QueryPlanBuilder::Build() && {
return std::move(plan_);
}
void QueryPlanBuilder::NonStringConstraint(
const FilterSpec& c,
const NonStringType& type,
const NonStringOp& op,
const bytecode::reg::ReadHandle<CastFilterValueResult>& result) {
const auto& col = GetColumn(c.col);
if (std::holds_alternative<bytecode::reg::RwHandle<Range>>(indices_reg_) &&
op.Is<Eq>() && col.null_storage.nullability().Is<NonNull>()) {
// Non null equality on an id column should have been handled earlier.
PERFETTO_CHECK(!type.Is<Id>());
auto non_id_type = type.TryDowncast<NonIdStorageType>();
PERFETTO_CHECK(non_id_type);
AddLinearFilterEqBytecode(c, result, *non_id_type);
return;
}
auto update = EnsureIndicesAreInSlab();
PruneNullIndices(c.col, update);
auto source = TranslateNonNullIndices(c.col, update, false);
{
using B = bytecode::NonStringFilterBase;
B& bc = AddOpcode<B>(
bytecode::Index<bytecode::NonStringFilter>(type, op),
op.Is<Eq>()
? RowCountModifier{EqualityFilterRowCount{col.duplicate_state}}
: RowCountModifier{NonEqualityFilterRowCount{}});
bc.arg<B::col>() = c.col;
bc.arg<B::val_register>() = result;
bc.arg<B::source_register>() = source;
bc.arg<B::update_register>() = update;
}
MaybeReleaseScratchSpanRegister();
}
base::Status QueryPlanBuilder::StringConstraint(
const FilterSpec& c,
const StringOp& op,
const bytecode::reg::ReadHandle<CastFilterValueResult>& result) {
const auto& col = GetColumn(c.col);
if (op.Is<Eq>() &&
std::holds_alternative<bytecode::reg::RwHandle<Range>>(indices_reg_) &&
col.null_storage.nullability().Is<NonNull>()) {
AddLinearFilterEqBytecode(c, result, NonIdStorageType{String{}});
return base::OkStatus();
}
if constexpr (!regex::IsRegexSupported()) {
if (op.Is<Regex>()) {
return base::ErrStatus(
"Regex is not supported on non-Unix platforms (e.g. Windows).");
}
}
auto update = EnsureIndicesAreInSlab();
PruneNullIndices(c.col, update);
auto source = TranslateNonNullIndices(c.col, update, false);
{
using B = bytecode::StringFilterBase;
B& bc = AddOpcode<B>(
bytecode::Index<bytecode::StringFilter>(op),
op.Is<Eq>()
? RowCountModifier{EqualityFilterRowCount{col.duplicate_state}}
: RowCountModifier{NonEqualityFilterRowCount{}});
bc.arg<B::col>() = c.col;
bc.arg<B::val_register>() = result;
bc.arg<B::source_register>() = source;
bc.arg<B::update_register>() = update;
}
MaybeReleaseScratchSpanRegister();
return base::OkStatus();
}
void QueryPlanBuilder::NullConstraint(const NullOp& op, FilterSpec& c) {
// Even if we don't need this to filter null/non-null, we add it so that
// the caller (i.e. SQLite) knows that we are able to handle the constraint.
c.value_index = plan_.params.filter_value_count++;
const auto& col = GetColumn(c.col);
uint32_t nullability_type_index = col.null_storage.nullability().index();
switch (nullability_type_index) {
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<SparseNullWithPopcountUntilFinalization>():
case Nullability::GetTypeIndex<DenseNull>(): {
auto indices = EnsureIndicesAreInSlab();
{
using B = bytecode::NullFilterBase;
B& bc = AddOpcode<B>(bytecode::Index<bytecode::NullFilter>(op),
NonEqualityFilterRowCount{});
bc.arg<B::col>() = c.col;
bc.arg<B::update_register>() = indices;
}
break;
}
case Nullability::GetTypeIndex<NonNull>():
if (op.Is<IsNull>()) {
SetGuaranteedToBeEmpty();
return;
}
// Nothing to do as the column is non-null.
return;
default:
PERFETTO_FATAL("Unreachable");
}
}
void QueryPlanBuilder::IndexConstraints(
std::vector<FilterSpec>& specs,
std::vector<uint8_t>& specs_handled,
uint32_t index_idx,
const std::vector<uint32_t>& filter_specs) {
bytecode::reg::RwHandle<Span<uint32_t>> reg{plan_.params.register_count++};
{
using B = bytecode::IndexPermutationVectorToSpan;
auto& bc_alloc = AddOpcode<B>(UnchangedRowCount{});
bc_alloc.arg<B::index>() = index_idx;
bc_alloc.arg<B::write_register>() = reg;
}
for (uint32_t spec_idx : filter_specs) {
FilterSpec& fs = specs[spec_idx];
const Column& column = GetColumn(fs.col);
auto value_reg = CastFilterValue(fs, column.storage.type(),
*fs.op.TryDowncast<NonNullOp>());
auto non_id = column.storage.type().TryDowncast<NonIdStorageType>();
PERFETTO_CHECK(non_id);
{
using B = bytecode::IndexedFilterEqBase;
using PopcountHandle = bytecode::reg::ReadHandle<Slab<uint32_t>>;
PopcountHandle popcount_register;
if (column.null_storage.nullability().IsAnyOf<SparseNullTypes>()) {
popcount_register = PrefixPopcountRegisterFor(fs.col);
} else {
// Dummy register for non-sparse null columns. IndexedFilterEq knows
// how to handle this.
popcount_register = bytecode::reg::ReadHandle<Slab<uint32_t>>{
plan_.params.register_count++};
}
auto& bc = AddOpcode<B>(
bytecode::Index<bytecode::IndexedFilterEq>(
*non_id, NullabilityToSparseNullCollapsedNullability(
column.null_storage.nullability())),
RowCountModifier{EqualityFilterRowCount{column.duplicate_state}});
bc.arg<B::col>() = fs.col;
bc.arg<B::filter_value_reg>() = value_reg;
bc.arg<B::popcount_register>() = popcount_register;
bc.arg<B::update_register>() = reg;
}
specs_handled[spec_idx] = true;
}
PERFETTO_CHECK(
std::holds_alternative<bytecode::reg::RwHandle<Range>>(indices_reg_));
const auto& indices_reg =
base::unchecked_get<bytecode::reg::RwHandle<Range>>(indices_reg_);
bytecode::reg::RwHandle<Slab<uint32_t>> output_slab_reg{
plan_.params.register_count++};
bytecode::reg::RwHandle<Span<uint32_t>> output_span_reg{
plan_.params.register_count++};
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::size>() = plan_.params.max_row_count;
bc.arg<B::dest_slab_register>() = output_slab_reg;
bc.arg<B::dest_span_register>() = output_span_reg;
}
{
using B = bytecode::CopySpanIntersectingRange;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::source_register>() = reg;
bc.arg<B::source_range_register>() = indices_reg;
bc.arg<B::update_register>() = output_span_reg;
}
indices_reg_ = output_span_reg;
}
bool QueryPlanBuilder::TrySortedConstraint(FilterSpec& fs,
const StorageType& ct,
const NonNullOp& op) {
const auto& col = GetColumn(fs.col);
const auto& nullability = col.null_storage.nullability();
if (!nullability.Is<NonNull>() || col.sort_state.Is<Unsorted>()) {
return false;
}
auto range_op = op.TryDowncast<RangeOp>();
if (!range_op) {
return false;
}
// We should have ordered the constraints such that we only reach this
// point with range indices.
PERFETTO_CHECK(
std::holds_alternative<bytecode::reg::RwHandle<Range>>(indices_reg_));
const auto& reg =
base::unchecked_get<bytecode::reg::RwHandle<Range>>(indices_reg_);
auto value_reg = CastFilterValue(fs, ct, op);
// Handle set id equality with a specialized opcode.
if (ct.Is<Uint32>() && col.sort_state.Is<SetIdSorted>() && op.Is<Eq>()) {
using B = bytecode::Uint32SetIdSortedEq;
auto& bc = AddOpcode<B>(
RowCountModifier{EqualityFilterRowCount{col.duplicate_state}});
bc.arg<B::col>() = fs.col;
bc.arg<B::val_register>() = value_reg;
bc.arg<B::update_register>() = reg;
return true;
}
if (col.specialized_storage.Is<SpecializedStorage::SmallValueEq>() &&
op.Is<Eq>()) {
using B = bytecode::SpecializedStorageSmallValueEq;
auto& bc = AddOpcode<B>(
RowCountModifier{EqualityFilterRowCount{col.duplicate_state}});
bc.arg<B::col>() = fs.col;
bc.arg<B::val_register>() = value_reg;
bc.arg<B::update_register>() = reg;
return true;
}
const auto& [bound, erlbub] = GetSortedFilterArgs(*range_op);
RowCountModifier modifier;
if (op.Is<Eq>()) {
modifier = EqualityFilterRowCount{col.duplicate_state};
} else {
modifier = NonEqualityFilterRowCount{};
}
{
using B = bytecode::SortedFilterBase;
auto& bc =
AddOpcode<B>(bytecode::Index<bytecode::SortedFilter>(ct, erlbub),
modifier, bytecode::SortedFilterBase::EstimateCost(ct));
bc.arg<B::col>() = fs.col;
bc.arg<B::val_register>() = value_reg;
bc.arg<B::update_register>() = reg;
bc.arg<B::write_result_to>() = bound;
}
return true;
}
void QueryPlanBuilder::PruneNullIndices(
uint32_t col,
bytecode::reg::RwHandle<Span<uint32_t>> indices) {
switch (GetColumn(col).null_storage.nullability().index()) {
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<SparseNullWithPopcountUntilFinalization>():
case Nullability::GetTypeIndex<DenseNull>(): {
using B = bytecode::NullFilter<IsNotNull>;
bytecode::NullFilterBase& bc = AddOpcode<B>(NonEqualityFilterRowCount{});
bc.arg<B::col>() = col;
bc.arg<B::update_register>() = indices;
break;
}
case Nullability::GetTypeIndex<NonNull>():
break;
default:
PERFETTO_FATAL("Unreachable");
}
}
bytecode::reg::RwHandle<Span<uint32_t>>
QueryPlanBuilder::TranslateNonNullIndices(
uint32_t col,
bytecode::reg::RwHandle<Span<uint32_t>> table_indices_register,
bool in_place) {
switch (GetColumn(col).null_storage.nullability().index()) {
case Nullability::GetTypeIndex<SparseNull>():
case Nullability::GetTypeIndex<SparseNullWithPopcountAlways>():
case Nullability::GetTypeIndex<SparseNullWithPopcountUntilFinalization>(): {
auto update =
in_place ? table_indices_register
: GetOrCreateScratchSpanRegister(plan_.params.max_row_count);
auto popcount_reg = PrefixPopcountRegisterFor(col);
{
using B = bytecode::TranslateSparseNullIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::col>() = col;
bc.arg<B::popcount_register>() = popcount_reg;
bc.arg<B::source_register>() = table_indices_register;
bc.arg<B::update_register>() = update;
}
return update;
}
case Nullability::GetTypeIndex<DenseNull>():
case Nullability::GetTypeIndex<NonNull>():
return table_indices_register;
default:
PERFETTO_FATAL("Unreachable");
}
}
PERFETTO_NO_INLINE bytecode::reg::RwHandle<Span<uint32_t>>
QueryPlanBuilder::EnsureIndicesAreInSlab() {
using SpanReg = bytecode::reg::RwHandle<Span<uint32_t>>;
using SlabReg = bytecode::reg::RwHandle<Slab<uint32_t>>;
if (PERFETTO_LIKELY(std::holds_alternative<SpanReg>(indices_reg_))) {
return base::unchecked_get<SpanReg>(indices_reg_);
}
using RegRange = bytecode::reg::RwHandle<Range>;
PERFETTO_DCHECK(std::holds_alternative<RegRange>(indices_reg_));
auto range_reg = base::unchecked_get<RegRange>(indices_reg_);
SlabReg slab_reg{plan_.params.register_count++};
SpanReg span_reg{plan_.params.register_count++};
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::size>() = plan_.params.max_row_count;
bc.arg<B::dest_slab_register>() = slab_reg;
bc.arg<B::dest_span_register>() = span_reg;
}
{
using B = bytecode::Iota;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::source_register>() = range_reg;
bc.arg<B::update_register>() = span_reg;
}
indices_reg_ = span_reg;
return span_reg;
}
PERFETTO_NO_INLINE bytecode::Bytecode& QueryPlanBuilder::AddRawOpcode(
uint32_t option,
RowCountModifier rc,
bytecode::Cost cost) {
static constexpr uint32_t kFixedBytecodeCost = 5;
switch (cost.index()) {
case base::variant_index<bytecode::Cost, bytecode::FixedCost>(): {
const auto& c = base::unchecked_get<bytecode::FixedCost>(cost);
plan_.params.estimated_cost += c.cost;
break;
}
case base::variant_index<bytecode::Cost, bytecode::LogPerRowCost>(): {
const auto& c = base::unchecked_get<bytecode::LogPerRowCost>(cost);
plan_.params.estimated_cost +=
plan_.params.estimated_row_count == 0
? kFixedBytecodeCost
: c.cost * log2(plan_.params.estimated_row_count);
break;
}
case base::variant_index<bytecode::Cost, bytecode::LinearPerRowCost>(): {
const auto& c = base::unchecked_get<bytecode::LinearPerRowCost>(cost);
plan_.params.estimated_cost +=
plan_.params.estimated_row_count == 0
? kFixedBytecodeCost
: c.cost * plan_.params.estimated_row_count;
break;
}
case base::variant_index<bytecode::Cost, bytecode::LogLinearPerRowCost>(): {
const auto& c = base::unchecked_get<bytecode::LogLinearPerRowCost>(cost);
plan_.params.estimated_cost +=
plan_.params.estimated_row_count == 0
? kFixedBytecodeCost
: c.cost * plan_.params.estimated_row_count *
log2(plan_.params.estimated_row_count);
break;
}
case base::variant_index<bytecode::Cost,
bytecode::PostOperationLinearPerRowCost>():
break;
default:
PERFETTO_FATAL("Unknown cost type");
}
switch (rc.index()) {
case base::variant_index<RowCountModifier, UnchangedRowCount>():
break;
case base::variant_index<RowCountModifier, NonEqualityFilterRowCount>():
if (plan_.params.estimated_row_count > 1) {
plan_.params.estimated_row_count = plan_.params.estimated_row_count / 2;
} else {
// Leave the estimated row count as is if it is already 1 or less.
}
break;
case base::variant_index<RowCountModifier, EqualityFilterRowCount>(): {
const auto& eq = base::unchecked_get<EqualityFilterRowCount>(rc);
if (eq.duplicate_state.Is<HasDuplicates>()) {
if (plan_.params.estimated_row_count > 1) {
double new_count = plan_.params.estimated_row_count /
(2 * log2(plan_.params.estimated_row_count));
plan_.params.estimated_row_count =
std::max(1u, static_cast<uint32_t>(new_count));
} else {
// Leave the estimated row count as is if it is already 1 or less.
}
} else {
PERFETTO_CHECK(eq.duplicate_state.Is<NoDuplicates>());
plan_.params.estimated_row_count =
std::min(1u, plan_.params.estimated_row_count);
plan_.params.max_row_count = std::min(1u, plan_.params.max_row_count);
}
break;
}
case base::variant_index<RowCountModifier, OneRowCount>():
plan_.params.estimated_row_count =
std::min(1u, plan_.params.estimated_row_count);
plan_.params.max_row_count = std::min(1u, plan_.params.max_row_count);
break;
case base::variant_index<RowCountModifier, ZeroRowCount>():
plan_.params.estimated_row_count = 0;
plan_.params.max_row_count = 0;
break;
case base::variant_index<RowCountModifier, LimitOffsetRowCount>(): {
const auto& lc = base::unchecked_get<LimitOffsetRowCount>(rc);
// Offset will cut out `offset` rows from the start of indices.
uint32_t remove_from_start =
std::min(plan_.params.max_row_count, lc.offset);
plan_.params.max_row_count -= remove_from_start;
// Limit will only preserve at most `limit` rows.
plan_.params.max_row_count =
std::min(lc.limit, plan_.params.max_row_count);
// The max row count is also the best possible estimate we can make for
// the row count.
plan_.params.estimated_row_count = plan_.params.max_row_count;
break;
}
default:
PERFETTO_FATAL("Unknown row count modifier type");
}
// Handle all the cost types which need to be calculated *post* the row
// estimate update.
if (cost.index() ==
base::variant_index<bytecode::Cost,
bytecode::PostOperationLinearPerRowCost>()) {
const auto& c =
base::unchecked_get<bytecode::PostOperationLinearPerRowCost>(cost);
plan_.params.estimated_cost += c.cost * plan_.params.estimated_cost;
}
plan_.bytecode.emplace_back();
plan_.bytecode.back().option = option;
return plan_.bytecode.back();
}
void QueryPlanBuilder::SetGuaranteedToBeEmpty() {
bytecode::reg::RwHandle<Slab<uint32_t>> slab_reg{
plan_.params.register_count++};
bytecode::reg::RwHandle<Span<uint32_t>> span_reg{
plan_.params.register_count++};
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(ZeroRowCount{});
bc.arg<B::size>() = 0;
bc.arg<B::dest_slab_register>() = slab_reg;
bc.arg<B::dest_span_register>() = span_reg;
}
indices_reg_ = span_reg;
}
bytecode::reg::ReadHandle<Slab<uint32_t>>
QueryPlanBuilder::PrefixPopcountRegisterFor(uint32_t col) {
auto& reg = column_states_[col].prefix_popcount;
if (!reg) {
reg =
bytecode::reg::RwHandle<Slab<uint32_t>>{plan_.params.register_count++};
{
using B = bytecode::PrefixPopcount;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::col>() = col;
bc.arg<B::dest_register>() = *reg;
}
}
return *reg;
}
bool QueryPlanBuilder::CanUseMinMaxOptimization(
const std::vector<SortSpec>& sort_specs,
const LimitSpec& limit_spec) {
return sort_specs.size() == 1 &&
GetColumn(sort_specs[0].col)
.null_storage.nullability()
.Is<NonNull>() &&
limit_spec.limit == 1 && limit_spec.offset.value_or(0) == 0;
}
bytecode::reg::ReadHandle<CastFilterValueResult>
QueryPlanBuilder::CastFilterValue(FilterSpec& c,
const StorageType& ct,
NonNullOp op) {
bytecode::reg::RwHandle<CastFilterValueResult> value_reg{
plan_.params.register_count++};
{
using B = bytecode::CastFilterValueBase;
auto& bc = AddOpcode<B>(bytecode::Index<bytecode::CastFilterValue>(ct),
UnchangedRowCount{});
bc.arg<B::fval_handle>() = {plan_.params.filter_value_count};
bc.arg<B::write_register>() = value_reg;
bc.arg<B::op>() = op;
c.value_index = plan_.params.filter_value_count++;
}
return value_reg;
}
bytecode::reg::RwHandle<Span<uint32_t>>
QueryPlanBuilder::GetOrCreateScratchSpanRegister(uint32_t size) {
bytecode::reg::RwHandle<Slab<uint32_t>> scratch_slab;
bytecode::reg::RwHandle<Span<uint32_t>> scratch_span;
if (scratch_indices_) {
PERFETTO_CHECK(size <= scratch_indices_->size);
PERFETTO_CHECK(!scratch_indices_->in_use);
scratch_slab = scratch_indices_->slab;
scratch_span = scratch_indices_->span;
} else {
scratch_slab =
bytecode::reg::RwHandle<Slab<uint32_t>>{plan_.params.register_count++};
scratch_span =
bytecode::reg::RwHandle<Span<uint32_t>>{plan_.params.register_count++};
}
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::size>() = size;
bc.arg<B::dest_slab_register>() = scratch_slab;
bc.arg<B::dest_span_register>() = scratch_span;
}
scratch_indices_ = ScratchIndices{size, scratch_slab, scratch_span, true};
return scratch_span;
}
void QueryPlanBuilder::MaybeReleaseScratchSpanRegister() {
if (scratch_indices_) {
scratch_indices_->in_use = false;
}
}
uint16_t QueryPlanBuilder::CalculateRowLayoutStride(
const std::vector<RowLayoutParams>& row_layout_params) {
PERFETTO_CHECK(!row_layout_params.empty());
uint16_t calculated_total_row_stride = 0;
for (const auto& param : row_layout_params) {
const Column& col = GetColumn(param.column);
bool is_non_null = col.null_storage.nullability().Is<NonNull>();
calculated_total_row_stride +=
(is_non_null ? 0u : 1u) + GetDataSize(col.storage.type());
}
return calculated_total_row_stride;
}
bytecode::reg::RwHandle<Slab<uint8_t>> QueryPlanBuilder::CopyToRowLayout(
uint16_t row_stride,
bytecode::reg::RwHandle<Span<uint32_t>> indices,
bytecode::reg::ReadHandle<bytecode::reg::StringIdToRankMap> rank_map,
const std::vector<RowLayoutParams>& row_layout_params) {
uint32_t buffer_size = plan_.params.max_row_count * row_stride;
bytecode::reg::RwHandle<Slab<uint8_t>> new_buffer_reg{
plan_.params.register_count++};
{
using B = bytecode::AllocateRowLayoutBuffer;
auto& op = AddOpcode<B>(UnchangedRowCount{});
op.arg<B::buffer_size>() = buffer_size;
op.arg<B::dest_buffer_register>() = new_buffer_reg;
}
uint16_t current_offset = 0;
for (const auto& param : row_layout_params) {
const Column& col = GetColumn(param.column);
const auto& nullability = col.null_storage.nullability();
auto popcount = nullability.IsAnyOf<SparseNullTypes>()
? PrefixPopcountRegisterFor(param.column)
: bytecode::reg::ReadHandle<Slab<uint32_t>>{
std::numeric_limits<uint32_t>::max()};
{
using B = bytecode::CopyToRowLayoutBase;
auto index = bytecode::Index<bytecode::CopyToRowLayout>(
col.storage.type(),
NullabilityToSparseNullCollapsedNullability(nullability));
auto& op = AddOpcode<B>(index, UnchangedRowCount{});
op.arg<B::col>() = param.column;
op.arg<B::source_indices_register>() = indices;
op.arg<B::dest_buffer_register>() = new_buffer_reg;
op.arg<B::rank_map_register>() = rank_map;
op.arg<B::row_layout_offset>() = current_offset;
op.arg<B::row_layout_stride>() = row_stride;
op.arg<B::invert_copied_bits>() = param.invert_copied_bits;
op.arg<B::popcount_register>() = popcount;
}
current_offset +=
(nullability.Is<NonNull>() ? 0u : 1u) + GetDataSize(col.storage.type());
}
PERFETTO_CHECK(current_offset == row_stride);
return new_buffer_reg;
}
void QueryPlanBuilder::AddLinearFilterEqBytecode(
const FilterSpec& c,
const bytecode::reg::ReadHandle<CastFilterValueResult>&
filter_value_result_reg,
const NonIdStorageType& non_id_storage_type) {
const auto& col = GetColumn(c.col);
PERFETTO_DCHECK(
std::holds_alternative<bytecode::reg::RwHandle<Range>>(indices_reg_));
PERFETTO_DCHECK(col.null_storage.nullability().Is<NonNull>());
PERFETTO_DCHECK(c.op.Is<Eq>());
using SpanReg = bytecode::reg::RwHandle<Span<uint32_t>>;
using SlabReg = bytecode::reg::RwHandle<Slab<uint32_t>>;
using RegRange = bytecode::reg::RwHandle<Range>;
auto range_reg = base::unchecked_get<RegRange>(indices_reg_);
SlabReg slab_reg{plan_.params.register_count++};
SpanReg span_reg{plan_.params.register_count++};
{
using B = bytecode::AllocateIndices;
auto& bc = AddOpcode<B>(UnchangedRowCount{});
bc.arg<B::size>() = plan_.params.max_row_count;
bc.arg<B::dest_slab_register>() = slab_reg;
bc.arg<B::dest_span_register>() = span_reg;
}
{
using B = bytecode::LinearFilterEqBase;
B& bc = AddOpcode<B>(
bytecode::Index<bytecode::LinearFilterEq>(non_id_storage_type),
RowCountModifier{EqualityFilterRowCount{col.duplicate_state}});
bc.arg<B::col>() = c.col;
bc.arg<B::filter_value_reg>() = filter_value_result_reg;
// For NonNull columns, popcount_register is not used by LinearFilterEq
// logic. Pass a default-constructed handle.
bc.arg<B::popcount_register>() =
bytecode::reg::ReadHandle<Slab<uint32_t>>{};
bc.arg<B::source_register>() = range_reg;
bc.arg<B::update_register>() = span_reg;
}
indices_reg_ = span_reg;
}
template <typename T>
T& QueryPlanBuilder::AddOpcode(RowCountModifier rc) {
return AddOpcode<T>(bytecode::Index<T>(), rc, T::kCost);
}
} // namespace perfetto::trace_processor::dataframe::impl