Browse Source

Merge pull request #8737 from saulshanabrook/benchmark-compare

Add ability to compare benchmarks
Eric Charles 4 years ago
parent
commit
290d76a13c

+ 3 - 1
benchmarks/.gitignore

@@ -1,3 +1,5 @@
 yarn.lock
 data/
-out.*
+out.*
+*.csv
+*.png

+ 36 - 1
benchmarks/README.md

@@ -8,7 +8,7 @@ of different notebook sizes and types.
 To run the benchmarks against notebooks with long outputs and notebooks with many outputs, and to see how the times change as the notebooks grow:
 
 ```bash
-jplm # First install dependencies
+jlpm # First install dependencies
 
 env 'BENCHMARK_NOTEBOOKS=["./longOutput", "./manyOutputs"]' jlpm all
 ```
@@ -62,3 +62,38 @@ is an example of a cell output that takes a while to render:
 ```bash
 env 'BENCHMARK_NOTEBOOKS=["./fixedDataTable", "./manyOutputs"]' jlpm all
 ```
+
+## Comparing Benchmarks
+
+We also support comparing two runs of benchmarks to give a sense of the relative changes. This is meant to answer the question "How much did these code changes affect
+the time it takes to open a notebook?"
+
+To do this, it uses the technique from Tomas Kalibera and Richard Jones in their paper
+["Quantifying Performance Changes with Effect Size Confidence Intervals."](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjEq7u9ovXqAhXYs54KHf3QCM0QFjACegQIBRAB&url=https%3A%2F%2Farxiv.org%2Fabs%2F2007.10899&usg=AOvVaw0ihkJJIaT6v95zlAtGtI2o) From their abstract:
+
+> Inspired by statistical methods used in other fields of science, and building on results in statistics that did not make it to introductory textbooks, we present a statistical model that allows us both to quantify uncertainty in the ratio of (execution time) means and to design experiments with a rigorous treatment of those multiple sources of non-determinism that might impact measured performance. Better still, under our framework summaries can be as simple as “system A is faster than system B by 5.5% ± 2.5%, with 95% confidence”, a more natural statement than those derived from typical current practice, which are often misinterpreted.
+
+```bash
+# Create `old.csv` and `new.csv` files
+env 'BENCHMARK_NOTEBOOKS=["./manyOutputs", "./longOutput"]' BENCHMARK_OUTPUT=old.csv jlpm start:benchmark
+env 'BENCHMARK_NOTEBOOKS=["./manyOutputs", "./longOutput"]' BENCHMARK_OUTPUT=new.csv jlpm start:benchmark
+# Create comparison csv file
+jlpm run start:compare
+# Create and open graph to view results
+jlpm run start:compare-graph
+```
+
+When creating the comparison file, it will also print out a textual form of the analysis, like this:
+
+```bash
+$ node lib/compare.js
+Writing output to diff.csv
+Parsing data { path: 'old.csv', version: 'old' }
+Parsing data { path: 'old.csv', version: 'new_' }
+In firefox opening a notebook with 100 n outputs each of a div where n=0 is between 36.4% slower and 26.7% faster with 95% confidence.
+In firefox opening a notebook with one output with 100 n divs where n=0 is between 1615.4% slower and 94.2% faster with 95% confidence.
+...
+✨  Done in 1.08s.
+```
+
+That's the same information as in the graph, but expressed as English.

+ 61 - 0
benchmarks/comparison.vl.json

@@ -0,0 +1,61 @@
+{
+  "data": {
+    "url": "./diff.csv",
+    "format": {
+      "type": "csv",
+      "parse": {
+        "confidenceInterval": "number",
+        "mean": "number",
+        "n": "number"
+      }
+    }
+  },
+  "facet": {
+    "row": { "field": "mode", "type": "nominal" },
+    "column": { "field": "type" }
+  },
+  "spec": {
+    "encoding": {
+      "color": {
+        "field": "browser",
+        "type": "nominal"
+      },
+      "x": {
+        "field": "n",
+        "type": "quantitative",
+        "scale": {}
+      }
+    },
+    "layer": [
+      {
+        "mark": {
+          "type": "errorband",
+          "clip": true
+        },
+        "encoding": {
+          "y": {
+            "field": "mean",
+            "type": "quantitative",
+            "scale": { "domain": [0, 2] },
+            "title": "Ratio of Time (95% CI)"
+          },
+          "yError": { "field": "confidenceInterval" }
+        }
+      },
+      {
+        "mark": {
+          "type": "line",
+          "clip": true,
+          "tooltip": true,
+          "size": 1
+        },
+        "encoding": {
+          "y": {
+            "field": "mean",
+            "type": "quantitative"
+          }
+        }
+      }
+    ]
+  }
+}

