/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
(function (_global) {
  var allTasks = _global['__zone_symbol__performance_tasks'];
  if (!allTasks) {
    allTasks = _global['__zone_symbol__performance_tasks'] = [];
  }

  var mark = (_global['__zone_symbol__mark'] = function (name) {
    performance && performance['mark'] && performance['mark'](name);
  });

  var measure = (_global['__zone_symbol__measure'] = function (name, label) {
    performance && performance['measure'] && performance['measure'](name, label);
  });

  var getEntries = (_global['__zone_symbol__getEntries'] = function () {
    performance && performance['getEntries'] && performance['getEntries']();
  });

  var getEntriesByName = (_global['__zone_symbol__getEntriesByName'] = function (name) {
    return performance && performance['getEntriesByName'] && performance['getEntriesByName'](name);
  });

  var clearMarks = (_global['__zone_symbol__clearMarks'] = function (name) {
    return performance && performance['clearMarks'] && performance['clearMarks'](name);
  });

  var clearMeasures = (_global['__zone_symbol__clearMeasures'] = function (name) {
    return performance && performance['clearMeasures'] && performance['clearMeasures'](name);
  });

  var averageMeasures = (_global['__zone_symbol__averageMeasures'] = function (name, times) {
    var sum = _global['__zone_symbol__getEntriesByName'](name)
      .filter(function (m) {
        return m.entryType === 'measure';
      })
      .map(function (m) {
        return m.duration;
      })
      .reduce(function (sum, d) {
        return sum + d;
      });
    return sum / times;
  });

  var serialPromise = (_global['__zone_symbol__serialPromise'] = function (promiseFactories) {
    let lastPromise;
    for (var i = 0; i < promiseFactories.length; i++) {
      var promiseFactory = promiseFactories[i];
      if (!lastPromise) {
        lastPromise = promiseFactory.factory(promiseFactory.context).then(function (value) {
          return {value, idx: 0};
        });
      } else {
        lastPromise = lastPromise.then(function (ctx) {
          var idx = ctx.idx + 1;
          var promiseFactory = promiseFactories[idx];
          return promiseFactory.factory(promiseFactory.context).then(function (value) {
            return {value, idx};
          });
        });
      }
    }
    return lastPromise;
  });

  var callbackContext = (_global['__zone_symbol__callbackContext'] = {});
  var zone = (_global['__zone_symbol__callbackZone'] = Zone.current.fork({
    name: 'callback',
    onScheduleTask: function (delegate, curr, target, task) {
      delegate.scheduleTask(target, task);
      if (
        task.type === callbackContext.type &&
        task.source.indexOf(callbackContext.source) !== -1
      ) {
        if (task.type === 'macroTask' || task.type === 'eventTask') {
          var invoke = task.invoke;
          task.invoke = function () {
            mark(callbackContext.measureName);
            var result = invoke.apply(this, arguments);
            measure(callbackContext.measureName, callbackContext.measureName);
            return result;
          };
        } else if (task.type === 'microTask') {
          var callback = task.callback;
          task.callback = function () {
            mark(callbackContext.measureName);
            var result = callback.apply(this, arguments);
            measure(callbackContext.measureName, callbackContext.measureName);
            return result;
          };
        }
      }
      return task;
    },
  }));

  var runAsync = (_global['__zone_symbol__runAsync'] = function (testFn, times, _delay) {
    var delay = _delay | 100;
    const fnPromise = function () {
      return new Promise(function (res, rej) {
        // run test with a setTimeout
        // several times to decrease measurement error
        setTimeout(function () {
          testFn().then(function () {
            res();
          });
        }, delay);
      });
    };
    var promiseFactories = [];
    for (var i = 0; i < times; i++) {
      promiseFactories.push({factory: fnPromise, context: {}});
    }

    return serialPromise(promiseFactories);
  });

  var getNativeMethodName = function (nativeWithSymbol) {
    return nativeWithSymbol.replace('__zone_symbol__', 'native_');
  };

  function testAddRemove(api, count) {
    var timerId = [];

    var name = api.method;
    mark(name);
    for (var i = 0; i < count; i++) {
      timerId.push(api.run());
    }
    measure(name, name);

    if (api.supportClear) {
      var clearName = api.clearMethod;
      mark(clearName);
      for (var i = 0; i < count; i++) {
        api.runClear(timerId[i]);
      }
      measure(clearName, clearName);
    }

    timerId = [];

    var nativeName = getNativeMethodName(api.nativeMethod);
    mark(nativeName);
    for (var i = 0; i < count; i++) {
      timerId.push(api.nativeRun());
    }
    measure(nativeName, nativeName);

    if (api.supportClear) {
      var nativeClearName = getNativeMethodName(api.nativeClearMethod);
      mark(nativeClearName);
      for (var i = 0; i < count; i++) {
        api.nativeRunClear(timerId[i]);
      }
      measure(nativeClearName, nativeClearName);
    }

    return Promise.resolve(1);
  }

  function testCallback(api, count) {
    var promises = [Promise.resolve(1)];
    for (var i = 0; i < count; i++) {
      var r = api.run();
      if (api.isAsync) {
        promises.push(r);
      }
    }

    for (var i = 0; i < count; i++) {
      var r = api.nativeRun();
      if (api.isAsync) {
        promises.push(r);
      }
    }
    return Promise.all(promises);
  }

  function measureCallback(api, ops) {
    var times = ops.times;
    var displayText = ops.displayText;
    var rawData = ops.rawData;
    var summary = ops.summary;

    var name = api.method;
    var nativeName = getNativeMethodName(api.nativeMethod);
    var measure = averageMeasures(name, times);
    var nativeMeasure = averageMeasures(nativeName, times);
    displayText += `- ${name} costs ${measure} ms\n`;
    displayText += `- ${nativeName} costs ${nativeMeasure} ms\n`;
    var absolute = Math.floor(1000 * (measure - nativeMeasure)) / 1000;
    displayText += `# ${name} is ${absolute}ms slower than ${nativeName}\n`;
    rawData[name + '_measure'] = measure;
    rawData[nativeName + '_measure'] = nativeMeasure;
    summary[name] = absolute + 'ms';
  }

  function measureAddRemove(api, ops) {
    var times = ops.times;
    var displayText = ops.displayText;
    var rawData = ops.rawData;
    var summary = ops.summary;

    var name = api.method;
    var nativeName = getNativeMethodName(api.nativeMethod);

    var measure = averageMeasures(name, times);
    var nativeMeasure = averageMeasures(nativeName, times);
    displayText += `- ${name} costs ${measure} ms\n`;
    displayText += `- ${nativeName} costs ${nativeMeasure} ms\n`;
    var percent = Math.floor((100 * (measure - nativeMeasure)) / nativeMeasure);
    displayText += `# ${name} is ${percent}% slower than ${nativeName}\n`;
    rawData[name + '_measure'] = measure;
    rawData[nativeName + '_measure'] = nativeMeasure;
    summary[name] = percent + '%';
    if (api.supportClear) {
      var clearName = api.clearMethod;
      var nativeClearName = getNativeMethodName(api.nativeClearMethod);
      var clearMeasure = averageMeasures(clearName, times);
      var nativeClearMeasure = averageMeasures(nativeClearName, times);
      var clearPercent = Math.floor(
        (100 * (clearMeasure - nativeClearMeasure)) / nativeClearMeasure,
      );
      displayText += `- ${clearName} costs ${clearMeasure} ms\n`;
      displayText += `- ${nativeClearName} costs ${nativeClearMeasure} ms\n`;
      displayText += `# ${clearName} is ${clearPercent}% slower than ${nativeClearName}\n`;
      rawData[clearName + '_measure'] = clearMeasure;
      rawData[nativeClearName + '_measure'] = nativeClearMeasure;
      summary[clearName] = clearPercent + '%';
    }
  }

  var testRunner = (_global['__zone_symbol__testRunner'] = function (testTarget) {
    var title = testTarget.title;
    var apis = testTarget.apis;
    var methods = apis.reduce(function (acc, api) {
      return acc.concat(
        [api.method, api.nativeMethod].concat(
          api.supportClear ? [api.clearMethod, api.nativeClearMethod] : [],
        ).concat[(api.method + '_callback', api.nativeMethod + '_callback')],
      );
    }, []);
    var times = testTarget.times;

    allTasks.push({
      title: title,
      cleanFn: function () {
        methods.forEach(function (m) {
          clearMarks(m);
          clearMeasures(m);
        });
      },
      before: function () {
        testTarget.before && testTarget.before();
      },
      after: function () {
        testTarget.after && testTarget.after();
      },
      testFn: function () {
        var count = typeof testTarget.count === 'number' ? testTarget.count : 10000;
        var times = typeof testTarget.times === 'number' ? testTarget.times : 5;

        var testFunction = function () {
          var promises = [];
          apis.forEach(function (api) {
            if (api.isCallback) {
              var r = testCallback(api, count / 100);
              promises.push(api.isAsync ? r : Promise.resolve(1));
            } else {
              var r = testAddRemove(api, count);
              promises.push[api.isAsync ? r : Promise.resolve(1)];
            }
          });
          return Promise.all(promises);
        };

        return runAsync(testFunction, times).then(function () {
          var displayText = `running ${count} times\n`;
          var rawData = {};
          var summary = {};
          apis.forEach(function (api) {
            if (api.isCallback) {
              measureCallback(api, {times, displayText, rawData, summary});
            } else {
              measureAddRemove(api, {times, displayText, rawData, summary});
            }
          });
          return Promise.resolve({displayText: displayText, rawData: rawData, summary: summary});
        });
      },
    });
  });
})(typeof window === 'undefined' ? global : window);
