blob: 1d2f352575c4281368e5eaf0657914597fd0549f [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/prelude/functions/create_function.h"
#include "perfetto/base/status.h"
#include "perfetto/trace_processor/basic_types.h"
#include "src/trace_processor/prelude/functions/create_function_internal.h"
#include "src/trace_processor/sqlite/perfetto_sql_engine.h"
#include "src/trace_processor/sqlite/scoped_db.h"
#include "src/trace_processor/sqlite/sqlite_engine.h"
#include "src/trace_processor/sqlite/sqlite_utils.h"
#include "src/trace_processor/tp_metatrace.h"
#include "src/trace_processor/util/status_macros.h"
namespace perfetto {
namespace trace_processor {
namespace {
base::StatusOr<ScopedStmt> CreateStatement(PerfettoSqlEngine* engine,
const std::string& sql,
const std::string& prototype) {
ScopedStmt stmt;
const char* tail = nullptr;
base::Status status = sqlite_utils::PrepareStmt(engine->sqlite_engine()->db(),
sql.c_str(), &stmt, &tail);
if (!status.ok()) {
return base::ErrStatus(
"CREATE_FUNCTION[prototype=%s]: SQLite error when preparing "
"statement %s",
prototype.c_str(), status.message().c_str());
}
return std::move(stmt);
}
struct CreatedFunction : public SqlFunction {
class Context;
static base::Status Run(Context* ctx,
size_t argc,
sqlite3_value** argv,
SqlValue& out,
Destructors&);
static base::Status VerifyPostConditions(Context*);
static void Cleanup(Context*);
};
class Memoizer {
public:
// Enables memoization.
// Only functions with a single int argument returning ints are supported.
base::Status EnableMemoization(const Prototype& prototype,
sql_argument::Type return_type) {
if (prototype.arguments.size() != 1 ||
TypeToSqlValueType(prototype.arguments[0].type()) !=
SqlValue::Type::kLong) {
return base::ErrStatus(
"EXPERIMENTAL_MEMOIZE: Function %s should take one int argument",
prototype.function_name.c_str());
}
if (TypeToSqlValueType(return_type) != SqlValue::Type::kLong) {
return base::ErrStatus(
"EXPERIMENTAL_MEMOIZE: Function %s should return an int",
prototype.function_name.c_str());
}
enabled_ = true;
return base::OkStatus();
}
// Returns the memoized value for the current invocation if it exists.
std::optional<SqlValue> GetMemoizedValue(size_t argc, sqlite3_value** argv) {
std::optional<int64_t> arg = ExtractArgForMemoization(argc, argv);
if (!arg) {
return std::nullopt;
}
int64_t* value = memoized_values_.Find(*arg);
if (!value) {
return std::nullopt;
}
is_returning_memoized_value_ = true;
return SqlValue::Long(*value);
}
// Saves the return value of the current invocation for memoization.
void Memoize(size_t argc, sqlite3_value** argv, SqlValue value) {
if (!enabled_ || value.type != SqlValue::Type::kLong) {
return;
}
std::optional<int64_t> arg = ExtractArgForMemoization(argc, argv);
if (!arg) {
return;
}
memoized_values_.Insert(*arg, value.AsLong());
}
// Returns true if memoization is enabled and the current invocation should
// bypass post-conditions (as we do not have a statement to check).
bool ShouldBypassPostConditions() {
bool is_returning_memoized_value = is_returning_memoized_value_;
is_returning_memoized_value_ = false;
return enabled_ && is_returning_memoized_value;
}
private:
std::optional<int64_t> ExtractArgForMemoization(size_t argc,
sqlite3_value** argv) {
if (!enabled_ || argc != 1) {
return std::nullopt;
}
SqlValue arg = sqlite_utils::SqliteValueToSqlValue(argv[0]);
if (arg.type != SqlValue::Type::kLong) {
return std::nullopt;
}
return arg.AsLong();
}
bool enabled_ = false;
base::FlatHashMap<int64_t, int64_t> memoized_values_;
// This is used to skip post-conditions when we are returning a memoized
// value. True between a successful call to GetMemoizedValue and the call to
// ValidatePostConditions, false otherwise.
bool is_returning_memoized_value_ = false;
};
// This class is used to store the state of a CREATE_FUNCTION call.
// It is used to store the state of the function across multiple invocations
// of the function (e.g. when the function is called recursively).
class CreatedFunction::Context {
public:
explicit Context(PerfettoSqlEngine* engine) : engine_(engine) {}
// Prepare a statement and push it into the stack of allocated statements
// for this function.
base::Status PrepareStatement() {
base::StatusOr<ScopedStmt> stmt =
CreateStatement(engine_, sql_, prototype_str_);
RETURN_IF_ERROR(stmt.status());
is_valid_ = true;
stmts_.push_back(std::move(stmt.value()));
return base::OkStatus();
}
// Sets the state of the function. Should be called only when the function
// is invalid (i.e. when it is first created or when the previous statement
// failed to prepare).
void Reset(Prototype prototype,
std::string prototype_str,
sql_argument::Type return_type,
std::string sql) {
// Re-registration of valid functions is not allowed.
PERFETTO_DCHECK(!is_valid_);
PERFETTO_DCHECK(stmts_.empty());
prototype_ = std::move(prototype);
prototype_str_ = std::move(prototype_str);
return_type_ = return_type;
sql_ = std::move(sql);
}
// This function is called each time the function is called.
// It ensures that we have a statement for the current recursion level,
// allocating a new one if needed.
base::Status PushStackEntry() {
++current_recursion_level_;
if (current_recursion_level_ > stmts_.size()) {
return PrepareStatement();
}
return base::OkStatus();
}
// Returns the statement that is used for the current invocation.
sqlite3_stmt* CurrentStatement() {
return stmts_[current_recursion_level_ - 1].get();
}
// This function is called each time the function returns and resets the
// statement that this invocation used.
void PopStackEntry() {
if (current_recursion_level_ > stmts_.size()) {
// This is possible if we didn't prepare the statement and returned
// an error.
return;
}
sqlite3_reset(CurrentStatement());
sqlite3_clear_bindings(CurrentStatement());
--current_recursion_level_;
}
base::Status EnableMemoization() {
return memoizer_.EnableMemoization(prototype_, return_type_);
}
PerfettoSqlEngine* engine() const { return engine_; }
const Prototype& prototype() const { return prototype_; }
sql_argument::Type return_type() const { return return_type_; }
const std::string& sql() const { return sql_; }
bool is_valid() const { return is_valid_; }
Memoizer& memoizer() { return memoizer_; }
private:
PerfettoSqlEngine* engine_;
Prototype prototype_;
std::string prototype_str_;
sql_argument::Type return_type_;
std::string sql_;
// Perfetto SQL functions support recursion. Given that each function call in
// the stack requires a dedicated statement, we maintain a stack of prepared
// statements and use the top one for each new call (allocating a new one if
// needed).
std::vector<ScopedStmt> stmts_;
size_t current_recursion_level_ = 0;
// Function re-registration is not allowed, but the user is allowed to define
// the function again if the first call failed. |is_valid_| flag helps that
// by tracking whether the current function definition is valid (in which case
// re-registration is not allowed).
bool is_valid_ = false;
Memoizer memoizer_;
};
base::Status CreatedFunction::Run(CreatedFunction::Context* ctx,
size_t argc,
sqlite3_value** argv,
SqlValue& out,
Destructors&) {
// Enter the function and ensure that we have a statement allocated.
RETURN_IF_ERROR(ctx->PushStackEntry());
if (argc != ctx->prototype().arguments.size()) {
return base::ErrStatus(
"%s: invalid number of args; expected %zu, received %zu",
ctx->prototype().function_name.c_str(),
ctx->prototype().arguments.size(), argc);
}
// Type check all the arguments.
for (size_t i = 0; i < argc; ++i) {
sqlite3_value* arg = argv[i];
sql_argument::Type type = ctx->prototype().arguments[i].type();
base::Status status = sqlite_utils::TypeCheckSqliteValue(
arg, sql_argument::TypeToSqlValueType(type),
sql_argument::TypeToHumanFriendlyString(type));
if (!status.ok()) {
return base::ErrStatus("%s[arg=%s]: argument %zu %s",
ctx->prototype().function_name.c_str(),
sqlite3_value_text(arg), i, status.c_message());
}
}
std::optional<SqlValue> memoized_value =
ctx->memoizer().GetMemoizedValue(argc, argv);
if (memoized_value) {
out = *memoized_value;
return base::OkStatus();
}
PERFETTO_TP_TRACE(
metatrace::Category::FUNCTION, "CREATE_FUNCTION",
[ctx, argv](metatrace::Record* r) {
r->AddArg("Function", ctx->prototype().function_name.c_str());
for (uint32_t i = 0; i < ctx->prototype().arguments.size(); ++i) {
std::string key = "Arg " + std::to_string(i);
const char* value =
reinterpret_cast<const char*>(sqlite3_value_text(argv[i]));
r->AddArg(base::StringView(key),
value ? base::StringView(value) : base::StringView("NULL"));
}
});
// Bind all the arguments to the appropriate places in the function.
for (size_t i = 0; i < argc; ++i) {
RETURN_IF_ERROR(MaybeBindArgument(ctx->CurrentStatement(),
ctx->prototype().function_name,
ctx->prototype().arguments[i], argv[i]));
}
int ret = sqlite3_step(ctx->CurrentStatement());
RETURN_IF_ERROR(SqliteRetToStatus(ctx->engine()->sqlite_engine()->db(),
ctx->prototype().function_name, ret));
if (ret == SQLITE_DONE) {
// No return value means we just return don't set |out|.
return base::OkStatus();
}
PERFETTO_DCHECK(ret == SQLITE_ROW);
size_t col_count =
static_cast<size_t>(sqlite3_column_count(ctx->CurrentStatement()));
if (col_count != 1) {
return base::ErrStatus(
"%s: SQL definition should only return one column: returned %zu "
"columns",
ctx->prototype().function_name.c_str(), col_count);
}
out = sqlite_utils::SqliteValueToSqlValue(
sqlite3_column_value(ctx->CurrentStatement(), 0));
ctx->memoizer().Memoize(argc, argv, out);
// If we return a bytes type but have a null pointer, SQLite will convert this
// to an SQL null. However, for proto build functions, we actively want to
// distinguish between nulls and 0 byte strings. Therefore, change the value
// to an empty string.
if (out.type == SqlValue::kBytes && out.bytes_value == nullptr) {
PERFETTO_DCHECK(out.bytes_count == 0);
out.bytes_value = "";
}
return base::OkStatus();
}
base::Status CreatedFunction::VerifyPostConditions(Context* ctx) {
// If we returned a memoized value, we don't need to verify post-conditions as
// we didn't run a statement.
if (ctx->memoizer().ShouldBypassPostConditions()) {
return base::OkStatus();
}
int ret = sqlite3_step(ctx->CurrentStatement());
RETURN_IF_ERROR(SqliteRetToStatus(ctx->engine()->sqlite_engine()->db(),
ctx->prototype().function_name, ret));
if (ret == SQLITE_ROW) {
auto expanded_sql =
sqlite_utils::ExpandedSqlForStmt(ctx->CurrentStatement());
return base::ErrStatus(
"%s: multiple values were returned when executing function body. "
"Executed SQL was %s",
ctx->prototype().function_name.c_str(), expanded_sql.get());
}
PERFETTO_DCHECK(ret == SQLITE_DONE);
return base::OkStatus();
}
void CreatedFunction::Cleanup(CreatedFunction::Context* ctx) {
// Clear the statement.
ctx->PopStackEntry();
}
} // namespace
base::Status CreateFunction::Run(PerfettoSqlEngine* engine,
size_t argc,
sqlite3_value** argv,
SqlValue&,
Destructors&) {
RETURN_IF_ERROR(sqlite_utils::CheckArgCount("CREATE_FUNCTION", argc, 3u));
sqlite3_value* prototype_value = argv[0];
sqlite3_value* return_type_value = argv[1];
sqlite3_value* sql_defn_value = argv[2];
// Type check all the arguments.
{
auto type_check = [prototype_value](sqlite3_value* value,
SqlValue::Type type, const char* desc) {
base::Status status = sqlite_utils::TypeCheckSqliteValue(value, type);
if (!status.ok()) {
return base::ErrStatus("CREATE_FUNCTION[prototype=%s]: %s %s",
sqlite3_value_text(prototype_value), desc,
status.c_message());
}
return base::OkStatus();
};
RETURN_IF_ERROR(type_check(prototype_value, SqlValue::Type::kString,
"function prototype (first argument)"));
RETURN_IF_ERROR(type_check(return_type_value, SqlValue::Type::kString,
"return type (second argument)"));
RETURN_IF_ERROR(type_check(sql_defn_value, SqlValue::Type::kString,
"SQL definition (third argument)"));
}
// Extract the arguments from the value wrappers.
auto extract_string = [](sqlite3_value* value) -> base::StringView {
return reinterpret_cast<const char*>(sqlite3_value_text(value));
};
base::StringView prototype_str = extract_string(prototype_value);
base::StringView return_type_str = extract_string(return_type_value);
std::string sql_defn_str = extract_string(sql_defn_value).ToStdString();
// Parse all the arguments into a more friendly form.
Prototype prototype;
base::Status status = ParsePrototype(prototype_str, prototype);
if (!status.ok()) {
return base::ErrStatus("CREATE_FUNCTION[prototype=%s]: %s",
prototype_str.ToStdString().c_str(),
status.c_message());
}
// Parse the return type into a enum format.
auto opt_return_type = sql_argument::ParseType(return_type_str);
if (!opt_return_type) {
return base::ErrStatus(
"CREATE_FUNCTION[prototype=%s, return=%s]: unknown return type "
"specified",
prototype_str.ToStdString().c_str(),
return_type_str.ToStdString().c_str());
}
std::string function_name = prototype.function_name;
int created_argc = static_cast<int>(prototype.arguments.size());
auto* ctx = static_cast<CreatedFunction::Context*>(
engine->sqlite_engine()->GetFunctionContext(prototype.function_name,
created_argc));
if (!ctx) {
// We register the function with SQLite before we prepare the statement so
// the statement can reference the function itself, enabling recursive
// calls.
std::unique_ptr<CreatedFunction::Context> created_fn_ctx =
std::make_unique<CreatedFunction::Context>(engine);
ctx = created_fn_ctx.get();
RETURN_IF_ERROR(engine->RegisterSqlFunction<CreatedFunction>(
function_name.c_str(), created_argc, std::move(created_fn_ctx)));
}
if (ctx->is_valid()) {
// If the function already exists, just verify that the prototype, return
// type and SQL matches exactly with what we already had registered. By
// doing this, we can avoid the problem plaguing C++ macros where macro
// ordering determines which one gets run.
if (ctx->prototype() != prototype) {
return base::ErrStatus(
"CREATE_FUNCTION[prototype=%s]: function prototype changed",
prototype_str.ToStdString().c_str());
}
if (ctx->return_type() != *opt_return_type) {
return base::ErrStatus(
"CREATE_FUNCTION[prototype=%s]: return type changed from %s to %s",
prototype_str.ToStdString().c_str(),
sql_argument::TypeToHumanFriendlyString(ctx->return_type()),
return_type_str.ToStdString().c_str());
}
if (ctx->sql() != sql_defn_str) {
return base::ErrStatus(
"CREATE_FUNCTION[prototype=%s]: function SQL changed from %s to %s",
prototype_str.ToStdString().c_str(), ctx->sql().c_str(),
sql_defn_str.c_str());
}
return base::OkStatus();
}
ctx->Reset(std::move(prototype), prototype_str.ToStdString(),
*opt_return_type, std::move(sql_defn_str));
// Ideally, we would unregister the function here if the statement prep
// failed, but SQLite doesn't allow unregistering functions inside active
// statements. So instead we'll just try to prepare the statement when calling
// this function, which will return an error.
return ctx->PrepareStatement();
}
base::Status ExperimentalMemoize::Run(PerfettoSqlEngine* engine,
size_t argc,
sqlite3_value** argv,
SqlValue&,
Destructors&) {
RETURN_IF_ERROR(sqlite_utils::CheckArgCount("EXPERIMENTAL_MEMOIZE", argc, 1));
base::StatusOr<std::string> function_name =
sqlite_utils::ExtractStringArg("MEMOIZE", "function_name", argv[0]);
RETURN_IF_ERROR(function_name.status());
constexpr size_t kSupportedArgCount = 1;
CreatedFunction::Context* ctx = static_cast<CreatedFunction::Context*>(
engine->sqlite_engine()->GetFunctionContext(function_name->c_str(),
kSupportedArgCount));
if (!ctx) {
return base::ErrStatus(
"EXPERIMENTAL_MEMOIZE: Function %s(INT) does not exist",
function_name->c_str());
}
return ctx->EnableMemoization();
}
} // namespace trace_processor
} // namespace perfetto