1// Copyright (C) 2021 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'use strict'; 16 17 18// This script takes care of: 19// - The build process for the whole UI and the chrome extension. 20// - The HTTP dev-server with live-reload capabilities. 21// The reason why this is a hand-rolled script rather than a conventional build 22// system is keeping incremental build fast and maintaining the set of 23// dependencies contained. 24// The only way to keep incremental build fast (i.e. O(seconds) for the 25// edit-one-line -> reload html cycles) is to run both the TypeScript compiler 26// and the rollup bundler in --watch mode. Any other attempt, leads to O(10s) 27// incremental-build times. 28// This script allows mixing build tools that support --watch mode (tsc and 29// rollup) and auto-triggering-on-file-change rules via node-watch. 30// When invoked without any argument (e.g., for production builds), this script 31// just runs all the build tasks serially. It doesn't to do any mtime-based 32// check, it always re-runs all the tasks. 33// When invoked with --watch, it mounts a pipeline of tasks based on node-watch 34// and runs them together with tsc --watch and rollup --watch. 35// The output directory structure is carefully crafted so that any change to UI 36// sources causes cascading triggers of the next steps. 37// The overall build graph looks as follows: 38// +----------------+ +-----------------------------+ 39// | protos/*.proto |----->| pbjs out/tsc/gen/protos.js |--+ 40// +----------------+ +-----------------------------+ | 41// +-----------------------------+ | 42// | pbts out/tsc/gen/protos.d.ts|<-+ 43// +-----------------------------+ 44// | 45// V +-------------------------+ 46// +---------+ +-----+ | out/tsc/frontend/*.js | 47// | ui/*.ts |------------->| tsc |-> +-------------------------+ +--------+ 48// +---------+ +-----+ | out/tsc/controller/*.js |-->| rollup | 49// ^ +-------------------------+ +--------+ 50// +------------+ | out/tsc/engine/*.js | | 51// +-----------+ |*.wasm.js | +-------------------------+ | 52// |ninja *.cc |->|*.wasm.d.ts | | 53// +-----------+ |*.wasm |-----------------+ | 54// +------------+ | | 55// V V 56// +-----------+ +------+ +------------------------------------------------+ 57// | ui/*.scss |->| scss |--->| Final out/dist/ dir | 58// +-----------+ +------+ +------------------------------------------------+ 59// +----------------------+ | +----------+ +---------+ +--------------------+| 60// | src/assets/*.png | | | assets/ | |*.wasm.js| | frontend_bundle.js || 61// +----------------------+ | | *.css | |*.wasm | +--------------------+| 62// | buildtools/typefaces |-->| | *.png | +---------+ | engine_bundle.js || 63// +----------------------+ | | *.woff2 | +--------------------+| 64// | buildtools/legacy_tv | | | tv.html | |traceconv_bundle.js || 65// +----------------------+ | +----------+ +--------------------+| 66// +------------------------------------------------+ 67 68const argparse = require('argparse'); 69const childProcess = require('child_process'); 70const crypto = require('crypto'); 71const fs = require('fs'); 72const http = require('http'); 73const path = require('path'); 74const fswatch = require('node-watch'); // Like fs.watch(), but works on Linux. 75const pjoin = path.join; 76 77const ROOT_DIR = path.dirname(__dirname); // The repo root. 78const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py'); 79const GEN_IMPORTS_SCRIPT = pjoin(ROOT_DIR, 'tools/gen_ui_imports'); 80 81const cfg = { 82 minifyJs: '', 83 watch: false, 84 verbose: false, 85 debug: false, 86 bigtrace: false, 87 startHttpServer: false, 88 httpServerListenHost: '127.0.0.1', 89 httpServerListenPort: 10000, 90 wasmModules: ['trace_processor', 'traceconv'], 91 crossOriginIsolation: false, 92 testFilter: '', 93 noOverrideGnArgs: false, 94 95 // The fields below will be changed by main() after cmdline parsing. 96 // Directory structure: 97 // out/xxx/ -> outDir : Root build dir, for both ninja/wasm and UI. 98 // ui/ -> outUiDir : UI dir. All outputs from this script. 99 // tsc/ -> outTscDir : Transpiled .ts -> .js. 100 // gen/ -> outGenDir : Auto-generated .ts/.js (e.g. protos). 101 // dist/ -> outDistRootDir : Only index.html and service_worker.js 102 // v1.2/ -> outDistDir : JS bundles and assets 103 // chrome_extension/ : Chrome extension. 104 outDir: pjoin(ROOT_DIR, 'out/ui'), 105 version: '', // v1.2.3, derived from the CHANGELOG + git. 106 outUiDir: '', 107 outUiTestArtifactsDir: '', 108 outDistRootDir: '', 109 outTscDir: '', 110 outGenDir: '', 111 outDistDir: '', 112 outExtDir: '', 113 outBigtraceDistDir: '', 114 outOpenPerfettoTraceDistDir: '', 115}; 116 117const RULES = [ 118 {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml}, 119 {r: /ui\/src\/assets\/bigtrace.html/, f: copyBigtraceHtml}, 120 {r: /ui\/src\/open_perfetto_trace\/index.html/, f: copyOpenPerfettoTraceHtml}, 121 {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets}, 122 {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets}, 123 {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets}, 124 {r: /ui\/src\/assets\/.+[.]scss/, f: compileScss}, 125 {r: /ui\/src\/chrome_extension\/.*/, f: copyExtensionAssets}, 126 { 127 r: /ui\/src\/test\/diff_viewer\/(.+[.](?:html|js))/, 128 f: copyUiTestArtifactsAssets, 129 }, 130 {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson}, 131 {r: /.*\/dist\/.*[.](js|html|css|wasm)$/, f: notifyLiveServer}, 132]; 133 134const tasks = []; 135let tasksTot = 0; 136let tasksRan = 0; 137const httpWatches = []; 138const tStart = Date.now(); 139const subprocesses = []; 140 141async function main() { 142 const parser = new argparse.ArgumentParser(); 143 parser.add_argument('--out', {help: 'Output directory'}); 144 parser.add_argument('--minify-js', { 145 help: 'Minify js files', 146 choices: ['preserve_comments', 'all'], 147 }); 148 parser.add_argument('--watch', '-w', {action: 'store_true'}); 149 parser.add_argument('--serve', '-s', {action: 'store_true'}); 150 parser.add_argument('--serve-host', {help: '--serve bind host'}); 151 parser.add_argument('--serve-port', {help: '--serve bind port', type: 'int'}); 152 parser.add_argument('--verbose', '-v', {action: 'store_true'}); 153 parser.add_argument('--no-build', '-n', {action: 'store_true'}); 154 parser.add_argument('--no-wasm', '-W', {action: 'store_true'}); 155 parser.add_argument('--run-unittests', '-t', {action: 'store_true'}); 156 parser.add_argument('--run-integrationtests', '-T', {action: 'store_true'}); 157 parser.add_argument('--debug', '-d', {action: 'store_true'}); 158 parser.add_argument('--bigtrace', {action: 'store_true'}); 159 parser.add_argument('--open-perfetto-trace', {action: 'store_true'}); 160 parser.add_argument('--interactive', '-i', {action: 'store_true'}); 161 parser.add_argument('--rebaseline', '-r', {action: 'store_true'}); 162 parser.add_argument('--no-depscheck', {action: 'store_true'}); 163 parser.add_argument('--cross-origin-isolation', {action: 'store_true'}); 164 parser.add_argument('--test-filter', '-f', { 165 help: 'filter Jest tests by regex, e.g. \'chrome_render\'', 166 }); 167 parser.add_argument('--no-override-gn-args', {action: 'store_true'}); 168 169 const args = parser.parse_args(); 170 const clean = !args.no_build; 171 cfg.outDir = path.resolve(ensureDir(args.out || cfg.outDir)); 172 cfg.outUiDir = ensureDir(pjoin(cfg.outDir, 'ui'), clean); 173 cfg.outUiTestArtifactsDir = ensureDir(pjoin(cfg.outDir, 'ui-test-artifacts')); 174 cfg.outExtDir = ensureDir(pjoin(cfg.outUiDir, 'chrome_extension')); 175 cfg.outDistRootDir = ensureDir(pjoin(cfg.outUiDir, 'dist')); 176 const proc = exec('python3', [VERSION_SCRIPT, '--stdout'], {stdout: 'pipe'}); 177 cfg.version = proc.stdout.toString().trim(); 178 cfg.outDistDir = ensureDir(pjoin(cfg.outDistRootDir, cfg.version)); 179 cfg.outTscDir = ensureDir(pjoin(cfg.outUiDir, 'tsc')); 180 cfg.outGenDir = ensureDir(pjoin(cfg.outUiDir, 'tsc/gen')); 181 cfg.testFilter = args.test_filter || ''; 182 cfg.watch = !!args.watch; 183 cfg.verbose = !!args.verbose; 184 cfg.debug = !!args.debug; 185 cfg.bigtrace = !!args.bigtrace; 186 cfg.openPerfettoTrace = !!args.open_perfetto_trace; 187 cfg.startHttpServer = args.serve; 188 cfg.noOverrideGnArgs = !!args.no_override_gn_args; 189 if (args.minify_js) { 190 cfg.minifyJs = args.minify_js; 191 } 192 if (args.bigtrace) { 193 cfg.outBigtraceDistDir = ensureDir(pjoin(cfg.outDistDir, 'bigtrace')); 194 } 195 if (cfg.openPerfettoTrace) { 196 cfg.outOpenPerfettoTraceDistDir = ensureDir(pjoin(cfg.outDistRootDir, 197 'open_perfetto_trace')); 198 } 199 if (args.serve_host) { 200 cfg.httpServerListenHost = args.serve_host; 201 } 202 if (args.serve_port) { 203 cfg.httpServerListenPort = args.serve_port; 204 } 205 if (args.interactive) { 206 process.env.PERFETTO_UI_TESTS_INTERACTIVE = '1'; 207 } 208 if (args.rebaseline) { 209 process.env.PERFETTO_UI_TESTS_REBASELINE = '1'; 210 } 211 if (args.cross_origin_isolation) { 212 cfg.crossOriginIsolation = true; 213 } 214 215 process.on('SIGINT', () => { 216 console.log('\nSIGINT received. Killing all child processes and exiting'); 217 for (const proc of subprocesses) { 218 if (proc) proc.kill('SIGINT'); 219 } 220 process.exit(130); // 130 -> Same behavior of bash when killed by SIGINT. 221 }); 222 223 if (!args.no_depscheck) { 224 // Check that deps are current before starting. 225 const installBuildDeps = pjoin(ROOT_DIR, 'tools/install-build-deps'); 226 const checkDepsPath = pjoin(cfg.outDir, '.check_deps'); 227 let args = [installBuildDeps, `--check-only=${checkDepsPath}`, '--ui']; 228 229 if (process.platform === 'darwin') { 230 const result = childProcess.spawnSync('arch', ['-arm64', 'true']); 231 const isArm64Capable = result.status === 0; 232 if (isArm64Capable) { 233 const archArgs = [ 234 'arch', 235 '-arch', 236 'arm64', 237 ]; 238 args = archArgs.concat(args); 239 } 240 } 241 const cmd = args.shift(); 242 exec(cmd, args); 243 } 244 245 console.log('Entering', cfg.outDir); 246 process.chdir(cfg.outDir); 247 248 // Enqueue empty task. This is needed only for --no-build --serve. The HTTP 249 // server is started when the task queue reaches quiescence, but it takes at 250 // least one task for that. 251 addTask(() => {}); 252 253 if (!args.no_build) { 254 updateSymlinks(); // Links //ui/out -> //out/xxx/ui/ 255 256 buildWasm(args.no_wasm); 257 scanDir('ui/src/assets'); 258 scanDir('ui/src/chrome_extension'); 259 scanDir('ui/src/test/diff_viewer'); 260 scanDir('buildtools/typefaces'); 261 scanDir('buildtools/catapult_trace_viewer'); 262 generateImports('ui/src/core_plugins', 'all_core_plugins.ts'); 263 generateImports('ui/src/plugins', 'all_plugins.ts'); 264 compileProtos(); 265 genVersion(); 266 267 const tsProjects = [ 268 'ui', 269 'ui/src/service_worker' 270 ]; 271 if (cfg.bigtrace) tsProjects.push('ui/src/bigtrace'); 272 if (cfg.openPerfettoTrace) { 273 scanDir('ui/src/open_perfetto_trace'); 274 tsProjects.push('ui/src/open_perfetto_trace'); 275 } 276 277 278 for (const prj of tsProjects) { 279 transpileTsProject(prj); 280 } 281 282 if (cfg.watch) { 283 for (const prj of tsProjects) { 284 transpileTsProject(prj, {watch: cfg.watch}); 285 } 286 } 287 288 bundleJs('rollup.config.js'); 289 genServiceWorkerManifestJson(); 290 291 // Watches the /dist. When changed: 292 // - Notifies the HTTP live reload clients. 293 // - Regenerates the ServiceWorker file map. 294 scanDir(cfg.outDistRootDir); 295 } 296 297 // We should enter the loop only in watch mode, where tsc and rollup are 298 // asynchronous because they run in watch mode. 299 if (args.no_build && !isDistComplete()) { 300 console.log('No build was requested, but artifacts are not available.'); 301 console.log('In case of execution error, re-run without --no-build.'); 302 } 303 if (!args.no_build) { 304 const tStart = Date.now(); 305 while (!isDistComplete()) { 306 const secs = Math.ceil((Date.now() - tStart) / 1000); 307 process.stdout.write( 308 `\t\tWaiting for first build to complete... ${secs} s\r`); 309 await new Promise((r) => setTimeout(r, 500)); 310 } 311 } 312 if (cfg.watch) console.log('\nFirst build completed!'); 313 314 if (cfg.startHttpServer) { 315 startServer(); 316 } 317 if (args.run_unittests) { 318 runTests('jest.unittest.config.js'); 319 } 320 if (args.run_integrationtests) { 321 runTests('jest.integrationtest.config.js'); 322 } 323} 324 325// ----------- 326// Build rules 327// ----------- 328 329function runTests(cfgFile) { 330 const args = [ 331 '--rootDir', 332 cfg.outTscDir, 333 '--verbose', 334 '--runInBand', 335 '--detectOpenHandles', 336 '--forceExit', 337 '--projects', 338 pjoin(ROOT_DIR, 'ui/config', cfgFile), 339 ]; 340 if (cfg.testFilter.length > 0) { 341 args.push('-t', cfg.testFilter); 342 } 343 if (cfg.watch) { 344 args.push('--watchAll'); 345 addTask(execModule, ['jest', args, {async: true}]); 346 } else { 347 addTask(execModule, ['jest', args]); 348 } 349} 350 351function cpHtml(src, filename) { 352 let html = fs.readFileSync(src).toString(); 353 // First copy the html as-is into the dist/v1.2.3/ directory. This is 354 // only used for archival purporses, so one can open 355 // ui.perfetto.dev/v1.2.3/ to skip the auto-update and channel logic. 356 fs.writeFileSync(pjoin(cfg.outDistDir, filename), html); 357 358 // Then copy it into the dist/ root by patching the version code. 359 // TODO(primiano): in next CLs, this script should take a 360 // --release_map=xxx.json argument, to populate this with multiple channels. 361 const versionMap = JSON.stringify({'stable': cfg.version}); 362 const bodyRegex = /data-perfetto_version='[^']*'/; 363 html = html.replace(bodyRegex, `data-perfetto_version='${versionMap}'`); 364 fs.writeFileSync(pjoin(cfg.outDistRootDir, filename), html); 365} 366 367function copyIndexHtml(src) { 368 addTask(cpHtml, [src, 'index.html']); 369} 370 371function copyBigtraceHtml(src) { 372 if (cfg.bigtrace) { 373 addTask(cpHtml, [src, 'bigtrace.html']); 374 } 375} 376 377function copyOpenPerfettoTraceHtml(src) { 378 if (cfg.openPerfettoTrace) { 379 addTask(cp, [src, pjoin(cfg.outOpenPerfettoTraceDistDir, 'index.html')]); 380 } 381} 382 383function copyAssets(src, dst) { 384 addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]); 385 if (cfg.bigtrace) { 386 addTask(cp, [src, pjoin(cfg.outBigtraceDistDir, 'assets', dst)]); 387 } 388} 389 390function copyUiTestArtifactsAssets(src, dst) { 391 addTask(cp, [src, pjoin(cfg.outUiTestArtifactsDir, dst)]); 392} 393 394function compileScss() { 395 const src = pjoin(ROOT_DIR, 'ui/src/assets/perfetto.scss'); 396 const dst = pjoin(cfg.outDistDir, 'perfetto.css'); 397 // In watch mode, don't exit(1) if scss fails. It can easily happen by 398 // having a typo in the css. It will still print an error. 399 const noErrCheck = !!cfg.watch; 400 const args = [src, dst]; 401 if (!cfg.verbose) { 402 args.unshift('--quiet'); 403 } 404 addTask(execModule, ['sass', args, {noErrCheck}]); 405 if (cfg.bigtrace) { 406 addTask(cp, [dst, pjoin(cfg.outBigtraceDistDir, 'perfetto.css')]); 407 } 408} 409 410function compileProtos() { 411 const dstJs = pjoin(cfg.outGenDir, 'protos.js'); 412 const dstTs = pjoin(cfg.outGenDir, 'protos.d.ts'); 413 // We've ended up pulling in all the protos (via trace.proto, 414 // trace_packet.proto) below which means |dstJs| ends up being 415 // 23k lines/12mb. We should probably not do that. 416 // TODO(hjd): Figure out how to use lazy with pbjs/pbts. 417 const inputs = [ 418 'protos/perfetto/common/trace_stats.proto', 419 'protos/perfetto/common/tracing_service_capabilities.proto', 420 'protos/perfetto/config/perfetto_config.proto', 421 'protos/perfetto/ipc/consumer_port.proto', 422 'protos/perfetto/ipc/wire_protocol.proto', 423 'protos/perfetto/trace/perfetto/perfetto_metatrace.proto', 424 'protos/perfetto/trace_processor/trace_processor.proto', 425 ]; 426 // Can't put --no-comments here - The comments are load bearing for 427 // the pbts invocation which follows. 428 const pbjsArgs = [ 429 '--no-beautify', 430 '--force-number', 431 '--no-delimited', 432 '--no-verify', 433 '-t', 434 'static-module', 435 '-w', 436 'commonjs', 437 '-p', 438 ROOT_DIR, 439 '-o', 440 dstJs, 441 ].concat(inputs); 442 addTask(execModule, ['pbjs', pbjsArgs]); 443 444 // Note: If you are looking into slowness of pbts it is not pbts 445 // itself that is slow. It invokes jsdoc to parse the comments out of 446 // the |dstJs| with https://github.com/hegemonic/catharsis which is 447 // pinning a CPU core the whole time. 448 const pbtsArgs = ['--no-comments', '-p', ROOT_DIR, '-o', dstTs, dstJs]; 449 addTask(execModule, ['pbts', pbtsArgs]); 450} 451 452function generateImports(dir, name) { 453 // We have to use the symlink (ui/src/gen) rather than cfg.outGenDir 454 // below since we want to generate the correct relative imports. For example: 455 // ui/src/frontend/foo.ts 456 // import '../gen/all_plugins.ts'; 457 // ui/src/gen/all_plugins.ts (aka ui/out/tsc/gen/all_plugins.ts) 458 // import '../frontend/some_plugin.ts'; 459 const dstTs = pjoin(ROOT_DIR, 'ui/src/gen', name); 460 const inputDir = pjoin(ROOT_DIR, dir); 461 const args = [GEN_IMPORTS_SCRIPT, inputDir, '--out', dstTs]; 462 addTask(exec, ['python3', args]); 463} 464 465// Generates a .ts source that defines the VERSION and SCM_REVISION constants. 466function genVersion() { 467 const cmd = 'python3'; 468 const args = 469 [VERSION_SCRIPT, '--ts_out', pjoin(cfg.outGenDir, 'perfetto_version.ts')]; 470 addTask(exec, [cmd, args]); 471} 472 473function updateSymlinks() { 474 // /ui/out -> /out/ui. 475 mklink(cfg.outUiDir, pjoin(ROOT_DIR, 'ui/out')); 476 477 // /ui/src/gen -> /out/ui/ui/tsc/gen) 478 mklink(cfg.outGenDir, pjoin(ROOT_DIR, 'ui/src/gen')); 479 480 // /out/ui/test/data -> /test/data (For UI tests). 481 mklink( 482 pjoin(ROOT_DIR, 'test/data'), 483 pjoin(ensureDir(pjoin(cfg.outDir, 'test')), 'data')); 484 485 // Creates a out/dist_version -> out/dist/v1.2.3 symlink, so rollup config 486 // can point to that without having to know the current version number. 487 mklink( 488 path.relative(cfg.outUiDir, cfg.outDistDir), 489 pjoin(cfg.outUiDir, 'dist_version')); 490 491 mklink( 492 pjoin(ROOT_DIR, 'ui/node_modules'), pjoin(cfg.outTscDir, 'node_modules')); 493} 494 495// Invokes ninja for building the {trace_processor, traceconv} Wasm modules. 496// It copies the .wasm directly into the out/dist/ dir, and the .js/.ts into 497// out/tsc/, so the typescript compiler and the bundler can pick them up. 498function buildWasm(skipWasmBuild) { 499 if (!skipWasmBuild) { 500 if (!cfg.noOverrideGnArgs) { 501 let gnVars = `is_debug=${cfg.debug}`; 502 if (childProcess.spawnSync('which', ['ccache']).status === 0) { 503 gnVars += ` cc_wrapper="ccache"`; 504 } 505 const gnArgs = ['gen', `--args=${gnVars}`, cfg.outDir]; 506 addTask(exec, [pjoin(ROOT_DIR, 'tools/gn'), gnArgs]); 507 } 508 509 const ninjaArgs = ['-C', cfg.outDir]; 510 ninjaArgs.push(...cfg.wasmModules.map((x) => `${x}_wasm`)); 511 addTask(exec, [pjoin(ROOT_DIR, 'tools/ninja'), ninjaArgs]); 512 } 513 514 const wasmOutDir = pjoin(cfg.outDir, 'wasm'); 515 for (const wasmMod of cfg.wasmModules) { 516 // The .wasm file goes directly into the dist dir (also .map in debug) 517 for (const ext of ['.wasm'].concat(cfg.debug ? ['.wasm.map'] : [])) { 518 const src = `${wasmOutDir}/${wasmMod}${ext}`; 519 addTask(cp, [src, pjoin(cfg.outDistDir, wasmMod + ext)]); 520 } 521 // The .js / .ts go into intermediates, they will be bundled by rollup. 522 for (const ext of ['.js', '.d.ts']) { 523 const fname = `${wasmMod}${ext}`; 524 addTask(cp, [pjoin(wasmOutDir, fname), pjoin(cfg.outGenDir, fname)]); 525 } 526 } 527} 528 529// This transpiles all the sources (frontend, controller, engine, extension) in 530// one go. The only project that has a dedicated invocation is service_worker. 531function transpileTsProject(project, options) { 532 const args = ['--project', pjoin(ROOT_DIR, project)]; 533 534 if (options !== undefined && options.watch) { 535 args.push('--watch', '--preserveWatchOutput'); 536 addTask(execModule, ['tsc', args, {async: true}]); 537 } else { 538 addTask(execModule, ['tsc', args]); 539 } 540} 541 542// Creates the three {frontend, controller, engine}_bundle.js in one invocation. 543function bundleJs(cfgName) { 544 const rcfg = pjoin(ROOT_DIR, 'ui/config', cfgName); 545 const args = ['-c', rcfg, '--no-indent']; 546 if (cfg.bigtrace) { 547 args.push('--environment', 'ENABLE_BIGTRACE:true'); 548 } 549 if (cfg.openPerfettoTrace) { 550 args.push('--environment', 'ENABLE_OPEN_PERFETTO_TRACE:true'); 551 } 552 if (cfg.minifyJs) { 553 args.push('--environment', `MINIFY_JS:${cfg.minifyJs}`); 554 } 555 args.push(...(cfg.verbose ? [] : ['--silent'])); 556 if (cfg.watch) { 557 // --waitForBundleInput is sadly quite busted so it is required ts 558 // has build at least once before invoking this. 559 args.push('--watch', '--no-watch.clearScreen'); 560 addTask(execModule, ['rollup', args, {async: true}]); 561 } else { 562 addTask(execModule, ['rollup', args]); 563 } 564} 565 566function genServiceWorkerManifestJson() { 567 function makeManifest() { 568 const manifest = {resources: {}}; 569 // When building the subresource manifest skip source maps, the manifest 570 // itself and the copy of the index.html which is copied under /v1.2.3/. 571 // The root /index.html will be fetched by service_worker.js separately. 572 const skipRegex = /(\.map|manifest\.json|index.html)$/; 573 walk(cfg.outDistDir, (absPath) => { 574 const contents = fs.readFileSync(absPath); 575 const relPath = path.relative(cfg.outDistDir, absPath); 576 const b64 = crypto.createHash('sha256').update(contents).digest('base64'); 577 manifest.resources[relPath] = 'sha256-' + b64; 578 }, skipRegex); 579 const manifestJson = JSON.stringify(manifest, null, 2); 580 fs.writeFileSync(pjoin(cfg.outDistDir, 'manifest.json'), manifestJson); 581 } 582 addTask(makeManifest, []); 583} 584 585function startServer() { 586 console.log( 587 'Starting HTTP server on', 588 `http://${cfg.httpServerListenHost}:${cfg.httpServerListenPort}`); 589 http.createServer(function(req, res) { 590 console.debug(req.method, req.url); 591 let uri = req.url.split('?', 1)[0]; 592 if (uri.endsWith('/')) { 593 uri += 'index.html'; 594 } 595 596 if (uri === '/live_reload') { 597 // Implements the Server-Side-Events protocol. 598 const head = { 599 'Content-Type': 'text/event-stream', 600 'Connection': 'keep-alive', 601 'Cache-Control': 'no-cache', 602 }; 603 res.writeHead(200, head); 604 const arrayIdx = httpWatches.length; 605 // We never remove from the array, the delete leaves an undefined item 606 // around. It makes keeping track of the index easier at the cost of a 607 // small leak. 608 httpWatches.push(res); 609 req.on('close', () => delete httpWatches[arrayIdx]); 610 return; 611 } 612 613 let absPath = path.normalize(path.join(cfg.outDistRootDir, uri)); 614 // We want to be able to use the data in '/test/' for e2e tests. 615 // However, we don't want do create a symlink into the 'dist/' dir, 616 // because 'dist/' gets shipped on the production server. 617 if (uri.startsWith('/test/')) { 618 absPath = pjoin(ROOT_DIR, uri); 619 } 620 621 // Don't serve contents outside of the project root (b/221101533). 622 if (path.relative(ROOT_DIR, absPath).startsWith('..')) { 623 res.writeHead(403); 624 res.end('403 Forbidden - Request path outside of the repo root'); 625 return; 626 } 627 628 fs.readFile(absPath, function(err, data) { 629 if (err) { 630 res.writeHead(404); 631 res.end(JSON.stringify(err)); 632 return; 633 } 634 635 const mimeMap = { 636 'html': 'text/html', 637 'css': 'text/css', 638 'js': 'application/javascript', 639 'wasm': 'application/wasm', 640 }; 641 const ext = uri.split('.').pop(); 642 const cType = mimeMap[ext] || 'octect/stream'; 643 const head = { 644 'Content-Type': cType, 645 'Content-Length': data.length, 646 'Last-Modified': fs.statSync(absPath).mtime.toUTCString(), 647 'Cache-Control': 'no-cache', 648 }; 649 if (cfg.crossOriginIsolation) { 650 head['Cross-Origin-Opener-Policy'] = 'same-origin'; 651 head['Cross-Origin-Embedder-Policy'] = 'require-corp'; 652 } 653 res.writeHead(200, head); 654 res.write(data); 655 res.end(); 656 }); 657 }) 658 .listen(cfg.httpServerListenPort, cfg.httpServerListenHost); 659} 660 661function isDistComplete() { 662 const requiredArtifacts = [ 663 'frontend_bundle.js', 664 'engine_bundle.js', 665 'traceconv_bundle.js', 666 'trace_processor.wasm', 667 'perfetto.css', 668 ]; 669 const relPaths = new Set(); 670 walk(cfg.outDistDir, (absPath) => { 671 relPaths.add(path.relative(cfg.outDistDir, absPath)); 672 }); 673 for (const fName of requiredArtifacts) { 674 if (!relPaths.has(fName)) return false; 675 } 676 return true; 677} 678 679// Called whenever a change in the out/dist directory is detected. It sends a 680// Server-Side-Event to the live_reload.ts script. 681function notifyLiveServer(changedFile) { 682 for (const cli of httpWatches) { 683 if (cli === undefined) continue; 684 cli.write( 685 'data: ' + path.relative(cfg.outDistRootDir, changedFile) + '\n\n'); 686 } 687} 688 689function copyExtensionAssets() { 690 addTask(cp, [ 691 pjoin(ROOT_DIR, 'ui/src/assets/logo-128.png'), 692 pjoin(cfg.outExtDir, 'logo-128.png'), 693 ]); 694 addTask(cp, [ 695 pjoin(ROOT_DIR, 'ui/src/chrome_extension/manifest.json'), 696 pjoin(cfg.outExtDir, 'manifest.json'), 697 ]); 698} 699 700// ----------------------- 701// Task chaining functions 702// ----------------------- 703 704function addTask(func, args) { 705 const task = new Task(func, args); 706 for (const t of tasks) { 707 if (t.identity === task.identity) { 708 return; 709 } 710 } 711 tasks.push(task); 712 setTimeout(runTasks, 0); 713} 714 715function runTasks() { 716 const snapTasks = tasks.splice(0); // snap = std::move(tasks). 717 tasksTot += snapTasks.length; 718 for (const task of snapTasks) { 719 const DIM = '\u001b[2m'; 720 const BRT = '\u001b[37m'; 721 const RST = '\u001b[0m'; 722 const ms = (new Date(Date.now() - tStart)).toISOString().slice(17, -1); 723 const ts = `[${DIM}${ms}${RST}]`; 724 const descr = task.description.substr(0, 80); 725 console.log(`${ts} ${BRT}${++tasksRan}/${tasksTot}${RST}\t${descr}`); 726 task.func.apply(/* this=*/ undefined, task.args); 727 } 728} 729 730// Executes all the RULES that match the given |absPath|. 731function scanFile(absPath) { 732 console.assert(fs.existsSync(absPath)); 733 console.assert(path.isAbsolute(absPath)); 734 const normPath = path.relative(ROOT_DIR, absPath); 735 for (const rule of RULES) { 736 const match = rule.r.exec(normPath); 737 if (!match || match[0] !== normPath) continue; 738 const captureGroup = match.length > 1 ? match[1] : undefined; 739 rule.f(absPath, captureGroup); 740 } 741} 742 743// Walks the passed |dir| recursively and, for each file, invokes the matching 744// RULES. If --watch is used, it also installs a fswatch() and re-triggers the 745// matching RULES on each file change. 746function scanDir(dir, regex) { 747 const filterFn = regex ? (absPath) => regex.test(absPath) : () => true; 748 const absDir = path.isAbsolute(dir) ? dir : pjoin(ROOT_DIR, dir); 749 // Add a fs watch if in watch mode. 750 if (cfg.watch) { 751 fswatch(absDir, {recursive: true}, (_eventType, filePath) => { 752 if (!filterFn(filePath)) return; 753 if (cfg.verbose) { 754 console.log('File change detected', _eventType, filePath); 755 } 756 if (fs.existsSync(filePath)) { 757 scanFile(filePath, filterFn); 758 } 759 }); 760 } 761 walk(absDir, (f) => { 762 if (filterFn(f)) scanFile(f); 763 }); 764} 765 766function exec(cmd, args, opts) { 767 opts = opts || {}; 768 opts.stdout = opts.stdout || 'inherit'; 769 if (cfg.verbose) console.log(`${cmd} ${args.join(' ')}\n`); 770 const spwOpts = {cwd: cfg.outDir, stdio: ['ignore', opts.stdout, 'inherit']}; 771 const checkExitCode = (code, signal) => { 772 if (signal === 'SIGINT' || signal === 'SIGTERM') return; 773 if (code !== 0 && !opts.noErrCheck) { 774 console.error(`${cmd} ${args.join(' ')} failed with code ${code}`); 775 process.exit(1); 776 } 777 }; 778 if (opts.async) { 779 const proc = childProcess.spawn(cmd, args, spwOpts); 780 const procIndex = subprocesses.length; 781 subprocesses.push(proc); 782 return new Promise((resolve, _reject) => { 783 proc.on('exit', (code, signal) => { 784 delete subprocesses[procIndex]; 785 checkExitCode(code, signal); 786 resolve(); 787 }); 788 }); 789 } else { 790 const spawnRes = childProcess.spawnSync(cmd, args, spwOpts); 791 checkExitCode(spawnRes.status, spawnRes.signal); 792 return spawnRes; 793 } 794} 795 796function execModule(module, args, opts) { 797 const modPath = pjoin(ROOT_DIR, 'ui/node_modules/.bin', module); 798 return exec(modPath, args || [], opts); 799} 800 801// ------------------------------------------ 802// File system & subprocess utility functions 803// ------------------------------------------ 804 805class Task { 806 constructor(func, args) { 807 this.func = func; 808 this.args = args || []; 809 // |identity| is used to dedupe identical tasks in the queue. 810 this.identity = JSON.stringify([this.func.name, this.args]); 811 } 812 813 get description() { 814 const ret = this.func.name.startsWith('exec') ? [] : [this.func.name]; 815 const flattenedArgs = [].concat(...this.args); 816 for (const arg of flattenedArgs) { 817 const argStr = `${arg}`; 818 if (argStr.startsWith('/')) { 819 ret.push(path.relative(cfg.outDir, arg)); 820 } else { 821 ret.push(argStr); 822 } 823 } 824 return ret.join(' '); 825 } 826} 827 828function walk(dir, callback, skipRegex) { 829 for (const child of fs.readdirSync(dir)) { 830 const childPath = pjoin(dir, child); 831 const stat = fs.lstatSync(childPath); 832 if (skipRegex !== undefined && skipRegex.test(child)) continue; 833 if (stat.isDirectory()) { 834 walk(childPath, callback, skipRegex); 835 } else if (!stat.isSymbolicLink()) { 836 callback(childPath); 837 } 838 } 839} 840 841function ensureDir(dirPath, clean) { 842 const exists = fs.existsSync(dirPath); 843 if (exists && clean) { 844 console.log('rm', dirPath); 845 fs.rmSync(dirPath, {recursive: true}); 846 } 847 if (!exists || clean) fs.mkdirSync(dirPath, {recursive: true}); 848 return dirPath; 849} 850 851function cp(src, dst) { 852 ensureDir(path.dirname(dst)); 853 if (cfg.verbose) { 854 console.log( 855 'cp', path.relative(ROOT_DIR, src), '->', path.relative(ROOT_DIR, dst)); 856 } 857 fs.copyFileSync(src, dst); 858} 859 860function mklink(src, dst) { 861 // If the symlink already points to the right place don't touch it. This is 862 // to avoid changing the mtime of the ui/ dir when unnecessary. 863 if (fs.existsSync(dst)) { 864 if (fs.lstatSync(dst).isSymbolicLink() && fs.readlinkSync(dst) === src) { 865 return; 866 } else { 867 fs.unlinkSync(dst); 868 } 869 } 870 fs.symlinkSync(src, dst); 871} 872 873main(); 874