feat(benchpress): initial support for firefox

Closes #2419
This commit is contained in:
Hank Duan
2015-06-08 13:51:10 -07:00
committed by Tobias Bosch
parent 7a4a3c850f
commit 0949a4b045
18 changed files with 380 additions and 69 deletions

View File

@ -1,11 +1,28 @@
declare var exportFunction;
declare var unsafeWindow;
exportFunction(function() { (<any>self).port.emit('startProfiler'); }, unsafeWindow,
{defineAs: "startProfiler"});
exportFunction(function() {
var curTime = unsafeWindow.performance.now();
(<any>self).port.emit('startProfiler', curTime);
}, unsafeWindow, {defineAs: "startProfiler"});
exportFunction(function(filePath) { (<any>self).port.emit('stopAndRecord', filePath); },
unsafeWindow, {defineAs: "stopAndRecord"});
exportFunction(function() { (<any>self).port.emit('stopProfiler'); }, unsafeWindow,
{defineAs: "stopProfiler"});
exportFunction(function(cb) {
(<any>self).port.once('perfProfile', cb);
(<any>self).port.emit('getProfile');
}, unsafeWindow, {defineAs: "getProfile"});
exportFunction(function() { (<any>self).port.emit('forceGC'); }, unsafeWindow,
{defineAs: "forceGC"});
exportFunction(function(name) {
var curTime = unsafeWindow.performance.now();
(<any>self).port.emit('markStart', name, curTime);
}, unsafeWindow, {defineAs: "markStart"});
exportFunction(function(name) {
var curTime = unsafeWindow.performance.now();
(<any>self).port.emit('markEnd', name, curTime);
}, unsafeWindow, {defineAs: "markEnd"});

View File

@ -1,52 +1,66 @@
/// <reference path="../../../../angular2/typings/node/node.d.ts" />
var file = require('sdk/io/file');
var {Cc, Ci, Cu} = require("chrome");
var {Cc, Ci, Cu} = require('chrome');
var os = Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
var ParserUtil = require('./parser_util');
class Profiler {
private _profiler;
constructor() { this._profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); }
private _markerEvents: List<any>;
private _profilerStartTime: number;
start(entries, interval, features) {
constructor() { this._profiler = Cc['@mozilla.org/tools/profiler;1'].getService(Ci.nsIProfiler); }
start(entries, interval, features, timeStarted) {
this._profiler.StartProfiler(entries, interval, features, features.length);
this._profilerStartTime = timeStarted;
this._markerEvents = [];
}
stop() { this._profiler.StopProfiler(); }
getProfileData() { return this._profiler.getProfileData(); }
}
getProfilePerfEvents() {
var profileData = this._profiler.getProfileData();
var perfEvents = ParserUtil.convertPerfProfileToEvents(profileData);
perfEvents = this._mergeMarkerEvents(perfEvents);
perfEvents.sort(function(event1, event2) { return event1.ts - event2.ts; }); // Sort by ts
return perfEvents;
}
function saveToFile(savePath: string, body: string) {
var textWriter = file.open(savePath, 'w');
textWriter.write(body);
textWriter.close();
_mergeMarkerEvents(perfEvents: List<any>): List<any> {
this._markerEvents.forEach(function(markerEvent) { perfEvents.push(markerEvent); });
return perfEvents;
}
addStartEvent(name: string, timeStarted: number) {
this._markerEvents.push({ph: 'b', ts: timeStarted - this._profilerStartTime, name: name});
}
addEndEvent(name: string, timeEnded: number) {
this._markerEvents.push({ph: 'e', ts: timeEnded - this._profilerStartTime, name: name});
}
}
function forceGC() {
Cu.forceGC();
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
os.notifyObservers(null, "child-gc-request", null);
}
os.notifyObservers(null, 'child-gc-request', null);
};
var mod = require('sdk/page-mod');
var data = require('sdk/self').data;
var profiler = new Profiler();
function startProfiler() {
profiler.start(/* = profiler memory */ 10000000, 1, ['leaf', 'js', "stackwalk", 'gc']);
};
function stopAndRecord(filePath) {
var profileData = profiler.getProfileData();
profiler.stop();
saveToFile(filePath, JSON.stringify(profileData, null, 2));
};
var mod = require("sdk/page-mod");
var data = require("sdk/self").data;
mod.PageMod({
include: ['*'],
contentScriptFile: data.url("installed_script.js"),
contentScriptFile: data.url('installed_script.js'),
onAttach: worker => {
worker.port.on('startProfiler', () => startProfiler());
worker.port.on('stopAndRecord', filePath => stopAndRecord(filePath));
worker.port.on('forceGC', () => forceGC());
worker.port.on('startProfiler',
(timeStarted) => profiler.start(/* = profiler memory */ 1000000, 1,
['leaf', 'js', 'stackwalk', 'gc'], timeStarted));
worker.port.on('stopProfiler', () => profiler.stop());
worker.port.on('getProfile',
() => worker.port.emit('perfProfile', profiler.getProfilePerfEvents()));
worker.port.on('forceGC', forceGC);
worker.port.on('markStart', (name, timeStarted) => profiler.addStartEvent(name, timeStarted));
worker.port.on('markEnd', (name, timeEnded) => profiler.addEndEvent(name, timeEnded));
}
});

