From 91821007ed640c0cfcba6b3c9d179c5bd11c4a50 Mon Sep 17 00:00:00 2001 From: Thomas Aylott Date: Tue, 17 Dec 2013 00:25:05 -0500 Subject: [PATCH] benchmark runner --- .travis.yml | 2 + Gruntfile.js | 36 +++- grunt/config/server.js | 2 + grunt/config/webdriver-all.js | 113 ++++++++++++ grunt/config/webdriver-jasmine.js | 80 +------- grunt/config/webdriver-perf.js | 48 +++++ grunt/tasks/download-previous-version.js | 47 +++++ grunt/tasks/webdriver-all.js | 73 ++++++++ grunt/tasks/webdriver-jasmine.js | 86 +-------- grunt/tasks/webdriver-perf.js | 23 +++ package.json | 3 + perf/index.html | 66 +++++++ perf/lib/BrowserPerfRunnerApp.react.js | 205 +++++++++++++++++++++ perf/lib/BrowserPerfRunnerContext.react.js | 195 ++++++++++++++++++++ perf/lib/perf-test-runner.browser.js | 204 ++++++++++++++++++++ perf/lib/todolist.browser.js | 117 ++++++++++++ perf/lib/todolist.html | 16 ++ perf/runner.html | 59 ++++++ perf/tests/basic-div.js | 16 ++ perf/tests/basic-unmount.js | 23 +++ perf/tests/renderComponent-basic.js | 24 +++ perf/tests/sanity.js | 15 ++ perf/tests/setState-callback-5.js | 41 +++++ perf/tests/setState-callback.js | 33 ++++ perf/tests/shouldComponentUpdate.js | 25 +++ perf/tests/todolist-add.js | 27 +++ perf/tests/todolist-do-stuff.js | 54 ++++++ perf/tests/todolist-edit.js | 32 ++++ perf/tests/todolist-mount.js | 23 +++ test/index.html | 1 + test/lib/postDataToURL.browser.js | 34 ++++ test/lib/reportTestResults.browser.js | 27 --- 32 files changed, 1568 insertions(+), 182 deletions(-) create mode 100644 grunt/config/webdriver-all.js create mode 100644 grunt/config/webdriver-perf.js create mode 100644 grunt/tasks/download-previous-version.js create mode 100644 grunt/tasks/webdriver-all.js create mode 100644 grunt/tasks/webdriver-perf.js create mode 100644 perf/index.html create mode 100644 perf/lib/BrowserPerfRunnerApp.react.js create mode 100644 perf/lib/BrowserPerfRunnerContext.react.js create mode 100644 perf/lib/perf-test-runner.browser.js create mode 100644 perf/lib/todolist.browser.js create mode 100644 perf/lib/todolist.html create mode 100644 perf/runner.html create mode 100644 perf/tests/basic-div.js create mode 100644 perf/tests/basic-unmount.js create mode 100644 perf/tests/renderComponent-basic.js create mode 100644 perf/tests/sanity.js create mode 100644 perf/tests/setState-callback-5.js create mode 100644 perf/tests/setState-callback.js create mode 100644 perf/tests/shouldComponentUpdate.js create mode 100644 perf/tests/todolist-add.js create mode 100644 perf/tests/todolist-do-stuff.js create mode 100644 perf/tests/todolist-edit.js create mode 100644 perf/tests/todolist-mount.js create mode 100644 test/lib/postDataToURL.browser.js diff --git a/.travis.yml b/.travis.yml index 527478ad7ad19..75aae891c61e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ env: matrix: - TEST_TYPE=test:full - TEST_TYPE=lint + - TEST_TYPE=perf:full - TEST_TYPE=test:coverage - TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie11 - TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie10 @@ -43,6 +44,7 @@ matrix: allow_failures: - env: TEST_TYPE=lint - env: TEST_TYPE=test:coverage + - env: TEST_TYPE=perf:full - env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie11 - env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie10 - env: TEST_TYPE=test:webdriver:saucelabs BROWSER_NAME=ie9 diff --git a/Gruntfile.js b/Gruntfile.js index a117066e05adc..0d554baa576d4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -21,7 +21,8 @@ module.exports = function(grunt) { browserify: require('./grunt/config/browserify'), populist: require('./grunt/config/populist'), connect: require('./grunt/config/server')(grunt), - "webdriver-jasmine": require('./grunt/config/webdriver-jasmine.js'), + "webdriver-jasmine": require('./grunt/config/webdriver-jasmine'), + "webdriver-perf": require('./grunt/config/webdriver-perf'), npm: require('./grunt/config/npm'), clean: ['./build', './*.gem', './docs/_site', './examples/shared/*.js', '.module-cache'], jshint: require('./grunt/config/jshint'), @@ -39,6 +40,8 @@ module.exports = function(grunt) { // Alias 'jshint' to 'lint' to better match the workflow we know grunt.registerTask('lint', ['jshint']); + grunt.registerTask('download-previous-version', require('./grunt/tasks/download-previous-version.js')); + // Register jsx:debug and :release tasks. grunt.registerMultiTask('jsx', jsxTask); @@ -51,6 +54,8 @@ module.exports = function(grunt) { grunt.registerMultiTask('webdriver-jasmine', webdriverJasmineTasks); + grunt.registerMultiTask('webdriver-perf', require('./grunt/tasks/webdriver-perf')); + grunt.registerMultiTask('npm', npmTask); grunt.registerTask('npm-react:release', npmReactTasks.buildRelease); @@ -67,6 +72,14 @@ module.exports = function(grunt) { 'version-check', 'browserify:withCodeCoverageLogging' ]); + grunt.registerTask('build:perf', [ + 'jsx:release', + 'version-check', + 'browserify:transformer', + 'browserify:basic', + 'browserify:min', + 'download-previous-version' + ]); grunt.registerTask('build:test', [ 'jsx:test', 'version-check', @@ -84,6 +97,12 @@ module.exports = function(grunt) { 'webdriver-jasmine:local' ]); + grunt.registerTask('perf:webdriver:phantomjs', [ + 'connect', + 'webdriver-phantomjs', + 'webdriver-perf:local' + ]); + grunt.registerTask('test:full', [ 'build:test', 'build:basic', @@ -98,6 +117,20 @@ module.exports = function(grunt) { 'webdriver-jasmine:saucelabs_chrome' ]); + grunt.registerTask('perf:full', [ + 'build:perf', + + 'connect', + 'webdriver-phantomjs', + 'webdriver-perf:local', + + 'sauce-tunnel', + 'webdriver-perf:saucelabs_firefox', + 'webdriver-perf:saucelabs_chrome', + 'webdriver-perf:saucelabs_ie11', + 'webdriver-perf:saucelabs_ie8', + ]); + grunt.registerTask('test:webdriver:saucelabs', [ 'build:test', 'build:basic', @@ -137,6 +170,7 @@ module.exports = function(grunt) { 'coverage:parse' ]); grunt.registerTask('test', ['build:test', 'build:basic', 'test:webdriver:phantomjs']); + grunt.registerTask('perf', ['build:perf', 'perf:webdriver:phantomjs']); grunt.registerTask('npm:test', ['build', 'npm:pack']); // Optimized build task that does all of our builds. The subtasks will be run diff --git a/grunt/config/server.js b/grunt/config/server.js index 4e2a323cbddd6..241ac109e60cb 100644 --- a/grunt/config/server.js +++ b/grunt/config/server.js @@ -44,6 +44,8 @@ module.exports = function(grunt){ coverageWriteStream.write(log.message + '\n'); } else if (log.type == 'coverage done') { grunt.task.run('finalize-coverage-stream'); + } else if (log.type == 'perf') { + grunt.event.emit('perf results', log.message); } else { grunt.verbose.writeln(log); } diff --git a/grunt/config/webdriver-all.js b/grunt/config/webdriver-all.js new file mode 100644 index 0000000000000..d41c99501fd12 --- /dev/null +++ b/grunt/config/webdriver-all.js @@ -0,0 +1,113 @@ +'use strict'; + +var grunt = require('grunt'); + +module.exports = function(props){ + if (typeof props.url != 'string') { + throw TypeError('expected url string'); + } + if ('isDoneTimeout' in props && typeof props.isDoneTimeout != 'number') { + throw TypeError('expected isDoneTimeout to be a number'); + } + if ('onStart' in props && typeof props.onStart != 'function') { + throw TypeError('expected onStart to be a function'); + } + if ('onComplete' in props && typeof props.onComplete != 'function') { + throw TypeError('expected onComplete to be a function'); + } + if ('onError' in props && typeof props.onError != 'function') { + throw TypeError('expected onError to be a function'); + } + + var exports = {}; + + exports.local = { + webdriver: { + remote: { protocol: 'http:', hostname: '127.0.0.1', port: 9515, path: '/' } + }, + url: props.url, + onStart: props.onStart, + onComplete: props.onComplete, + onError: props.onError, + isDoneTimeout: props.isDoneTimeout + }; + + if (grunt.option('debug')) { + exports.local.url += (exports.local.url.indexOf('?') == -1 ? '?' : '&') + 'debug=' + grunt.option('debug'); + } + + exports.saucelabs = { + webdriver: { + remote: { + /* https://github.com/admc/wd/blob/master/README.md#named-parameters */ + user: process.env.SAUCE_USERNAME || 'React', + pwd: process.env.SAUCE_ACCESS_KEY || '339d32ca-d594-4570-a3c2-94c50a91919b', + protocol: 'http:', + hostname: 'ondemand.saucelabs.com', + port: '80', + path: '/wd/hub' + } + }, + desiredCapabilities: { + "build": process.env.TRAVIS_BUILD_NUMBER || 'dev' + Date.now(), + "tunnel-identifier": process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel', + "browserName": "chrome" + }, + url: exports.local.url, + onStart: function(browser){ + grunt.log.writeln("Starting WebDriver Test. Watch results here: http://saucelabs.com/tests/" + browser.sessionID); + if (props.onStart) { + return props.onStart(browser); + } + }, + onComplete: exports.local.onComplete, + onError: exports.local.onError, + isDoneTimeout: exports.local.isDoneTimeout + }; + + /* https://saucelabs.com/platforms */ + exports.saucelabs_ios = + exports.saucelabs_ios7 = sauceItUp({ browserName: 'iphone', version: '7', platform:'OS X 10.9' }); + exports.saucelabs_ios6_1 = sauceItUp({ browserName: 'iphone', version: '6.1', platform:'OS X 10.8' }); + exports.saucelabs_ios6 = sauceItUp({ browserName: 'iphone', version: '6', platform:'OS X 10.8' }); + exports.saucelabs_ios5_1 = sauceItUp({ browserName: 'iphone', version: '5.1', platform:'OS X 10.8' }); + exports.saucelabs_ios5 = sauceItUp({ browserName: 'iphone', version: '5', platform:'OS X 10.6' }); + exports.saucelabs_ios4 = sauceItUp({ browserName: 'iphone', version: '4', platform:'OS X 10.6' }); + + exports.saucelabs_ipad = + exports.saucelabs_ipad7 = sauceItUp({ browserName: 'ipad', version: '7', platform:'OS X 10.9' }); + exports.saucelabs_ipad6_1 = sauceItUp({ browserName: 'ipad', version: '6.1', platform:'OS X 10.8' }); + exports.saucelabs_ipad6 = sauceItUp({ browserName: 'ipad', version: '6', platform:'OS X 10.8' }); + exports.saucelabs_ipad5_1 = sauceItUp({ browserName: 'ipad', version: '5.1', platform:'OS X 10.8' }); + exports.saucelabs_ipad5 = sauceItUp({ browserName: 'ipad', version: '5', platform:'OS X 10.6' }); + exports.saucelabs_ipad4 = sauceItUp({ browserName: 'ipad', version: '4', platform:'OS X 10.6' }); + + exports.saucelabs_android = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux' }); + exports.saucelabs_android_tablet = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux', 'device-type':'tablet' }); + + exports.saucelabs_safari = sauceItUp({ browserName: 'safari' }); + exports.saucelabs_chrome = sauceItUp({ browserName: 'chrome' }); + exports.saucelabs_firefox = sauceItUp({ browserName: 'firefox' }); + + exports.saucelabs_ie = + exports.saucelabs_ie8 = sauceItUp({ browserName: 'internet explorer', version: 8 }); + exports.saucelabs_ie9 = sauceItUp({ browserName: 'internet explorer', version: 9 }); + exports.saucelabs_ie10 = sauceItUp({ browserName: 'internet explorer', version: 10 }); + exports.saucelabs_ie11 = sauceItUp({ browserName: 'internet explorer', version: 11, platform:'Windows 8.1' }); + + function sauceItUp(desiredCapabilities) { + desiredCapabilities["build"] = exports.saucelabs.desiredCapabilities["build"]; + desiredCapabilities["tunnel-identifier"] = exports.saucelabs.desiredCapabilities["tunnel-identifier"]; + return { + webdriver: exports.saucelabs.webdriver, + url: exports.saucelabs.url, + onStart: exports.saucelabs.onStart, + onComplete: exports.saucelabs.onComplete, + onError: exports.saucelabs.onError, + isDoneTimeout: exports.saucelabs.isDoneTimeout, + desiredCapabilities: desiredCapabilities, + }; + } + + return exports; +} diff --git a/grunt/config/webdriver-jasmine.js b/grunt/config/webdriver-jasmine.js index b01660ddeb81c..3990c6271a64a 100644 --- a/grunt/config/webdriver-jasmine.js +++ b/grunt/config/webdriver-jasmine.js @@ -2,11 +2,7 @@ var grunt = require('grunt'); - -exports.local = { - webdriver: { - remote: { protocol: 'http:', hostname: '127.0.0.1', port: 9515, path: '/' } - }, +module.exports = require('./webdriver-all')({ url: "http://127.0.0.1:9999/test/index.html", onComplete: function(passed){ if (!passed){ @@ -16,76 +12,4 @@ exports.local = { onError: function(error){ grunt.fatal(error); } -}; - -if (grunt.option('debug')) { - exports.local.url += '?debug=' + grunt.option('debug'); -} - - -exports.saucelabs = { - webdriver: { - remote: { - /* https://github.com/admc/wd/blob/master/README.md#named-parameters */ - user: process.env.SAUCE_USERNAME || 'React', - pwd: process.env.SAUCE_ACCESS_KEY || '339d32ca-d594-4570-a3c2-94c50a91919b', - protocol: 'http:', - hostname: 'ondemand.saucelabs.com', - port: '80', - path: '/wd/hub' - } - }, - desiredCapabilities: { - "build": process.env.TRAVIS_BUILD_NUMBER || 'dev' + Date.now(), - "tunnel-identifier": process.env.TRAVIS_JOB_NUMBER || 'my awesome tunnel', - "browserName": "chrome" - }, - url: exports.local.url, - onStart: function(browser){ - grunt.log.writeln("Starting WebDriver Test. Watch results here: http://saucelabs.com/tests/" + browser.sessionID); - }, - onComplete: exports.local.onComplete, - onError: exports.local.onError -}; - -/* https://saucelabs.com/docs/platforms */ -exports.saucelabs_ios = -exports.saucelabs_ios6_1 = sauceItUp({ browserName: 'iphone', version: '6.1', platform:'OS X 10.8' }); -exports.saucelabs_ios6 = sauceItUp({ browserName: 'iphone', version: '6', platform:'OS X 10.8' }); -exports.saucelabs_ios5_1 = sauceItUp({ browserName: 'iphone', version: '5.1', platform:'OS X 10.8' }); -exports.saucelabs_ios5 = sauceItUp({ browserName: 'iphone', version: '5', platform:'OS X 10.6' }); -exports.saucelabs_ios4 = sauceItUp({ browserName: 'iphone', version: '4', platform:'OS X 10.6' }); - -exports.saucelabs_ipad = -exports.saucelabs_ipad6_1 = sauceItUp({ browserName: 'ipad', version: '6.1', platform:'OS X 10.8' }); -exports.saucelabs_ipad6 = sauceItUp({ browserName: 'ipad', version: '6', platform:'OS X 10.8' }); -exports.saucelabs_ipad5_1 = sauceItUp({ browserName: 'ipad', version: '5.1', platform:'OS X 10.8' }); -exports.saucelabs_ipad5 = sauceItUp({ browserName: 'ipad', version: '5', platform:'OS X 10.6' }); -exports.saucelabs_ipad4 = sauceItUp({ browserName: 'ipad', version: '4', platform:'OS X 10.6' }); - -exports.saucelabs_android = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux' }); -exports.saucelabs_android_tablet = sauceItUp({ browserName: 'android', version: '4.0', platform:'Linux', 'device-type':'tablet' }); - -exports.saucelabs_safari = sauceItUp({ browserName: 'safari' }); -exports.saucelabs_chrome = sauceItUp({ browserName: 'chrome' }); -exports.saucelabs_firefox = sauceItUp({ browserName: 'firefox' }); - -exports.saucelabs_ie = -exports.saucelabs_ie8 = sauceItUp({ browserName: 'internet explorer', version: 8 }); -exports.saucelabs_ie9 = sauceItUp({ browserName: 'internet explorer', version: 9 }); -exports.saucelabs_ie10 = sauceItUp({ browserName: 'internet explorer', version: 10 }); -exports.saucelabs_ie11 = sauceItUp({ browserName: 'internet explorer', version: 11, platform:'Windows 8.1' }); - - -function sauceItUp(desiredCapabilities) { - desiredCapabilities["build"] = exports.saucelabs.desiredCapabilities["build"]; - desiredCapabilities["tunnel-identifier"] = exports.saucelabs.desiredCapabilities["tunnel-identifier"]; - return { - webdriver: exports.saucelabs.webdriver, - url: exports.saucelabs.url, - onStart: exports.saucelabs.onStart, - onComplete: exports.saucelabs.onComplete, - onError: exports.saucelabs.onError, - desiredCapabilities: desiredCapabilities, - }; -} +}); diff --git a/grunt/config/webdriver-perf.js b/grunt/config/webdriver-perf.js new file mode 100644 index 0000000000000..b53dd5f7db0f6 --- /dev/null +++ b/grunt/config/webdriver-perf.js @@ -0,0 +1,48 @@ +'use strict'; + +var grunt = require('grunt'); + +var tests = grunt.file.expand(__dirname + '/../../perf/tests/*'); + +var maxTime = 5; + +var reactVersions = [ + 'edge', + 'previous' +]; + +var params = [] + .concat('headless=false') + .concat('maxTime=' + maxTime) + .concat(tests + .map(function(path){ return path.split(/tests./i).reverse()[0]; }) + .map(encodeURIComponent) + .map(function(filename){ return 'test=' + filename; }) + ) + .concat(reactVersions + .map(encodeURIComponent) + .map(function(version){ return 'react=' + version } + ) +); + +module.exports = require('./webdriver-all')({ + + url: "http://127.0.0.1:9999/perf/index.html?" + params.join('&'), + + isDoneTimeout: 15 * 60 * 1000, + + onStart: function(){ + grunt.event.on('perf results', function(results){ + console.log(results); + }); + }, + + onComplete: function(completedTestKeys){ + grunt.verbose.writeln('onComplete ' + JSON.stringify(completedTestKeys)); + }, + + onError: function(error){ + grunt.fatal(error); + } + +}); diff --git a/grunt/tasks/download-previous-version.js b/grunt/tasks/download-previous-version.js new file mode 100644 index 0000000000000..fbb2c22d7de15 --- /dev/null +++ b/grunt/tasks/download-previous-version.js @@ -0,0 +1,47 @@ +"use strict"; + +var grunt = require('grunt'); +var http = require('http'); +var fs = require('fs'); + +module.exports = function() { + var completedSuccessfully = this.async(); + get( + "http://react.zpao.com/builds/master/latest/react.min.js", + __dirname + '/../../build/react-previous.min.js', + function(success){ + if (!success) { + return completedSuccessfully(success); + } + get( + "http://react.zpao.com/builds/master/latest/JSXTransformer.js", + __dirname + '/../../build/JSXTransformer-previous.js', + completedSuccessfully + ); + } + ); + + function get(url, targetFilePath, completedSuccessfully) { + grunt.verbose.writeln('getting url "' + url + '"'); + http.get(url, function(response) { + grunt.verbose.writeln('Received status code ' + response.statusCode + ' for "' + url + '"'); + + if (response.statusCode != 200) { + if (response.headers.location) { + get(response.headers.location, targetFilePath); + return; + } else { + grunt.fatal('Nothing else to do.'); + completedSuccessfully(false); + return; + } + } + grunt.verbose.writeln('Writing url to "' + targetFilePath + '"'); + response.pipe(fs.createWriteStream(targetFilePath)) + .on('close', function() { + completedSuccessfully(true); + }) + ; + }); + } +}; diff --git a/grunt/tasks/webdriver-all.js b/grunt/tasks/webdriver-all.js new file mode 100644 index 0000000000000..4571f697c958a --- /dev/null +++ b/grunt/tasks/webdriver-all.js @@ -0,0 +1,73 @@ +/* jshint evil: true */ + +'use strict'; + +var grunt = require("grunt"); +var wd = require('wd'); + +module.exports = function task(getJSReport){ + var config = this.data; + var taskSucceeded = this.async(); + getJSReport = getJSReport.bind(this, config, wd); + + var desiredCapabilities = {}; + if (config.desiredCapabilities) { + Object.keys(config.desiredCapabilities).forEach(function(key) { + if (config.desiredCapabilities[key] === undefined) { + return; + } + desiredCapabilities[key] = config.desiredCapabilities[key]; + }); + } + grunt.verbose.writeln("desiredCapabilities", JSON.stringify(desiredCapabilities)); + + var browser = wd.promiseChainRemote(config.webdriver.remote); + + browser.on('status', function(info) { + grunt.verbose.writeln(info); + }); + + browser.on('command', function(meth, path, data) { + grunt.verbose.writeln(' > ' + meth, path, data || ''); + }); + + var results = null; + + // browser._debugPromise(); + browser + .init(desiredCapabilities) + .then(config.onStart && config.onStart.bind(config, browser)) + .get(config.url) + .then(function(){return browser;}) + .then(getJSReport) + .then(function(data){ results = data; }) + .fail(function(error){ + grunt.log.error(error); + return browser + .eval('document.documentElement.innerText || document.documentElement.textContent') + .then(grunt.verbose.writeln.bind(grunt.verbose)) + .then(function(){ throw error; }) + ; + }) + .finally(function(){ + if (grunt.option('webdriver-keep-open')) { + return; + } + grunt.verbose.writeln('Closing the browser window. To keep it open, pass the --webdriver-keep-open flag to grunt.'); + return browser.quit(); + }) + .done( + function() { + if (config.onComplete) { + config.onComplete(results); + } + taskSucceeded(true); + }, + function(error) { + if (config.onError) { + config.onError(error); + } + taskSucceeded(false); + } + ); +}; diff --git a/grunt/tasks/webdriver-jasmine.js b/grunt/tasks/webdriver-jasmine.js index 95a57305f94e2..613627dc927d5 100644 --- a/grunt/tasks/webdriver-jasmine.js +++ b/grunt/tasks/webdriver-jasmine.js @@ -2,82 +2,16 @@ 'use strict'; -var grunt = require("grunt"); -var wd = require('wd'); - module.exports = function(){ - var config = this.data; - var taskSucceeded = this.async(); - - var desiredCapabilities = {}; - if (config.desiredCapabilities) { - Object.keys(config.desiredCapabilities).forEach(function(key) { - if (config.desiredCapabilities[key] === undefined) { - return; - } - desiredCapabilities[key] = config.desiredCapabilities[key]; - }); - } - grunt.verbose.writeln("desiredCapabilities", JSON.stringify(desiredCapabilities)); - - var browser = wd.promiseChainRemote(config.webdriver.remote); - - browser.on('status', function(info) { - grunt.verbose.writeln(info); + return require('./webdriver-all').call(this, function(config, wd, browser){ + return browser + .waitFor(wd.asserters.jsCondition("typeof window.jasmine != 'undefined'"), 5e3, 50) + .fail(function(error){ + throw Error("The test page didn't load properly. " + error); + }) + .waitFor(wd.asserters.jsCondition("typeof window.jasmine.getJSReport != 'undefined'"), 60e3, 100) + .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500) + .eval("jasmine.getJSReport().passed") + ; }); - - browser.on('command', function(meth, path, data) { - grunt.verbose.writeln(' > ' + meth, path, data || ''); - }); - - var results = null; - - // browser._debugPromise(); - browser - .init(desiredCapabilities) - .then(config.onStart && config.onStart.bind(config, browser)) - .get(config.url) - .then(function(){return browser;}) - .then(getJSReport) - .then(function(data){ results = data; }) - .fail(function(error){ - grunt.log.error(error); - return browser - .eval('document.documentElement.innerText || document.documentElement.textContent') - .then(grunt.verbose.writeln.bind(grunt.verbose)) - .then(function(){ throw error; }) - ; - }) - .finally(function(){ - if (grunt.option('webdriver-keep-open')) { - return; - } - grunt.verbose.writeln('Closing the browser window. To keep it open, pass the --webdriver-keep-open flag to grunt.'); - return browser.quit(); - }) - .done( - function() { - if (config.onComplete) { - config.onComplete(results); - } - taskSucceeded(true); - }, - function(error) { - if (config.onError) { - config.onError(error); - } - taskSucceeded(false); - } - ); }; - -function getJSReport(browser){ - return browser - .waitFor(wd.asserters.jsCondition("typeof window.jasmine != 'undefined'"), 5e3, 50) - .fail(function(error){ - throw Error("The test page didn't load properly. " + error); - }) - .waitFor(wd.asserters.jsCondition("typeof window.jasmine.getJSReport != 'undefined'"), 60e3, 100) - .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500) - .eval("jasmine.getJSReport().passed"); -} diff --git a/grunt/tasks/webdriver-perf.js b/grunt/tasks/webdriver-perf.js new file mode 100644 index 0000000000000..27143cf6a570e --- /dev/null +++ b/grunt/tasks/webdriver-perf.js @@ -0,0 +1,23 @@ +/* jshint evil: true */ + +'use strict'; + +var grunt = require('grunt'); + +module.exports = function(){ + return require('./webdriver-all').call(this, function(config, wd, browser){ + if (!config.isDoneTimeout) { + grunt.verbose.writeln('Expected isDoneTimeout config, using default value'); + } + grunt.verbose.writeln('isDoneTimeout:' + config.isDoneTimeout); + return browser + .waitFor(wd.asserters.jsCondition("window.isDone === false"), 5e3, 50) + .fail(function(error){ + throw Error("The test page didn't load properly. " + error); + }) + .waitFor(wd.asserters.jsCondition("window.isDone === true"), config.isDoneTimeout || 30e3, 1e3) + .waitFor(wd.asserters.jsCondition("window.postDataToURL.running <= 0"), 30e3, 500) + .eval("window.completedTestKeys || window._unhandledError") + ; + }); +}; diff --git a/package.json b/package.json index 32c1aba27df66..96358394537af 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "jstransform": "~2.0.1" }, "devDependencies": { + "benchmark": "~1.0.0", "browserify": "~2.36.1", "coverify": "~0.1.1", "envify": "~1.0.1", @@ -53,8 +54,10 @@ "grunt-contrib-jshint": "~0.7.2", "gzip-js": "~0.3.2", "jasmine-tapreporter": "~0.2.2", + "microtime": "~0.5.1", "optimist": "~0.6.0", "phantomjs": "~1.9", + "platform": "~1.0.0", "populist": "~0.1.6", "recast": "~0.5.6", "sauce-tunnel": "~1.1.0", diff --git a/perf/index.html b/perf/index.html new file mode 100644 index 0000000000000..4b91dda47cbba --- /dev/null +++ b/perf/index.html @@ -0,0 +1,66 @@ + + +Perf Tests + + + + + + + + + + + + diff --git a/perf/lib/BrowserPerfRunnerApp.react.js b/perf/lib/BrowserPerfRunnerApp.react.js new file mode 100644 index 0000000000000..5b34f3bcaaec5 --- /dev/null +++ b/perf/lib/BrowserPerfRunnerApp.react.js @@ -0,0 +1,205 @@ +var BrowserPerfRunnerApp = React.createClass({ + + propTypes: { + tests: React.PropTypes.array.isRequired, + react: React.PropTypes.array.isRequired, + maxTime: React.PropTypes.number, + onCompleteEach: React.PropTypes.func, + onComplete: React.PropTypes.func, + onError: React.PropTypes.func, + headless: React.PropTypes.bool + }, + + getInitialState: function(){ + var queue = []; + this.props.tests.forEach(function(testName){ + this.props.react.forEach(function(version){ + queue.push({ + test: testName, + react: version + }); + },this); + },this); + return { + queue: queue, + results: {} + }; + }, + + handleResults: function(results){ + this.state.results[results.test + '@' + results.react] = results; + this.replaceState(this.state); + }, + + handleComplete: function(queueItem){ + queueItem.completed = true; + + if (!this.props.onCompleteEach) { + return; + } + // Can't get the resultsForAllVersions if there are still some queued + var incompleteCount = 0; + for (var index = this.state.queue.length; --index >= 0;){ + if (this.state.queue[index].completed) { + continue; + } + if (this.state.queue[index].test === queueItem.test) { + return; + } + incompleteCount ++; + } + var resultsForAllVersions = Object.keys(this.state.results) + .filter(function(key){return key.indexOf(queueItem.test) === 0;}) + .map(function(key){return this.state.results[key];}, this) + ; + this.props.onCompleteEach(resultsForAllVersions); + + if (this.props.onComplete && incompleteCount === 0) { + this.props.onComplete(this.state.results); + } + }, + + render: function(){ + var grid = null; + + if (!this.props.headless) { + grid = GridViewTable({ + rows: this.props.tests, + cols: this.props.react, + renderCell: BrowserPerfRunnerApp.renderBenchmarkCell, + value: this.state.results + }); + } + + return React.DOM.div(null, + BenchmarkQueue({ + initialQueue: this.state.queue, + onChange: this.handleResults, + maxTime: this.props.maxTime, + onCompleteEach: this.handleComplete, + onError: this.props.onError + }), + grid + ); + } +}); + +BrowserPerfRunnerApp.renderBenchmarkCell = function(props, row, col){ + if (col == null && row == null) return React.DOM.th(null); + if (row == null) return React.DOM.th({style:{verticalAlign:'top', textAlign:'center'}}, col); + + var benchmarks = Object.keys(props.value) + .filter(function(key){ + return key.indexOf(row) === 0; + }) + .map(function(key){ + return props.value[key]; + }) + .filter(function(benchmark){ + return benchmark && !benchmark.isRunning && benchmark.stats; + }) + ; + + if (col == null) return React.DOM.th({style:{verticalAlign:'top', textAlign:'right'}}, + React.DOM.a({href:'?test=' + row}, benchmarks[0] && benchmarks[0].name || row) + ); + + var key = row + '@' + col; + var benchmark = props.value[key]; + if (!(benchmark && benchmark.stats)) return React.DOM.td({key:key}); + + + var colors = [ + '000000', + 'AA0000', + '00AA00', + 'AA5500', + '0000AA', + 'AA00AA', + '00AAAA', + 'AAAAAA', + + '555555', + 'FF5555', + '55FF55', + 'FFFF55', + '5555FF', + 'FF55FF', + '55FFFF', + 'FFFFFF' + ]; + + function chartValue(value){ + return Math.round(valueFromRangeToRange(value, chartValue.min, chartValue.max, 0, 100)); + } + chartValue.min = Math.min.apply(Math, benchmarks.map(function(benchmark){return Math.min.apply(Math, benchmark.stats.sample);})); + chartValue.max = Math.max.apply(Math, benchmarks.map(function(benchmark){return Math.max.apply(Math, benchmark.stats.sample);})); + + var means = benchmarks.map(function(benchmark){ + return benchmark.stats.mean; + }); + benchmarks.forEach(function(benchmark){ + benchmark.isTheWinner = benchmark.stats.mean <= Math.min.apply(Math, means); + }); + + var chartValues = benchmarks.map(function(benchmark){ + // benchmark.stats.sample.sort(function(a,b){return b - a;}); + return benchmark.stats.sample.map(chartValue).join(','); + }).join('|'); + + return ( + React.DOM.td({key:key, style:{textAlign:'center', width:234, verticalAlign:'top'}}, + benchmark.error && benchmark.error.message || '', + React.DOM.div({style: benchmark.isTheWinner ? { backgroundColor:'#0A5', color:'#AFA' } : {backgroundColor:'transparent', color:'inherit'}}, + Math.round(1 / benchmark.stats.mean * 100) / 100, " op/s ", + React.DOM.strong(null, Math.round(benchmark.stats.mean * 1000 * 100) / 100, " ms/op "), + React.DOM.small(null, "(±" + (Math.round(benchmark.stats.rme * 10) / 10) + "%)") + ), + benchmark.isRunning && 'Running' || React.DOM.img({ + style: { + borderWidth: 2, + borderStyle: 'solid', + color: '#' + colors[benchmarks.indexOf(benchmark)] + }, + width: 230, + height: 50, + src: 'https://chart.googleapis.com/chart?cht=ls&chs=460x100&chd=t:' + chartValues + '&chco=' + colors.join(',') + }) + ) + ); +} + +function valueFromRangeToRange(value, fromMin, fromMax, toMin, toMax){ + var fromRange = fromMax - fromMin; + var toRange = toMax - toMin; + return (((value - fromMin) * toRange) / fromRange) + toMin; +} + +var GridViewTable = React.createClass({ + + propTypes: { + rows: React.PropTypes.array.isRequired, + cols: React.PropTypes.array.isRequired, + renderCell: React.PropTypes.func.isRequired + }, + + _renderCell: function(col){ + return this.props.renderCell({ value:this.props.value }, this._row, col); + }, + + _renderRow: function(row){ + this._row = row; + return React.DOM.tr({key:row}, + this._renderCell(null, 0), + this.props.cols.map(this._renderCell, this) + ); + }, + + render: function(){ + return React.DOM.table(null, + this._renderRow(null, 0), + this.props.rows.map(this._renderRow, this) + ); + } + +}); diff --git a/perf/lib/BrowserPerfRunnerContext.react.js b/perf/lib/BrowserPerfRunnerContext.react.js new file mode 100644 index 0000000000000..0fdbca839058f --- /dev/null +++ b/perf/lib/BrowserPerfRunnerContext.react.js @@ -0,0 +1,195 @@ +var BenchmarkQueue = React.createClass({ + propTypes: { + debug: React.PropTypes.bool, + onChange: React.PropTypes.func.isRequired, + initialQueue: React.PropTypes.array.isRequired, + maxTime: React.PropTypes.number, + onCompleteEach: React.PropTypes.func, + onError: React.PropTypes.func + }, + + getDefaultProps: function(){ + return { + maxTime: 5 + }; + }, + + getInitialState: function(){ + return { + queue: this.props.initialQueue.slice() + }; + }, + + setItemState: function(state){ + state.test = this.state.queue[0].test; + state.react = this.state.queue[0].react; + this.props.onChange(state); + }, + + handleContextReady: function(window){ + var benchmark = window.Benchmark(window.exports); + benchmark.options.maxTime = this.props.maxTime; //DEBUG + + var itemState = { + testRunnerURL: window.location.href, + + name: window.exports.name, + platform: window.Benchmark.platform.description, + reactVersion: window.React.version, + + isMinified: (function(){ + var code = window.React.renderComponent.toString(); + return code.indexOf(',') - code.indexOf('(') <= 2; + }()) + }; + + this.setItemState(itemState); + + var self = this; + benchmark.on('start error cycle complete', function(){ + var stats = JSON.parse(JSON.stringify(benchmark.stats)); + itemState.stats = stats; + itemState.isRunning = benchmark.running; + itemState.error = benchmark.error; + self.setItemState(itemState); + }); + if (this.props.onError) { + benchmark.on('error', this.props.onError); + } + benchmark.on('complete', function(){ + var queue = self.state.queue.slice(); + var queueItem = queue.shift(); + if (self.props.onCompleteEach) { + self.props.onCompleteEach(queueItem); + } + self.setState({ queue:queue }); + }); + benchmark.run({async:true}); + }, + + shouldComponentUpdate: function(nextProps, nextState){ + return nextState.queue.length < this.state.queue.length; + }, + + render: function(){ + if (!(this.state.queue && this.state.queue.length > 0)){ + return React.DOM.div({style:{display:'none'}}); + } + return BrowserPerfRunnerContext({ + debug: this.props.debug, + test: this.state.queue[0].test, + react: this.state.queue[0].react, + onReady: this.handleContextReady + }); + } +}); + +var BrowserPerfRunnerContext = React.createClass({ + + propTypes: { + debug: React.PropTypes.bool, + test: function(object, key){ + React.PropTypes.string.isRequired(object, key); + if (/\.jsx?$/i.test(object[key])) return; + throw Error('Expected `' + key + '` to be a test file name with extension `.js` or `.jsx`'); + }, + react: function(object, key){ + React.PropTypes.string.isRequired(object, key); + if (/^(?:builds\/.+|edge|previous|(?:\d+\.){2}\d+)$/.test(object[key])) return; + throw Error('Expected `' + key + '` prop to be a valid react version string, build string or "edge" or "previous"'); + }, + onReady: React.PropTypes.func.isRequired + }, + + getInitialState: function(){ + return { + testRunnerURL:'about:blank' + }; + }, + + // _handleFrameError: function(error){ + // console.error('BrowserPerfRunnerContext', error); + // }, + // + // _handleFrameLoad: function(event){ + // console.log('BrowserPerfRunnerContext', event); + // }, + // + _handleMessage: function(event){ + if (location.href.indexOf(event.origin) !== 0) + return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message from ' + event.origin); + if (event.source.location.href.indexOf(this.state.testRunnerURL) === -1) + return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message from ' + event.source.location.href); + if (event.data !== 'Ready!') + return console.debug('BrowserPerfRunnerContext#_handleMessage ignored message ' + JSON.stringify(event.data)); + + this.props.onReady(event.source); + }, + + _getTestRunnerURL: function(props){ + return 'runner.html' + + '?' + + 'debug=' + (props.debug ? 1 : 0) + + '&' + + 'react=' + encodeURIComponent(props.react) + + '&' + + 'test=' + encodeURIComponent(props.test) + }, + + _renderState: function(props){ + return { + testRunnerURL: this._getTestRunnerURL(props) + }; + }, + + componentDidMount: function(){ + var node = this.refs.iframe.getDOMNode(); + // node.onload = this._handleFrameLoad; + // node.onerror = this._handleFrameError; + if (window.addEventListener) { + window.addEventListener('message', this._handleMessage, false); + } else if (window.attachEvent) { + window.attachEvent('onmessage', this._handleMessage); + } else { + throw Error('cannot attach onmessage listener'); + } + this.setState(this._renderState(this.props)); + }, + + componentWillUnmount: function(){ + if (window.removeEventListener) { + window.removeEventListener('message', this._handleMessage); + } else if (window.detachEvent) { + window.detachEvent('onmessage', this._handleMessage); + } else { + throw Error('cannot detach onmessage listener'); + } + this.refs.iframe.getDOMNode().src = ''; + }, + + componentWillReceiveProps: function(nextProps){ + this.setState(this._renderState(nextProps)); + }, + + shouldComponentUpdate: function(nextProps, nextState){ + return nextState.testRunnerURL != this.state.testRunnerURL; + }, + + render: function(){ + return ( + React.DOM.iframe({ + ref: 'iframe', + name: "BrowserPerfRunnerContextFrame", + style: this.style, + src: this.state.testRunnerURL + }) + ); + }, + + style: { + position: 'absolute', + right: '100%', + bottom: '100%' + } + +}); diff --git a/perf/lib/perf-test-runner.browser.js b/perf/lib/perf-test-runner.browser.js new file mode 100644 index 0000000000000..26e059f8c38c7 --- /dev/null +++ b/perf/lib/perf-test-runner.browser.js @@ -0,0 +1,204 @@ +if (typeof console == 'undefined') console = { + log: function(){}, + warn: function(){}, + error: function(){}, + debug: function(){} +}; + +var perfRunner; +if (typeof exports == 'object') { + perfRunner = exports; +} else { + perfRunner = {}; +} + +perfRunner.assert = function(test, message){ + if (typeof test == 'function') test = test(); + if (test) return; + throw Error(message); +} + +perfRunner.WriteScript = function(props){ + var type = ''; + if (props.jsx) { + type = ' type="text/jsx"'; + } + var src = props.src; + if (!props.cache) { + src += src.indexOf('?') === -1 ? '?_' : '&_'; + src += perfRunner.WriteScript.cacheBust; + } + document.write('<\/script>'); +} +perfRunner.WriteScript.cacheBust = (+new Date).toString(36); + +perfRunner.WriteReactLibScript = function(params){ + var src; + var minSuffix; + if (params.debug) { + minSuffix = ''; + } else { + minSuffix = '.min'; + } + + if (params.version && typeof params.version != 'string') throw TypeError("Expected 'version' to be a string"); + + if (params.version == 'edge' || !params.version) { + console.log('React edge (local)'); + perfRunner.WriteScript({src:'../build/react' + minSuffix + '.js'}); + perfRunner.WriteScript({src:'../build/JSXTransformer.js'}); + } else if (params.version == 'previous') { + console.log('React previous (local)'); + perfRunner.WriteScript({cache:true, src:'../build/react-previous' + minSuffix + '.js'}); + perfRunner.WriteScript({cache:true, src:'../build/JSXTransformer-previous.js'}); + } else if (params.version.indexOf('builds/') === 0) { + perfRunner.WriteScript({cache:true, src:'http://react.zpao.com/' + params.version + '/react' + minSuffix + '.js'}); + perfRunner.WriteScript({cache:true, src:'http://react.zpao.com/' + params.version + '/JSXTransformer.js'}); + } else { + console.log('React ' + params.version); + perfRunner.WriteScript({cache:true, src:'http://fb.me/react-' + params.version + minSuffix + '.js'}); + perfRunner.WriteScript({cache:true, src:'http://fb.me/JSXTransformer-' + params.version + '.js'}); + } + if (params.debug) { + console.warn('Loading the unminified build of React, performance may suffer.'); + console.warn('Load "' + location.href.replace(/\bdebug=\w+&?|&\bdebug=\w+/ig, '') + '" for better perf.'); + } else { + console.warn('Loading the minified build of React, debugging may be harder.'); + console.warn('Load "' + location.href.replace(/\bdebug=\w+&?|&\bdebug=\w+/ig, '') + '&debug=1' + '" for easier debugging.'); + } +} + +perfRunner.WriteTestScript = function(params){ + if (Array.isArray(params.test)) { + return params.test + .map(function(test){return {test:test};}) + .map(perfRunner.WriteTestScript) + ; + } + perfRunner.assert(params.test.indexOf('..') === -1, 'no relative paths allowed'); + return perfRunner.WriteScript({jsx:true, src: './tests/' + params.test}); +} + +perfRunner.getQueryParamArray = function(key){ + var values; + var queryString = location.search.substr(1); + var _key = encodeURIComponent(key) + '='; + + if (queryString.indexOf(_key) > -1) { + values = queryString + .split(_key) + .slice(1) + .map(function(part){return part.split('&')[0];}) + .map(decodeURIComponent) + .map(function(string){ + try { + return JSON.parse(string); + } catch(e){} + return string; + }) + ; + } + + perfRunner.assert(values && values.length && values[0], 'expected ' + key + ' query param'); + return values; +} + +perfRunner.getQueryParamArrayOrDefault = function(key, defaultValue){ + try { + return perfRunner.getQueryParamArray(key); + } catch (e) {} + return defaultValue; +} + +perfRunner.Polyfill = function(){ + if (typeof Function.prototype.bind != 'undefined') return; + perfRunner.WriteScript({src:'/node_modules/es5-shim/es5-shim.js', cache:true}); + perfRunner.WriteScript({src:'/node_modules/es5-shim/es5-sham.js', cache:true}); +} + +perfRunner.BenchmarkResults = function(props){ + return perfRunner.roundNumberWithPrecision(props.stats.mean * 1000) + 'ms/op' +} + +perfRunner.roundNumberWithPrecision = function(number, precision){ + if (!precision) precision = 1000; + return Math.round(number * precision) / precision; +} + +perfRunner.quickBench = function(benchmarkOptions, onComplete, onBeforeStart){ + var bench = new Benchmark(benchmarkOptions); + if (onBeforeStart) onBeforeStart(null, bench); + + bench.on('error', function(event){ + console.error(event.message); + console.log(event.target.compiled.toString()); + onComplete(Error(event.error)); + }); + + bench.on('start', function(){ + console.log('starting', bench.name); + }); + + bench.on('cycle', function(){ + var bench = this, + size = bench.stats.size; + + if (!bench.aborted) { + console.warn(bench.name + ' × ' + bench.count + + ' (' + bench.stats.sample.length + ' samples)' + + ' (' + Math.round(1 / bench.stats.mean) + ' ops/sec' + ')' + + ' (' + (bench.stats.mean * 1000) + ' ms/op' + ')' + + ' (±' + bench.stats.rme.toFixed(2) + '%)' + + ' with ' + React.version + ); + } + }); + + bench.on('complete', function(){ + var results = { + platform: Benchmark.platform.description, + react: React.version, + name: bench.name, + // times: bench.times, + // stats: bench.stats + }; + + results['s/op'] = bench.stats.mean + results['ms/op'] = results['s/op'] * 1000 + results['op/s'] = 1 / results['s/op'] + results["% frame 60"] = results['ms/op'] / (1000 / 60) * 100 + + console.log(results); + onComplete(null, results); + }); + + bench.run(); +}; + +perfRunner.singleTest = function(benchmarkOptions, onComplete){ + var bench = Benchmark(exports); + bench.on('complete', function(){ + var results = { + platform: Benchmark.platform.description, + react: React.version, + name: bench.name, + stats: bench.stats + }; + onComplete(results); + }); + bench.run(); +} + +perfRunner.ViewObject = function(props){ + var value = props.value; + delete props.value; + + if (typeof value != 'object') return React.DOM.span(props, [JSON.stringify(value), " ", typeof value]); + + return React.DOM.table(props, Object.keys(value).map(function(key){ + return React.DOM.tr(null, + React.DOM.th(null, key), + React.DOM.td(null, perfRunner.ViewObject({key:key, value:value[key]})) + ); + })); +} diff --git a/perf/lib/todolist.browser.js b/perf/lib/todolist.browser.js new file mode 100644 index 0000000000000..662f4fc116eea --- /dev/null +++ b/perf/lib/todolist.browser.js @@ -0,0 +1,117 @@ +/*global*/todolist = {}; + +todolist.ID = Date.now(); + +todolist.now = window.performance && window.performance.now && window.performance.now.bind(window.performance) || Date.now.bind(Date); + +todolist.App = React.createClass({ + propTypes: { + fakeDataCount: React.PropTypes.number + }, + + getInitialState: function(){ + var todos; + if (this.props.fakeDataCount) { + todos = Array(this.props.fakeDataCount + 1).join(',').split(',').map(function(ignore, index){ + return {id:index, title:"Title " + index + " " + Math.random().toString(36).substring(2,16), completed:!!(index % 2)}; + }); + } + return { + timerStart: todolist.now(), + timerEnd: null, + timerEvent: 'getInitialState', + todos: todos || this.props.initialData || [] + }; + }, + componentWillUpdate: function(props, state){ + state.todos = state.todos.filter(function(todo){ + return !todo.deleted; + }); + }, + addItem: function(title, callback){ + if (title == null || title === '') { + var error = Error('invalid title'); + if (!callback) throw error; + return callback(error); + } + var todos = this.state.todos.slice(); + var todo = { + id: ++todolist.ID, + title: title, + completed: false + }; + todos.push(todo); + if (callback) callback = callback.bind(this, todo); + this.setState({ timerEvent:'addItem', timerStart:todolist.now(), timerEnd:null, todos:todos }, callback); + return todo; + }, + deleteItemById: function(id, callback){ + var todo = this._getById(id); + if (!todo) return callback && callback(Error('todo with id ' + id + ' not found')); + todo.deleted = true; + this.setState({ timerEvent:'deleteItemById', timerStart:todolist.now(), timerEnd:null, todos:this.state.todos }, callback); + }, + setItemCompleted: function(id, completed, callback){ + var todo = this._getById(id); + if (!todo) return callback && callback(Error('todo with id ' + id + ' not found')); + todo.completed = completed; + this.setState({ timerEvent:'setItemCompleted', timerStart:todolist.now(), timerEnd:null, todos:this.state.todos }, callback); + }, + _getById: function(id){ + id = +id; + var todos = this.state.todos; + for (var index = todos.length; --index >= 0;){ + if (todos[index].id === id) return todos[index]; + } + return null; + }, + _handleItemCompletedCheckboxChange: function(event){ + var node = event.target; + this.setItemCompleted(node.value, node.checked); + }, + _handleItemDeletedButton: function(event){ + var node = event.target; + this.deleteItemById(node.value); + }, + _renderTodoItem: function(todo, index){ + return ( + React.DOM.li({key:todo.id}, + React.DOM.button({value:todo.id, onClick:this._handleItemDeletedButton}, 'x'), + React.DOM.input({type:"checkbox", value:todo.id, checked:todo.completed, onChange:this._handleItemCompletedCheckboxChange}), + " ", + React.DOM.span({style:{"text-decoration":todo.completed ? "line-through" : ""}}, todo.title) + ) + ); + }, + render: function(){ + if (!this.state.timerEnd) this.state.timerEnd = todolist.now(); + if (this.props.onRender) this.props.onRender(); + return ( + React.DOM.div(null, + React.DOM.h1(null, "TODO"), + React.DOM.h3(null, this.state.timerEvent, " ", this.state.timerEnd - this.state.timerStart, 'ms'), + todolist.NewItemForm({onEnter:this.addItem, autoFocus:true}), + React.DOM.ol(null, this.state.todos.map(this._renderTodoItem)) + ) + ); + }, + componentDidMount: function(rootNode){ + if (this.props.onDidMount) this.props.onDidMount(rootNode); + } +}); + +todolist.NewItemForm = React.createClass({ + _handleNewItemKeyDown: function(event){ + if (event.which !== 13/*enter key*/) return; + var node = this.refs.text.getDOMNode(); + var value = node.value; + node.value = ''; + this.props.onEnter(value); + return false; + }, + render: function(){ + return this.transferPropsTo( + React.DOM.input({ref:"text", onKeyDown:this.props.onEnter && this._handleNewItemKeyDown}) + ); + } +}); diff --git a/perf/lib/todolist.html b/perf/lib/todolist.html new file mode 100644 index 0000000000000..7c2de136e01a0 --- /dev/null +++ b/perf/lib/todolist.html @@ -0,0 +1,16 @@ + + +todolist + + + + + + + diff --git a/perf/runner.html b/perf/runner.html new file mode 100644 index 0000000000000..976841ff37b20 --- /dev/null +++ b/perf/runner.html @@ -0,0 +1,59 @@ + + +Perf Test Runner + + + + + + + + + + + +
+ + + +
+ +

+
diff --git a/perf/tests/basic-div.js b/perf/tests/basic-div.js
new file mode 100644
index 0000000000000..b72d6ded6d490
--- /dev/null
+++ b/perf/tests/basic-div.js
@@ -0,0 +1,16 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'React.DOM.div, no props';
+
+exports.fn = function(){
+  React.DOM.div(null, 'lol, perf testing ', this.count);
+};
diff --git a/perf/tests/basic-unmount.js b/perf/tests/basic-unmount.js
new file mode 100644
index 0000000000000..25be4a1a9f2c6
--- /dev/null
+++ b/perf/tests/basic-unmount.js
@@ -0,0 +1,23 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'unmountComponentAtNode';
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  var _firstChild = React.DOM.div(null, 'lol, perf testing ', this.count);
+  React.renderComponent(_firstChild, _rootNode);
+};
+exports.fn = function(){
+  if (React.unmountAndReleaseReactRootNode) React.unmountAndReleaseReactRootNode(_rootNode);
+  else React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/renderComponent-basic.js b/perf/tests/renderComponent-basic.js
new file mode 100644
index 0000000000000..36ad3674190eb
--- /dev/null
+++ b/perf/tests/renderComponent-basic.js
@@ -0,0 +1,24 @@
+/* jshint undef: true, unused: true */
+
+/* global document */
+/* global window */
+/* global Benchmark */
+/* global React */
+
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'React.renderComponent single div';
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+};
+exports.fn = function(){
+  React.renderComponent(React.DOM.div(null, 'lol, perf testing ', this.count), _rootNode);
+};
+exports.teardown = function(){
+  if (React.unmountAndReleaseReactRootNode) React.unmountAndReleaseReactRootNode(_rootNode);
+  else React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/sanity.js b/perf/tests/sanity.js
new file mode 100644
index 0000000000000..0150114f16d94
--- /dev/null
+++ b/perf/tests/sanity.js
@@ -0,0 +1,15 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'Trivial benchmark to verify that everything works';
+
+exports.setup = function(){
+  var foo;
+};
+exports.fn = function(){
+  foo = Array(999).join('Howdy!\n');
+};
+exports.teardown = function(){
+  foo = null;
+};
diff --git a/perf/tests/setState-callback-5.js b/perf/tests/setState-callback-5.js
new file mode 100644
index 0000000000000..5868baaab5f08
--- /dev/null
+++ b/perf/tests/setState-callback-5.js
@@ -0,0 +1,41 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'From setState to callback (x5)';
+
+exports.defer = true;
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  /*global*/setState = null;
+
+  var AwesomeComponent = React.createClass({
+    getInitialState: function(){
+      return { random:null };
+    },
+    render: function(){
+      if (!setState) setState = this.setState.bind(this);
+      return React.DOM.div(null, this.state.random);
+    }
+  });
+
+  React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.fn = function(deferred){
+  setState({random: Date.now() + Math.random()}, function(){
+    setState({random: Date.now() + Math.random()}, function(){
+      setState({random: Date.now() + Math.random()}, function(){
+        setState({random: Date.now() + Math.random()}, function(){
+          setState({random: Date.now() + Math.random()}, function(){
+            deferred.resolve();
+          });
+        });
+      });
+    });
+  });
+};
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/setState-callback.js b/perf/tests/setState-callback.js
new file mode 100644
index 0000000000000..c5a66ac174b7a
--- /dev/null
+++ b/perf/tests/setState-callback.js
@@ -0,0 +1,33 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'From setState to callback';
+
+exports.defer = true;
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  /*global*/setState = null;
+
+  var AwesomeComponent = React.createClass({
+    getInitialState: function(){
+      return { random:null };
+    },
+    render: function(){
+      if (!setState) setState = this.setState.bind(this);
+      return React.DOM.div(null, this.state.random);
+    }
+  });
+
+  React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.fn = function(deferred){
+  setState({random: Date.now() + Math.random()}, function(){
+    deferred.resolve();
+  });
+};
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/shouldComponentUpdate.js b/perf/tests/shouldComponentUpdate.js
new file mode 100644
index 0000000000000..3bdb4e70b2c85
--- /dev/null
+++ b/perf/tests/shouldComponentUpdate.js
@@ -0,0 +1,25 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'shouldComponentUpdate';
+
+exports.setup = function(){
+  var AwesomeComponent = React.createClass({
+    shouldComponentUpdate: function(){
+      return false;
+    },
+    render: function(){
+      return React.DOM.div({});
+    }
+  });
+
+  var _rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+};
+exports.fn = function(){
+  React.renderComponent(AwesomeComponent(null), _rootNode);
+};
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+};
diff --git a/perf/tests/todolist-add.js b/perf/tests/todolist-add.js
new file mode 100644
index 0000000000000..7cccbbd6ac4ab
--- /dev/null
+++ b/perf/tests/todolist-add.js
@@ -0,0 +1,27 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist from addItem to callback';
+
+exports.defer = true;
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  /*global*/_app = todolist.App({ fakeDataCount: 333 });
+  React.renderComponent(_app, _rootNode);
+};
+exports.fn = function(deferred){
+  var liCount = document.getElementsByTagName('li').length;
+  _app.addItem(Math.random(), function(){
+    if (document.getElementsByTagName('li').length <= liCount) throw Error('expected a list item to be added to the dom');
+    deferred.resolve();
+  });
+};
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+  _rootNode.parentNode.removeChild(_rootNode);
+  _rootNode = null;
+  _app = null;
+};
diff --git a/perf/tests/todolist-do-stuff.js b/perf/tests/todolist-do-stuff.js
new file mode 100644
index 0000000000000..aee008c7b8e13
--- /dev/null
+++ b/perf/tests/todolist-do-stuff.js
@@ -0,0 +1,54 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+/*global*/_timesToRun = 2;
+
+exports.name = 'todolist add, complete, remove (x' + _timesToRun + ')';
+
+exports.defer = true;
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  /*global*/_app = todolist.App({ fakeDataCount: 333 });
+  React.renderComponent(_app, _rootNode);
+};
+
+exports.fn = function(deferred){
+  var originalLiCount = document.getElementsByTagName('li').length;
+
+  var todos = [];
+  var times = _timesToRun;
+  while (times-- >= 0){
+    todos.push(_app.addItem(times+1));
+  }
+
+  todos.forEach(function(todo){
+    _app.setItemCompleted(todo.id);
+  });
+
+  todos.forEach(function(todo){
+    _app.deleteItemById(todo.id);
+  });
+
+  todos = null;
+
+  _app.addItem(Math.random(), function(todo){
+    if (document.getElementsByTagName('li').length <= originalLiCount)
+      throw Error('expected a list item to be added to the dom');
+
+    _app.deleteItemById(todo.id, function(){
+      if (document.getElementsByTagName('li').length != originalLiCount)
+        throw Error('expected everything to be done by now');
+      deferred.resolve();
+    });
+  });
+};
+
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+  _rootNode.parentNode.removeChild(_rootNode);
+  _rootNode = null;
+  _app = null;
+};
diff --git a/perf/tests/todolist-edit.js b/perf/tests/todolist-edit.js
new file mode 100644
index 0000000000000..bd12898001c53
--- /dev/null
+++ b/perf/tests/todolist-edit.js
@@ -0,0 +1,32 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist setItemCompleted';
+
+exports.defer = true;
+
+exports.setup = function(){
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+  /*global*/_app = todolist.App({ fakeDataCount: 333 });
+  React.renderComponent(_app, _rootNode);
+  /*global*/_todo1 = _app.addItem("Howdy 1!");
+  /*global*/_todo2 = _app.addItem("Howdy 2!");
+  /*global*/_todo3 = _app.addItem("Howdy 3!");
+};
+
+exports.fn = function(deferred){
+  _app.setItemCompleted(_todo1.id, !_todo1.completed);
+  _app.setItemCompleted(_todo2.id, !_todo2.completed);
+  _app.setItemCompleted(_todo3.id, !_todo3.completed, function(){
+    deferred.resolve();
+  });
+};
+
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+  _rootNode.parentNode.removeChild(_rootNode);
+  _rootNode = null;
+  _app = null;
+};
diff --git a/perf/tests/todolist-mount.js b/perf/tests/todolist-mount.js
new file mode 100644
index 0000000000000..71bdc91dbeb88
--- /dev/null
+++ b/perf/tests/todolist-mount.js
@@ -0,0 +1,23 @@
+if (typeof exports == 'undefined') exports = {};
+
+/*http://benchmarkjs.com/docs#options*/
+
+exports.name = 'todolist from renderComponent to renderComponent callback (333 rows)';
+
+exports.defer = true;
+
+exports.setup = function(){
+  if (typeof _rootNode != 'undefined') throw Error("should teardown before running setup again");
+  /*global*/_rootNode = document.createElement('div');
+  document.body.appendChild(_rootNode);
+};
+
+exports.fn = function(deferred){
+  React.renderComponent(todolist.App({ fakeDataCount: 333 }), _rootNode, function(){ deferred.resolve(); });
+};
+
+exports.teardown = function(){
+  React.unmountComponentAtNode(_rootNode);
+  _rootNode.parentNode.removeChild(_rootNode);
+  _rootNode = undefined;
+};
diff --git a/test/index.html b/test/index.html
index 5370e23824e3e..31621b1903e1b 100644
--- a/test/index.html
+++ b/test/index.html
@@ -11,6 +11,7 @@
     '../node_modules/jasmine-tapreporter/src/tapreporter.js',
     '../vendor/jasmine-jsreporter/jasmine-jsreporter.js',
 
