blob: 2f545b603215a2277a5d81872a4381f3c926e3d9 [file] [edit]
/*
* Copyright (C) 2026 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.
*/
// pfsql: offline tool for working with PerfettoSQL source files. Does not
// load a trace. Each subcommand defines its own input shape.
//
// Subcommands:
// lineage Cross-module dependency graph over `.sql` trees.
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "perfetto/base/status.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/flat_hash_map.h"
#include "perfetto/ext/base/getopt.h"
#include "src/perfetto_sql/analysis/source_tree.h"
#include "src/trace_processor/util/json_value.h"
#include "src/trace_processor/util/simple_json_serializer.h"
namespace perfetto::pfsql {
namespace {
namespace json = trace_processor::json;
namespace analysis = ::perfetto::perfetto_sql::analysis;
// ---------- lineage subcommand ----------
std::string DirOf(const std::string& path) {
auto slash = path.find_last_of('/');
return slash == std::string::npos ? std::string(".") : path.substr(0, slash);
}
std::string ResolvePath(const std::string& base_dir, const std::string& p) {
return (!p.empty() && p[0] == '/') ? p : base_dir + "/" + p;
}
base::Status LoadConfig(const std::string& config_path,
analysis::SourceTreeAnalyzer& analyzer) {
std::string text;
if (!base::ReadFile(config_path, &text))
return base::ErrStatus("failed to read config %s", config_path.c_str());
auto parsed = json::Parse(text);
if (!parsed.ok())
return base::ErrStatus("invalid JSON in %s: %s", config_path.c_str(),
parsed.status().c_message());
const json::Dom& doc = *parsed;
if (!doc.IsObject() || !doc.HasMember("trees") || !doc["trees"].IsArray())
return base::ErrStatus("config must be { \"trees\": [ ... ] }");
const std::string base_dir = DirOf(config_path);
bool empty = true;
for (const auto& t : doc["trees"]) {
std::string root;
if (t.IsString()) {
root = t.AsString();
} else if (t.IsObject() && t.HasMember("root") && t["root"].IsString()) {
root = t["root"].AsString();
} else {
return base::ErrStatus(
"each tree must be a string or { \"root\": \"...\" }");
}
analyzer.AddTree(ResolvePath(base_dir, root));
empty = false;
}
return empty ? base::ErrStatus("config has no trees") : base::OkStatus();
}
struct SymbolRef {
std::string_view name;
analysis::SymbolKind kind;
};
using SymbolRefsByModule =
base::FlatHashMap<std::string, std::vector<SymbolRef>>;
using SortedUseEntry = std::pair<std::string, const std::vector<SymbolRef>*>;
std::vector<SortedUseEntry> SortedUses(const SymbolRefsByModule& by_mod) {
std::vector<SortedUseEntry> out;
for (auto it = by_mod.GetIterator(); it; ++it)
out.emplace_back(it.key(), &it.value());
std::sort(out.begin(), out.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
return out;
}
std::vector<std::string_view> SortedUniqueStrings(
std::vector<std::string_view> values) {
std::sort(values.begin(), values.end());
values.erase(std::unique(values.begin(), values.end()), values.end());
return values;
}
bool IsPrelude(std::string_view module) {
constexpr std::string_view prefix = "prelude.";
return module.size() >= prefix.size() &&
module.substr(0, prefix.size()) == prefix;
}
SymbolRefsByModule UsesByModule(const analysis::Program& program,
const analysis::Symbol& symbol,
bool prelude) {
SymbolRefsByModule out;
for (const analysis::SymbolReference& reference : symbol.references) {
const analysis::Symbol& target = program.symbol(reference.symbol_id);
std::string_view module = program.module(target.module_id).name;
if (IsPrelude(module) != prelude) {
continue;
}
std::string key(module);
std::vector<SymbolRef>* refs = out.Find(key);
if (!refs) {
out.Insert(key, {});
refs = out.Find(key);
}
refs->push_back({target.name, target.kind});
}
return out;
}
std::vector<std::string_view> MissingIncludes(const analysis::Program& program,
const analysis::Module& module) {
std::vector<std::string_view> touched;
for (analysis::SymbolId symbol_id : module.symbol_ids) {
for (const analysis::SymbolReference& reference :
program.symbol(symbol_id).references) {
std::string_view target_module =
program.module(program.symbol(reference.symbol_id).module_id).name;
if (IsPrelude(target_module)) {
continue;
}
bool declared =
std::find(module.declared_includes.begin(),
module.declared_includes.end(),
target_module) != module.declared_includes.end();
if (!declared) {
touched.push_back(target_module);
}
}
}
return SortedUniqueStrings(std::move(touched));
}
std::vector<std::string_view> UnresolvedNames(const analysis::Symbol& symbol) {
std::vector<std::string_view> out;
out.reserve(symbol.unresolved_references.size());
for (const analysis::UnresolvedReference& reference :
symbol.unresolved_references) {
out.push_back(reference.name);
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::string EmitLineageJson(const analysis::Program& program) {
auto write_strings = [](const auto& items) {
return [&items](json::JsonArraySerializer& a) {
for (const auto& s : items)
a.AppendString(s);
};
};
auto write_use_map = [](const SymbolRefsByModule& by_mod) {
auto sorted = SortedUses(by_mod);
return [sorted = std::move(sorted)](json::JsonDictSerializer& d) {
for (const auto& kv : sorted) {
d.AddArray(kv.first, [&kv](json::JsonArraySerializer& a) {
for (const auto& u : *kv.second) {
a.AppendDict([&u](json::JsonDictSerializer& e) {
e.AddString("name", u.name);
e.AddString("kind", analysis::SymbolKindName(u.kind));
});
}
});
}
};
};
std::string out = json::SerializeJson([&](json::JsonValueSerializer&& w) {
std::move(w).WriteDict([&](json::JsonDictSerializer& root) {
root.AddArray("modules", [&](json::JsonArraySerializer& arr) {
for (const analysis::Module& m : program.modules()) {
arr.AppendDict([&](json::JsonDictSerializer& mod) {
mod.AddString("module", m.name);
mod.AddString("path", m.path);
mod.AddArray("declared_includes",
write_strings(m.declared_includes));
mod.AddArray("symbols", [&](json::JsonArraySerializer& syms) {
for (analysis::SymbolId symbol_id : m.symbol_ids) {
const analysis::Symbol& s = program.symbol(symbol_id);
SymbolRefsByModule uses =
UsesByModule(program, s, /*prelude=*/false);
SymbolRefsByModule implicit_uses =
UsesByModule(program, s, /*prelude=*/true);
std::vector<std::string_view> unresolved = UnresolvedNames(s);
syms.AppendDict([&](json::JsonDictSerializer& sd) {
sd.AddString("name", s.name);
sd.AddString("kind", analysis::SymbolKindName(s.kind));
sd.AddDict("uses", write_use_map(uses));
sd.AddDict("implicit_uses", write_use_map(implicit_uses));
sd.AddArray("intrinsics_or_external",
write_strings(unresolved));
});
}
});
std::vector<std::string_view> missing = MissingIncludes(program, m);
mod.AddArray("missing_includes", write_strings(missing));
mod.AddArray("errors", write_strings(m.diagnostics));
});
}
});
});
});
out.push_back('\n');
return out;
}
const char* kLineageHelp = R"(Usage:
pfsql lineage <tree_path>...
pfsql lineage --config <file.json>
Cross-module dependency graph over PerfettoSQL `.sql` trees. Input is given
EITHER as positional tree paths (resolved against the CWD) OR as a JSON
config (not both). The JSON shape is:
{ "trees": [ "path/to/stdlib",
{ "root": "path/to/other" } ] }
Paths inside the JSON are resolved against the config's dir.
All trees share one module namespace; the path within each tree determines
the dotted module name (`slices/with_context.sql` -> `slices.with_context`).
First-tree-wins on name collisions.
Anything under `prelude.*` is treated as auto-included (the runtime engine
auto-loads it). Macros are expanded recursively references inside macro
bodies surface in the resolved record of the invoking symbol.
Output: a single JSON object on stdout with a `modules` array, one entry per
module:
- module / path
- declared_includes: authored INCLUDE PERFETTO MODULE stmts
- symbols: one entry per CREATE PERFETTO
TABLE/VIEW/FUNCTION/MACRO, in source order. Each
carries its OWN:
- name / kind
- uses: cross-module refs by defining
module
- implicit_uses: refs into prelude.*
- intrinsics_or_external:
bare names not defined anywhere
- missing_includes: non-prelude modules used by any symbol but not
declared
- errors: parse errors or symbol-name collisions
)";
int RunLineage(int argc, char** argv) {
std::string config_path;
static const option long_opts[] = {
{"config", required_argument, nullptr, 'c'},
{"help", no_argument, nullptr, 'h'},
{nullptr, 0, nullptr, 0}};
optind = 1; // restart getopt
for (;;) {
int c = getopt_long(argc, argv, "c:h", long_opts, nullptr);
if (c < 0)
break;
if (c == 'c') {
config_path = optarg;
} else if (c == 'h') {
fputs(kLineageHelp, stdout);
return 0;
} else {
fputs(kLineageHelp, stderr);
return 1;
}
}
std::vector<std::string> tree_paths;
for (int i = optind; i < argc; ++i)
tree_paths.emplace_back(argv[i]);
const bool have_config = !config_path.empty();
const bool have_paths = !tree_paths.empty();
if (have_config && have_paths) {
fprintf(stderr,
"pfsql lineage: pass either positional tree paths or --config, "
"not both\n");
return 1;
}
if (!have_config && !have_paths) {
fprintf(stderr,
"pfsql lineage: expected one or more tree paths, or --config "
"FILE\n");
return 1;
}
analysis::SourceTreeAnalyzer analyzer;
if (have_config) {
if (auto st = LoadConfig(config_path, analyzer); !st.ok()) {
fprintf(stderr, "%s\n", st.c_message());
return 1;
}
} else {
for (const auto& p : tree_paths)
analyzer.AddTree(p);
}
base::StatusOr<analysis::Program> program = analyzer.Analyze();
if (!program.ok()) {
fprintf(stderr, "pfsql lineage: %s\n", program.status().c_message());
return 1;
}
std::string s = EmitLineageJson(*program);
fwrite(s.data(), 1, s.size(), stdout);
return 0;
}
// ---------- top-level dispatcher ----------
const char* kTopLevelHelp =
R"(pfsql: offline tool for working with PerfettoSQL.
Usage: pfsql <subcommand> [args]
Subcommands:
lineage Cross-module dependency graph over `.sql` trees.
Run 'pfsql <subcommand> --help' for subcommand details.
)";
int Main(int argc, char** argv) {
if (argc < 2) {
fputs(kTopLevelHelp, stderr);
return 1;
}
std::string_view sub(argv[1]);
if (sub == "-h" || sub == "--help" || sub == "help") {
fputs(kTopLevelHelp, stdout);
return 0;
}
if (sub == "lineage") {
// Shift argv so getopt sees `pfsql lineage [args]` as `lineage [args]`.
return RunLineage(argc - 1, argv + 1);
}
fprintf(stderr, "pfsql: unknown subcommand '%.*s'\n\n",
static_cast<int>(sub.size()), sub.data());
fputs(kTopLevelHelp, stderr);
return 1;
}
} // namespace
} // namespace perfetto::pfsql
int main(int argc, char** argv) {
return perfetto::pfsql::Main(argc, argv);
}