+ 12 - 3
benchmarks/package.json

@@ -2,23 +2,32 @@
   "name": "@jupyterlab/benchmarks",
   "version": "0.0.0",
   "private": true,
-  "type": "module",
   "scripts": {
     "build:jlab": "cd .. && jlpm run build",
     "build:benchmark": "tsc",
     "start:jlab": "fkill -s :9999 && cd .. && jupyter lab --dev --no-browser --port 9999 --LabApp.password= --LabApp.token=",
-    "start:benchmark": "wait-on http-get://localhost:9999/lab && node --experimental-modules --es-module-specifier-resolution=node lib/index.js",
+    "start:benchmark": "wait-on http-get://localhost:9999/lab && node lib/index.js",
     "start:all": "npm-run-all --parallel start:jlab start:benchmark --race",
-    "start:analysis": "cd .. && jupyter lab --dev benchmarks/analysis.vl.json",
+    "start:analysis": "vl2png analysis.vl.json > analysis.png && open analysis.png",
+    "start:compare": "node lib/compare.js",
+    "start:compare-graph": "vl2png comparison.vl.json > comparison.png && open comparison.png",
     "all": "npm-run-all build:* start:all start:analysis"
   },
   "dependencies": {
+    "@stdlib/stdlib": "0.0.92",
+    "csv-parser": "2.3.3",
+    "neat-csv": "5.2.0",
     "playwright": "0.11.1"
   },
   "devDependencies": {
     "fkill-cli": "6.0.1",
     "npm-run-all": "4.1.5",
     "typescript": "~3.7.3",
+    "vega-cli": "5.13.0",
+    "vega-lite": "4.14.0",
     "wait-on": "4.0.1"
+  },
+  "bin": {
+    "benchmark": "lib/compare.js"
   }
 }

+ 278 - 0
benchmarks/src/compare.ts

