angular-cn/tools/perf/util.js

232 lines
7.2 KiB
JavaScript

var webdriver = require('protractor/node_modules/selenium-webdriver');
module.exports = {
perfLogs: perfLogs,
sumTimelineRecords: sumTimelineRecords,
runClickBenchmark: runClickBenchmark,
verifyNoErrors: verifyNoErrors,
printObjectAsMarkdown: printObjectAsMarkdown
};
function runClickBenchmark(config) {
var buttons = config.buttons.map(function(selector) {
return $(selector);
});
var globalParams = browser.params;
// empty perflogs queue and gc
gc();
perfLogs();
var sampleQueue = [];
var bestSampleStats = null;
loop(globalParams.maxRepeatCount).then(function(stats) {
printObjectAsMarkdown(config.name, stats);
});
function loop(count) {
if (!count) {
return bestSampleStats;
}
return webdriver.promise.all(buttons.map(function(button) {
// Note: even though we remove the gc time from the script time,
// we still get a high standard devication if we don't gc after every click...
return button.click().then(gc);
})).then(function() {
return perfLogs();
}).then(function(logs) {
var stats = calculateStatsBasedOnLogs(logs);
if (stats) {
if (stats.script.error < globalParams.exitOnErrorLowerThan) {
return stats;
}
if (!bestSampleStats || stats.script.error < bestSampleStats.script.error) {
bestSampleStats = stats;
}
}
return loop(count-1);
});
}
function calculateStatsBasedOnLogs(logs) {
sampleQueue.push(sumTimelineRecords(logs['Timeline.eventRecorded']));
if (sampleQueue.length >= globalParams.sampleSize) {
sampleQueue.splice(0, sampleQueue.length - globalParams.sampleSize);
// TODO: gc numbers don't have much meaning right now,
// as a benchmark run destroys everything.
// We need to measure the heap size after gc as well!
return calculateObjectSampleStats(sampleQueue, ['script', 'render', 'gcTime', 'gcAmount']);
}
return null;
}
}
function gc() {
// TODO(tbosch): this only works on chrome, and we actually should
// extend chromedriver to use the Debugger.CollectGarbage call of the
// remote debugger protocol.
// See http://src.chromium.org/viewvc/blink/trunk/Source/devtools/protocol.json
// For iOS Safari we need an extension to appium that uses
// the webkit remote debug protocol. See
// https://github.com/WebKit/webkit/blob/master/Source/WebInspectorUI/Versions/Inspector-iOS-8.0.json
return browser.executeScript('window.gc()');
}
function verifyNoErrors() {
browser.manage().logs().get('browser').then(function(browserLog) {
var filteredLog = browserLog.filter(function(logEntry) {
return logEntry.level.value > webdriver.logging.Level.WARNING.value;
});
expect(filteredLog.length).toEqual(0);
if (filteredLog.length) {
console.log('browser console errors: ' + require('util').inspect(filteredLog));
}
});
}
function perfLogs() {
return plainLogs('performance').then(function(entries) {
var entriesByMethod = {};
entries.forEach(function(entry) {
var message = JSON.parse(entry.message).message;
var entries = entriesByMethod[message.method];
if (!entries) {
entries = entriesByMethod[message.method] = [];
}
entries.push(message.params);
});
return entriesByMethod;
});
}
// Needed as selenium-webdriver does not forward
// performance logs in the correct way
function plainLogs(type) {
var webdriver = require('protractor/node_modules/selenium-webdriver');
return browser.driver.schedule(
new webdriver.Command(webdriver.CommandName.GET_LOG).
setParameter('type', type),
'WebDriver.manage().logs().get(' + type + ')');
};
function sumTimelineRecords(messages) {
var recordStats = {
script: 0,
gcTime: 0,
gcAmount: 0,
render: 0
};
messages.forEach(function(message) {
processRecord(message.record, recordStats);
});
return recordStats;
function processRecord(record, recordStats) {
var summedChildrenDuration = 0;
if (record.children) {
record.children.forEach(function(child) {
summedChildrenDuration += processRecord(child, recordStats);
});
}
var recordDuration;
var recordUsed = false;
if (recordStats) {
// we need to substract the time of child records
// that have been added to the stats from this record.
// E.g. for a script record that triggered a gc or reflow while executing.
recordDuration = (record.endTime ? record.endTime - record.startTime : 0)
- summedChildrenDuration;
if (record.type === 'FunctionCall') {
if (!record.data || record.data.scriptName !== 'InjectedScript') {
// ignore scripts that were injected by Webdriver (e.g. calculation of element positions, ...)
recordStats.script += recordDuration;
recordUsed = true;
}
} else if (record.type === 'GCEvent') {
recordStats.gcTime += recordDuration;
recordStats.gcAmount += record.data.usedHeapSizeDelta;
recordUsed = true;
} else if (record.type === 'RecalculateStyles' ||
record.type === 'Layout' ||
record.type === 'UpdateLayerTree' ||
record.type === 'Paint' ||
record.type === 'Rasterize' ||
record.type === 'CompositeLayers') {
recordStats.render += recordDuration;
recordUsed = true;
}
}
return recordUsed ? recordDuration : summedChildrenDuration;
}
}
function printObjectAsMarkdown(name, obj) {
var props = [['name']];
var vals = [name];
flattenObj(obj, [], props, vals);
// log header
var separators = [];
var header = props.map(function(propPath) {
separators.push('----');
return propPath.join('.');
}).join(' | ');
console.log('\n'+header);
console.log(separators.join(' | '));
console.log(vals.join(' | '));
console.log('\n');
function flattenObj(obj, propPathPrefix, targetProps, targetVals) {
for (var prop in obj) {
var val = obj[prop];
var currPropPath = propPathPrefix.concat([prop]);
if (val && typeof val === 'object') {
flattenObj(val, currPropPath, targetProps, targetVals);
} else {
targetProps.push(currPropPath);
var valStr = val;
if (typeof val === 'number') {
valStr = val.toFixed(2);
}
targetVals.push(valStr);
}
}
}
}
function calculateObjectSampleStats(objectSamples, properties) {
var result = {};
properties.forEach(function(prop) {
var samples = objectSamples.map(function(objectSample) {
return objectSample[prop];
});
var mean = calculateMean(samples);
var error = calculateCoefficientOfVariation(samples, mean);
result[prop] = {
mean: mean,
error: error
};
});
return result;
}
function calculateCoefficientOfVariation(sample, mean) {
return calculateStandardDeviation(sample, mean) / mean * 100;
}
function calculateMean(sample) {
var total = 0;
sample.forEach(function(x) { total += x; });
return total / sample.length;
}
function calculateStandardDeviation(sample, mean) {
var deviation = 0;
sample.forEach(function(x) {
deviation += Math.pow(x - mean, 2);
});
deviation = deviation / (sample.length -1);
deviation = Math.sqrt(deviation);
return deviation;
};