You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
12 KiB
371 lines
12 KiB
// 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();
|