@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+import * as quantile from '@stdlib/stats/base/dists/t/quantile';
+import * as meanpw from '@stdlib/stats/base/meanpw';
+import * as variancepn from '@stdlib/stats/base/variancepn';
+import * as neatCSV from 'neat-csv';
+import * as fs from 'fs';
+
+const OUTPUT_FILE = process.env['BENCHMARK_OUTPUT'] || 'diff.csv';
+const OLD_FILE = process.env['BENCHMARK_INPUT_OLD'] || 'old.csv';
+const NEW_FILE = process.env['BENCHMARK_INPUT_NEW'] || 'new.csv';
+
+const stream = fs.createWriteStream(OUTPUT_FILE);
+
+function writeLine(line: string): Promise<void> {
+  return new Promise(function(resolve, reject) {
+    stream.write(line + '\n', error => (error ? reject(error) : resolve()));
+  });
+}
+tests();
+
+void main();
+
+const MODE_LABEL = {
+  open: 'opening a notebook',
+  switch: 'switching to a notebook'
+};
+async function main() {
+  console.log(`Writing output to ${OUTPUT_FILE}`);
+  await writeLine('mode,browser,n,type,mean,confidenceInterval');
+
+  for await (const {
+    mode,
+    browser,
+    n,
+    mean,
+    type,
+    confidenceInterval
+  } of compare(OLD_FILE, NEW_FILE, 0.95)) {
+    console.log(
+      `In ${browser} ${
+        MODE_LABEL[mode as keyof typeof MODE_LABEL]
+      } with ${type} where n=${n} is ${formatChange({
+        mean,
+        confidenceInterval
+      })} with 95% confidence.`
+    );
+    await writeLine(
+      [mode, browser, n, type, mean, confidenceInterval].join(',')
+    );
+  }
+}
+
+type OutputRow = {
+  mode: string;
+  browser: string;
+  type: string;
+  n: number;
+  mean: number;
+  confidenceInterval: number;
+};
+
+async function* compare(
+  oldCSVPath: string,
+  newCSVPath: string,
+  confidenceInterval: number = 0.95
+): AsyncIterable<OutputRow> {
+  const collected: {
+    // turn key into string so we can lookup easily with it
+    [key: string]: {
+      mode: string;
+      browser: string;
+      type: string;
+      n: number;
+      times: { [VERSION in 'old' | 'new_']: number[] };
+    };
+  } = {};
+  for (const { path, version } of [
+    { path: oldCSVPath, version: 'old' as 'old' },
+    { path: newCSVPath, version: 'new_' as 'new_' }
+  ]) {
+    console.log('Parsing data', { path, version });
+    const text = await fs.promises.readFile(path);
+    for (const { mode, browser, n, type, time } of await neatCSV(text)) {
+      const key = `${mode}-${browser}-${n}-${type}`;
+      // get key if we have it, otherwise create new
+      const data =
+        collected[key] ||
+        (collected[key] = {
+          mode,
+          browser,
+          n: parseInt(n),
+          type,
+          times: { old: [], new_: [] }
+        });
+      data.times[version].push(parseFloat(time));
+    }
+  }
+  for (const {
+    mode,
+    browser,
+    type,
+    n,
+    times: { old, new_ }
+  } of Object.values(collected)) {
+    if (old.length != new_.length) {
+      console.warn('Skipping because different lengths between runs', {
+        mode,
+        browser,
+        type,
+        n
+      });
+      continue;
+    }
+    if (old.length <= 2) {
+      console.warn('Skipping because not enough runs', {
+        mode,
+        browser,
+        type,
+        n
+      });
+      continue;
+    }
+    yield {
+      mode,
+      browser,
+      type,
+      n,
+      ...performanceChangeFromData(old, new_, confidenceInterval)
+    };
+  }
+}
+
+/**
+ * Quantifies the performance changes between two measures systems. Assumes we gathered
+ * n independent measurement from each, and calculated their means and varience.
+ *
+ * Based on the work by Tomas Kalibera and Richard Jones. See their paper
+ * "Quantifying Performance Changes with Effect Size Confidence Intervals", section 6.2,
+ * formula "Quantifying Performance Change".
+ *
+ * However, it simplifies it to only assume one level of benchmarks, not multiple levels.
+ * If you do have multiple levels, simply use the mean of the lower levels as your data,
+ * like they do in the paper.
+ *
+ * @param oldSystem The old system we measured
+ * @param newSystem The new system we measured
+ * @param n The number of samples from each system (must be equal)
+ * @param confidenceInterval The confidence interval for the results.
+ *  The default is a 95% confidence interval (95% of the time the true mean will be
+ *  between the resulting mean +- the resulting CI)
+ */
+export function performanceChange(
+  { mean: y_o, variance: s_o }: { mean: number; variance: number },
+  { mean: y_n, variance: s_n }: { mean: number; variance: number },
+  n: number,
+  confidenceInterval: number = 0.95
+): { mean: number; confidenceInterval: number } {
+  const dof = n - 1;
+  const t = quantile(1 - (1 - confidenceInterval) / 2, dof);
+  const oldFactor = sq(y_o) - (sq(t) * s_o) / n;
+  const newFactor = sq(y_n) - (sq(t) * s_n) / n;
+  const meanNum = y_o * y_n;
+  const ciNum = Math.sqrt(sq(y_o * y_n) - newFactor * oldFactor);
+  return {
+    mean: meanNum / oldFactor,
+    confidenceInterval: ciNum / oldFactor
+  };
+}
+
+/**
+ * Compute the performance change based on a number of old and new measurements.
+ */
+export function performanceChangeFromData(
+  old: number[],
+  new_: number[],
+  confidenceInterval: number = 0.95
+): { mean: number; confidenceInterval: number } {
+  const n = old.length;
+  if (n !== new_.length) {
+    throw new Error('Data have different length');
+  }
+  return performanceChange(
+    { mean: mean(...old), variance: variance(...old) },
+    { mean: mean(...new_), variance: variance(...new_) },
+    n,
+    confidenceInterval
+  );
+}
+
+/**
+ * Format a performance changes like `between 20.1% slower and 30.3% faster`
+ */
+function formatChange({
+  mean,
+  confidenceInterval
+}: {
+  mean: number;
+  confidenceInterval: number;
+}): string {
+  return `between ${formatPercent(
+    mean + confidenceInterval
+  )} and ${formatPercent(mean - confidenceInterval)}`;
+}
+
+function formatPercent(percent: number): string {
+  if (percent < 1) {
+    return `${((1 - percent) * 100).toFixed(1)}% faster`;
+  }
+  return `${((percent - 1) * 100).toFixed(1)}% slower`;
+}
+
+/**
+ * Reproduce examples from paper, and verify we have implemented things correctly.
+ */
+export function tests() {
+  assertAboutEqual(quantile(1 - 0.05 / 2, 2), 4.3, 'quantile');
+
+  const paperResult = {
+    mean: 68.3 / 74.5,
+    confidenceInterval: 60.2 / 70.2
+  };
+  assertResultsEqual(
+    performanceChange(
+      { variance: 5.8, mean: 10.5 },
+      { variance: 4.6, mean: 6.5 },
+      3,
+      0.95
+    ),
+    paperResult,
+    'performanceChange'
+  );
+
+  //   Data from table V, uses means of top level
+  assertResultsEqual(
+    performanceChangeFromData(
+      [mean(9, 11, 5, 6), mean(16, 13, 12, 8), mean(15, 7, 10, 14)],
+      [mean(10, 12, 6, 7), mean(9, 1, 11, 4), mean(8, 5, 3, 2)],
+      0.95
+    ),
+    paperResult,
+    'performanceChangeFromData'
+  );
+}
+
+function assertResultsEqual(
+  l: {
+    mean: number;
+    confidenceInterval: number;
+  },
+  r: { mean: number; confidenceInterval: number },
+  message: string
+) {
+  assertAboutEqual(l.mean, r.mean, `${message}: means`);
+  assertAboutEqual(
+    r.confidenceInterval,
+    r.confidenceInterval,
+    `${message}: confidence interval`
+  );
+}
+
+function assertAboutEqual(x: number, y: number, msg: string): void {
+  console.assert(Math.abs(x - y) <= 0.005, `${msg}: ${x} != ${y}`);
+}
+
+function sq(x: number): number {
+  return Math.pow(x, 2);
+}
+
+function mean(...x: number[]): number {
+  return meanpw(x.length, x, 1);
+}
+
+function variance(...x: number[]): number {
+  return variancepn(x.length, 1, x, 1);
+}