+    'lib/postDataToURL.browser.js',
     'lib/reportTestResults.browser.js',
 
     '../build/react.js',
diff --git a/test/lib/postDataToURL.browser.js b/test/lib/postDataToURL.browser.js
new file mode 100644
index 0000000000000..5b543459213d0
--- /dev/null
+++ b/test/lib/postDataToURL.browser.js
@@ -0,0 +1,34 @@
+function createXMLHttpRequest(){
+  try{return new XMLHttpRequest();}
+  catch(e){}
+  try {return new ActiveXObject("Msxml2.XMLHTTP");}
+  catch (e) {}
+  try {return new ActiveXObject("Microsoft.XMLHTTP");}
+  catch (e) {}
+}
+
+function getURLSync(url){
+  var request = createXMLHttpRequest();
+  request.open('GET', url, /*asynchronous?*/false);
+  return request.responseText;
+}
+
+function postDataToURL(data, url, callback) {
+  if (!callback) callback = postDataToURL.defaultCallback;
+  var request = createXMLHttpRequest();
+  if (!request) return callback(Error('XMLHttpRequest is unsupported'));
+  postDataToURL.running = (postDataToURL.running||0) + 1;
+  request.onreadystatechange = function(){
+    if (request.readyState != 4) return;
+    request.onreadystatechange = null;
+    postDataToURL.running = (postDataToURL.running||0) - 1;
+    callback(request.status == 200 ? null : request.status, request.responseText);
+  };
+  request.open('POST', url);
+  request.setRequestHeader('Content-Type', 'application/json');
+  request.send(JSON.stringify(data));
+}
+
+postDataToURL.defaultCallback = function(error){
+  // console.log('postDataToURL.defaultCallback', arguments)
+}
diff --git a/test/lib/reportTestResults.browser.js b/test/lib/reportTestResults.browser.js
index a521b735d458b..983a427270ed7 100644
--- a/test/lib/reportTestResults.browser.js
+++ b/test/lib/reportTestResults.browser.js
@@ -48,30 +48,3 @@ console._flush = function(){
   };
 
 }(window.jasmine.getEnv()));
-
-function createXMLHttpRequest(){
-  try{return new XMLHttpRequest();}
-  catch(e){}
-  try {return new ActiveXObject("Msxml2.XMLHTTP");}
-  catch (e) {}
-  try {return new ActiveXObject("Microsoft.XMLHTTP");}
-  catch (e) {}
-}
-function postDataToURL(data, url, callback) {
-  if (!callback) callback = postDataToURL.defaultCallback;
-  var request = createXMLHttpRequest();
-  if (!request) return callback(Error('XMLHttpRequest is unsupported'));
-  postDataToURL.running = (postDataToURL.running||0) + 1;
-  request.onreadystatechange = function(){
-    if (request.readyState != 4) return;
-    request.onreadystatechange = null;
-    postDataToURL.running = (postDataToURL.running||0) - 1;
-    callback(request.status == 200 ? null : request.status, request.responseText);
-  };
-  request.open('POST', url);
-  request.setRequestHeader('Content-Type', 'application/json');
-  request.send(JSON.stringify(data));
-}
-postDataToURL.defaultCallback = function(error){
-  // console.log('postDataToURL.defaultCallback', arguments)
-}