browsertime 14.5.0 → 14.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/bin/browsertime.js +1 -1
  3. package/lib/android/index.js +1 -1
  4. package/lib/chrome/har.js +4 -4
  5. package/lib/chrome/longTaskMetrics.js +1 -1
  6. package/lib/chrome/speedline.js +2 -2
  7. package/lib/chrome/webdriver/builder.js +1 -1
  8. package/lib/chrome/webdriver/setupChromiumOptions.js +7 -2
  9. package/lib/connectivity/trafficShapeParser.js +2 -2
  10. package/lib/connectivity/tsProxy.js +3 -3
  11. package/lib/core/engine/command/measure.js +16 -10
  12. package/lib/core/engine/command/mouse/clickAndHold.js +4 -16
  13. package/lib/core/engine/command/switch.js +2 -8
  14. package/lib/core/engine/iteration.js +4 -5
  15. package/lib/core/engine/run.js +2 -2
  16. package/lib/core/seleniumRunner.js +23 -22
  17. package/lib/core/webdriver/index.js +1 -1
  18. package/lib/edge/webdriver/builder.js +1 -1
  19. package/lib/extensionserver/setup.js +1 -1
  20. package/lib/firefox/geckoProfiler.js +6 -5
  21. package/lib/firefox/getHAR.js +1 -1
  22. package/lib/firefox/memoryReport.js +5 -1
  23. package/lib/firefox/webdriver/builder.js +12 -7
  24. package/lib/firefox/webdriver/firefox.js +6 -7
  25. package/lib/safari/webdriver/builder.js +1 -1
  26. package/lib/support/browserScript.js +1 -1
  27. package/lib/support/cli.js +12 -0
  28. package/lib/support/engineUtils.js +1 -1
  29. package/lib/support/getViewPort.js +1 -1
  30. package/lib/support/har/index.js +13 -14
  31. package/lib/support/pathToFolder.js +4 -10
  32. package/lib/support/preURL.js +1 -1
  33. package/lib/support/setResourceTimingBufferSize.js +1 -1
  34. package/lib/support/statistics.js +1 -1
  35. package/lib/support/stop.js +1 -1
  36. package/lib/support/storageManager.js +5 -10
  37. package/lib/video/postprocessing/finetune/addTextToVideo.js +1 -1
  38. package/lib/video/postprocessing/finetune/convertFps.js +1 -1
  39. package/lib/video/postprocessing/finetune/getFont.js +1 -1
  40. package/lib/video/postprocessing/finetune/getTimingMetrics.js +5 -4
  41. package/lib/video/postprocessing/finetune/index.js +1 -1
  42. package/lib/video/postprocessing/finetune/removeOrange.js +1 -1
  43. package/lib/video/postprocessing/visualmetrics/extraMetrics.js +1 -1
  44. package/lib/video/postprocessing/visualmetrics/getVideoMetrics.js +1 -1
  45. package/lib/video/screenRecording/desktop/ffmpegRecorder.js +3 -3
  46. package/lib/video/screenRecording/desktop/osx/getSPDisplaysDataType.js +1 -1
  47. package/lib/video/screenRecording/desktop/osx/getScreen.js +1 -1
  48. package/lib/video/screenRecording/firefox/firefoxWindowRecorder.js +2 -2
  49. package/lib/video/screenRecording/setOrangeBackground.js +1 -1
  50. package/package.json +10 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 14.8.0 - 2021-10-27