View File

@ -0,0 +1,2 @@
library benchpress.src.firefox_extension.lib.parser_util;
//no dart implementation

View File

@ -0,0 +1,84 @@
/// <reference path="../../../../angular2/typings/node/node.d.ts" />
/**
* @param {Object} perfProfile The perf profile JSON object.
* @return {Array<Object>} An array of recognized events that are captured
* within the perf profile.
*/
export function convertPerfProfileToEvents(perfProfile: any): List<any> {
var inProgressEvents = new Map(); // map from event name to start time
var finishedEvents = []; // Array<Event> finished events
var addFinishedEvent = function(eventName, startTime, endTime) {
var categorizedEventName = categorizeEvent(eventName);
var args = undefined;
if (categorizedEventName == 'gc') {
// TODO: We cannot measure heap size at the moment
args = {usedHeapSize: 0};
}
if (startTime == endTime) {
// Finished instantly
finishedEvents.push({ph: 'X', ts: startTime, name: categorizedEventName, args: args});
} else {
// Has duration
finishedEvents.push({ph: 'B', ts: startTime, name: categorizedEventName, args: args});
finishedEvents.push({ph: 'E', ts: endTime, name: categorizedEventName, args: args});
}
};
var samples = perfProfile.threads[0].samples;
// In perf profile, firefox samples all the frames in set time intervals. Here
// we go through all the samples and construct the start and end time for each
// event.
for (var i = 0; i < samples.length; ++i) {
var sample = samples[i];
var sampleTime = sample.time;
// Add all the frames into a set so it's easier/faster to find the set
// differences
var sampleFrames = new Set();
sample.frames.forEach(function(frame) { sampleFrames.add(frame.location); });
// If an event is in the inProgressEvents map, but not in the current sample,
// then it must have just finished. We add this event to the finishedEvents
// array and remove it from the inProgressEvents map.
var previousSampleTime = (i == 0 ? /* not used */ -1 : samples[i - 1].time);
inProgressEvents.forEach(function(startTime, eventName) {
if (!(sampleFrames.has(eventName))) {
addFinishedEvent(eventName, startTime, previousSampleTime);
inProgressEvents.delete(eventName);
}
});
// If an event is in the current sample, but not in the inProgressEvents map,
// then it must have just started. We add this event to the inProgressEvents
// map.
sampleFrames.forEach(function(eventName) {
if (!(inProgressEvents.has(eventName))) {
inProgressEvents.set(eventName, sampleTime);
}
});
}
// If anything is still in progress, we need to included it as a finished event
// since recording ended.
var lastSampleTime = samples[samples.length - 1].time;
inProgressEvents.forEach(function(startTime, eventName) {
addFinishedEvent(eventName, startTime, lastSampleTime);
});
// Remove all the unknown categories.
return finishedEvents.filter(function(event) { return event.name != 'unknown'; });
}
// TODO: this is most likely not exhaustive.
export function categorizeEvent(eventName: string): string {
if (eventName.indexOf('PresShell::Paint') > -1) {
return 'render';
} else if (eventName.indexOf('FirefoxDriver.prototype.executeScript') > -1) {
return 'script';
} else if (eventName.indexOf('forceGC') > -1) {
return 'gc';
} else {
return 'unknown';
}
}

View File

@ -1,5 +1 @@
{
"version": "0.0.1",
"main": "lib/main.js",
"name": "ffperf-addon"
}
{ "version" : "0.0.1", "main" : "lib/main.js", "name" : "ffperf-addon" }

View File

