blob: 7d4214797d5d6fd9df8ce2cae4c008bd371924e7 [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.
*/
#ifndef SRC_TRACE_PROCESSOR_DATAFRAME_IMPL_QUERY_PLAN_H_
#define SRC_TRACE_PROCESSOR_DATAFRAME_IMPL_QUERY_PLAN_H_
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
#include "perfetto/base/logging.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/base64.h"
#include "perfetto/ext/base/small_vector.h"
#include "perfetto/ext/base/status_macros.h"
#include "perfetto/ext/base/status_or.h"
#include "perfetto/ext/base/string_view.h"
#include "perfetto/public/compiler.h"
#include "src/trace_processor/dataframe/impl/bytecode_core.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/types.h"
namespace perfetto::trace_processor::dataframe::impl {
// A QueryPlan encapsulates all the information needed to execute a query,
// including the bytecode instructions and interpreter configuration.
struct QueryPlan {
// Contains various parameters required for execution of this query plan.
struct ExecutionParams {
// An estimate for the cost of executing the query plan.
double estimated_cost = 0;
// Register holding the final filtered indices.
bytecode::reg::ReadHandle<Span<uint32_t>> output_register;
// The maximum number of rows it's possible for this query plan to return.
uint32_t max_row_count = 0;
// The number of rows this query plan estimates it will return.
uint32_t estimated_row_count = 0;
// The number of registers used by this query plan.
uint32_t register_count = 0;
// Number of filter values used by this query.
uint32_t filter_value_count = 0;
// Number of output indices per row.
uint32_t output_per_row = 0;
};
static_assert(std::is_trivially_copyable_v<ExecutionParams>);
static_assert(std::is_trivially_destructible_v<ExecutionParams>);
static_assert(sizeof(ExecutionParams) == 32);
// Serializes the query plan to a Base64-encoded string.
// This allows plans to be stored or transmitted between processes.
std::string Serialize() const {
size_t size = sizeof(params) + sizeof(size_t) +
(bytecode.size() * sizeof(bytecode::Bytecode)) +
sizeof(size_t) +
(col_to_output_offset.size() * sizeof(uint32_t));
std::string res(size, '\0');
char* p = res.data();
{
memcpy(p, &params, sizeof(params));
p += sizeof(params);
}
{
size_t bytecode_size = bytecode.size();
memcpy(p, &bytecode_size, sizeof(bytecode_size));
p += sizeof(bytecode_size);
}
{
memcpy(p, bytecode.data(), bytecode.size() * sizeof(bytecode::Bytecode));
p += bytecode.size() * sizeof(bytecode::Bytecode);
}
{
size_t columns_size = col_to_output_offset.size();
memcpy(p, &columns_size, sizeof(columns_size));
p += sizeof(columns_size);
}
{
size_t columns_size = col_to_output_offset.size();
memcpy(p, col_to_output_offset.data(), columns_size * sizeof(uint32_t));
p += columns_size * sizeof(uint32_t);
}
PERFETTO_CHECK(p == res.data() + res.size());
return base::Base64Encode(base::StringView(res));
}
// Deserializes a query plan from a Base64-encoded string.
// Returns the reconstructed QueryPlan.
static QueryPlan Deserialize(std::string_view serialized) {
QueryPlan res;
std::optional<std::string> raw_data = base::Base64Decode(
base::StringView(serialized.data(), serialized.size()));
PERFETTO_CHECK(raw_data);
const char* p = raw_data->data();
size_t bytecode_size;
size_t columns_size;
{
memcpy(&res.params, p, sizeof(res.params));
p += sizeof(res.params);
}
{
memcpy(&bytecode_size, p, sizeof(bytecode_size));
p += sizeof(bytecode_size);
}
{
for (uint32_t i = 0; i < bytecode_size; ++i) {
res.bytecode.emplace_back();
}
memcpy(res.bytecode.data(), p,
bytecode_size * sizeof(bytecode::Bytecode));
p += bytecode_size * sizeof(bytecode::Bytecode);
}
{
memcpy(&columns_size, p, sizeof(columns_size));
p += sizeof(columns_size);
}
{
for (uint32_t i = 0; i < columns_size; ++i) {
res.col_to_output_offset.emplace_back();
}
memcpy(res.col_to_output_offset.data(), p,
columns_size * sizeof(uint32_t));
p += columns_size * sizeof(uint32_t);
}
PERFETTO_CHECK(p == raw_data->data() + raw_data->size());
return res;
}
ExecutionParams params;
bytecode::BytecodeVector bytecode;
base::SmallVector<uint32_t, 24> col_to_output_offset;
};
// Builder class for creating query plans.
//
// QueryPlans contain the bytecode instructions and interpreter configuration
// needed to execute a query.
class QueryPlanBuilder {
public:
static base::StatusOr<QueryPlan> Build(
uint32_t row_count,
const std::vector<std::shared_ptr<Column>>& columns,
const std::vector<Index>& indexes,
std::vector<FilterSpec>& specs,
const std::vector<DistinctSpec>& distinct,
const std::vector<SortSpec>& sort_specs,
const LimitSpec& limit_spec,
uint64_t cols_used) {
QueryPlanBuilder builder(row_count, columns, indexes);
RETURN_IF_ERROR(builder.Filter(specs));
builder.Distinct(distinct);
if (builder.CanUseMinMaxOptimization(sort_specs, limit_spec)) {
builder.MinMax(sort_specs[0]);
builder.Output({}, cols_used);
} else {
builder.Sort(sort_specs);
builder.Output(limit_spec, cols_used);
}
return std::move(builder).Build();
}
private:
// Represents register types for holding indices.
using IndicesReg = std::variant<bytecode::reg::RwHandle<Range>,
bytecode::reg::RwHandle<Span<uint32_t>>>;
// Indicates that the bytecode does not change the estimated or maximum number
// of rows.
struct UnchangedRowCount {};
// Indicates that the bytecode is a non-equality filter.
struct NonEqualityFilterRowCount {};
// Indicates that the bytecode is a equality filter with given duplicate
// state.
struct EqualityFilterRowCount {
DuplicateState duplicate_state;
};
// Indicates that the bytecode produces *exactly* one row and the estimated
// and maximum should be set to 1.
struct OneRowCount {};
// Indicates that the bytecode produces *exactly* zero rows and the estimated
// and maximum should be set to 0.
struct ZeroRowCount {};
// Indicates that the bytecode produces `limit` rows starting at `offset`.
struct LimitOffsetRowCount {
uint32_t limit;
uint32_t offset;
};
using RowCountModifier = std::variant<UnchangedRowCount,
NonEqualityFilterRowCount,
EqualityFilterRowCount,
OneRowCount,
ZeroRowCount,
LimitOffsetRowCount>;
// State information for a column during query planning.
struct ColumnState {
std::optional<bytecode::reg::RwHandle<Slab<uint32_t>>> prefix_popcount;
};
// Constructs a builder for the given number of rows and columns.
QueryPlanBuilder(uint32_t row_count,
const std::vector<std::shared_ptr<Column>>& columns,
const std::vector<Index>& indexes);
// Adds filter operations to the query plan based on filter specifications.
// Optimizes the order of filters for efficiency.
base::Status Filter(std::vector<FilterSpec>& specs);
// Adds distinct operations to the query plan based on distinct
// specifications. Distinct are applied after filters, in reverse order of
// specification.
void Distinct(const std::vector<DistinctSpec>& distinct_specs);
// Adds min/max operations to the query plan given a single column which
// should be sorted on.
void MinMax(const SortSpec& spec);
// Adds sort operations to the query plan based on sort specifications.
// Sorts are applied after filters and disinct.
void Sort(const std::vector<SortSpec>& sort_specs);
// Configures output handling for the filtered rows.
// |cols_used_bitmap| is a bitmap with bits set for columns that will be
// accessed.
void Output(const LimitSpec&, uint64_t cols_used_bitmap);
// Finalizes and returns the built query plan.
QueryPlan Build() &&;
// Processes non-string filter constraints.
void NonStringConstraint(
const FilterSpec& c,
const NonStringType& type,
const NonStringOp& op,
const bytecode::reg::ReadHandle<CastFilterValueResult>& result);
// Processes string filter constraints.
base::Status StringConstraint(
const FilterSpec& c,
const StringOp& op,
const bytecode::reg::ReadHandle<CastFilterValueResult>& result);
// Processes null filter constraints.
void NullConstraint(const NullOp&, FilterSpec&);
// Processes constraints which can be handled with an index.
void IndexConstraints(std::vector<FilterSpec>&,
std::vector<uint8_t>& specs_handled,
uint32_t,
const std::vector<uint32_t>&);
// Attempts to apply optimized filtering on sorted data.
// Returns true if the optimization was applied.
bool TrySortedConstraint(FilterSpec& fs,
const StorageType& ct,
const NonNullOp& op);
// Given a list of indices, prunes any indices that point to null rows
// in the given column. The indices are pruned in-place, and the
// `indices_register` is updated to contain only non-null indices.
void PruneNullIndices(uint32_t col,
bytecode::reg::RwHandle<Span<uint32_t>> indices);
// Given a list of table indices pointing to *only* non-null rows,
// if necessary, translates them to the storage indices for the given column.
// If no translation is needed, the indices are returned as-is.
// If translation *is* needed, the value of `in_place` determines
// whether the translation is done in-place or whether the data is stored
// in the scratch register.
//
// Returns a register handle to the translated indices (either
// `indices_register` or the scratch register).
bytecode::reg::RwHandle<Span<uint32_t>> TranslateNonNullIndices(
uint32_t col,
bytecode::reg::RwHandle<Span<uint32_t>> indices_register,
bool in_place);
// Ensures indices are stored in a Slab, converting from Range if necessary.
PERFETTO_NO_INLINE bytecode::reg::RwHandle<Span<uint32_t>>
EnsureIndicesAreInSlab();
// Adds a new bytecode instruction of type T to the plan.
template <typename T>
T& AddOpcode(RowCountModifier rc);
// Adds a new bytecode instruction of type T with the given option value.
template <typename T>
T& AddOpcode(uint32_t option, RowCountModifier rc) {
return static_cast<T&>(AddRawOpcode(option, rc, T::kCost));
}
// Adds a new bytecode instruction of type T with the given option value.
template <typename T>
T& AddOpcode(uint32_t option, RowCountModifier rc, bytecode::Cost cost) {
return static_cast<T&>(AddRawOpcode(option, rc, cost));
}
PERFETTO_NO_INLINE bytecode::Bytecode& AddRawOpcode(uint32_t option,
RowCountModifier rc,
bytecode::Cost cost);
// Sets the result to an empty set. Use when a filter guarantees no matches.
void SetGuaranteedToBeEmpty();
// Returns the prefix popcount register for the given column.
bytecode::reg::ReadHandle<Slab<uint32_t>> PrefixPopcountRegisterFor(
uint32_t col);
bytecode::reg::ReadHandle<CastFilterValueResult>
CastFilterValue(FilterSpec& c, const StorageType& ct, NonNullOp non_null_op);
bytecode::reg::RwHandle<Span<uint32_t>> GetOrCreateScratchSpanRegister(
uint32_t size);
// Parameters for conversion to row layout.
struct RowLayoutParams {
// The column to be copied.
uint32_t column;
// Whether, instead of copying the string column, we should replace it
// with a rank of the string.
bool replace_string_with_rank = false;
// Whether the bits when copied should be inverted.
bool invert_copied_bits = false;
};
uint16_t CalculateRowLayoutStride(
const std::vector<RowLayoutParams>& row_layout_params);
bytecode::reg::RwHandle<Slab<uint8_t>> 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);
void MaybeReleaseScratchSpanRegister();
void AddLinearFilterEqBytecode(
const FilterSpec&,
const bytecode::reg::ReadHandle<CastFilterValueResult>&,
const NonIdStorageType&);
bool CanUseMinMaxOptimization(const std::vector<SortSpec>&, const LimitSpec&);
const Column& GetColumn(uint32_t idx) { return *columns_[idx]; }
// Reference to the columns being queried.
const std::vector<std::shared_ptr<Column>>& columns_;
// Reference to the indexes available.
const std::vector<Index>& indexes_;
// The query plan being built.
QueryPlan plan_;
// State information for each column during planning.
std::vector<ColumnState> column_states_;
// Current register holding the set of matching indices.
IndicesReg indices_reg_;
// If scratch indices are needed, this holds the size and handles to
// the scratch indices in both Span and Slab forms.
struct ScratchIndices {
uint32_t size;
bytecode::reg::RwHandle<Slab<uint32_t>> slab;
bytecode::reg::RwHandle<Span<uint32_t>> span;
bool in_use = false;
};
std::optional<ScratchIndices> scratch_indices_;
};
} // namespace perfetto::trace_processor::dataframe::impl
#endif // SRC_TRACE_PROCESSOR_DATAFRAME_IMPL_QUERY_PLAN_H_