| // Copyright (C) 2021 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. |
| |
| 'use strict'; |
| |
| // This script builds the perfetto.dev docs website. |
| |
| const argparse = require('argparse'); |
| const child_process = require('child_process'); |
| const fs = require('fs'); |
| const http = require('http'); |
| const path = require('path'); |
| const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux. |
| const pjoin = path.join; |
| |
| const ROOT_DIR = path.dirname(path.dirname(__dirname)); // The repo root. |
| |
| const cfg = { |
| watch: false, |
| verbose: false, |
| startHttpServer: false, |
| |
| outDir: pjoin(ROOT_DIR, 'out/perfetto.dev'), |
| }; |
| |
| const RULES = [ |
| {r: /infra\/perfetto.dev\/src\/assets\/((.*)\.png)/, f: copyAssets}, |
| {r: /infra\/perfetto.dev\/src\/assets\/((.*)\.js)/, f: copyAssets}, |
| {r: /infra\/perfetto.dev\/node_modules\/.*\/(.*\.css|.*\.js)/, f: copyAssets}, |
| {r: /infra\/perfetto.dev\/src\/assets\/.+\.scss/, f: compileScss}, |
| { |
| r: /protos\/perfetto\/config\/trace_config\.proto/, |
| f: s => genProtoReference(s, 'perfetto.protos.TraceConfig') |
| }, |
| { |
| r: /protos\/perfetto\/trace\/trace_packet\.proto/, |
| f: s => genProtoReference(s, 'perfetto.protos.TracePacket') |
| }, |
| {r: /src\/trace_processor\/storage\/stats\.h/, f: genSqlStatsReference}, |
| {r: /src\/trace_processor\/tables\/.*\.h/, f: s => sqlTables.add(s)}, |
| {r: /docs\/toc[.]md/, f: genNav}, |
| {r: /docs\/.*[.]md/, f: renderDoc}, |
| ]; |
| |
| let sqlTables = new Set(); |
| let tasks = []; |
| let tasksTot = 0, tasksRan = 0; |
| let tStart = Date.now(); |
| |
| function main() { |
| const parser = new argparse.ArgumentParser(); |
| parser.add_argument('--out', {help: 'Output directory'}); |
| parser.add_argument('--watch', '-w', {action: 'store_true'}); |
| parser.add_argument('--serve', '-s', {action: 'store_true'}); |
| parser.add_argument('--verbose', '-v', {action: 'store_true'}); |
| |
| const args = parser.parse_args(); |
| cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir, /*clean=*/ true)); |
| cfg.watch = !!args.watch; |
| cfg.verbose = !!args.verbose; |
| cfg.startHttpServer = args.serve; |
| |
| // Check that deps are current before starting. |
| const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps'); |
| |
| // --filter=nodejs is to match what cloud_build_entrypoint.sh passes to |
| // install-build-deps. It doesn't bother installing the full toolchains |
| // because, unlike the Perfetto UI, it doesn't need Wasm. |
| const depsArgs = ['--check-only=/dev/null', '--ui', '--filter=nodejs']; |
| exec(installBuildDeps, depsArgs); |
| |
| console.log('Entering', cfg.outDir); |
| process.chdir(cfg.outDir); |
| |
| scanDir('infra/perfetto.dev/src/assets'); |
| scanFile( |
| 'infra/perfetto.dev/node_modules/highlight.js/styles/tomorrow-night.css'); |
| scanFile('infra/perfetto.dev/node_modules/mermaid/dist/mermaid.min.js'); |
| scanFile('docs/toc.md'); |
| genIndex(); |
| scanFile('src/trace_processor/storage/stats.h'); |
| scanDir('src/trace_processor/tables'); |
| scanDir('protos'); |
| genSqlTableReference(); |
| scanDir('docs'); |
| if (args.serve) { |
| addTask(startServer); |
| } |
| } |
| |
| // ----------- |
| // Build rules |
| // ----------- |
| |
| function copyAssets(src, dst) { |
| addTask(cp, [src, pjoin(cfg.outDir, 'assets', dst)]); |
| } |
| |
| function compileScss() { |
| const src = pjoin(__dirname, 'src/assets/style.scss'); |
| const dst = pjoin(cfg.outDir, 'assets/style.css'); |
| // In watch mode, don't exit(1) if scss fails. It can easily happen by |
| // having a typo in the css. It will still print an errror. |
| const noErrCheck = !!cfg.watch; |
| addTask( |
| execNode, |
| ['node_modules/.bin/node-sass', ['--quiet', src, dst], {noErrCheck}]); |
| } |
| |
| function md2html(src, dst, template) { |
| const script = pjoin(__dirname, 'src/markdown_render.js'); |
| const args = ['-i', src, '--odir', cfg.outDir, '-o', dst]; |
| ensureDir(path.dirname(dst)); |
| if (template) args.push('-t', pjoin(__dirname, 'src', template)); |
| execNode(script, args); |
| } |
| |
| function proto2md(src, dst, protoRootType) { |
| const script = pjoin(__dirname, 'src/gen_proto_reference.js'); |
| const args = ['-i', src, '-p', protoRootType, '-o', dst]; |
| ensureDir(path.dirname(dst)); |
| execNode(script, args); |
| } |
| |
| function genNav(src) { |
| const dst = pjoin(cfg.outDir, 'docs', '_nav.html'); |
| addTask(md2html, [src, dst]); |
| } |
| |
| function genIndex() { |
| const dst = pjoin(cfg.outDir, 'index.html'); |
| addTask(md2html, ['/dev/null', dst, 'template_index.html']); |
| } |
| |
| function renderDoc(src) { |
| let dstRel = path.relative(ROOT_DIR, src); |
| dstRel = dstRel.replace('.md', '').replace(/\bREADME$/, 'index.html'); |
| const dst = pjoin(cfg.outDir, dstRel); |
| addTask(md2html, [src, dst, 'template_markdown.html']); |
| } |
| |
| function genProtoReference(src, protoRootType) { |
| const fname = path.basename(src); |
| const dstFname = fname.replace(/[._]/g, '-'); |
| const dstHtml = pjoin(cfg.outDir, 'docs/reference', dstFname); |
| const dstMd = dstHtml + '.md'; |
| addTask(proto2md, [src, dstMd, protoRootType]); |
| addTask(md2html, [dstMd, dstHtml, 'template_markdown.html']); |
| addTask(exec, ['rm', [dstMd]]); |
| } |
| |
| function genSqlStatsReference(src) { |
| const dstDir = ensureDir(pjoin(cfg.outDir, 'docs/analysis')); |
| const dstHtml = pjoin(dstDir, 'sql-stats'); |
| const dstMd = dstHtml + '.md'; |
| const script = pjoin(__dirname, 'src/gen_stats_reference.js'); |
| const args = ['-i', src, '-o', dstMd]; |
| addTask(execNode, [script, args]); |
| addTask(md2html, [dstMd, dstHtml, 'template_markdown.html']); |
| addTask(exec, ['rm', [dstMd]]); |
| } |
| |
| function genSqlTableReference() { |
| const dstDir = ensureDir(pjoin(cfg.outDir, 'docs/analysis')); |
| const dstHtml = pjoin(dstDir, 'sql-tables'); |
| const dstMd = dstHtml + '.md'; |
| const script = pjoin(__dirname, 'src/gen_sql_tables_reference.js'); |
| const args = ['-o', dstMd]; |
| sqlTables.forEach(f => args.push('-i', f)); |
| addTask(execNode, [script, args]); |
| addTask(md2html, [dstMd, dstHtml, 'template_markdown.html']); |
| addTask(exec, ['rm', [dstMd]]); |
| } |
| |
| function startServer() { |
| const port = 8082; |
| console.log(`Starting HTTP server on http://localhost:${port}`) |
| http.createServer(function(req, res) { |
| console.debug(req.method, req.url); |
| let uri = req.url.split('?', 1)[0]; |
| uri += uri.endsWith('/') ? 'index.html' : ''; |
| |
| const absPath = path.normalize(path.join(cfg.outDir, uri)); |
| fs.readFile(absPath, function(err, data) { |
| if (err) { |
| res.writeHead(404); |
| res.end(JSON.stringify(err)); |
| return; |
| } |
| const mimeMap = { |
| 'css': 'text/css', |
| 'svg': 'image/svg+xml', |
| 'js': 'application/javascript', |
| }; |
| const contentType = mimeMap[uri.split('.').pop()] || 'text/html'; |
| const head = { |
| 'Content-Type': contentType, |
| 'Content-Length': data.length, |
| 'Cache-Control': 'no-cache', |
| }; |
| res.writeHead(200, head); |
| res.end(data); |
| }); |
| }) |
| .listen(port); |
| } |
| |
| |
| // ----------------------- |
| // Task chaining functions |
| // ----------------------- |
| |
| function addTask(func, args) { |
| const task = new Task(func, args); |
| for (const t of tasks) { |
| if (t.identity === task.identity) { |
| return; |
| } |
| } |
| tasks.push(task); |
| setTimeout(runTasks, 0); |
| } |
| |
| function runTasks() { |
| const snapTasks = tasks.splice(0); // snap = std::move(tasks). |
| tasksTot += snapTasks.length; |
| for (const task of snapTasks) { |
| const DIM = '\u001b[2m'; |
| const BRT = '\u001b[37m'; |
| const RST = '\u001b[0m'; |
| const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1); |
| const ts = `[${DIM}${ms}${RST}]`; |
| const descr = task.description.substr(0, 80); |
| console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`); |
| task.func.apply(/*this=*/ undefined, task.args); |
| } |
| } |
| |
| // Executes the first rule in RULES that match the given |absPath|. |
| function scanFile(file) { |
| const absPath = path.isAbsolute(file) ? file : pjoin(ROOT_DIR, file); |
| console.assert(fs.existsSync(absPath)); |
| const normPath = path.relative(ROOT_DIR, absPath); |
| for (const rule of RULES) { |
| const match = rule.r.exec(normPath); |
| if (!match || match[0] !== normPath) continue; |
| const captureGroup = match.length > 1 ? match[1] : undefined; |
| rule.f(absPath, captureGroup); |
| return; |
| } |
| } |
| |
| // Walks the passed |dir| recursively and, for each file, invokes the matching |
| // RULES. If --watch is used, it also installs a fswatch() and re-triggers the |
| // matching RULES on each file change. |
| function scanDir(dir, regex) { |
| const filterFn = regex ? absPath => regex.test(absPath) : () => true; |
| const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir); |
| // Add a fs watch if in watch mode. |
| if (cfg.watch) { |
| fswatch(absDir, {recursive: true}, (_eventType, filePath) => { |
| if (!filterFn(filePath)) return; |
| if (cfg.verbose) { |
| console.log('File change detected', _eventType, filePath); |
| } |
| if (fs.existsSync(filePath)) { |
| scanFile(filePath, filterFn); |
| } |
| }); |
| } |
| walk(absDir, f => { |
| if (filterFn(f)) scanFile(f); |
| }); |
| } |
| |
| function exec(cmd, args, opts) { |
| opts = opts || {}; |
| opts.stdout = opts.stdout || 'inherit'; |
| if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`); |
| const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']}; |
| const checkExitCode = (code, signal) => { |
| if (signal === 'SIGINT' || signal === 'SIGTERM') return; |
| if (code !== 0 && !opts.noErrCheck) { |
| console.error(`${cmd} ${args.join(' ')} failed with code ${code}`); |
| process.exit(1); |
| } |
| }; |
| const spawnRes = child_process.spawnSync(cmd, args, spwOpts); |
| checkExitCode(spawnRes.status, spawnRes.signal); |
| return spawnRes; |
| } |
| |
| function execNode(script, args, opts) { |
| const modPath = path.isAbsolute(script) ? script : pjoin(__dirname, script); |
| const nodeBin = pjoin(ROOT_DIR, 'tools/node'); |
| args = [modPath].concat(args || []); |
| return exec(nodeBin, args, opts); |
| } |
| |
| // ------------------------------------------ |
| // File system & subprocess utility functions |
| // ------------------------------------------ |
| |
| class Task { |
| constructor(func, args) { |
| this.func = func; |
| this.args = args || []; |
| // |identity| is used to dedupe identical tasks in the queue. |
| this.identity = JSON.stringify([this.func.name, this.args]); |
| } |
| |
| get description() { |
| const ret = this.func.name.startsWith('exec') ? [] : [this.func.name]; |
| const flattenedArgs = [].concat.apply([], this.args); |
| for (const arg of flattenedArgs) { |
| const argStr = `${arg}`; |
| if (argStr.startsWith('/')) { |
| ret.push(path.relative(cfg.outDir, arg)); |
| } else { |
| ret.push(argStr); |
| } |
| } |
| return ret.join(' '); |
| } |
| } |
| |
| function walk(dir, callback, skipRegex) { |
| for (const child of fs.readdirSync(dir)) { |
| const childPath = pjoin(dir, child); |
| const stat = fs.lstatSync(childPath); |
| if (skipRegex !== undefined && skipRegex.test(child)) continue; |
| if (stat.isDirectory()) { |
| walk(childPath, callback, skipRegex); |
| } else if (!stat.isSymbolicLink()) { |
| callback(childPath); |
| } |
| } |
| } |
| |
| function ensureDir(dirPath, clean) { |
| const exists = fs.existsSync(dirPath); |
| if (exists && clean) { |
| if (cfg.verbose) console.log('rm', dirPath); |
| fs.rmSync(dirPath, {recursive: true}); |
| } |
| if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true}); |
| return dirPath; |
| } |
| |
| function cp(src, dst) { |
| ensureDir(path.dirname(dst)); |
| if (cfg.verbose) { |
| console.log( |
| 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst)); |
| } |
| fs.copyFileSync(src, dst); |
| } |
| |
| main(); |