+ 4 - 0
benchmarks/src/fixedDataTable.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 import makeNotebook from './makeNotebook';
 import NotebookType from './notebookType';
 

+ 8 - 6
benchmarks/src/index.ts

@@ -1,13 +1,15 @@
 /**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
  * Runs a number of benchmarks and saves the results to the
  */
-import child_process from 'child_process';
-import fs from 'fs';
-import playwright from 'playwright';
-import util from 'util';
+import * as child_process from 'child_process';
+import * as fs from 'fs';
+import * as playwright from 'playwright';
+import * as util from 'util';
 import NotebookType from './notebookType';
 
-const DATA_PATH = 'out.csv';
+const DATA_PATH = process.env['BENCHMARK_OUTPUT'] || 'out.csv';
 
 const BROWSERS: Array<'firefox' | 'chromium'> = ['firefox', 'chromium'];
 // The maximium N
@@ -15,7 +17,7 @@ const MAX_N = 100;
 // The number of different n's to try out
 const NUMBER_SAMPLES = 20;
 // How many times to switch between each notebook
-const SWITCHES = 5;
+const SWITCHES = 10;
 
 /**
  * Max time to stop testing if mean of previous sample was > this.

+ 4 - 0
benchmarks/src/largePlotly.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 import makeNotebook from './makeNotebook';
 import waitForPlotly from './waitForPlotly';
 import NotebookType from './notebookType';

+ 4 - 0
benchmarks/src/longOutput.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 import makeNotebook from './makeNotebook';
 import NotebookType from './notebookType';
 

+ 4 - 0
benchmarks/src/makeNotebook.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 export default function makeNotebook(cells: Array<object>): object {
   return {
     cells,

+ 4 - 0
benchmarks/src/manyOutputs.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 import makeNotebook from './makeNotebook';
 import NotebookType from './notebookType';
 

+ 4 - 0
benchmarks/src/manyPlotly.ts

@@ -1,3 +1,7 @@
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
 import waitForPlotly from './waitForPlotly';
 import makeNotebook from './makeNotebook';
 import NotebookType from './notebookType';

+ 5 - 1
benchmarks/src/notebookType.ts

@@ -1,4 +1,8 @@
-import playwright from 'playwright';
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+import * as playwright from 'playwright';
 
 type NotebookType = {
   label: string;

+ 5 - 1
benchmarks/src/waitForPlotly.ts

@@ -1,4 +1,8 @@
-import playwright from 'playwright';
+/**
+ * Copyright (c) Jupyter Development Team.
+ * Distributed under the terms of the Modified BSD License.
+ */
+import * as playwright from 'playwright';
 
 /**
  * Wait for width to be changed to greater than the default of 700px which happens after rendering is done.

+ 1 - 1
benchmarks/tsconfig.json

@@ -8,7 +8,7 @@
     "rootDir": "./src",
     "strict": true,
     "lib": ["ES2019", "DOM"],
-    "module": "ESNEXT",
+    "module": "CommonJS",
     "forceConsistentCasingInFileNames": true,
     "skipLibCheck": true,
     "allowSyntheticDefaultImports": true

+ 14 - 35
yarn.lock

@@ -5045,7 +5045,7 @@ acorn@^7.1.0:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c"
   integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==
 
-acorn@^7.1.1, acorn@^7.3.0, acorn@^7.3.1:
+acorn@^7.3.0, acorn@^7.3.1:
   version "7.3.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd"
   integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==
@@ -7334,7 +7334,7 @@ create-react-context@^0.3.0:
     gud "^1.0.0"
     warning "^4.0.3"
 
-cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+cross-spawn@6.0.5, cross-spawn@^6.0.0:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
   integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@@ -8560,13 +8560,6 @@ eslint-scope@^5.1.0:
     esrecurse "^4.1.0"
     estraverse "^4.1.1"
 
-eslint-utils@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
-  integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
-  dependencies:
-    eslint-visitor-keys "^1.1.0"
-
 eslint-utils@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd"
@@ -8676,15 +8669,6 @@ eslint@^7.5.0:
     text-table "^0.2.0"
     v8-compile-cache "^2.0.3"
 
-espree@^6.1.2:
-  version "6.2.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
-  integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==
-  dependencies:
-    acorn "^7.1.1"
-    acorn-jsx "^5.2.0"
-    eslint-visitor-keys "^1.1.0"
-
 espree@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/espree/-/espree-7.2.0.tgz#1c263d5b513dbad0ac30c4991b93ac354e948d69"
@@ -8704,7 +8688,7 @@ esprima@~3.1.0:
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
   integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
 
-esquery@^1.0.1, esquery@^1.2.0:
+esquery@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
   integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==
@@ -12220,14 +12204,6 @@ levenary@^1.1.1:
   dependencies:
     leven "^3.1.0"
 
-levn@^0.3.0, levn@~0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
-  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
-  dependencies:
-    prelude-ls "~1.1.2"
-    type-check "~0.3.2"
-
 levn@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -12236,6 +12212,14 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+levn@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+  integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+  dependencies:
+    prelude-ls "~1.1.2"
+    type-check "~0.3.2"
+
 lighthouse-logger@^1.0.0, lighthouse-logger@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz#b76d56935e9c137e86a04741f6bb9b2776e886ca"
@@ -13872,7 +13856,7 @@ optimist@^0.6.1:
     minimist "~0.0.1"
     wordwrap "~0.0.2"
 
-optionator@^0.8.1, optionator@^0.8.3:
+optionator@^0.8.1:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
   integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
@@ -15459,11 +15443,6 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
     define-properties "^1.1.3"
     es-abstract "^1.17.0-next.1"
 
-regexpp@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
-  integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
-
 regexpp@^3.0.0, regexpp@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2"
@@ -15987,7 +15966,7 @@ semver-regex@^2.0.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
-semver@6.x, semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
+semver@6.x, semver@^6.0.0, semver@^6.2.0, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
@@ -16794,7 +16773,7 @@ strip-indent@^2.0.0:
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
   integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
 
-strip-json-comments@^3.0.1, strip-json-comments@^3.1.0:
+strip-json-comments@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==