4
+ ### Added
5
+ * New Edgedriver 95 and Edge 95 in the Docker container [#1665](https://github.com/sitespeedio/browsertime/pull/1665).
6
+
7
+ ### Fixed
8
+ * Fix for storing Firefox memory report and geckoprofile on Android 10+. Thank you [Andrew Creskey](https://github.com/acreskeyMoz) for PR [#1663](https://github.com/sitespeedio/browsertime/pull/1663).
9
+
10
+ ## 14.7.0 - 2021-10-20
11
+ ### Added
12
+ * Updated to Chromedriver 95. Added Chrome 95 in the Docker container.
13
+
14
+ ## 14.6.1 - 2021-10-18
15
+ ### Fixed
16
+ * If loading the URL fails (we get an exception, the browser crashes etc) make sure we stop the video correctly so it can be used [#1657](https://github.com/sitespeedio/browsertime/pull/1657).
17
+
18
+
19
+ ## 14.6.0 - 2021-10-13
20
+ ### Added
21
+ * Updated to Selenium 4.0.0 [#1655](https://github.com/sitespeedio/browsertime/pull/1655).
22
+ * You can skip setting Browsertime default Firefox preferences with `--firefox.noDefaultPrefs`, thank you [Gregory Mierzwinski](https://github.com/gmierz) for PR [#1650](https://github.com/sitespeedio/browsertime/pull/1650).
23
+ * You can skip setting Browsertime default Chrome options with `--chrome.noDefaultOptions`, fixed in [#1651](https://github.com/sitespeedio/browsertime/pull/1651).
24
+ * Updated to Firefox 93 in the Docker container.
25
+
26
+ ### Fixed
27
+ * Updated eslint and prettier dependencies [#1652](https://github.com/sitespeedio/browsertime/pull/1652).
28
+ * Log Firefox preference and Chrome args on debug level [#1654](https://github.com/sitespeedio/browsertime/pull/1654)
3
29
  ## 14.5.0 - 2021-09-30
4
30
  ### Added
5
31
  * Added CSS selector support for mouse commands [#1645](https://github.com/sitespeedio/browsertime/pull/1645)
@@ -9,7 +35,8 @@
9
35
 
10
36
  ## 14.4.0 - 2021-09-22
11
37
  ### Added
12
- * Updated to Chromedriver 94 and Chrome 94 in the Docker container [#1643](https://github.com/sitespeedio/browsertime/pull/1643).
38
+ * Updated to Ch
39
+ romedriver 94 and Chrome 94 in the Docker container [#1643](https://github.com/sitespeedio/browsertime/pull/1643).
13
40
  ## 14.3.0 - 2021-09-16
14
41
  ### Added
15
42
  * Update to Edgedriver 93. Update to Edge 93 in the Docker container [#1641](https://github.com/sitespeedio/browsertime/pull/1641).
@@ -11,7 +11,7 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const log = require('intel').getLogger('browsertime');
13
13
  const engineUtils = require('../lib/support/engineUtils');
14
- const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
14
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
15
15
  const delay = ms => new Promise(res => setTimeout(res, ms));
16
16
 
17
17
  async function parseUserScripts(scripts) {
@@ -91,7 +91,7 @@ class Android {
91
91
  const transfer = await this.client.pull(this.id, sourcePath);
92
92
 
93
93
  return new Promise((resolve, reject) => {
94
- transfer.on('end', function() {
94
+ transfer.on('end', function () {
95
95
  resolve();
96
96
  });
97
97
  transfer.on('error', reject);
package/lib/chrome/har.js CHANGED
@@ -5,7 +5,7 @@ const perflogParser = require('chrome-har');
5
5
  const harBuilder = require('../support/har');
6
6
  const { Type } = require('selenium-webdriver').logging;
7
7
 
8
- module.exports = async function(
8
+ module.exports = async function (
9
9
  runner,
10
10
  result,
11
11
  index,
@@ -38,9 +38,9 @@ module.exports = async function(
38
38
  await cdpClient.setResponseBodies(har);
39
39
  }
40
40
 
41
- const versionInfo = (await cdpClient.send(
42
- 'Browser.getVersion'
43
- )).product.split('/');
41
+ const versionInfo = (
42
+ await cdpClient.send('Browser.getVersion')
43
+ ).product.split('/');
44
44
  const info = {
45
45
  name: versionInfo[0],
46
46
  version: versionInfo[1]
@@ -2,7 +2,7 @@
2
2
 
3
3
  const get = require('lodash.get');
4
4
 
5
- module.exports = function(result, options) {
5
+ module.exports = function (result, options) {
6
6
  let totalDurationFirstPaint = 0;
7
7
  let totalDurationFirstContentFulPaint = 0;
8
8
  let totalDurationAfterLoadEventEnd = 0;
@@ -18,13 +18,13 @@ function pad(n) {
18
18
  * Use SpeedLine to analyse the Chrome trace log to get Visual Metrics.
19
19
  * Not as good as using a video but maybe usable in some cases.
20
20
  */
21
- module.exports = async function(trace, result, index) {
21
+ module.exports = async function (trace, result, index) {
22
22
  try {
23
23
  const navStart = trace.traceEvents.filter(
24
24
  task =>
25
25
  task.cat === 'blink.user_timing' && task.name === 'navigationStart'
26
26
  );
27
- navStart.sort(function(a, b) {
27
+ navStart.sort(function (a, b) {
28
28
  return a.ts - b.ts;
29
29
  });
30
30
  log.debug('Get Speedline result from the trace');
@@ -14,7 +14,7 @@ let hasConfiguredChromeDriverService = false;
14
14
  * @param builder
15
15
  * @param {Object} options the options for a web driver.
16
16
  */
17
- module.exports.configureBuilder = function(builder, baseDir, options) {
17
+ module.exports.configureBuilder = function (builder, baseDir, options) {
18
18
  const chromeConfig = options.chrome || {};
19
19
 
20
20
  if (!hasConfiguredChromeDriverService) {
@@ -5,7 +5,7 @@ const getViewPort = require('../../support/getViewPort');
5
5
  const util = require('../../support/util');
6
6
  const log = require('intel').getLogger('browsertime.chrome');
7
7
 
8
- module.exports = function(seleniumOptions, browserOptions, options, baseDir) {
8
+ module.exports = function (seleniumOptions, browserOptions, options, baseDir) {
9
9
  // Fixing save password popup, only on Desktop
10
10
  if (!options.android) {
11
11
  seleniumOptions.setUserPreferences({
@@ -109,10 +109,15 @@ module.exports = function(seleniumOptions, browserOptions, options, baseDir) {
109
109
  '--remote-debugging-port=' + options.devToolsPort
110
110
  );
111
111
  } else {
112
- seleniumOptions.addArguments(defaultChromeOptions);
112
+ if (!browserOptions.noDefaultOptions) {
113
+ seleniumOptions.addArguments(defaultChromeOptions);
114
+ } else {
115
+ log.info('Skip setting default options for Chrome');
116
+ }
113
117
  }
114
118
 
115
119
  if (browserOptions.args) {
120
+ log.debug('Set Chrome args %j', browserOptions.args);
116
121
  seleniumOptions.addArguments(browserOptions.args);
117
122
  }
118
123
 
@@ -104,7 +104,7 @@ const legacyProfiles = {
104
104
  };
105
105
 
106
106
  module.exports = {
107
- parseTrafficShapeConfig: function(options) {
107
+ parseTrafficShapeConfig: function (options) {
108
108
  if (options.connectivity && options.connectivity.profile === 'custom') {
109
109
  return {
110
110
  down: options.connectivity.down,
@@ -133,7 +133,7 @@ module.exports = {
133
133
  return null;
134
134
  }
135
135
  },
136
- getProfiles: function() {
136
+ getProfiles: function () {
137
137
  return newProfiles;
138
138
  }
139
139
  };
@@ -14,7 +14,7 @@ const TS_PROXY_SCRIPT_PATH = path.join(
14
14
  );
15
15
 
16
16
  function getStartupCriteriaListener(resolve, reject) {
17
- return function(data) {
17
+ return function (data) {
18
18
  const logLine = data.toString();
19
19
  if (logLine.startsWith('Started Socks5 proxy server on')) {
20
20
  log.debug('Started TSproxy');
@@ -65,12 +65,12 @@ class TSProxy {
65
65
  const tsProxyProcess = this.tsProxyProcess;
66
66
 
67
67
  if (this.logVerbose) {
68
- tsProxyProcess.stderr.on('data', function(data) {
68
+ tsProxyProcess.stderr.on('data', function (data) {
69
69
  log.verbose(data.toString().trim());
70
70
  });
71
71
  }
72
72
 
73
- return new Promise(function(resolve, reject) {
73
+ return new Promise(function (resolve, reject) {
74
74
  tsProxyProcess.stdout.on(
75
75
  'data',
76
76
  getStartupCriteriaListener(resolve, reject)
@@ -96,6 +96,13 @@ class Measure {
96
96
  }
97
97
  }
98
98
 
99
+ async _stopVideo(url) {
100
+ if (this.recordVideo && !this.options.videoParams.debug) {
101
+ await this.video.stop(url);
102
+ this.videos.push(this.video);
103
+ }
104
+ }
105
+
99
106
  _error(message) {
100
107
  // If we already are measuring a page, associate the error woth that page
101
108
  if (this.areWeMeasuring === true) {
@@ -223,6 +230,9 @@ class Measure {
223
230
  this.numberOfMeasuredPages++;
224
231
  this.numberOfVisitedPages++;
225
232
  this.engineDelegate.failing(url);
233
+ // If we got an error we still should stop the video to make sure we can use it to
234
+ // try to understand what went wrong
235
+ await this._stopVideo(url);
226
236
  throw e;
227
237
  }
228
238
  } else {
@@ -261,14 +271,12 @@ class Measure {
261
271
  // we also need to add that to the meta data so that the correct folder is created
262
272
  if (this.result[this.numberOfMeasuredPages].alias) {
263
273
  if (this.options.urlMetaData) {
264
- this.options.urlMetaData[url] = this.result[
265
- this.numberOfMeasuredPages
266
- ].alias;
274
+ this.options.urlMetaData[url] =
275
+ this.result[this.numberOfMeasuredPages].alias;
267
276
  } else {
268
277
  this.options.urlMetaData = {};
269
- this.options.urlMetaData[url] = this.result[
270
- this.numberOfMeasuredPages
271
- ].alias;
278
+ this.options.urlMetaData[url] =
279
+ this.result[this.numberOfMeasuredPages].alias;
272
280
  }
273
281
  }
274
282
 
@@ -276,10 +284,8 @@ class Measure {
276
284
  await this.storageManager.createSubDataDir(
277
285
  path.join(pathToFolder(url, this.options))
278
286
  );
279
- if (this.recordVideo && !this.options.videoParams.debug) {
280
- await this.video.stop(url);
281
- this.videos.push(this.video);
282
- }
287
+ await this._stopVideo(url);
288
+
283
289
  const alias = this.options.urlMetaData
284
290
  ? this.options.urlMetaData[url]
285
291
  : undefined || this.result[this.numberOfMeasuredPages].alias;
@@ -18,10 +18,7 @@ class ClickAndHold {
18
18
  async byXpath(xpath) {
19
19
  try {
20
20
  const element = await this.driver.findElement(webdriver.By.xpath(xpath));
21
- return this.actions
22
- .move({ origin: element })
23
- .press()
24
- .perform();
21
+ return this.actions.move({ origin: element }).press().perform();
25
22
  } catch (e) {
26
23
  log.error('Could not click and hold on element with xpath %s', xpath);
27
24
  log.verbose(e);
@@ -38,10 +35,7 @@ class ClickAndHold {
38
35
  async bySelector(selector) {
39
36
  try {
40
37
  const element = await this.driver.findElement(webdriver.By.css(selector));
41
- return this.actions
42
- .move({ origin: element })
43
- .press()
44
- .perform();
38
+ return this.actions.move({ origin: element }).press().perform();
45
39
  } catch (e) {
46
40
  log.error(
47
41
  'Could not click and hold on element with selector %s',
@@ -103,10 +97,7 @@ class ClickAndHold {
103
97
  async releaseAtXpath(xpath) {
104
98
  try {
105
99
  const element = await this.driver.findElement(webdriver.By.xpath(xpath));
106
- return this.actions
107
- .move({ origin: element })
108
- .release()
109
- .perform();
100
+ return this.actions.move({ origin: element }).release().perform();
110
101
  } catch (e) {
111
102
  log.error('Could not release on xpath %s', xpath);
112
103
  log.verbose(e);
@@ -123,10 +114,7 @@ class ClickAndHold {
123
114
  async releaseAtSelector(selector) {
124
115
  try {
125
116
  const element = await this.driver.findElement(webdriver.By.css(selector));
126
- return this.actions
127
- .move({ origin: element })
128
- .release()
129
- .perform();
117
+ return this.actions.move({ origin: element }).release().perform();
130
118
  } catch (e) {
131
119
  log.error('Could not release on selector %s', selector);
132
120
  log.verbose(e);
@@ -58,10 +58,7 @@ class Switch {
58
58
  */
59
59
  async toNewTab(url) {
60
60
  try {
61
- await this.browser
62
- .getDriver()
63
- .switchTo()
64
- .newWindow('tab');
61
+ await this.browser.getDriver().switchTo().newWindow('tab');
65
62
  if (url) {
66
63
  await this.navigate(url);
67
64
  await this.browser.extraWait(this.pageCompleteCheck);
@@ -80,10 +77,7 @@ class Switch {
80
77
  */
81
78
  async toNewWindow(url) {
82
79
  try {
83
- await this.browser
84
- .getDriver()
85
- .switchTo()
86
- .newWindow('window');
80
+ await this.browser.getDriver().switchTo().newWindow('window');
87
81
  if (url) {
88
82
  await this.navigate(url);
89
83
  await this.browser.extraWait(this.pageCompleteCheck);
@@ -262,11 +262,10 @@ class Iteration {
262
262
  'PerceptualSpeedIndexProgress'
263
263
  ]) {
264
264
  if (videoMetrics.visualMetrics[progress]) {
265
- videoMetrics.visualMetrics[
266
- progress
267
- ] = util.jsonifyVisualProgress(
268
- videoMetrics.visualMetrics[progress]
269
- );
265
+ videoMetrics.visualMetrics[progress] =
266
+ util.jsonifyVisualProgress(
267
+ videoMetrics.visualMetrics[progress]
268
+ );
270
269
  }
271
270
  }
272
271
  result[i].videoRecordingStart = videoMetrics.videoRecordingStart;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- module.exports = function(urlOrFunctions) {
4
- return async function(context, commands) {
3
+ module.exports = function (urlOrFunctions) {
4
+ return async function (context, commands) {
5
5
  for (let urlOrFunction of urlOrFunctions) {
6
6
  if (typeof urlOrFunction === 'function') {
7
7
  await urlOrFunction(context, commands);
@@ -70,8 +70,9 @@ class SeleniumRunner {
70
70
  this.driver = await timeout(
71
71
  builder.createWebDriver(this.baseDir, this.options),
72
72
  this.options.timeouts.browserStart,
73
- `Failed to start ${this.options.browser} in ${this.options.timeouts
74
- .browserStart / 1000} seconds.`
73
+ `Failed to start ${this.options.browser} in ${
74
+ this.options.timeouts.browserStart / 1000
75
+ } seconds.`
75
76
  );
76
77
  break;
77
78
  } catch (e) {
@@ -85,9 +86,9 @@ class SeleniumRunner {
85
86
  throw e;
86
87
  } else {
87
88
  log.info(
88
- `${this.options.browser} failed to start, trying ${tries -
89
- i -
90
- 1} more time(s): ${e.message}`
89
+ `${this.options.browser} failed to start, trying ${
90
+ tries - i - 1
91
+ } more time(s): ${e.message}`
91
92
  );
92
93
  }
93
94
  }
@@ -167,11 +168,13 @@ class SeleniumRunner {
167
168
  try {
168
169
  const pageCompleteCheckCondition = new Condition(
169
170
  'for page complete check script to return true',
170
- function(d) {
171
- return d.executeScript(pageCompleteCheck, waitTime).then(function(t) {
172
- log.verbose('PageCompleteCheck returned %s', t);
173
- return t === true;
174
- });
171
+ function (d) {
172
+ return d
173
+ .executeScript(pageCompleteCheck, waitTime)
174
+ .then(function (t) {
175
+ log.verbose('PageCompleteCheck returned %s', t);
176
+ return t === true;
177
+ });
175
178
  }
176
179
  );
177
180
  log.debug(
@@ -297,9 +300,9 @@ class SeleniumRunner {
297
300
  log.info(
298
301
  `URL ${url} failed to load, the ${
299
302
  this.options.browser
300
- } are still on ${startURI} , trying ${tries -
301
- i -
302
- 1} more time(s) but first wait for ${waitTime} ms.`
303
+ } are still on ${startURI} , trying ${
304
+ tries - i - 1
305
+ } more time(s) but first wait for ${waitTime} ms.`
303
306
  );
304
307
 
305
308
  if (i === tries - 1) {
@@ -476,13 +479,11 @@ class SeleniumRunner {
476
479
  */
477
480
  async getLogs(logType) {
478
481
  return timeout(
479
- this.driver
480
- .manage()
481
- .logs()
482
- .get(logType),
482
+ this.driver.manage().logs().get(logType),
483
483
  this.options.timeouts.logs,
484
- `Extracting logs from ${this.options.browser} took more than ${this
485
- .options.timeouts.logs / 1000} seconds.`
484
+ `Extracting logs from ${this.options.browser} took more than ${
485
+ this.options.timeouts.logs / 1000
486
+ } seconds.`
486
487
  );
487
488
  }
488
489
 
@@ -626,9 +627,9 @@ class SeleniumRunner {
626
627
  if (!script) {
627
628
  let func = category[scriptName].function;
628
629
  if (!func) {
629
- throw 'Function and script cannot both be null in ' +
630
- scriptName +
631
- '.';
630
+ throw (
631
+ 'Function and script cannot both be null in ' + scriptName + '.'
632
+ );
632
633
  }
633
634
  // We wrap the source code of the function in parenthesis
634
635
  // "(...)" to contain it in a separate scope. We add a
@@ -13,7 +13,7 @@ const safari = require('../../safari/webdriver/builder');
13
13
  * @returns {!Promise<webdriver.WebDriver>} a promise that resolves to the webdriver,
14
14
  * or rejects if the current configuration is invalid.
15
15
  */
16
- module.exports.createWebDriver = async function(baseDir, options) {
16
+ module.exports.createWebDriver = async function (baseDir, options) {
17
17
  const browser = options.browser || 'chrome';
18
18
  const seleniumUrl = options.selenium ? options.selenium.url : undefined;
19
19
  const capabilities = options.selenium
@@ -7,7 +7,7 @@ const isEmpty = require('lodash.isempty');
7
7
  const webdriver = require('selenium-webdriver');
8
8
  const setupChromiumOptions = require('../../chrome/webdriver/setupChromiumOptions');
9
9
 
10
- module.exports.configureBuilder = function(builder, baseDir, options) {
10
+ module.exports.configureBuilder = function (builder, baseDir, options) {
11
11
  const edgeConfig = options.edge || {};
12
12
  const chromeConfig = options.chrome || {};
13
13
 
@@ -46,7 +46,7 @@ function generateURL(port, testUrl, options) {
46
46
  });
47
47
  }
48
48
 
49
- module.exports = async function(url, browser, port, options) {
49
+ module.exports = async function (url, browser, port, options) {
50
50
  const configUrl = generateURL(port, url, options);
51
51
  log.debug('Configuring browser plugin via %s', configUrl);
52
52
  await browser.loadAndWait(configUrl);
@@ -40,9 +40,8 @@ class GeckoProfiler {
40
40
  const runner = this.runner;
41
41
  const firefoxConfig = this.firefoxConfig;
42
42
  const options = this.options;
43
- const chosenFeatures = firefoxConfig.geckoProfilerParams.features.split(
44
- ','
45
- );
43
+ const chosenFeatures =
44
+ firefoxConfig.geckoProfilerParams.features.split(',');
46
45
  const featureString = '["' + chosenFeatures.join('","') + '"]';
47
46
 
48
47
  const chosenThreads = firefoxConfig.geckoProfilerParams.threads.split(',');
@@ -104,11 +103,13 @@ class GeckoProfiler {
104
103
 
105
104
  let deviceProfileFilename = destinationFilename;
106
105
  if (isAndroidConfigured(options)) {
107
- deviceProfileFilename = `/sdcard/geckoProfile-${index}.json`;
106
+ deviceProfileFilename = `/sdcard/Android/data/${this.firefoxConfig.android.package}/files/geckoProfile-${index}.json`;
108
107
  }
109
108
 
110
109
  // Must use String.raw or else the backslashes on Windows will be escapes.
111
- log.info(`Collecting Gecko profile in ${destinationFilename}`);
110
+ log.info(
111
+ `Collecting Gecko profile from ${deviceProfileFilename} to ${destinationFilename}`
112
+ );
112
113
  const script = `
113
114
  var callback = arguments[arguments.length - 1];
114
115
  Services.profiler.dumpProfileToFileAsync(String.raw\`${deviceProfileFilename}\`)
@@ -4,7 +4,7 @@ const log = require('intel').getLogger('browsertime.firefox');
4
4
 
5
5
  // Get the HAR from Firefox
6
6
  // Using https://github.com/firefox-devtools/har-export-trigger
7
- module.exports = async function(runner, includeResponseBodies) {
7
+ module.exports = async function (runner, includeResponseBodies) {
8
8
  const script = `
9
9
  const callback = arguments[arguments.length - 1];
10
10
  async function triggerExport() {
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const log = require('intel').getLogger('browsertime.firefox');
2
3
  const path = require('path');
3
4
  const pathToFolder = require('../support/pathToFolder');
4
5
  const { BrowserError } = require('../support/errors');
@@ -76,7 +77,7 @@ class MemoryReport {
76
77
 
77
78
  let deviceReportFilename = destinationFilename;
78
79
  if (isAndroidConfigured(options)) {
79
- deviceReportFilename = `/sdcard/memoryReport-${index}.json`;
80
+ deviceReportFilename = `/sdcard/Android/data/${firefoxConfig.android.package}/files/memoryReport-${index}.json`;
80
81
  }
81
82
 
82
83
  let minimizeFirst = 'false';
@@ -84,6 +85,9 @@ class MemoryReport {
84
85
  minimizeFirst = 'true';
85
86
  }
86
87
 
88
+ log.info(
89
+ `Collecting memory report from ${deviceReportFilename} to ${destinationFilename}`
90
+ );
87
91
  let script = `Cc['@mozilla.org/memory-info-dumper;1'].getService(Ci.nsIMemoryInfoDumper).dumpMemoryReportsToNamedFile(String.raw\`${deviceReportFilename}\`, null, null, false, ${minimizeFirst});`;
88
92
  await timeout(
89
93
  runner.runPrivilegedScript(script, 'Collecting memory report'),
@@ -13,7 +13,7 @@ const disableTrackingProtectionPreferences = require('../settings/disableTrackin
13
13
  const util = require('../../support/util');
14
14
  const { Android } = require('../../android');
15
15
 
16
- module.exports.configureBuilder = function(builder, baseDir, options) {
16
+ module.exports.configureBuilder = function (builder, baseDir, options) {
17
17
  // Here we configure everything that we need to start Firefox
18
18
  const firefoxConfig = options.firefox || {};
19
19
  const moduleRootPath = path.resolve(__dirname, '..', '..', '..');
@@ -83,18 +83,22 @@ module.exports.configureBuilder = function(builder, baseDir, options) {
83
83
  }
84
84
  }
85
85
 
86
- Object.keys(defaultFirefoxPreferences).forEach(function(pref) {
87
- ffOptions.setPreference(pref, defaultFirefoxPreferences[pref]);
88
- });
86
+ if (!firefoxConfig.noDefaultPrefs) {
87
+ Object.keys(defaultFirefoxPreferences).forEach(function (pref) {
88
+ ffOptions.setPreference(pref, defaultFirefoxPreferences[pref]);
89
+ });
90
+ } else {
91
+ log.info('Skip setting default preferences for Firefox');
92
+ }
89
93
 
90
94
  if (firefoxConfig.disableSafeBrowsing) {
91
- Object.keys(disableSafeBrowsingPreferences).forEach(function(pref) {
95
+ Object.keys(disableSafeBrowsingPreferences).forEach(function (pref) {
92
96
  ffOptions.setPreference(pref, disableSafeBrowsingPreferences[pref]);
93
97
  });
94
98
  }
95
99
 
96
100
  if (firefoxConfig.disableTrackingProtection) {
97
- Object.keys(disableTrackingProtectionPreferences).forEach(function(pref) {
101
+ Object.keys(disableTrackingProtectionPreferences).forEach(function (pref) {
98
102
  ffOptions.setPreference(pref, disableTrackingProtectionPreferences[pref]);
99
103
  });
100
104
  }
@@ -140,6 +144,7 @@ module.exports.configureBuilder = function(builder, baseDir, options) {
140
144
  ffOptions.setPreference('devtools.chrome.enabled', true);
141
145
 
142
146
  const userPrefs = util.toArray(firefoxConfig.preference);
147
+ log.debug('Set Firefox preference %j', userPrefs);
143
148
  for (const pref of userPrefs) {
144
149
  const name = pref.substr(0, pref.indexOf(':'));
145
150
  let value = pref.substr(pref.indexOf(':') + 1);
@@ -163,7 +168,7 @@ module.exports.configureBuilder = function(builder, baseDir, options) {
163
168
  get(firefoxConfig, 'developer') ? firefox.Channel.AURORA : undefined
164
169
  ];
165
170
 
166
- firefoxTypes = firefoxTypes.filter(function(n) {
171
+ firefoxTypes = firefoxTypes.filter(function (n) {
167
172
  return n !== undefined;
168
173
  });
169
174
 
@@ -206,13 +206,12 @@ class Firefox {
206
206
  ]) {
207
207
  // You can configure to not use content and perceptual.
208
208
  if (results[i].visualMetrics[progress]) {
209
- results[i].visualMetrics[
210
- progress
211
- ] = util.adjustVisualProgressTimestamps(
212
- results[i].visualMetrics[progress],
213
- geckoProfile.meta.startTime,
214
- firstFrameStartTime
215
- );
209
+ results[i].visualMetrics[progress] =
210
+ util.adjustVisualProgressTimestamps(
211
+ results[i].visualMetrics[progress],
212
+ geckoProfile.meta.startTime,
213
+ firstFrameStartTime
214
+ );
216
215
  }
217
216
  }
218
217
  geckoProfile.meta.visualMetrics = results[i].visualMetrics;
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  const s = require('selenium-webdriver/safari');
3
3
 
4
- module.exports.configureBuilder = async function(builder, baseDir, options) {
4
+ module.exports.configureBuilder = async function (builder, baseDir, options) {
5
5
  const safariOptions = options.safari || {};
6
6
  builder
7
7
  .getCapabilities()
@@ -80,7 +80,7 @@ function generateScriptObject(name, path, contents) {
80
80
  content: null,
81
81
  isAsync:
82
82
  Object.getPrototypeOf(scriptAndMetadataObject.function) ===
83
- Object.getPrototypeOf(async function() {})
83
+ Object.getPrototypeOf(async function () {})
84
84
  };
85
85
  }
86
86
  } catch (error) {
@@ -282,6 +282,12 @@ module.exports.parseCommandLine = function parseCommandLine() {
282
282
  describe: 'Collect Chromes console log and save to disk.',
283
283
  group: 'chrome'
284
284
  })
285
+ .option('chrome.noDefaultOptions', {
286
+ type: 'boolean',
287
+ describe:
288
+ 'Prevent Browsertime from setting its default options for Chrome',
289
+ group: 'chrome'
290
+ })
285
291
  .option('cpu', {
286
292
  type: 'boolean',
287
293
  describe:
@@ -483,6 +489,12 @@ module.exports.parseCommandLine = function parseCommandLine() {
483
489
  type: 'boolean',
484
490
  group: 'firefox'
485
491
  })
492
+ .option('firefox.noDefaultPrefs', {
493
+ describe: 'Prevents browsertime from setting its default preferences.',
494
+ default: false,
495
+ type: 'boolean',
496
+ group: 'firefox'
497
+ })
486
498
  .option('firefox.disableSafeBrowsing', {
487
499
  describe: 'Disable safebrowsing.',
488
500
  default: true,
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const dayjs = require('dayjs');
5
5
  const util = require('../support/util');
6
6
  const log = require('intel').getLogger('browsertime');
7
- const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
7
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
8
8
 
9
9
  module.exports = {
10
10
  loadPrePostScripts(scripts) {
@@ -25,7 +25,7 @@ const sizeMap = {
25
25
  'iPad Pro': '1024x1366'
26
26
  };
27
27
 
28
- module.exports = function(options) {
28
+ module.exports = function (options) {
29
29
  // you cannot set the width/height for phone so just keep the viewport undefined
30
30
  if (isAndroidConfigured(options)) {
31
31
  return;
@@ -97,20 +97,19 @@ function addMetaToHAR(index, harPage, url, browserScript, options) {
97
97
  ? options.resultURL
98
98
  : options.resultURL + '/';
99
99
  if (options.screenshot) {
100
- _meta.screenshot = `${base}${pathToFolder(
101
- url,
102
- options
103
- )}screenshots/${index + 1}/afterPageCompleteCheck.${
104
- options.screenshotParams.type
105
- }`;
100
+ _meta.screenshot = `${base}${pathToFolder(url, options)}screenshots/${
101
+ index + 1
102
+ }/afterPageCompleteCheck.${options.screenshotParams.type}`;
106
103
  }
107
104
  if (options.video) {
108
- _meta.video = `${base}${pathToFolder(url, options)}video/${index +
109
- 1}.mp4`;
105
+ _meta.video = `${base}${pathToFolder(url, options)}video/${
106
+ index + 1
107
+ }.mp4`;
110
108
  }
111
109
  if (options.chrome && options.chrome.timeline) {
112
- _meta.timeline = `${base}${pathToFolder(url, options)}trace-${index +
113
- 1}.json.gz`;
110
+ _meta.timeline = `${base}${pathToFolder(url, options)}trace-${
111
+ index + 1
112
+ }.json.gz`;
114
113
  }
115
114
  }
116
115
  if (browserScript && browserScript.pageinfo) {
@@ -129,7 +128,7 @@ function jsonifyVisualProgress(visualProgress) {
129
128
  }
130
129
 
131
130
  module.exports = {
132
- addBrowser: function(har, name, version, comment) {
131
+ addBrowser: function (har, name, version, comment) {
133
132
  merge(har.log, {
134
133
  browser: {
135
134
  name,
@@ -145,7 +144,7 @@ module.exports = {
145
144
  return har;
146
145
  },
147
146
 
148
- addCreator: function(har, comment) {
147
+ addCreator: function (har, comment) {
149
148
  merge(har.log, {
150
149
  creator: {
151
150
  name: 'Browsertime',
@@ -161,7 +160,7 @@ module.exports = {
161
160
  return har;
162
161
  },
163
162
 
164
- getFullyLoaded: function(har) {
163
+ getFullyLoaded: function (har) {
165
164
  const fullyLoaded = [];
166
165
  const entries = Array.from(har.log.entries);
167
166
 
@@ -190,7 +189,7 @@ module.exports = {
190
189
  return fullyLoaded;
191
190
  },
192
191
 
193
- mergeHars: function(hars) {
192
+ mergeHars: function (hars) {
194
193
  if (isEmpty(hars)) {
195
194
  return undefined;
196
195
  }
@@ -10,7 +10,7 @@ function toSafeKey(key) {
10
10
  return key.replace(/[.~ /+|,:?&%–)(]|%7C/g, '-');
11
11
  }
12
12
 
13
- module.exports = function(url, options) {
13
+ module.exports = function (url, options) {
14
14
  if (options.useSameDir) {
15
15
  return '';
16
16
  } else {
@@ -31,19 +31,13 @@ module.exports = function(url, options) {
31
31
 
32
32
  if (useHash && !isEmpty(parsedUrl.hash)) {
33
33
  const md5 = crypto.createHash('md5'),
34
- hash = md5
35
- .update(parsedUrl.hash)
36
- .digest('hex')
37
- .substring(0, 8);
34
+ hash = md5.update(parsedUrl.hash).digest('hex').substring(0, 8);
38
35
  urlSegments.push('hash-' + hash);
39
36
  }
40
37
 
41
38
  if (!isEmpty(parsedUrl.search)) {
42
39
  const md5 = crypto.createHash('md5'),
43
- hash = md5
44
- .update(parsedUrl.search)
45
- .digest('hex')
46
- .substring(0, 8);
40
+ hash = md5.update(parsedUrl.search).digest('hex').substring(0, 8);
47
41
  urlSegments.push('query-' + hash);
48
42
  }
49
43
 
@@ -65,7 +59,7 @@ module.exports = function(url, options) {
65
59
 
66
60
  pathSegments.push('data');
67
61
 
68
- pathSegments.forEach(function(segment, index) {
62
+ pathSegments.forEach(function (segment, index) {
69
63
  if (segment) {
70
64
  pathSegments[index] = segment.replace(
71
65
  /[^-a-z0-9_.\u0621-\u064A]/gi,
@@ -3,7 +3,7 @@
3
3
  const log = require('intel').getLogger('browsertime');
4
4
  const delay = ms => new Promise(res => setTimeout(res, ms));
5
5
 
6
- module.exports = async function(browser, options) {
6
+ module.exports = async function (browser, options) {
7
7
  log.info('Accessing preURL %s', options.preURL);
8
8
  await browser.loadAndWait(options.preURL);
9
9
  await delay(options.preURLDelay ? options.preURLDelay : 1500);
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- module.exports = async function(startURL, driver, size) {
3
+ module.exports = async function (startURL, driver, size) {
4
4
  await driver.get(startURL);
5
5
  await driver.executeScript(
6
6
  `window.performance.setResourceTimingBufferSize(${size});`
@@ -137,7 +137,7 @@ class Statistics {
137
137
  stats.stddev() / Math.sqrt(stats.length).toFixed(decimals)
138
138
  ) // "standard deviation of the mean"
139
139
  };
140
- percentiles.forEach(function(p) {
140
+ percentiles.forEach(function (p) {
141
141
  let name = percentileName(p);
142
142
  node[name] = parseFloat(stats.percentile(p).toFixed(decimals));
143
143
  });
@@ -3,7 +3,7 @@
3
3
  const execa = require('execa');
4
4
  const log = require('intel').getLogger('browsertime');
5
5
 
6
- module.exports = async function(processName) {
6
+ module.exports = async function (processName) {
7
7
  const scriptArgs = ['-9', processName];
8
8
 
9
9
  log.debug('Kill all processes ' + processName);
@@ -17,9 +17,7 @@ const unlink = promisify(fs.unlink);
17
17
  const mkdir = promisify(fs.mkdir);
18
18
 
19
19
  const defaultDir = 'browsertime-results';
20
- let timestamp = dayjs()
21
- .format()
22
- .replace(/:/g, '');
20
+ let timestamp = dayjs().format().replace(/:/g, '');
23
21
 
24
22
  function pathNameFromUrl(url) {
25
23
  const parsedUrl = urlParser.parse(url),
@@ -29,10 +27,7 @@ function pathNameFromUrl(url) {
29
27
 
30
28
  if (!isEmpty(parsedUrl.search)) {
31
29
  const md5 = crypto.createHash('md5'),
32
- hash = md5
33
- .update(parsedUrl.search)
34
- .digest('hex')
35
- .substring(0, 8);
30
+ hash = md5.update(parsedUrl.search).digest('hex').substring(0, 8);
36
31
  pathSegments.push('query-' + hash);
37
32
  }
38
33
 
@@ -115,18 +110,18 @@ class StorageManager {
115
110
  }
116
111
 
117
112
  async gzip(inputFile, outputFile, removeInput) {
118
- const promise = new Promise(function(resolve, reject) {
113
+ const promise = new Promise(function (resolve, reject) {
119
114
  const gzip = zlib.createGzip();
120
115
  const input = fs.createReadStream(inputFile);
121
116
  const out = fs.createWriteStream(outputFile);
122
- out.on('finish', function() {
117
+ out.on('finish', function () {
123
118
  if (removeInput) {
124
119
  unlink(inputFile).then(() => resolve());
125
120
  } else {
126
121
  resolve();
127
122
  }
128
123
  });
129
- out.on('error', function(e) {
124
+ out.on('error', function (e) {
130
125
  log.error('Could not gzip %s to %s', inputFile, outputFile, e);
131
126
  reject();
132
127
  });
@@ -15,7 +15,7 @@ function isSmallish(options) {
15
15
  );
16
16
  }
17
17
 
18
- module.exports = async function(
18
+ module.exports = async function (
19
19
  inputFile,
20
20
  outputFile,
21
21
  videoMetrics,
@@ -3,7 +3,7 @@
3
3
  const execa = require('execa');
4
4
  const log = require('intel').getLogger('browsertime.video');
5
5
 
6
- module.exports = async function(src, dest, framerate) {
6
+ module.exports = async function (src, dest, framerate) {
7
7
  const scriptArgs = ['-nostdin', '-i', src, '-r', framerate, dest];
8
8
  log.info('Converting video to %s fps', framerate);
9
9
  return execa('ffmpeg', scriptArgs);
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- module.exports = function(options) {
4
+ module.exports = function (options) {
5
5
  // If the font is not part of the params and we're on macOS
6
6
  // we check that SFNSMono.ttf is available
7
7
  if (!options.videoParams.fontPath && process.platform === 'darwin') {
@@ -26,11 +26,12 @@ function get(metric, metricName, pos, options) {
26
26
  }
27
27
 
28
28
  // H-h/8
29
- return `drawtext=${fontFile}enable='between(t,${Number(metric) /
30
- 1000},30)':x=(w-tw)/2: y=H-${pos}-h/${x}:fontcolor=white:fontsize=h/${fontSize}:box=1:boxcolor=0x000000AA:boxborderw=2:text='${metricName} ${metric}'`;
29
+ return `drawtext=${fontFile}enable='between(t,${
30
+ Number(metric) / 1000
31
+ },30)':x=(w-tw)/2: y=H-${pos}-h/${x}:fontcolor=white:fontsize=h/${fontSize}:box=1:boxcolor=0x000000AA:boxborderw=2:text='${metricName} ${metric}'`;
31
32
  }
32
33
 
33
- module.exports = function(videoMetrics, timingMetrics, options) {
34
+ module.exports = function (videoMetrics, timingMetrics, options) {
34
35
  let text = '';
35
36
  const vm = videoMetrics.visualMetrics;
36
37
  const startPosition = 'h/10';
@@ -59,7 +60,7 @@ module.exports = function(videoMetrics, timingMetrics, options) {
59
60
  });
60
61
  }
61
62
 
62
- metricsAndValues.sort(function(a, b) {
63
+ metricsAndValues.sort(function (a, b) {
63
64
  return b.value - a.value;
64
65
  });
65
66
 
@@ -12,7 +12,7 @@ const rename = promisify(fs.rename);
12
12
  const unlink = promisify(fs.unlink);
13
13
  const defaults = require('../../defaults');
14
14
 
15
- module.exports = async function(
15
+ module.exports = async function (
16
16
  videoDir,
17
17
  videoPath,
18
18
  index,
@@ -3,7 +3,7 @@
3
3
  const execa = require('execa');
4
4
  const log = require('intel').getLogger('browsertime.video');
5
5
 
6
- module.exports = async function(
6
+ module.exports = async function (
7
7
  inputFile,
8
8
  outputFile,
9
9
  newStart,
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- module.exports = function(metrics) {
3
+ module.exports = function (metrics) {
4
4
  const videoMetrics = {};
5
5
  if (!metrics.VisualReadiness) {
6
6
  metrics.VisualReadiness =
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const visualMetrics = require('./visualMetrics');
6
6
  const extraMetrics = require('./extraMetrics');
7
7
 
8
- module.exports = async function(
8
+ module.exports = async function (
9
9
  videoDir,
10
10
  filmstripDir,
11
11
  videoPath,
@@ -38,9 +38,9 @@ async function buildX11FfmpegArgs({
38
38
  '-r',
39
39
  30,
40
40
  '-filter:v',
41
- `crop=${widthAndHeight[0] * devicePixelRatio}:${widthAndHeight[1] *
42
- devicePixelRatio}:${offset.x * devicePixelRatio}:${offset.y *
43
- devicePixelRatio}`,
41
+ `crop=${widthAndHeight[0] * devicePixelRatio}:${
42
+ widthAndHeight[1] * devicePixelRatio
43
+ }:${offset.x * devicePixelRatio}:${offset.y * devicePixelRatio}`,
44
44
  '-codec:v',
45
45
  'libx264rgb',
46
46
  '-threads',
@@ -2,7 +2,7 @@
2
2
 
3
3
  const execa = require('execa');
4
4
 
5
- module.exports = async function() {
5
+ module.exports = async function () {
6
6
  const output = await execa.command('system_profiler SPDisplaysDataType', {
7
7
  shell: true
8
8
  });
@@ -3,7 +3,7 @@
3
3
  const execa = require('execa');
4
4
  const log = require('intel').getLogger('browsertime.video');
5
5
 
6
- module.exports = async function() {
6
+ module.exports = async function () {
7
7
  const scriptArgs = [
8
8
  '-hide_banner',
9
9
  '-f',
@@ -81,7 +81,7 @@ function writeFrameDurationsToFile(directoryName, imageFiles) {
81
81
  const stream = fs.createWriteStream(
82
82
  path.join(directoryName, 'durations.txt')
83
83
  );
84
- stream.once('open', function() {
84
+ stream.once('open', function () {
85
85
  stream.write(
86
86
  "file '" + path.join(directoryName, imageFiles[0].filename) + "'\n"
87
87
  );
@@ -120,7 +120,7 @@ async function generateVideo(destination, recordingDirectoryName) {
120
120
  imageFiles.push({ filename: newFilename, offset: offset });
121
121
  });
122
122
 
123
- imageFiles.sort(function(a, b) {
123
+ imageFiles.sort(function (a, b) {
124
124
  if (a.filename < b.filename) return -1;
125
125
  if (a.filename > b.filename) return 1;
126
126
  return 0;
@@ -2,7 +2,7 @@
2
2
 
3
3
  const log = require('intel').getLogger('browsertime.video');
4
4
  const { until, By } = require('selenium-webdriver');
5
- module.exports = async function(driver, options) {
5
+ module.exports = async function (driver, options) {
6
6
  log.debug('Add orange color');
7
7
  // We tried other ways for Android (access an orange page)
8
8
  // That works fine ... but break scripts
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "description": "Browsertime",
3
- "version": "14.5.0",
3
+ "version": "14.8.0",
4
4
  "bin": "./bin/browsertime.js",
5
5
  "dependencies": {
6
6
  "@sitespeed.io/throttle": "3.0.0",
7
7
  "@devicefarmer/adbkit": "2.11.3",
8
8
  "btoa": "1.2.1",
9
- "@sitespeed.io/chromedriver": "94.0.4606-41",
9
+ "@sitespeed.io/chromedriver": "95.0.4638-17",
10
10
  "chrome-har": "0.11.12",
11
11
  "chrome-remote-interface": "0.31.0",
12
12
  "dayjs": "1.10.5",
@@ -22,8 +22,8 @@
22
22
  "lodash.merge": "4.6.2",
23
23
  "lodash.pick": "4.4.0",
24
24
  "lodash.set": "4.3.2",
25
- "selenium-webdriver": "4.0.0-rc-1",
26
- "@sitespeed.io/edgedriver": "93.0.910-5",
25
+ "selenium-webdriver": "4.0.0",
26
+ "@sitespeed.io/edgedriver": "95.0.1020-30",
27
27
  "@sitespeed.io/geckodriver": "0.29.1-2",
28
28
  "@sitespeed.io/tracium": "0.3.3",
29
29
  "speedline-core": "1.4.3",
@@ -34,15 +34,15 @@
34
34
  "jimp": "0.16.1"
35
35
  },
36
36
  "devDependencies": {
37
- "bluebird": "3.5.5",
38
- "chai": "4.2.0",
37
+ "bluebird": "3.7.2",
38
+ "chai": "4.3.4",
39
39
  "chai-as-promised": "7.1.1",
40
- "eslint": "6.2.2",
41
- "eslint-config-prettier": "6.1.0",
42
- "eslint-plugin-prettier": "3.1.0",
40
+ "eslint": "8.0.1",
41
+ "eslint-config-prettier": "8.3.0",
42
+ "eslint-plugin-prettier": "4.0.0",
43
43
  "longjohn": "0.2.12",
44
44
  "mocha": "6.2.0",
45
- "prettier": "1.18.2"
45
+ "prettier": "2.4.1"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=10.13.0"