index.js
  1  /**
  2   * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
  3   *
  4   * This source code is licensed under the MIT license found in the
  5   * LICENSE file in the root directory of this source tree.
  6   */
  7  
  8  // Make sure to run node with --expose-gc option!
  9  
 10  // The times are reliable if about 1% relative mean error if you run it:
 11  
 12  // * immediately after restart
 13  // * with 100% battery charge
 14  // * not connected to network
 15  
 16  /* eslint import/no-extraneous-dependencies: "off" */
 17  
 18  const Benchmark = require('benchmark');
 19  const diffBaseline = require('diff').diffLines;
 20  const diffImproved = require('../build/index.js').default;
 21  
 22  const testBaseline = (a, b) => {
 23    const benchmark = new Benchmark({
 24      fn() {
 25        diffBaseline(a, b);
 26      },
 27      name: 'baseline',
 28      onCycle() {
 29        global.gc(); // after run cycle
 30      },
 31      onStart() {
 32        global.gc(); // when benchmark starts
 33      },
 34    });
 35  
 36    benchmark.run({async: false});
 37  
 38    return benchmark.stats;
 39  };
 40  
 41  const testImproved = function (a, b) {
 42    const benchmark = new Benchmark({
 43      fn() {
 44        // Split string arguments to make fair comparison with baseline.
 45        const aItems = a.split('\n');
 46        const bItems = b.split('\n');
 47  
 48        const isCommon = (aIndex, bIndex) => aItems[aIndex] === bItems[bIndex];
 49  
 50        // This callback obviously does less than baseline `diff` package,
 51        // but avoiding double work and memory churn is the goal.
 52        // For example, `jest-diff` has had to split strings that `diff` joins.
 53        const foundSubsequence = () => {};
 54  
 55        diffImproved(aItems.length, bItems.length, isCommon, foundSubsequence);
 56      },
 57      name: 'improved',
 58      onCycle() {
 59        global.gc(); // after run cycle
 60      },
 61      onStart() {
 62        global.gc(); // when benchmark starts
 63      },
 64    });
 65  
 66    benchmark.run({async: false});
 67  
 68    return benchmark.stats;
 69  };
 70  
 71  const writeHeading2 = () => {
 72    console.log('## Benchmark time for `diff-sequences` versus `diff`\n');
 73    console.log('A ratio less than 1.0 means `diff-sequences` is faster.');
 74  };
 75  
 76  const writeHeading3 = n => {
 77    console.log(`\n### n = ${n}\n`);
 78    console.log('| name | % | ratio | improved | rme | baseline | rme |');
 79    console.log('| :--- | ---: | :--- | :--- | ---: | :--- | ---: |');
 80  };
 81  
 82  const writeRow = (name, percent, statsImproved, statsBaseline) => {
 83    const {mean: meanImproved, rme: rmeImproved} = statsImproved;
 84    const {mean: meanBaseline, rme: rmeBaseline} = statsBaseline;
 85    const ratio = meanImproved / meanBaseline;
 86  
 87    console.log(
 88      `| ${name} | ${percent}% | ${ratio.toFixed(
 89        4,
 90      )} | ${meanImproved.toExponential(4)} | ${rmeImproved.toFixed(
 91        2,
 92      )}% | ${meanBaseline.toExponential(4)} | ${rmeBaseline.toFixed(2)}% |`,
 93    );
 94  };
 95  
 96  const testDeleteInsert = (tenths, more, less) => {
 97    // For improved `diff-sequences` package, delete is often slower than insert.
 98    const statsDeleteImproved = testImproved(more, less);
 99    const statsDeleteBaseline = testBaseline(more, less);
100    writeRow('delete', tenths * 10, statsDeleteImproved, statsDeleteBaseline);
101  
102    // For baseline `diff` package, many insertions is serious perf problem.
103    // However, the benchmark package cannot accurately measure for large n.
104    const statsInsertBaseline = testBaseline(less, more);
105    const statsInsertImproved = testImproved(less, more);
106    writeRow('insert', tenths * 10, statsInsertImproved, statsInsertBaseline);
107  };
108  
109  const testChange = (tenths, expected, received) => {
110    const statsImproved = testImproved(expected, received);
111    const statsBaseline = testBaseline(expected, received);
112    writeRow('change', tenths * 10, statsImproved, statsBaseline);
113  };
114  
115  const getItems = (n, callback) => {
116    const items = [];
117  
118    for (let i = 0; i !== n; i += 1) {
119      const item = callback(i);
120      if (typeof item === 'string') {
121        items.push(item);
122      }
123    }
124  
125    return items.join('\n');
126  };
127  
128  // Simulate change of property name which is usually not same line.
129  // Expected: 0 1 2 3 4 5 6 7 8 9 and so on
130  // Received: 1 2 3 4 x0 5 6 7 8 9 and so on
131  const change2 = i => {
132    const j = i % 10;
133    return j === 4 ? `x${i - 4}` : j < 4 ? `${i + 1}` : `${i}`;
134  };
135  
136  const testLength = n => {
137    const all = getItems(n, i => `${i}`);
138  
139    writeHeading3(n);
140  
141    [2, 4, 8].forEach(tenth => {
142      testDeleteInsert(
143        tenth,
144        all,
145        getItems(n, i => i % 10 >= tenth && `${i}`),
146      );
147    });
148    testChange(
149      1,
150      all,
151      getItems(n, i => (i % 10 === 0 ? `x${i}` : `${i}`)),
152    );
153    testChange(2, all, getItems(n, change2));
154    testChange(
155      5,
156      all,
157      getItems(n, i => (i % 2 === 0 ? `x${i}` : `${i}`)),
158    );
159    testChange(
160      10,
161      all,
162      getItems(n, i => `x${i}`),
163    ); // simulate TDD
164  };
165  
166  writeHeading2();
167  
168  testLength(20);
169  testLength(200);
170  testLength(2000);