@ -12,6 +12,7 @@ import {Validator} from './validator';
import {PerflogMetric} from './metric/perflog_metric';
import {MultiMetric} from './metric/multi_metric';
import {ChromeDriverExtension} from './webdriver/chrome_driver_extension';
import {FirefoxDriverExtension} from './webdriver/firefox_driver_extension';
import {IOsDriverExtension} from './webdriver/ios_driver_extension';
import {WebDriverExtension} from './web_driver_extension';
import {SampleDescription} from './sample_description';
@ -62,6 +63,7 @@ var _DEFAULT_BINDINGS = [
RegressionSlopeValidator.BINDINGS,
SizeValidator.BINDINGS,
ChromeDriverExtension.BINDINGS,
FirefoxDriverExtension.BINDINGS,
IOsDriverExtension.BINDINGS,
PerflogMetric.BINDINGS,
SampleDescription.BINDINGS,
@ -70,7 +72,7 @@ var _DEFAULT_BINDINGS = [
Reporter.bindTo(MultiReporter),
Validator.bindTo(RegressionSlopeValidator),
WebDriverExtension.bindTo([ChromeDriverExtension, IOsDriverExtension]),
WebDriverExtension.bindTo([ChromeDriverExtension, FirefoxDriverExtension, IOsDriverExtension]),
Metric.bindTo(MultiMetric),
bind(Options.CAPABILITIES)

View File

@ -16,6 +16,7 @@ export class WebDriverAdapter {
waitFor(callback: Function): Promise<any> { throw new BaseException('NYI'); }
executeScript(script: string): Promise<any> { throw new BaseException('NYI'); }
executeAsyncScript(script: string): Promise<any> { throw new BaseException('NYI'); }
capabilities(): Promise<Map<string, any>> { throw new BaseException('NYI'); }
logs(type: string): Promise<List<any>> { throw new BaseException('NYI'); }
}

View File

@ -16,6 +16,10 @@ class AsyncWebDriverAdapter extends WebDriverAdapter {
return _driver.execute(script, const []);
}
Future executeAsyncScript(String script) {
return _driver.executeAsync(script, const []);
}
Future<Map> capabilities() {
return new Future.value(_driver.capabilities);
}

View File

@ -0,0 +1,49 @@
import {bind, Binding} from 'angular2/di';
import {isPresent, StringWrapper} from 'angular2/src/facade/lang';
import {WebDriverExtension, PerfLogFeatures} from '../web_driver_extension';
import {WebDriverAdapter} from '../web_driver_adapter';
import {Promise} from 'angular2/src/facade/async';
export class FirefoxDriverExtension extends WebDriverExtension {
static get BINDINGS(): List<Binding> { return _BINDINGS; }
private _profilerStarted: boolean;
constructor(private _driver: WebDriverAdapter) {
super();
this._profilerStarted = false;
}
gc() { return this._driver.executeScript('window.forceGC()'); }
timeBegin(name: string): Promise<any> {
if (!this._profilerStarted) {
this._profilerStarted = true;
this._driver.executeScript('window.startProfiler();');
}
return this._driver.executeScript('window.markStart("' + name + '");');
}
timeEnd(name: string, restartName: string = null): Promise<any> {
var script = 'window.markEnd("' + name + '");';
if (isPresent(restartName)) {
script += 'window.markStart("' + restartName + '");';
}
return this._driver.executeScript(script);
}
readPerfLog(): Promise<any> {
return this._driver.executeAsyncScript('var cb = arguments[0]; window.getProfile(cb);');
}
perfLogFeatures(): PerfLogFeatures { return new PerfLogFeatures({render: true, gc: true}); }
supports(capabilities: StringMap<string, any>): boolean {
return StringWrapper.equals(capabilities['browserName'].toLowerCase(), 'firefox');
}
}
var _BINDINGS = [
bind(FirefoxDriverExtension)
.toFactory((driver) => new FirefoxDriverExtension(driver), [WebDriverAdapter])
];

View File

@ -30,6 +30,10 @@ export class SeleniumWebDriverAdapter extends WebDriverAdapter {
return this._convertPromise(this._driver.executeScript(script));
}
executeAsyncScript(script: string): Promise<any> {
return this._convertPromise(this._driver.executeAsyncScript(script));
}
capabilities(): Promise<any> {
return this._convertPromise(
this._driver.getCapabilities().then((capsObject) => capsObject.toJSON()));