blob: 4b94cc87c5714508a76ed44b1bb5d06343a83b86 [file] [log] [blame]
// Copyright (C) 2020 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.
// Generation of SQL table references from C++ headers.
'use strict';
const fs = require('fs');
const argv = require('yargs').argv
// Removes \n due to 80col wrapping and preserves only end-of-sentence line
// breaks.
// TODO dedupe, this is copied from the other gen_proto file.
function singleLineComment(comment) {
comment = comment || '';
comment = comment.trim();
comment = comment.replaceAll('|', '\\|');
comment = comment.replace(/\.\n/g, '<br>');
comment = comment.replace(/\n/g, ' ');
return comment;
}
// Returns an object describing the table as follows:
// { name: 'HeapGraphObjectTable',
// cols: [ {name: 'upid', type: 'uint32_t', optional: false },
// {name: 'graph_sample_ts', type: 'int64_t', optional: false },
function parseTableDef(tableDefName, tableDef) {
const tableDesc = {
name: '', // The SQL table name, e.g. stack_profile_mapping.
cppClassName: '', // e.g., StackProfileMappingTable.
defMacro: tableDefName, // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF.
comment: '',
parent: undefined, // Will be filled afterwards in the resolution phase.
parentDefName: '', // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF.
tablegroup: 'Misc', // From @tablegroup in comments.
cols: {},
};
const getOrCreateColumn = (name) => {
if (name in tableDesc.cols)
return tableDesc.cols[name];
tableDesc.cols[name] = {
name: name,
type: '',
comment: '',
optional: false,
refTableCppName: undefined,
joinTable: undefined,
joinCol: undefined,
};
return tableDesc.cols[name];
};
// Reserve the id and type columns so they appear first in the column list
// They will only be kept in case this is a root table - otherwise they will
// be deleted below..
const id = getOrCreateColumn('id');
const type = getOrCreateColumn('type');
let lastColumn = undefined;
for (const line of tableDef.split('\n')) {
if (line.startsWith('#define'))
continue; // Skip the first line.
let m;
if (line.startsWith('//')) {
let comm = line.replace(/^\s*\/\/\s*/, '');
if (m = comm.match(/@tablegroup (.*)/)) {
tableDesc.tablegroup = m[1];
continue;
}
if (m = comm.match(/@name (\w+)/)) {
tableDesc.name = m[1];
continue;
}
if (m = comm.match(/@param\s+([^ ]+)\s*({\w+})?\s*(.*)/)) {
lastColumn = getOrCreateColumn(/*name=*/ m[1]);
lastColumn.type = (m[2] || '').replace(/(^{)|(}$)/g, '');
lastColumn.comment = m[3];
continue;
}
if (lastColumn === undefined) {
tableDesc.comment += `${comm}\n`;
} else {
lastColumn.comment = `${lastColumn.comment}\n${comm}`;
}
continue;
}
if (m = line.match(/^\s*NAME\((\w+)\s*,\s*"(\w+)"/)) {
tableDesc.cppClassName = m[1];
if (tableDesc.name === '') {
tableDesc.name = m[2]; // Set only if not overridden by @name.
}
continue;
}
if (m = line.match(/(PERFETTO_TP_ROOT_TABLE|PARENT)\((\w+)/)) {
if (m[1] === 'PARENT') {
tableDesc.parentDefName = m[2];
}
continue;
}
if (m = line.match(/^\s*C\(([^,]+)\s*,\s*(\w+)/)) {
const col = getOrCreateColumn(/*name=*/ m[2]);
col.type = m[1];
if (m = col.type.match(/Optional<(.*)>/)) {
col.type = m[1];
col.optional = true;
}
if (col.type === 'StringPool::Id') {
col.type = 'string';
}
const sep = col.type.indexOf('::');
if (sep > 0) {
col.refTableCppName = col.type.substr(0, sep);
}
continue;
}
throw new Error(`Cannot parse line "${line}" from ${tableDefName}`);
}
if (tableDesc.parentDefName === '') {
id.type = `${tableDesc.cppClassName}::Id`;
type.type = 'string';
} else {
delete tableDesc.cols['id'];
delete tableDesc.cols['type'];
}
// Process {@joinable xxx} annotations.
const regex = /\s?\{@joinable\s*(\w+)\.(\w+)\s*\}/;
for (const col of Object.values(tableDesc.cols)) {
const m = col.comment.match(regex)
if (m) {
col.joinTable = m[1];
col.joinCol = m[2];
col.comment = col.comment.replace(regex, '');
}
}
return tableDesc;
}
function parseTablesInCppFile(filePath) {
const hdr = fs.readFileSync(filePath, 'UTF8');
const regex = /^\s*PERFETTO_TP_TABLE\((\w+)\)/mg;
let match = regex.exec(hdr);
const tables = [];
while (match != null) {
const tableDefName = match[1];
match = regex.exec(hdr);
// Now let's extract the table definition, that looks like this:
// // Some
// // Multiline
// // Comment
// #define PERFETTO_TP_STACK_PROFILE_FRAME_DEF(NAME, PARENT, C) \
// NAME(StackProfileFrameTable, "stack_profile_frame") \
// PERFETTO_TP_ROOT_TABLE(PARENT, C) \
// C(StringPool::Id, name) \
// C(StackProfileMappingTable::Id, mapping) \
// C(int64_t, rel_pc) \
// C(base::Optional<uint32_t>, symbol_set_id)
//
// Where PERFETTO_TP_STACK_PROFILE_FRAME_DEF is |tableDefName|.
let pattern = `(^[ ]*//.*\n)*`;
pattern += `^\s*#define\\s+${tableDefName}\\s*\\(`;
pattern += `(.*\\\\\\s*\n)+`;
pattern += `.+`;
const r = new RegExp(pattern, 'mi');
const tabMatch = r.exec(hdr);
if (!tabMatch) {
console.error(`could not find table ${tableDefName}`);
continue;
}
tables.push(parseTableDef(tableDefName, tabMatch[0]));
}
return tables;
}
function parseTablesInJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'UTF8'));
}
function overrideCppTablesWithJsonTables(cpp, json) {
const out = [];
var jsonLookup = new Map(json.map(i => [i.name, i]));
for (const table of cpp) {
const jsonTable = jsonLookup.get(table.name);
out.push(jsonTable === undefined ? table : jsonTable);
}
return out;
}
function genLink(table) {
return `[${table.name}](#${table.name})`;
}
function tableToMarkdown(table) {
let md = `### ${table.name}\n\n`;
if (table.parent) {
md += `_Extends ${genLink(table.parent)}_\n\n`;
}
md += table.comment + '\n\n';
md += 'Column | Type | Description\n';
md += '------ | ---- | -----------\n';
let curTable = table;
while (curTable) {
if (curTable != table) {
md += `||_Columns inherited from_ ${genLink(curTable)}\n`
}
for (const col of Object.values(curTable.cols)) {
const type = col.type + (col.optional ? '<br>`optional`' : '');
let description = col.comment;
if (col.joinTable) {
description += `\nJoinable with ` +
`[${col.joinTable}.${col.joinCol}](#${col.joinTable})`;
}
md += `${col.name} | ${type} | ${singleLineComment(description)}\n`
}
curTable = curTable.parent;
}
md += '\n\n';
return md;
}
function main() {
const inFile = argv['i'];
const outFile = argv['o'];
const jsonFile = argv['j'];
if (!inFile) {
console.error('Usage: -i hdr1.h -i hdr2.h -j tbls.json -[-o out.md]');
process.exit(1);
}
// Can be either a string (-i single) or an array (-i one -i two).
const inFiles = (inFile instanceof Array) ? inFile : [inFile];
const cppTables =
Array.prototype.concat(...inFiles.map(parseTablesInCppFile));
// Can be either a string (-j single) or an array (-j one -j two).
const jsonFiles = (jsonFile instanceof Array) ? jsonFile : [jsonFile];
const jsonTables =
Array.prototype.concat(...jsonFiles.map(parseTablesInJson));
const tables = overrideCppTablesWithJsonTables(cppTables, jsonTables)
// Resolve parents.
const tablesIndex = {}; // 'TP_SCHED_SLICE_TABLE_DEF' -> table
const tablesByGroup = {}; // 'profilers' => [table1, table2]
const tablesCppName = {}; // 'StackProfileMappingTable' => table
const tablesByName = {}; // 'profile_mapping' => table
for (const table of tables) {
tablesIndex[table.defMacro] = table;
if (tablesByGroup[table.tablegroup] === undefined) {
tablesByGroup[table.tablegroup] = [];
}
tablesCppName[table.cppClassName] = table;
tablesByName[table.name] = table;
tablesByGroup[table.tablegroup].push(table);
}
const tableGroups = Object.keys(tablesByGroup).sort((a, b) => {
const keys = {'Tracks': '1', 'Events': '2', 'Misc': 'z'};
a = `${keys[a]}_${a}`;
b = `${keys[b]}_${b}`;
return a.localeCompare(b);
});
for (const table of tables) {
if (table.parentDefName) {
table.parent = tablesIndex[table.parentDefName];
}
}
// Builds a graph of the tables' relationship that can be rendererd with
// mermaid.js.
let graph = '## Tables diagram\n';
const mkLabel = (table) => `${table.defMacro}["${table.name}"]`;
for (const tableGroup of tableGroups) {
let graphEdges = '';
let graphLinks = '';
graph += `#### ${tableGroup} tables\n`;
graph += '```mermaid\ngraph TD\n';
graph += ` subgraph ${tableGroup}\n`;
for (const table of tablesByGroup[tableGroup]) {
graph += ` ${mkLabel(table)}\n`;
graphLinks += ` click ${table.defMacro} "#${table.name}"\n`
if (table.parent) {
graphEdges += ` ${mkLabel(table)} --> ${mkLabel(table.parent)}\n`
}
for (const col of Object.values(table.cols)) {
let refTable = undefined;
if (col.refTableCppName) {
refTable = tablesCppName[col.refTableCppName];
} else if (col.joinTable) {
refTable = tablesByName[col.joinTable];
if (!refTable) {
throw new Error(`Cannot find @joinable table ${col.joinTable}`);
}
}
if (!refTable)
continue;
graphEdges +=
` ${mkLabel(table)} -. ${col.name} .-> ${mkLabel(refTable)}\n`
graphLinks += ` click ${refTable.defMacro} "#${refTable.name}"\n`
}
}
graph += ` end\n`;
graph += graphEdges;
graph += graphLinks;
graph += '\n```\n';
}
let md = graph;
for (const tableGroup of tableGroups) {
md += `## ${tableGroup}\n`
for (const table of tablesByGroup[tableGroup]) {
md += tableToMarkdown(table);
}
}
if (outFile) {
fs.writeFileSync(outFile, md);
} else {
console.log(md);
}
process.exit(0);
}
main();