// 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 reference from protos '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.replace(/\.\n/g, '
'); 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}${comm}\n`; } 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, 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 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 ? '
`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']; if (!inFile) { console.error('Usage: -i hdr1.h -i hdr2.h -[-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 tables = Array.prototype.concat(...inFiles.map(parseTablesInCppFile)); // 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 gaphEdges = ''; let gaphLinks = ''; graph += `#### ${tableGroup} tables\n`; graph += '```mermaid\ngraph TD\n'; graph += ` subgraph ${tableGroup}\n`; for (const table of tablesByGroup[tableGroup]) { graph += ` ${mkLabel(table)}\n`; gaphLinks += ` click ${table.defMacro} "#${table.name}"\n` if (table.parent) { gaphEdges += ` ${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; gaphEdges += ` ${mkLabel(table)} -. ${col.name} .-> ${mkLabel(refTable)}\n` gaphLinks += ` click ${refTable.defMacro} "#${refTable.name}"\n` } } graph += ` end\n`; graph += gaphEdges; graph += gaphLinks; 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();