| #!/usr/bin/env python3 |
| # Copyright (C) 2022 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 |
| # disibuted under the License is disibuted 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. |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import argparse |
| import sys |
| import json |
| from typing import Any, List, Dict |
| |
| INTRODUCTION = ''' |
| # PerfettoSQL standard library |
| *This page documents the PerfettoSQL standard library.* |
| |
| ## Introduction |
| The PerfettoSQL standard library is a repository of tables, views, functions |
| and macros, contributed by domain experts, which make querying traces easier |
| Its design is heavily inspired by standard libraries in languages like Python, |
| C++ and Java. |
| |
| Some of the purposes of the standard library include: |
| 1) Acting as a way of sharing and commonly written queries without needing |
| to copy/paste large amounts of SQL. |
| 2) Raising the abstraction level when exposing data in the trace. Many |
| modules in the standard library convert low-level trace concepts |
| e.g. slices, tracks and into concepts developers may be more familar with |
| e.g. for Android developers: app startups, binder transactions etc. |
| |
| Standard library modules can be included as follows: |
| ``` |
| -- Include all tables/views/functions from the android.startup.startups |
| -- module in the standard library. |
| INCLUDE PERFETTO MODULE android.startup.startups; |
| |
| -- Use the android_startups table defined in the android.startup.startups |
| -- module. |
| SELECT * |
| FROM android_startups; |
| ``` |
| |
| Prelude is a special module is automatically imported. It contains key helper |
| tables, views and functions which are universally useful. |
| |
| More information on importing modules is available in the |
| [syntax documentation](/docs/analysis/perfetto-sql-syntax#including-perfettosql-modules) |
| for the `INCLUDE PERFETTO MODULE` statement. |
| |
| <!-- TODO(b/290185551): talk about experimental module and contributions. --> |
| |
| ## Summary |
| ''' |
| |
| |
| def _escape_in_table(desc: str): |
| """Escapes special characters in a markdown table.""" |
| return desc.replace('|', '\\|') |
| |
| |
| def _md_table(cols: List[str]): |
| col_str = ' | '.join(cols) + '\n' |
| lines = ['-' * len(col) for col in cols] |
| underlines = ' | '.join(lines) |
| return col_str + underlines |
| |
| |
| def _write_summary(sql_type: str, table_cols: List[str], |
| summary_objs: List[str]) -> str: |
| table_data = '\n'.join(s.strip() for s in summary_objs if s) |
| return f""" |
| ### {sql_type} |
| |
| {_md_table(table_cols)} |
| {table_data} |
| |
| """ |
| |
| |
| class FileMd: |
| """Responsible for file level markdown generation.""" |
| |
| def __init__(self, module_name, file_dict): |
| self.import_key = file_dict['import_key'] |
| import_key_name = self.import_key if module_name != 'prelude' else 'N/A' |
| self.objs, self.funs, self.view_funs, self.macros = [], [], [], [] |
| summary_objs_list, summary_funs_list, summary_view_funs_list, summary_macros_list = [], [], [], [] |
| |
| # Add imports if in file. |
| for data in file_dict['imports']: |
| # Anchor |
| anchor = f'''obj/{module_name}/{data['name']}''' |
| |
| # Add summary of imported view/table |
| summary_objs_list.append(f'''[{data['name']}](#{anchor})|''' |
| f'''{import_key_name}|''' |
| f'''{_escape_in_table(data['summary_desc'])}''') |
| |
| self.objs.append(f'''\n\n<a name="{anchor}"></a>''' |
| f'''**{data['name']}**, {data['type']}\n\n''' |
| f'''{_escape_in_table(data['desc'])}\n''') |
| |
| self.objs.append(_md_table(['Column', 'Type', 'Description'])) |
| for name, info in data['cols'].items(): |
| self.objs.append( |
| f'{name} | {info["type"]} | {_escape_in_table(info["desc"])}') |
| |
| self.objs.append('\n\n') |
| |
| # Add functions if in file |
| for data in file_dict['functions']: |
| # Anchor |
| anchor = f'''fun/{module_name}/{data['name']}''' |
| |
| # Add summary of imported function |
| summary_funs_list.append(f'''[{data['name']}](#{anchor})|''' |
| f'''{import_key_name}|''' |
| f'''{data['return_type']}|''' |
| f'''{_escape_in_table(data['summary_desc'])}''') |
| self.funs.append( |
| f'''\n\n<a name="{anchor}"></a>''' |
| f'''**{data['name']}**\n\n''' |
| f'''{data['desc']}\n\n''' |
| f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''') |
| if data['args']: |
| self.funs.append(_md_table(['Argument', 'Type', 'Description'])) |
| for name, arg_dict in data['args'].items(): |
| self.funs.append( |
| f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}''' |
| ) |
| |
| self.funs.append('\n\n') |
| |
| # Add table functions if in file |
| for data in file_dict['table_functions']: |
| # Anchor |
| anchor = rf'''view_fun/{module_name}/{data['name']}''' |
| # Add summary of imported view function |
| summary_view_funs_list.append( |
| f'''[{data['name']}](#{anchor})|''' |
| f'''{import_key_name}|''' |
| f'''{_escape_in_table(data['summary_desc'])}''') |
| |
| self.view_funs.append(f'''\n\n<a name="{anchor}"></a>''' |
| f'''**{data['name']}**\n''' |
| f'''{data['desc']}\n\n''') |
| if data['args']: |
| self.funs.append(_md_table(['Argument', 'Type', 'Description'])) |
| for name, arg_dict in data['args'].items(): |
| self.view_funs.append( |
| f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}''' |
| ) |
| self.view_funs.append('\n') |
| self.view_funs.append(_md_table(['Column', 'Type', 'Description'])) |
| for name, column in data['cols'].items(): |
| self.view_funs.append(f'{name} | {column["type"]} | {column["desc"]}') |
| |
| self.view_funs.append('\n\n') |
| |
| # Add macros if in file |
| for data in file_dict['macros']: |
| # Anchor |
| anchor = rf'''macro/{module_name}/{data['name']}''' |
| # Add summary of imported view function |
| summary_macros_list.append( |
| f'''[{data['name']}](#{anchor})|''' |
| f'''{import_key_name}|''' |
| f'''{_escape_in_table(data['summary_desc'])}''') |
| |
| self.macros.append( |
| f'''\n\n<a name="{anchor}"></a>''' |
| f'''**{data['name']}**\n''' |
| f'''{data['desc']}\n\n''' |
| f'''Returns: {data['return_type']}, {data['return_desc']}\n\n''') |
| if data['args']: |
| self.macros.append(_md_table(['Argument', 'Type', 'Description'])) |
| for name, arg_dict in data['args'].items(): |
| self.macros.append( |
| f'''{name} | {arg_dict['type']} | {_escape_in_table(arg_dict['desc'])}''' |
| ) |
| self.macros.append('\n') |
| self.macros.append('\n\n') |
| |
| self.summary_objs = '\n'.join(summary_objs_list) |
| self.summary_funs = '\n'.join(summary_funs_list) |
| self.summary_view_funs = '\n'.join(summary_view_funs_list) |
| self.summary_macros = '\n'.join(summary_macros_list) |
| |
| |
| class ModuleMd: |
| """Responsible for module level markdown generation.""" |
| |
| def __init__(self, module_name: str, module_files: List[Dict[str, |
| Any]]) -> None: |
| self.module_name = module_name |
| self.files_md = sorted( |
| [FileMd(module_name, file_dict) for file_dict in module_files], |
| key=lambda x: x.import_key) |
| self.summary_objs = '\n'.join( |
| file.summary_objs for file in self.files_md if file.summary_objs) |
| self.summary_funs = '\n'.join( |
| file.summary_funs for file in self.files_md if file.summary_funs) |
| self.summary_view_funs = '\n'.join(file.summary_view_funs |
| for file in self.files_md |
| if file.summary_view_funs) |
| self.summary_macros = '\n'.join( |
| file.summary_macros for file in self.files_md if file.summary_macros) |
| |
| def get_prelude_description(self) -> str: |
| if not self.module_name == 'prelude': |
| raise ValueError("Only callable on prelude module") |
| |
| lines = [] |
| lines.append(f'## Module: {self.module_name}') |
| |
| # Prelude is a special module which is automatically imported and doesn't |
| # have any include keys. |
| objs = '\n'.join(obj for file in self.files_md for obj in file.objs) |
| if objs: |
| lines.append('#### Views/Tables') |
| lines.append(objs) |
| |
| funs = '\n'.join(fun for file in self.files_md for fun in file.funs) |
| if funs: |
| lines.append('#### Functions') |
| lines.append(funs) |
| |
| table_funs = '\n'.join( |
| view_fun for file in self.files_md for view_fun in file.view_funs) |
| if table_funs: |
| lines.append('#### Table Functions') |
| lines.append(table_funs) |
| |
| macros = '\n'.join(macro for file in self.files_md for macro in file.macros) |
| if macros: |
| lines.append('#### Macros') |
| lines.append(macros) |
| |
| return '\n'.join(lines) |
| |
| def get_description(self) -> str: |
| if not self.files_md: |
| return '' |
| |
| if self.module_name == 'prelude': |
| raise ValueError("Can't be called with prelude module") |
| |
| lines = [] |
| lines.append(f'## Module: {self.module_name}') |
| |
| for file in self.files_md: |
| if not any((file.objs, file.funs, file.view_funs, file.macros)): |
| continue |
| |
| lines.append(f'### {file.import_key}') |
| if file.objs: |
| lines.append('#### Views/Tables') |
| lines.append('\n'.join(file.objs)) |
| if file.funs: |
| lines.append('#### Functions') |
| lines.append('\n'.join(file.funs)) |
| if file.view_funs: |
| lines.append('#### Table Functions') |
| lines.append('\n'.join(file.view_funs)) |
| if file.macros: |
| lines.append('#### Macros') |
| lines.append('\n'.join(file.macros)) |
| |
| return '\n'.join(lines) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--input', required=True) |
| parser.add_argument('--output', required=True) |
| args = parser.parse_args() |
| |
| with open(args.input) as f: |
| modules_json_dict = json.load(f) |
| |
| # Fetch the modules from json documentation. |
| modules_dict: Dict[str, ModuleMd] = {} |
| for module_name, module_files in modules_json_dict.items(): |
| # Remove 'common' when it has been removed from the code. |
| if module_name not in ['deprecated', 'common']: |
| modules_dict[module_name] = ModuleMd(module_name, module_files) |
| |
| prelude_module = modules_dict.pop('prelude') |
| |
| with open(args.output, 'w') as f: |
| f.write(INTRODUCTION) |
| |
| summary_objs = [prelude_module.summary_objs |
| ] if prelude_module.summary_objs else [] |
| summary_objs += [ |
| module.summary_objs |
| for module in modules_dict.values() |
| if (module.summary_objs) |
| ] |
| |
| summary_funs = [prelude_module.summary_funs |
| ] if prelude_module.summary_funs else [] |
| summary_funs += [module.summary_funs for module in modules_dict.values()] |
| summary_view_funs = [prelude_module.summary_view_funs |
| ] if prelude_module.summary_view_funs else [] |
| summary_view_funs += [ |
| module.summary_view_funs for module in modules_dict.values() |
| ] |
| summary_macros = [prelude_module.summary_macros |
| ] if prelude_module.summary_macros else [] |
| summary_macros += [ |
| module.summary_macros for module in modules_dict.values() |
| ] |
| |
| if summary_objs: |
| f.write( |
| _write_summary('Views/tables', ['Name', 'Import', 'Description'], |
| summary_objs)) |
| |
| if summary_funs: |
| f.write( |
| _write_summary('Functions', |
| ['Name', 'Import', 'Return type', 'Description'], |
| summary_funs)) |
| |
| if summary_view_funs: |
| f.write( |
| _write_summary('Table functions', ['Name', 'Import', 'Description'], |
| summary_view_funs)) |
| |
| if summary_macros: |
| f.write( |
| _write_summary('Macros', ['Name', 'Import', 'Description'], |
| summary_macros)) |
| |
| f.write('\n\n') |
| f.write(prelude_module.get_prelude_description()) |
| f.write('\n') |
| f.write('\n'.join( |
| module.get_description() for module in modules_dict.values())) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |