diff --git a/bench/.eslintrc b/bench/.eslintrc new file mode 100644 index 00000000000..d34a4506724 --- /dev/null +++ b/bench/.eslintrc @@ -0,0 +1,7 @@ +{ + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + } +} diff --git a/bench/README.md b/bench/README.md index e89e251b1bf..b4224ce9505 100644 --- a/bench/README.md +++ b/bench/README.md @@ -2,7 +2,7 @@ Benchmarks help us catch performance regressions and improve performance. -## Running a Benchmark +## Running Benchmarks Start the benchmark server @@ -10,10 +10,9 @@ Start the benchmark server MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} npm start ``` -Open a benchmark runner page +To run all benchmarks, open [the benchmark page, `http://localhost:9966/bench`](http://localhost:9966/bench). - - **buffer benchmark** http://localhost:9966/bench/buffer - - **fps benchmark** http://localhost:9966/bench/fps +To run a specific benchmark, append its name to the url, for example [`http://localhost:9966/bench/buffer`](http://localhost:9966/bench/buffer). ## Writing a Benchmark diff --git a/bench/benchmarks/fps.js b/bench/benchmarks/fps.js index aa7a77f6028..2543e082679 100644 --- a/bench/benchmarks/fps.js +++ b/bench/benchmarks/fps.js @@ -31,7 +31,7 @@ module.exports = function(options) { evented.fire('error', { error: err }); } else { evented.fire('end', { - message: formatNumber(fps) + ' frames per second', + message: formatNumber(fps) + ' fps', score: 1 / fps }); } diff --git a/bench/benchmarks/frame_duration.js b/bench/benchmarks/frame_duration.js index d9374342c2c..c3c115ffc10 100644 --- a/bench/benchmarks/frame_duration.js +++ b/bench/benchmarks/frame_duration.js @@ -30,16 +30,14 @@ module.exports = function(options) { measureFrameTime(options, zooms[index], function(err_, result) { results[index] = result; evented.fire('log', { - message: formatNumber(result.sum / result.count * 10) / 10 + ' ms per frame at zoom ' + zooms[index] + '. ' + - formatNumber(result.countAbove16 / result.count * 100) + '% of frames took longer than 16ms.' + message: formatNumber(result.sum / result.count * 10) / 10 + ' ms, ' + + formatNumber(result.countAbove16 / result.count * 100) + '% > 16 ms at zoom ' + zooms[index] }); callback(); }); } function done() { - document.getElementById('map').remove(); - var sum = 0; var count = 0; var countAbove16 = 0; @@ -50,7 +48,7 @@ module.exports = function(options) { countAbove16 += result.countAbove16; } evented.fire('end', { - message: formatNumber(sum / count * 10) / 10 + ' ms per frame. ' + formatNumber(countAbove16 / count * 100) + '% of frames took longer than 16ms.', + message: formatNumber(sum / count * 10) / 10 + ' ms, ' + formatNumber(countAbove16 / count * 100) + '% > 16ms', score: sum / count }); } @@ -96,6 +94,7 @@ function measureFrameTime(options, zoom, callback) { if (frameEnd - start > DURATION_MILLISECONDS) { map.repaint = false; map.remove(); + map.getContainer().remove(); callback(undefined, { sum: sum, count: count, diff --git a/bench/benchmarks/geojson_setdata_large.js b/bench/benchmarks/geojson_setdata_large.js index 0006b23a90b..39a8e3bbfb3 100644 --- a/bench/benchmarks/geojson_setdata_large.js +++ b/bench/benchmarks/geojson_setdata_large.js @@ -27,7 +27,7 @@ module.exports = function(options) { evented.fire('log', {message: 'loading large feature collection'}); setDataPerf(source, 50, featureCollection, function(err, ms) { if (err) return evented.fire('error', {error: err}); - evented.fire('end', {message: 'average load time: ' + formatNumber(ms) + ' ms', score: ms}); + evented.fire('end', {message: formatNumber(ms) + ' ms', score: ms}); }); }); diff --git a/bench/benchmarks/geojson_setdata_small.js b/bench/benchmarks/geojson_setdata_small.js index 38906586a03..ee5e09eeea4 100644 --- a/bench/benchmarks/geojson_setdata_small.js +++ b/bench/benchmarks/geojson_setdata_small.js @@ -37,7 +37,7 @@ module.exports = function(options) { evented.fire('log', {message: 'loading small feature collection'}); setDataPerf(source, 50, featureCollection, function(err, ms) { if (err) return evented.fire('error', {error: err}); - evented.fire('end', {message: 'average load time: ' + formatNumber(ms) + ' ms', score: ms}); + evented.fire('end', {message: formatNumber(ms) + ' ms', score: ms}); }); }); diff --git a/bench/benchmarks/query_box.js b/bench/benchmarks/query_box.js index 9379f50a617..e2bab94a96d 100644 --- a/bench/benchmarks/query_box.js +++ b/bench/benchmarks/query_box.js @@ -28,7 +28,7 @@ module.exports = function(options) { center: [-77.032194, 38.912753], style: 'mapbox://styles/mapbox/streets-v9' }); - document.getElementById('map').style.display = 'none'; + map.getContainer().style.display = 'none'; map.on('load', function() { @@ -36,7 +36,7 @@ module.exports = function(options) { var zoomCount = 0; asyncSeries(numSamples, function(n, callback) { var start = performance.now(); - map.queryRenderedFeatures(); + map.queryRenderedFeatures({}); var duration = performance.now() - start; sum += duration; count++; @@ -45,7 +45,7 @@ module.exports = function(options) { callback(); }, function() { evented.fire('log', { - message: 'zoom ' + zoomLevel + ' average: ' + (zoomSum / zoomCount).toFixed(2) + ' ms' + message: (zoomSum / zoomCount).toFixed(2) + ' ms at zoom ' + zoomLevel }); callback(); }); @@ -80,4 +80,3 @@ function asyncSeries(times, work, callback) { callback(); } } - diff --git a/bench/benchmarks/query_point.js b/bench/benchmarks/query_point.js index 49962b633bc..1b3ac282483 100644 --- a/bench/benchmarks/query_point.js +++ b/bench/benchmarks/query_point.js @@ -37,7 +37,7 @@ module.exports = function(options) { center: [-77.032194, 38.912753], style: 'mapbox://styles/mapbox/streets-v9' }); - document.getElementById('map').style.display = 'none'; + map.getContainer().style.display = 'none'; map.on('load', function() { @@ -46,7 +46,7 @@ module.exports = function(options) { asyncSeries(queryPoints.length, function(n, callback) { var queryPoint = queryPoints[queryPoints.length - n]; var start = performance.now(); - map.queryRenderedFeatures(queryPoint); + map.queryRenderedFeatures(queryPoint, {}); var duration = performance.now() - start; sum += duration; count++; @@ -55,7 +55,7 @@ module.exports = function(options) { callback(); }, function() { evented.fire('log', { - message: 'zoom ' + zoomLevel + ' average: ' + (zoomSum / zoomCount).toFixed(2) + ' ms' + message: (zoomSum / zoomCount).toFixed(2) + ' ms at zoom ' + zoomLevel }); callback(); }); @@ -90,4 +90,3 @@ function asyncSeries(times, work, callback) { callback(); } } - diff --git a/bench/index.html b/bench/index.html index 9b346bd4175..fc698ac3d92 100644 --- a/bench/index.html +++ b/bench/index.html @@ -8,34 +8,10 @@ - - - +
-
-
diff --git a/bench/index.js b/bench/index.js index 538df4a5a71..95f39df40ef 100644 --- a/bench/index.js +++ b/bench/index.js @@ -1,67 +1,223 @@ 'use strict'; +/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "BenchmarksView|clipboard" }]*/ -var mapboxgl = require('../js/mapbox-gl'); +var React = require('react'); +var ReactDOM = require('react-dom'); var util = require('../js/util/util'); - -try { - main(); -} catch (err) { - log('red', err.toString()); - throw err; -} - -function main() { - log('dark', 'please keep this window in the foreground and close the debugger'); - - var benchmarks = { - buffer: require('./benchmarks/buffer'), - fps: require('./benchmarks/fps'), - 'frame-duration': require('./benchmarks/frame_duration'), - 'query-point': require('./benchmarks/query_point'), - 'query-box': require('./benchmarks/query_box'), - 'geojson-setdata-small': require('./benchmarks/geojson_setdata_small'), - 'geojson-setdata-large': require('./benchmarks/geojson_setdata_large') - }; - - var benchmarksDiv = document.getElementById('benchmarks'); - - Object.keys(benchmarks).forEach(function(id) { - benchmarksDiv.innerHTML += '' + id + ''; - }); - - var pathnameArray = location.pathname.split('/'); - var benchmarkName = pathnameArray[pathnameArray.length - 1] || pathnameArray[pathnameArray.length - 2]; - var createBenchmark = benchmarks[benchmarkName]; - if (!createBenchmark) throw new Error('unknown benchmark "' + benchmarkName + '"'); - - var benchmark = createBenchmark({ - accessToken: getAccessToken(), - createMap: createMap - }); - - benchmark.on('log', function(event) { - log(event.color || 'blue', event.message); - scrollToBottom(); - }); - - benchmark.on('end', function(event) { - log('green', '' + event.message + ''); - scrollToBottom(); - }); - - benchmark.on('error', function(event) { - log('red', event.error); - scrollToBottom(); - }); +var async = require('async'); +var mapboxgl = require('../js/mapbox-gl'); +var Clipboard = require('clipboard'); +var URL = require('url'); + +var BenchmarksView = React.createClass({ + + render: function() { + return
+ {this.renderBenchmarkSummaries()} + {this.renderBenchmarkDetails()} +
; + }, + + renderBenchmarkSummaries: function() { + return
+

Benchmarks

+
+ {Object.keys(this.state.results).map(this.renderBenchmarkSummary)} +
+ + Copy Results + +
; + }, + + renderBenchmarkSummary: function(name) { + var results = this.state.results[name]; + var that = this; + + return
+ {name}: {results.message || '...'} +
; + }, + + renderBenchmarkTextSummaries: function() { + var output = '# Benchmarks\n\n'; + for (var name in this.state.results) { + var result = this.state.results[name]; + output += '**' + name + ':** ' + (result.message || '...') + '\n'; + } + return output; + }, + + renderBenchmarkDetails: function() { + return
+ {Object.keys(this.state.results).map(this.renderBenchmarkDetail)} +
; + }, + + renderBenchmarkDetail: function(name) { + var results = this.state.results[name]; + return ( +
+ +

"{name}" Benchmark

+ {results.logs.map(function(log, index) { + return
{log.message}
; + })} +
+ ); + }, + + scrollToBenchmark: function(name) { + var duration = 300; + var startTime = (new Date()).getTime(); + var startYOffset = window.pageYOffset; + + requestAnimationFrame(function frame() { + var endYOffset = document.getElementById(name).offsetTop; + var time = (new Date()).getTime(); + var yOffset = Math.min((time - startTime) / duration, 1) * (endYOffset - startYOffset) + startYOffset; + window.scrollTo(0, yOffset); + if (time < startTime + duration) requestAnimationFrame(frame); + }); + }, + + getInitialState: function() { + return { + state: 'waiting', + runningBenchmark: Object.keys(this.props.benchmarks)[0], + results: util.mapObject(this.props.benchmarks, function(_, name) { + return { + name: name, + state: 'waiting', + logs: [] + }; + }) + }; + }, + + componentDidMount: function() { + var that = this; + setTimeout(function() { + async.eachSeries( + Object.keys(that.props.benchmarks), + that.runBenchmark, + function() { + that.setState({state: 'ended'}); + that.scrollToBenchmark(Object.keys(that.props.benchmarks)[0]); + } + ); + }, 500); + }, + + runBenchmark: function(name, outerCallback) { + var that = this; + var results = this.state.results[name]; + var maps = []; + + results.state = 'running'; + this.scrollToBenchmark(name); + this.setState({runningBenchmark: name}); + log('dark', 'starting'); + + this.props.benchmarks[name]({ + accessToken: getAccessToken(), + createMap: createMap + + }).on('log', function(event) { + log(event.color, event.message); + + }).on('end', function(event) { + results.message = event.message; + results.state = 'ended'; + log('green', event.message); + callback(); + + }).on('error', function(event) { + results.state = 'errored'; + log('red', event.error); + callback(); + }); + + function log(color, message) { + results.logs.push({ + color: color || 'blue', + message: message + }); + that.forceUpdate(); + } + + function callback() { + for (var i = 0; i < maps.length; i++) { + maps[i].remove(); + maps[i].getContainer().remove(); + } + setTimeout(function() { + that.setState({runningBenchmark: null}); + outerCallback(); + }, 500); + } + + function createMap(options) { + options = util.extend({width: 512, height: 512}, options); + + var element = document.createElement('div'); + element.style.width = options.width + 'px'; + element.style.height = options.height + 'px'; + element.style.margin = '0 auto'; + document.body.appendChild(element); + + var map = new mapboxgl.Map(util.extend({ + container: element, + style: 'mapbox://styles/mapbox/streets-v9', + interactive: false + }, options)); + maps.push(map); + + return map; + } + } +}); + +var benchmarks = { + buffer: require('./benchmarks/buffer'), + fps: require('./benchmarks/fps'), + 'frame-duration': require('./benchmarks/frame_duration'), + 'query-point': require('./benchmarks/query_point'), + 'query-box': require('./benchmarks/query_box'), + 'geojson-setdata-small': require('./benchmarks/geojson_setdata_small'), + 'geojson-setdata-large': require('./benchmarks/geojson_setdata_large') +}; + +var filteredBenchmarks = {}; +var benchmarkName = URL.parse(window.location.toString()).pathname.split('/')[2]; +if (!benchmarkName) { + filteredBenchmarks = benchmarks; +} else { + filteredBenchmarks[benchmarkName] = benchmarks[benchmarkName]; } -function scrollToBottom() { - window.scrollTo(0, document.body.scrollHeight); -} +ReactDOM.render(, document.getElementById('benchmarks')); -function log(color, message) { - document.getElementById('logs').innerHTML += '

' + message + '

'; -} +var clipboard = new Clipboard('.clipboard'); function getAccessToken() { var accessToken = ( @@ -76,23 +232,6 @@ function getAccessToken() { function getURLParameter(name) { var regexp = new RegExp('[?&]' + name + '=([^&#]*)', 'i'); - var output = regexp.exec(window.location.href); - return output && output[1]; -} - -function createMap(options) { - var mapElement = document.getElementById('map'); - - options = util.extend({width: 512, height: 512}, options); - - mapElement.style.display = 'block'; - mapElement.style.width = options.width + 'px'; - mapElement.style.height = options.height + 'px'; - - mapboxgl.accessToken = getAccessToken(); - return new mapboxgl.Map(util.extend({ - container: 'map', - style: 'mapbox://styles/mapbox/streets-v9', - interactive: false - }, options)); + var results = regexp.exec(window.location.href); + return results && results[1]; } diff --git a/js/ui/map.js b/js/ui/map.js index ec93aa44206..88aca907597 100755 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -1058,6 +1058,8 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ if (typeof window !== 'undefined') { window.removeEventListener('resize', this._onWindowResize, false); } + var extension = this.painter.gl.getExtension('WEBGL_lose_context'); + if (extension) extension.loseContext(); removeNode(this._canvasContainer); removeNode(this._controlContainer); this._container.classList.remove('mapboxgl-map'); diff --git a/package.json b/package.json index c6fda13570f..1c6c9b734a1 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "gl-matrix": "^2.3.1", "grid-index": "^1.0.0", "mapbox-gl-function": "^1.2.1", - "mapbox-gl-supported": "^1.2.0", "mapbox-gl-shaders": "mapbox/mapbox-gl-shaders#4d1f89514bf03536c8e682439df165c33a37122a", "mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#83b1a3e5837d785af582efd5ed1a212f2df6a4ae", + "mapbox-gl-supported": "^1.2.0", "pbf": "^1.3.2", "pngjs": "^2.2.0", "point-geometry": "^0.0.0", @@ -39,9 +39,13 @@ "whoots-js": "^2.0.0" }, "devDependencies": { + "async": "^2.0.1", + "babel-preset-react": "^6.11.1", + "babelify": "^7.3.0", "benchmark": "~2.1.0", "browserify": "^13.0.0", "browserify-middleware": "^7.0.0", + "clipboard": "^1.5.12", "concat-stream": "1.5.1", "coveralls": "^2.11.8", "doctrine": "^1.2.1", @@ -63,6 +67,8 @@ "minifyify": "^7.0.1", "nyc": "6.4.0", "proxyquire": "^1.7.9", + "react": "^15.3.0", + "react-dom": "^15.3.0", "remark": "4.2.2", "remark-html": "3.0.0", "sinon": "^1.15.4", diff --git a/server.js b/server.js index acd8eaef7ea..d79c31b5114 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,10 @@ var fs = require('fs'); var http = require('http'); var path = require('path'); +app.use(express.static(path.join(__dirname, 'debug'))); +app.use('/dist', express.static(path.join(__dirname, 'dist'))); +app.use('/mapbox-gl-test-suite', express.static(path.join(__dirname, 'node_modules/mapbox-gl-test-suite'))); + app.get('/mapbox-gl.js', browserify('./js/mapbox-gl.js', { ignoreTransform: ['unassertify'], standalone: 'mapboxgl', @@ -23,13 +27,17 @@ app.get('/access-token.js', browserify('./debug/access-token.js', { })); app.get('/bench/index.js', browserify('./bench/index.js', { - transform: ['unassertify', 'envify'], + transform: [['babelify', {presets: ['react']}], 'unassertify', 'envify'], debug: true, minify: true, cache: 'dynamic', precompile: true })); +app.get('/bench', function(req, res) { + res.sendFile(path.join(__dirname, 'bench', 'index.html')); +}); + app.get('/bench/:name', function(req, res) { res.sendFile(path.join(__dirname, 'bench', 'index.html')); }); @@ -38,13 +46,6 @@ app.get('/debug', function(req, res) { res.redirect('/'); }); -app.use(express.static(path.join(__dirname, 'debug'))); -app.use('/dist', express.static(path.join(__dirname, 'dist'))); - -// serve files in mapbox-gl-test-suite, so that debug files can use its -// data/image/video/etc. assets -app.use('/mapbox-gl-test-suite', express.static(path.join(__dirname, 'node_modules/mapbox-gl-test-suite'))); - downloadBenchData(function() { app.listen(9966, function () { console.log('mapbox-gl-js debug server running at http://localhost:9966');