1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Generation of reference from protos 16 17'use strict'; 18 19const fs = require('fs'); 20const argv = require('yargs').argv 21 22// Removes \n due to 80col wrapping and preserves only end-of-sentence line 23// breaks. 24// TODO dedupe, this is copied from the other gen_proto file. 25function singleLineComment(comment) { 26 comment = comment || ''; 27 comment = comment.trim(); 28 comment = comment.replace(/\.\n/g, '<br>'); 29 comment = comment.replace(/\n/g, ' '); 30 return comment; 31} 32 33// Returns an object describing the table as follows: 34// { name: 'HeapGraphObjectTable', 35// cols: [ {name: 'upid', type: 'uint32_t', optional: false }, 36// {name: 'graph_sample_ts', type: 'int64_t', optional: false }, 37function parseTableDef(tableDefName, tableDef) { 38 const tableDesc = { 39 name: '', // The SQL table name, e.g. stack_profile_mapping. 40 cppClassName: '', // e.g., StackProfileMappingTable. 41 defMacro: tableDefName, // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF. 42 comment: '', 43 parent: undefined, // Will be filled afterwards in the resolution phase. 44 parentDefName: '', // e.g., PERFETTO_TP_STACK_PROFILE_MAPPING_DEF. 45 tablegroup: 'Misc', // From @tablegroup in comments. 46 cols: {}, 47 }; 48 const getOrCreateColumn = (name) => { 49 if (name in tableDesc.cols) 50 return tableDesc.cols[name]; 51 tableDesc.cols[name] = { 52 name: name, 53 type: '', 54 comment: '', 55 optional: false, 56 refTableCppName: undefined, 57 joinTable: undefined, 58 joinCol: undefined, 59 }; 60 return tableDesc.cols[name]; 61 }; 62 63 // Reserve the id and type columns so they appear first in the column list 64 // They will only be kept in case this is a root table - otherwise they will 65 // be deleted below.. 66 const id = getOrCreateColumn('id'); 67 const type = getOrCreateColumn('type'); 68 69 let lastColumn = undefined; 70 for (const line of tableDef.split('\n')) { 71 if (line.startsWith('#define')) 72 continue; // Skip the first line. 73 let m; 74 if (line.startsWith('//')) { 75 let comm = line.replace(/^\s*\/\/\s*/, ''); 76 if (m = comm.match(/@tablegroup (.*)/)) { 77 tableDesc.tablegroup = m[1]; 78 continue; 79 } 80 if (m = comm.match(/@name (\w+)/)) { 81 tableDesc.name = m[1]; 82 continue; 83 } 84 if (m = comm.match(/@param\s+([^ ]+)\s*({\w+})?\s*(.*)/)) { 85 lastColumn = getOrCreateColumn(/*name=*/ m[1]); 86 lastColumn.type = (m[2] || '').replace(/(^{)|(}$)/g, ''); 87 lastColumn.comment = m[3]; 88 continue; 89 } 90 if (lastColumn === undefined) { 91 tableDesc.comment += `${comm}\n`; 92 } else { 93 lastColumn.comment = `${lastColumn.comment}${comm}\n`; 94 } 95 continue; 96 } 97 if (m = line.match(/^\s*NAME\((\w+)\s*,\s*"(\w+)"/)) { 98 tableDesc.cppClassName = m[1]; 99 if (tableDesc.name === '') { 100 tableDesc.name = m[2]; // Set only if not overridden by @name. 101 } 102 continue; 103 } 104 if (m = line.match(/(PERFETTO_TP_ROOT_TABLE|PARENT)\((\w+)/)) { 105 if (m[1] === 'PARENT') { 106 tableDesc.parentDefName = m[2]; 107 } 108 continue; 109 } 110 if (m = line.match(/^\s*C\(([^,]+)\s*,\s*(\w+)/)) { 111 const col = getOrCreateColumn(/*name=*/ m[2]); 112 col.type = m[1]; 113 if (m = col.type.match(/Optional<(.*)>/)) { 114 col.type = m[1]; 115 col.optional = true; 116 } 117 if (col.type === 'StringPool::Id') { 118 col.type = 'string'; 119 } 120 const sep = col.type.indexOf('::'); 121 if (sep > 0) { 122 col.refTableCppName = col.type.substr(0, sep); 123 } 124 continue; 125 } 126 throw new Error(`Cannot parse line "${line}" from ${tableDefName}`); 127 } 128 129 if (tableDesc.parentDefName === '') { 130 id.type = `${tableDesc.cppClassName}::Id`; 131 type.type = 'string'; 132 } else { 133 delete tableDesc.cols['id']; 134 delete tableDesc.cols['type']; 135 } 136 137 // Process {@joinable xxx} annotations. 138 const regex = /\s?\{@joinable\s*(\w+)\.(\w+)\s*\}/; 139 for (const col of Object.values(tableDesc.cols)) { 140 const m = col.comment.match(regex) 141 if (m) { 142 col.joinTable = m[1]; 143 col.joinCol = m[2]; 144 col.comment = col.comment.replace(regex, ''); 145 } 146 } 147 return tableDesc; 148} 149 150 151function parseTablesInCppFile(filePath) { 152 const hdr = fs.readFileSync(filePath, 'UTF8'); 153 const regex = /^\s*PERFETTO_TP_TABLE\((\w+)\)/mg; 154 let match = regex.exec(hdr); 155 const tables = []; 156 while (match != null) { 157 const tableDefName = match[1]; 158 match = regex.exec(hdr); 159 160 // Now let's extract the table definition, that looks like this: 161 // // Some 162 // // Multiline 163 // // Comment 164 // #define PERFETTO_TP_STACK_PROFILE_FRAME_DEF(NAME, PARENT, C) \ 165 // NAME(StackProfileFrameTable, "stack_profile_frame") \ 166 // PERFETTO_TP_ROOT_TABLE(PARENT, C) \ 167 // C(StringPool::Id, name) \ 168 // C(StackProfileMappingTable::Id, mapping) \ 169 // C(int64_t, rel_pc) \ 170 // C(base::Optional<uint32_t>, symbol_set_id) 171 // 172 // Where PERFETTO_TP_STACK_PROFILE_FRAME_DEF is |tableDefName|. 173 let pattern = `(^[ ]*//.*\n)*`; 174 pattern += `^\s*#define\\s+${tableDefName}\\s*\\(`; 175 pattern += `(.*\\\\\\s*\n)+`; 176 pattern += `.+`; 177 const r = new RegExp(pattern, 'mi'); 178 const tabMatch = r.exec(hdr); 179 if (!tabMatch) { 180 console.error(`could not find table ${tableDefName}`); 181 continue; 182 } 183 tables.push(parseTableDef(tableDefName, tabMatch[0])); 184 } 185 return tables; 186} 187 188 189function genLink(table) { 190 return `[${table.name}](#${table.name})`; 191} 192 193function tableToMarkdown(table) { 194 let md = `### ${table.name}\n\n`; 195 if (table.parent) { 196 md += `_Extends ${genLink(table.parent)}_\n\n`; 197 } 198 md += table.comment + '\n\n'; 199 md += 'Column | Type | Description\n'; 200 md += '------ | ---- | -----------\n'; 201 202 let curTable = table; 203 while (curTable) { 204 if (curTable != table) { 205 md += `||_Columns inherited from_ ${genLink(curTable)}\n` 206 } 207 for (const col of Object.values(curTable.cols)) { 208 const type = col.type + (col.optional ? '<br>`optional`' : ''); 209 let description = col.comment; 210 if (col.joinTable) { 211 description += `\nJoinable with ` + 212 `[${col.joinTable}.${col.joinCol}](#${col.joinTable})`; 213 } 214 md += `${col.name} | ${type} | ${singleLineComment(description)}\n` 215 } 216 curTable = curTable.parent; 217 } 218 md += '\n\n'; 219 return md; 220} 221 222function main() { 223 const inFile = argv['i']; 224 const outFile = argv['o']; 225 if (!inFile) { 226 console.error('Usage: -i hdr1.h -i hdr2.h -[-o out.md]'); 227 process.exit(1); 228 } 229 230 // Can be either a string (-i single) or an array (-i one -i two). 231 const inFiles = (inFile instanceof Array) ? inFile : [inFile]; 232 233 const tables = Array.prototype.concat(...inFiles.map(parseTablesInCppFile)); 234 235 // Resolve parents. 236 const tablesIndex = {}; // 'TP_SCHED_SLICE_TABLE_DEF' -> table 237 const tablesByGroup = {}; // 'profilers' => [table1, table2] 238 const tablesCppName = {}; // 'StackProfileMappingTable' => table 239 const tablesByName = {}; // 'profile_mapping' => table 240 for (const table of tables) { 241 tablesIndex[table.defMacro] = table; 242 if (tablesByGroup[table.tablegroup] === undefined) { 243 tablesByGroup[table.tablegroup] = []; 244 } 245 tablesCppName[table.cppClassName] = table; 246 tablesByName[table.name] = table; 247 tablesByGroup[table.tablegroup].push(table); 248 } 249 const tableGroups = Object.keys(tablesByGroup).sort((a, b) => { 250 const keys = {'Tracks': '1', 'Events': '2', 'Misc': 'z'}; 251 a = `${keys[a]}_${a}`; 252 b = `${keys[b]}_${b}`; 253 return a.localeCompare(b); 254 }); 255 256 for (const table of tables) { 257 if (table.parentDefName) { 258 table.parent = tablesIndex[table.parentDefName]; 259 } 260 } 261 262 // Builds a graph of the tables' relationship that can be rendererd with 263 // mermaid.js. 264 let graph = '## Tables diagram\n'; 265 const mkLabel = (table) => `${table.defMacro}["${table.name}"]`; 266 for (const tableGroup of tableGroups) { 267 let gaphEdges = ''; 268 let gaphLinks = ''; 269 graph += `#### ${tableGroup} tables\n`; 270 graph += '```mermaid\ngraph TD\n'; 271 graph += ` subgraph ${tableGroup}\n`; 272 for (const table of tablesByGroup[tableGroup]) { 273 graph += ` ${mkLabel(table)}\n`; 274 gaphLinks += ` click ${table.defMacro} "#${table.name}"\n` 275 if (table.parent) { 276 gaphEdges += ` ${mkLabel(table)} --> ${mkLabel(table.parent)}\n` 277 } 278 279 for (const col of Object.values(table.cols)) { 280 let refTable = undefined; 281 if (col.refTableCppName) { 282 refTable = tablesCppName[col.refTableCppName]; 283 } else if (col.joinTable) { 284 refTable = tablesByName[col.joinTable]; 285 if (!refTable) { 286 throw new Error(`Cannot find @joinable table ${col.joinTable}`); 287 } 288 } 289 if (!refTable) 290 continue; 291 gaphEdges += 292 ` ${mkLabel(table)} -. ${col.name} .-> ${mkLabel(refTable)}\n` 293 gaphLinks += ` click ${refTable.defMacro} "#${refTable.name}"\n` 294 } 295 } 296 graph += ` end\n`; 297 graph += gaphEdges; 298 graph += gaphLinks; 299 graph += '\n```\n'; 300 } 301 302 let md = graph; 303 for (const tableGroup of tableGroups) { 304 md += `## ${tableGroup}\n` 305 for (const table of tablesByGroup[tableGroup]) { 306 md += tableToMarkdown(table); 307 } 308 } 309 310 if (outFile) { 311 fs.writeFileSync(outFile, md); 312 } else { 313 console.log(md); 314 } 315 process.exit(0); 316} 317 318main(); 319