1cb93a386Sopenharmony_ci/**
2cb93a386Sopenharmony_ci * Command line application to run CanvasKit benchmarks in webpages using puppeteer. Different
3cb93a386Sopenharmony_ci * webpages can be specified to measure different aspects. The HTML page run contains the JS code
4cb93a386Sopenharmony_ci * to run the scenario and either 1) produce the perf output as a JSON object or 2) defer to
5cb93a386Sopenharmony_ci * puppeteer reading the tracing data.
6cb93a386Sopenharmony_ci *
7cb93a386Sopenharmony_ci */
8cb93a386Sopenharmony_ciconst puppeteer = require('puppeteer');
9cb93a386Sopenharmony_ciconst express = require('express');
10cb93a386Sopenharmony_ciconst fs = require('fs');
11cb93a386Sopenharmony_ciconst commandLineArgs = require('command-line-args');
12cb93a386Sopenharmony_ciconst commandLineUsage= require('command-line-usage');
13cb93a386Sopenharmony_ci
14cb93a386Sopenharmony_ciconst opts = [
15cb93a386Sopenharmony_ci  {
16cb93a386Sopenharmony_ci    name: 'bench_html',
17cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
18cb93a386Sopenharmony_ci    description: 'An HTML file containing the bench harness.'
19cb93a386Sopenharmony_ci  },
20cb93a386Sopenharmony_ci  {
21cb93a386Sopenharmony_ci    name: 'canvaskit_js',
22cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
23cb93a386Sopenharmony_ci    description: '(required) The path to canvaskit.js.'
24cb93a386Sopenharmony_ci  },
25cb93a386Sopenharmony_ci  {
26cb93a386Sopenharmony_ci    name: 'canvaskit_wasm',
27cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
28cb93a386Sopenharmony_ci    description: '(required) The path to canvaskit.wasm.'
29cb93a386Sopenharmony_ci  },
30cb93a386Sopenharmony_ci  {
31cb93a386Sopenharmony_ci    name: 'input_lottie',
32cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
33cb93a386Sopenharmony_ci    description: 'The Lottie JSON file to process.'
34cb93a386Sopenharmony_ci  },
35cb93a386Sopenharmony_ci  {
36cb93a386Sopenharmony_ci    name: 'input_skp',
37cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
38cb93a386Sopenharmony_ci    description: 'The SKP file to process.'
39cb93a386Sopenharmony_ci  },
40cb93a386Sopenharmony_ci  {
41cb93a386Sopenharmony_ci    name: 'assets',
42cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
43cb93a386Sopenharmony_ci    description: 'A directory containing any assets needed by lottie files or tests (e.g. images/fonts).'
44cb93a386Sopenharmony_ci  },
45cb93a386Sopenharmony_ci  {
46cb93a386Sopenharmony_ci    name: 'output',
47cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
48cb93a386Sopenharmony_ci    description: 'The perf file to write. Defaults to perf.json',
49cb93a386Sopenharmony_ci  },
50cb93a386Sopenharmony_ci  {
51cb93a386Sopenharmony_ci    name: 'chromium_executable_path',
52cb93a386Sopenharmony_ci    typeLabel: '{underline file}',
53cb93a386Sopenharmony_ci    description: 'The chromium executable to be used by puppeteer to run tests',
54cb93a386Sopenharmony_ci  },
55cb93a386Sopenharmony_ci  {
56cb93a386Sopenharmony_ci    name: 'merge_output_as',
57cb93a386Sopenharmony_ci    typeLabel: String,
58cb93a386Sopenharmony_ci    description: 'Overwrites a json property in an existing output file.',
59cb93a386Sopenharmony_ci  },
60cb93a386Sopenharmony_ci  {
61cb93a386Sopenharmony_ci    name: 'use_gpu',
62cb93a386Sopenharmony_ci    description: 'Whether we should run in non-headless mode with GPU.',
63cb93a386Sopenharmony_ci    type: Boolean,
64cb93a386Sopenharmony_ci  },
65cb93a386Sopenharmony_ci  {
66cb93a386Sopenharmony_ci    name: 'use_tracing',
67cb93a386Sopenharmony_ci    description: 'If non-empty, will be interpreted as the tracing categories that should be ' +
68cb93a386Sopenharmony_ci      'measured and returned in the output JSON. Example: "blink,cc,gpu"',
69cb93a386Sopenharmony_ci    type: String,
70cb93a386Sopenharmony_ci  },
71cb93a386Sopenharmony_ci  {
72cb93a386Sopenharmony_ci    name: 'enable_simd',
73cb93a386Sopenharmony_ci    description: 'enable execution of wasm SIMD operations in chromium',
74cb93a386Sopenharmony_ci    type: Boolean
75cb93a386Sopenharmony_ci  },
76cb93a386Sopenharmony_ci  {
77cb93a386Sopenharmony_ci    name: 'port',
78cb93a386Sopenharmony_ci    description: 'The port number to use, defaults to 8081.',
79cb93a386Sopenharmony_ci    type: Number,
80cb93a386Sopenharmony_ci  },
81cb93a386Sopenharmony_ci  {
82cb93a386Sopenharmony_ci    name: 'query_params',
83cb93a386Sopenharmony_ci    description: 'The query params to be added to the testing page URL. Useful for passing' +
84cb93a386Sopenharmony_ci      'options to the perf html page.',
85cb93a386Sopenharmony_ci    type: String,
86cb93a386Sopenharmony_ci    multiple: true
87cb93a386Sopenharmony_ci  },
88cb93a386Sopenharmony_ci  {
89cb93a386Sopenharmony_ci    name: 'help',
90cb93a386Sopenharmony_ci    alias: 'h',
91cb93a386Sopenharmony_ci    type: Boolean,
92cb93a386Sopenharmony_ci    description: 'Print this usage guide.'
93cb93a386Sopenharmony_ci  },
94cb93a386Sopenharmony_ci  {
95cb93a386Sopenharmony_ci    name: 'timeout',
96cb93a386Sopenharmony_ci    description: 'Number of seconds to allow test to run.',
97cb93a386Sopenharmony_ci    type: Number,
98cb93a386Sopenharmony_ci  },
99cb93a386Sopenharmony_ci];
100cb93a386Sopenharmony_ci
101cb93a386Sopenharmony_ciconst usage = [
102cb93a386Sopenharmony_ci  {
103cb93a386Sopenharmony_ci    header: 'Skia Web-based Performance Metrics of CanvasKit',
104cb93a386Sopenharmony_ci    content: "Command line application to capture performance metrics from a browser."
105cb93a386Sopenharmony_ci  },
106cb93a386Sopenharmony_ci  {
107cb93a386Sopenharmony_ci    header: 'Options',
108cb93a386Sopenharmony_ci    optionList: opts,
109cb93a386Sopenharmony_ci  },
110cb93a386Sopenharmony_ci];
111cb93a386Sopenharmony_ci
112cb93a386Sopenharmony_ci// Parse and validate flags.
113cb93a386Sopenharmony_ciconst options = commandLineArgs(opts);
114cb93a386Sopenharmony_ci
115cb93a386Sopenharmony_ciif (!options.output) {
116cb93a386Sopenharmony_ci  options.output = 'perf.json';
117cb93a386Sopenharmony_ci}
118cb93a386Sopenharmony_ciif (!options.port) {
119cb93a386Sopenharmony_ci  options.port = 8081;
120cb93a386Sopenharmony_ci}
121cb93a386Sopenharmony_ciif (!options.timeout) {
122cb93a386Sopenharmony_ci  options.timeout = 60;
123cb93a386Sopenharmony_ci}
124cb93a386Sopenharmony_ci
125cb93a386Sopenharmony_ciif (options.help) {
126cb93a386Sopenharmony_ci  console.log(commandLineUsage(usage));
127cb93a386Sopenharmony_ci  process.exit(0);
128cb93a386Sopenharmony_ci}
129cb93a386Sopenharmony_ci
130cb93a386Sopenharmony_ciif (!options.bench_html) {
131cb93a386Sopenharmony_ci  console.error('You must supply the bench_html file to run.');
132cb93a386Sopenharmony_ci  console.log(commandLineUsage(usage));
133cb93a386Sopenharmony_ci  process.exit(1);
134cb93a386Sopenharmony_ci}
135cb93a386Sopenharmony_ciconst driverHTML = fs.readFileSync(options.bench_html, 'utf8');
136cb93a386Sopenharmony_ci
137cb93a386Sopenharmony_ci// This express webserver will serve the HTML file running the benchmark and any additional assets
138cb93a386Sopenharmony_ci// needed to run the tests.
139cb93a386Sopenharmony_ciconst app = express();
140cb93a386Sopenharmony_ciapp.get('/', (req, res) => res.send(driverHTML));
141cb93a386Sopenharmony_ci
142cb93a386Sopenharmony_ciif (!options.canvaskit_js) {
143cb93a386Sopenharmony_ci  console.error('You must supply path to canvaskit.js.');
144cb93a386Sopenharmony_ci  console.log(commandLineUsage(usage));
145cb93a386Sopenharmony_ci  process.exit(1);
146cb93a386Sopenharmony_ci}
147cb93a386Sopenharmony_ci
148cb93a386Sopenharmony_ciif (!options.canvaskit_wasm) {
149cb93a386Sopenharmony_ci  console.error('You must supply path to canvaskit.wasm.');
150cb93a386Sopenharmony_ci  console.log(commandLineUsage(usage));
151cb93a386Sopenharmony_ci  process.exit(1);
152cb93a386Sopenharmony_ci}
153cb93a386Sopenharmony_ci
154cb93a386Sopenharmony_ciconst benchmarkJS = fs.readFileSync('benchmark.js', 'utf8');
155cb93a386Sopenharmony_ciconst canvasPerfJS = fs.readFileSync('canvas_perf.js', 'utf8');
156cb93a386Sopenharmony_ciconst canvasKitJS = fs.readFileSync(options.canvaskit_js, 'utf8');
157cb93a386Sopenharmony_ciconst canvasKitWASM = fs.readFileSync(options.canvaskit_wasm, 'binary');
158cb93a386Sopenharmony_ci
159cb93a386Sopenharmony_ciapp.get('/static/benchmark.js', (req, res) => res.send(benchmarkJS));
160cb93a386Sopenharmony_ciapp.get('/static/canvas_perf.js', (req, res) => res.send(canvasPerfJS));
161cb93a386Sopenharmony_ciapp.get('/static/canvaskit.js', (req, res) => res.send(canvasKitJS));
162cb93a386Sopenharmony_ciapp.get('/static/canvaskit.wasm', function(req, res) {
163cb93a386Sopenharmony_ci  // Set the MIME type so it can be streamed efficiently.
164cb93a386Sopenharmony_ci  res.type('application/wasm');
165cb93a386Sopenharmony_ci  res.send(new Buffer(canvasKitWASM, 'binary'));
166cb93a386Sopenharmony_ci});
167cb93a386Sopenharmony_ci
168cb93a386Sopenharmony_ci
169cb93a386Sopenharmony_ciif (options.input_lottie) {
170cb93a386Sopenharmony_ci  const lottieJSON = fs.readFileSync(options.input_lottie, 'utf8');
171cb93a386Sopenharmony_ci  app.get('/static/lottie.json', (req, res) => res.send(lottieJSON));
172cb93a386Sopenharmony_ci}
173cb93a386Sopenharmony_ciif (options.input_skp) {
174cb93a386Sopenharmony_ci  const skpBytes = fs.readFileSync(options.input_skp, 'binary');
175cb93a386Sopenharmony_ci  app.get('/static/test.skp', (req, res) => {
176cb93a386Sopenharmony_ci    res.send(new Buffer(skpBytes, 'binary'));
177cb93a386Sopenharmony_ci  });
178cb93a386Sopenharmony_ci}
179cb93a386Sopenharmony_ciif (options.assets) {
180cb93a386Sopenharmony_ci  app.use('/static/assets/', express.static(options.assets));
181cb93a386Sopenharmony_ci  console.log('assets served from', options.assets);
182cb93a386Sopenharmony_ci}
183cb93a386Sopenharmony_ci
184cb93a386Sopenharmony_ciapp.listen(options.port, () => console.log('- Local web server started.'));
185cb93a386Sopenharmony_ci
186cb93a386Sopenharmony_cilet hash = "#cpu";
187cb93a386Sopenharmony_ciif (options.use_gpu) {
188cb93a386Sopenharmony_ci  hash = "#gpu";
189cb93a386Sopenharmony_ci}
190cb93a386Sopenharmony_cilet query_param_string = '?';
191cb93a386Sopenharmony_ciif (options.query_params) {
192cb93a386Sopenharmony_ci  for (const string of options.query_params) {
193cb93a386Sopenharmony_ci    query_param_string += string + '&';
194cb93a386Sopenharmony_ci  }
195cb93a386Sopenharmony_ci}
196cb93a386Sopenharmony_ciconst targetURL = `http://localhost:${options.port}/${query_param_string}${hash}`;
197cb93a386Sopenharmony_ciconst viewPort = {width: 1000, height: 1000};
198cb93a386Sopenharmony_ci
199cb93a386Sopenharmony_ci// Drive chrome to load the web page from the server we have running.
200cb93a386Sopenharmony_ciasync function driveBrowser() {
201cb93a386Sopenharmony_ci  console.log('- Launching chrome for ' + options.input);
202cb93a386Sopenharmony_ci  let browser;
203cb93a386Sopenharmony_ci  let page;
204cb93a386Sopenharmony_ci  const headless = !options.use_gpu;
205cb93a386Sopenharmony_ci  let browser_args = [
206cb93a386Sopenharmony_ci      '--no-sandbox',
207cb93a386Sopenharmony_ci      '--disable-setuid-sandbox',
208cb93a386Sopenharmony_ci      '--window-size=' + viewPort.width + ',' + viewPort.height,
209cb93a386Sopenharmony_ci      // The following two params allow Chrome to run at an unlimited fps. Note, if there is
210cb93a386Sopenharmony_ci      // already a chrome instance running, these arguments will have NO EFFECT, as the existing
211cb93a386Sopenharmony_ci      // Chrome instance will be used instead of puppeteer spinning up a new one.
212cb93a386Sopenharmony_ci      '--disable-frame-rate-limit',
213cb93a386Sopenharmony_ci      '--disable-gpu-vsync',
214cb93a386Sopenharmony_ci  ];
215cb93a386Sopenharmony_ci  if (options.enable_simd) {
216cb93a386Sopenharmony_ci    browser_args.push('--enable-features=WebAssemblySimd');
217cb93a386Sopenharmony_ci  }
218cb93a386Sopenharmony_ci  if (options.use_gpu) {
219cb93a386Sopenharmony_ci    browser_args.push('--ignore-gpu-blacklist');
220cb93a386Sopenharmony_ci    browser_args.push('--ignore-gpu-blocklist');
221cb93a386Sopenharmony_ci    browser_args.push('--enable-gpu-rasterization');
222cb93a386Sopenharmony_ci  }
223cb93a386Sopenharmony_ci  console.log("Running with headless: " + headless + " args: " + browser_args);
224cb93a386Sopenharmony_ci  try {
225cb93a386Sopenharmony_ci    browser = await puppeteer.launch({
226cb93a386Sopenharmony_ci      headless: headless,
227cb93a386Sopenharmony_ci      args: browser_args,
228cb93a386Sopenharmony_ci      executablePath: options.chromium_executable_path
229cb93a386Sopenharmony_ci    });
230cb93a386Sopenharmony_ci    page = await browser.newPage();
231cb93a386Sopenharmony_ci    await page.setViewport(viewPort);
232cb93a386Sopenharmony_ci  } catch (e) {
233cb93a386Sopenharmony_ci    console.log('Could not open the browser.', e);
234cb93a386Sopenharmony_ci    process.exit(1);
235cb93a386Sopenharmony_ci  }
236cb93a386Sopenharmony_ci  console.log("Loading " + targetURL);
237cb93a386Sopenharmony_ci  try {
238cb93a386Sopenharmony_ci    await page.goto(targetURL, {
239cb93a386Sopenharmony_ci      timeout: 60000,
240cb93a386Sopenharmony_ci      waitUntil: 'networkidle0'
241cb93a386Sopenharmony_ci    });
242cb93a386Sopenharmony_ci
243cb93a386Sopenharmony_ci    // Page is mostly loaded, wait for benchmark page to report itself ready.
244cb93a386Sopenharmony_ci    console.log('Waiting 15s for benchmark to be ready');
245cb93a386Sopenharmony_ci    await page.waitForFunction(`(window._perfReady === true) || window._error`, {
246cb93a386Sopenharmony_ci      timeout: 15000,
247cb93a386Sopenharmony_ci    });
248cb93a386Sopenharmony_ci
249cb93a386Sopenharmony_ci    let err = await page.evaluate('window._error');
250cb93a386Sopenharmony_ci    if (err) {
251cb93a386Sopenharmony_ci      console.log(`ERROR: ${err}`);
252cb93a386Sopenharmony_ci      process.exit(1);
253cb93a386Sopenharmony_ci    }
254cb93a386Sopenharmony_ci
255cb93a386Sopenharmony_ci    // Start trace if requested
256cb93a386Sopenharmony_ci    if (options.use_tracing) {
257cb93a386Sopenharmony_ci      const categories = options.use_tracing.split(',');
258cb93a386Sopenharmony_ci      console.log('Collecting tracing data for categories', categories);
259cb93a386Sopenharmony_ci      await page.tracing.start({
260cb93a386Sopenharmony_ci        path: options.output,
261cb93a386Sopenharmony_ci        screenshots: false,
262cb93a386Sopenharmony_ci        categories: categories,
263cb93a386Sopenharmony_ci      });
264cb93a386Sopenharmony_ci    }
265cb93a386Sopenharmony_ci
266cb93a386Sopenharmony_ci    // Benchmarks should have a button with id #start_bench to click (this also makes manual
267cb93a386Sopenharmony_ci    // debugging easier).
268cb93a386Sopenharmony_ci    await page.click('#start_bench');
269cb93a386Sopenharmony_ci
270cb93a386Sopenharmony_ci    console.log(`Waiting ${options.timeout}s for run to be done`);
271cb93a386Sopenharmony_ci    await page.waitForFunction(`(window._perfDone === true) || window._error`, {
272cb93a386Sopenharmony_ci      timeout: options.timeout*1000,
273cb93a386Sopenharmony_ci    });
274cb93a386Sopenharmony_ci
275cb93a386Sopenharmony_ci    err = await page.evaluate('window._error');
276cb93a386Sopenharmony_ci    if (err) {
277cb93a386Sopenharmony_ci      console.log(`ERROR: ${err}`);
278cb93a386Sopenharmony_ci      process.exit(1);
279cb93a386Sopenharmony_ci    }
280cb93a386Sopenharmony_ci
281cb93a386Sopenharmony_ci    if (options.use_tracing) {
282cb93a386Sopenharmony_ci      // Stop Trace.
283cb93a386Sopenharmony_ci      await page.tracing.stop();
284cb93a386Sopenharmony_ci    } else {
285cb93a386Sopenharmony_ci      const perfResults = await page.evaluate('window._perfData');
286cb93a386Sopenharmony_ci      console.debug('Perf results: ', perfResults);
287cb93a386Sopenharmony_ci
288cb93a386Sopenharmony_ci      if (options.merge_output_as) {
289cb93a386Sopenharmony_ci        const existing_output_file_contents = fs.readFileSync(options.output, 'utf8');
290cb93a386Sopenharmony_ci        let existing_dataset = {};
291cb93a386Sopenharmony_ci        try {
292cb93a386Sopenharmony_ci          existing_dataset = JSON.parse(existing_output_file_contents);
293cb93a386Sopenharmony_ci        } catch (e) {}
294cb93a386Sopenharmony_ci
295cb93a386Sopenharmony_ci        existing_dataset[options.merge_output_as] = perfResults;
296cb93a386Sopenharmony_ci        fs.writeFileSync(options.output, JSON.stringify(existing_dataset));
297cb93a386Sopenharmony_ci      } else {
298cb93a386Sopenharmony_ci        fs.writeFileSync(options.output, JSON.stringify(perfResults));
299cb93a386Sopenharmony_ci      }
300cb93a386Sopenharmony_ci    }
301cb93a386Sopenharmony_ci
302cb93a386Sopenharmony_ci  } catch(e) {
303cb93a386Sopenharmony_ci    console.log('Timed out while loading or drawing.', e);
304cb93a386Sopenharmony_ci    await browser.close();
305cb93a386Sopenharmony_ci    process.exit(1);
306cb93a386Sopenharmony_ci  }
307cb93a386Sopenharmony_ci
308cb93a386Sopenharmony_ci  await browser.close();
309cb93a386Sopenharmony_ci  // Need to call exit() because the web server is still running.
310cb93a386Sopenharmony_ci  process.exit(0);
311cb93a386Sopenharmony_ci}
312cb93a386Sopenharmony_ci
313cb93a386Sopenharmony_cidriveBrowser();
314