diff --git a/.eslintrc b/.eslintrc index 807529fa2b2..437d9942c97 100644 --- a/.eslintrc +++ b/.eslintrc @@ -26,6 +26,10 @@ "no-eq-null": "off", "no-lonely-if": "off", "no-new": "off", + "no-restricted-properties": [2, { + "object": "Object", + "property": "assign" + }], "no-unused-vars": ["error", {"argsIgnorePattern": "^_$"}], "no-var": "error", "no-warning-comments": "error", diff --git a/bench/.eslintrc b/bench/.eslintrc index dbba3c8b0d7..ab022442043 100644 --- a/bench/.eslintrc +++ b/bench/.eslintrc @@ -4,8 +4,13 @@ "jsx": true } }, + "plugins": [ + "react" + ], "rules": { - "flowtype/require-valid-file-annotation": [0] + "flowtype/require-valid-file-annotation": [0], + "react/jsx-uses-vars": [2], + "no-restricted-properties": "off" }, "globals": { "React": false, diff --git a/bench/README.md b/bench/README.md index 989bbe3d60a..cbd2826822e 100644 --- a/bench/README.md +++ b/bench/README.md @@ -7,12 +7,12 @@ Benchmarks help us catch performance regressions and improve performance. Start the benchmark server ```bash -MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} npm run start-bench +MAPBOX_ACCESS_TOKEN={YOUR MAPBOX ACCESS TOKEN} yarn start ``` To run all benchmarks, open [the benchmark page, `http://localhost:9966/bench`](http://localhost:9966/bench). -To run a specific benchmark, add its name to the url hash, for example [`http://localhost:9966/bench/#buffer`](http://localhost:9966/bench/#buffer). +To run a specific benchmark, add its name to the url hash, for example [`http://localhost:9966/bench/#Layout`](http://localhost:9966/bench/#Layout). ## Writing a Benchmark @@ -20,82 +20,36 @@ Good benchmarks - are precise (i.e. running it multiple times returns roughly the same result) - operate in a way that mimics real-world usage - - use a large quantity of diverse real-world data + - use a significant quantity of real-world data - are conceptually simple -Benchmarks are implemented as a function that returns an instance of `Evented`. - -```js -createBenchmark(options: { - accessToken: string; - createMap: (options: { - width: number; - height: number; - ... // supports all options for the Map constructor - }):Map -}): Evented -``` - -The instance of `Evented` may fire any number of `log` and `error` events. The -instance of `Evented` must fire exactly one `end` event. - -### `log` - -Fire the `log` event to report benchmark progress to the user. +Benchmarks are implemented by extending the `Benchmark` class and implementing at least the `bench` method. +If the benchmark needs to do setup or teardown work that should not be included in the measured time, you +can implement the `setup` or `teardown` methods. All three of these methods may return a `Promise` if they +need to do asynchronous work (or they can act synchronously and return `undefined`). -```js -{ - message: string; - color: string = 'blue'; // name of a Mapbox base color https://mapbox.com/base/styling/color -} -``` - -If your benchmark has a notion of running multiple "samples", you might emit -one `log` event per sample. - -```js -benchmark.fire('log', { - message: 'Finished sample ' + i + ' in ' + formatNumber(time) + ' ms' -}); -``` +See the JSDoc comments on the `Benchmark` class for more details, and the existing benchmarks for examples. -These events have no machine-semantic meaning. +## Interpreting benchmark results -### `end` +The benchmark harness runs each benchmark's `bench` method a lot of times -- until it thinks it has enough +samples to do an analysis -- and records how long each call takes. From these samples, it creates summary +statistics and plots that help in determining whether a given change increased or decreased performance. -Fire the `end` event to indicate the benchmark has finished running and report -its results. +* **Mean**, **Minimum**, and **Deviation** are the standard summary statistics. +* **R? Slope / Correlation** are measures derived from comparing increasingly large groups of samples (1 sample, +2 samples, 3 samples, ...) to the sum of those samples' execution time. Ideally, the number of samples is +perfectly linearly correlated to the sum of execution times. If it is, then the slope of the line is equivalent +the average execution time. But usually, the correlation is not perfect due to natural variance and outliers. +The R? correlation indicates how good the linear approximation is. Values greater than 0.99 are good. Less +than 0.99 is iffy (??), and less than 0.90 means something is confounding the results, and they should be +regarded as unreliable (??). +* The top plot shows the distribution of samples, both by plotting each individual sample (on the right), +and by plotting a kernel density estimate. On the right, you can also see (from left to right) the mean, +minimum and maximum sample, and sample values at the first quartile, second quartile (median), and third quartile. +* The bottom plot shows the R? analysis and resulting linear approximation. -These events have both human-readable results (`message`) and machine-readable results (`score`). Smaller `score`s are "better." Optionally, an array of raw sample data (`samples`) may also be included. +## Posting benchmark results to PRs -```js -{ - message: string; - score: number; - ?samples: Array -} -``` - -```js -benchmark.fire('end', { - message: 'Average time is ' + formatNumber(averageTime)) + 'ms', - score: averageTime, - samples: [ - ['sample', 'value'], - [1, sample1Time], - [2, sample2Time], - [3, sample3Time], - ... - ] -}); -``` - -### `error` - -Fire the `error` event to indicate the benchmark has encountered an error. - -```js -{ - error: Error; -} -``` +We recommend installing a browser extension that can take full-page snapshots, e.g. +[FireShot](https://chrome.google.com/webstore/detail/take-webpage-screenshots/mcbpblocgmgfnpjjppndjkmgjaogfceg). diff --git a/bench/benchmarks.js b/bench/benchmarks.js index 9018538ab19..a1675b54015 100644 --- a/bench/benchmarks.js +++ b/bench/benchmarks.js @@ -1,24 +1,30 @@ +// @flow + 'use strict'; +require('../src').accessToken = require('./lib/access_token'); + +window.mapboxglVersions = window.mapboxglVersions || []; window.mapboxglBenchmarks = window.mapboxglBenchmarks || {}; const version = process.env.BENCHMARK_VERSION; -function registerBenchmark(name, benchmark) { - window.mapboxglBenchmarks[name] = window.mapboxglBenchmarks[name] || {}; - window.mapboxglBenchmarks[name][version] = benchmark; +window.mapboxglVersions.push(version); + +function register(Benchmark) { + window.mapboxglBenchmarks[Benchmark.name] = window.mapboxglBenchmarks[Benchmark.name] || {}; + window.mapboxglBenchmarks[Benchmark.name][version] = new Benchmark(); } -registerBenchmark('map-load', require('./benchmarks/map_load')); -registerBenchmark('style-load', require('./benchmarks/style_load')); -registerBenchmark('buffer', require('./benchmarks/buffer')); -registerBenchmark('tile_layout_dds', require('./benchmarks/tile_layout_dds')); -registerBenchmark('fps', require('./benchmarks/fps')); -registerBenchmark('frame-duration', require('./benchmarks/frame_duration')); -registerBenchmark('query-point', require('./benchmarks/query_point')); -registerBenchmark('query-box', require('./benchmarks/query_box')); -registerBenchmark('geojson-setdata-small', require('./benchmarks/geojson_setdata_small')); -registerBenchmark('geojson-setdata-large', require('./benchmarks/geojson_setdata_large')); -registerBenchmark('filter', require('./benchmarks/filter')); +register(require('./benchmarks/layout')); +register(require('./benchmarks/layout_dds')); +register(require('./benchmarks/paint')); +register(require('./benchmarks/map_load')); +register(require('./benchmarks/style_validate')); +register(require('./benchmarks/style_layer_create')); +register(require('./benchmarks/query_point')); +register(require('./benchmarks/query_box')); +register(require('./benchmarks/filter_create')); +register(require('./benchmarks/filter_evaluate')); // Ensure the global worker pool is never drained. Browsers have resource limits // on the max number of workers that can be created per page. diff --git a/bench/benchmarks/buffer.js b/bench/benchmarks/buffer.js deleted file mode 100644 index dbf411b3489..00000000000 --- a/bench/benchmarks/buffer.js +++ /dev/null @@ -1,189 +0,0 @@ -'use strict'; - -const VT = require('@mapbox/vector-tile'); -const Protobuf = require('pbf'); -const assert = require('assert'); - -const WorkerTile = require('../../src/source/worker_tile'); -const ajax = require('../../src/util/ajax'); -const Style = require('../../src/style/style'); -const StyleLayerIndex = require('../../src/style/style_layer_index'); -const util = require('../../src/util/util'); -const Evented = require('../../src/util/evented'); -const config = require('../../src/util/config'); -const coordinates = require('../lib/coordinates'); -const formatNumber = require('../lib/format_number'); -const accessToken = require('../lib/access_token'); -const deref = require('../../src/style-spec/deref'); - -const SAMPLE_COUNT = 10; - -module.exports = function run() { - config.ACCESS_TOKEN = accessToken; - - const evented = new Evented(); - - const stylesheetURL = `https://api.mapbox.com/styles/v1/mapbox/streets-v9?access_token=${accessToken}`; - ajax.getJSON({ url: stylesheetURL }, (err, stylesheet) => { - if (err) return evented.fire('error', {error: err}); - - evented.fire('log', { - message: 'preloading assets', - color: 'dark' - }); - - preloadAssets(stylesheet, (err, assets) => { - if (err) return evented.fire('error', {error: err}); - - evented.fire('log', { - message: 'starting first test', - color: 'dark' - }); - - function getGlyphs(params, callback) { - callback(null, assets.glyphs[JSON.stringify(params)]); - } - - function getIcons(params, callback) { - callback(null, assets.icons[JSON.stringify(params)]); - } - - function getTile(url, callback) { - callback(null, assets.tiles[url]); - } - - let timeSum = 0; - let timeCount = 0; - - asyncTimesSeries(SAMPLE_COUNT, (callback) => { - runSample(stylesheet, getGlyphs, getIcons, getTile, (err, time) => { - if (err) return evented.fire('error', { error: err }); - timeSum += time; - timeCount++; - evented.fire('log', { message: `${formatNumber(time)} ms` }); - callback(); - }); - }, (err) => { - if (err) { - evented.fire('error', { error: err }); - - } else { - const timeAverage = timeSum / timeCount; - evented.fire('end', { - message: `${formatNumber(timeAverage)} ms`, - score: timeAverage - }); - } - }); - }); - - }); - - return evented; -}; - -class StubMap extends Evented { - _transformRequest(url) { - return { url }; - } -} - -function preloadAssets(stylesheet, callback) { - const assets = { - glyphs: {}, - icons: {}, - tiles: {} - }; - - const style = new Style(stylesheet, new StubMap()); - - style.on('style.load', () => { - function getGlyphs(params, callback) { - style.getGlyphs(0, params, (err, glyphs) => { - assets.glyphs[JSON.stringify(params)] = glyphs; - callback(err, glyphs); - }); - } - - function getImages(params, callback) { - style.getImages(0, params, (err, icons) => { - assets.icons[JSON.stringify(params)] = icons; - callback(err, icons); - }); - } - - function getTile(url, callback) { - ajax.getArrayBuffer({ url }, (err, response) => { - assets.tiles[url] = response.data; - callback(err, response.data); - }); - } - - runSample(stylesheet, getGlyphs, getImages, getTile, (err) => { - style._remove(); - callback(err, assets); - }); - }); - - style.on('error', (event) => { - callback(event.error); - }); - -} - -function runSample(stylesheet, getGlyphs, getImages, getTile, callback) { - const layerIndex = new StyleLayerIndex(deref(stylesheet.layers)); - - const timeStart = performance.now(); - - util.asyncAll(coordinates, (coordinate, eachCallback) => { - const url = `https://a.tiles.mapbox.com/v4/mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v6/${coordinate.zoom}/${coordinate.row}/${coordinate.column}.vector.pbf?access_token=${config.ACCESS_TOKEN}`; - - const workerTile = new WorkerTile({ - coord: coordinate, - zoom: coordinate.zoom, - tileSize: 512, - overscaling: 1, - angle: 0, - pitch: 0, - showCollisionBoxes: false, - source: 'composite', - uid: url - }); - - const actor = { - send: function(action, params, sendCallback) { - setTimeout(() => { - if (action === 'getImages') { - getImages(params, sendCallback); - } else if (action === 'getGlyphs') { - getGlyphs(params, sendCallback); - } else assert(false); - }, 0); - } - }; - - getTile(url, (err, response) => { - if (err) throw err; - const data = new VT.VectorTile(new Protobuf(response)); - workerTile.parse(data, layerIndex, actor, (err) => { - if (err) return callback(err); - eachCallback(); - }); - }); - }, (err) => { - const timeEnd = performance.now(); - callback(err, timeEnd - timeStart); - }); -} - -function asyncTimesSeries(times, work, callback) { - if (times > 0) { - work((err) => { - if (err) callback(err); - else asyncTimesSeries(times - 1, work, callback); - }); - } else { - callback(); - } -} diff --git a/bench/benchmarks/filter.js b/bench/benchmarks/filter.js deleted file mode 100644 index 2259e671115..00000000000 --- a/bench/benchmarks/filter.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -const VectorTile = require('@mapbox/vector-tile').VectorTile; -const Pbf = require('pbf'); -const Evented = require('../../src/util/evented'); -const createFilter = require('../../src/style-spec/feature_filter'); -const formatNumber = require('../lib/format_number'); -const ajax = require('../../src/util/ajax'); -const filters = require('../data/filters.json'); - -const NRepeat = 100; - -module.exports = function () { - const evented = new Evented(); - - ajax.getArrayBuffer({url: 'data/785.vector.pbf'}, (err, response) => { - if (err) { - return evented.fire('error', err); - } - const assets = { - tile: new VectorTile(new Pbf(response.data)) - }; - run(evented, assets); - }); - - return evented; -}; - -function run (evented, assets) { - const tile = assets.tile; - const results = [['task', 'iteration', 'time']]; - const layers = []; - for (const name in tile.layers) { - const layer = tile.layers[name]; - if (!layer.length) continue; - - const features = []; - for (let j = 0; j < layer.length; j++) { - features.push(layer.feature(j)); - } - - const layerFilters = []; - for (let j = 0; j < filters.length; j++) { - if (filters[j].layer === name) layerFilters.push(filters[j].filter); - } - - layers.push({ - name: name, - features: features, - rawFilters: layerFilters - }); - } - - let start = performance.now(); - let created = 0; - for (let m = 0; m < NRepeat; m++) { - for (let i = 0; i < layers.length; i++) { - const layer = layers[i]; - layer.filters = []; - for (let j = 0; j < layer.rawFilters.length; j++) { - layer.filters.push(createFilter(layer.rawFilters[j])); - results.push(['create', ++created, performance.now() - start]); - } - } - } - - evented.fire('log', {message: `Create filter: ${formatNumber(performance.now() - start)}ms (${created} iterations)`}); - - start = performance.now(); - let applied = 0; - for (let m = 0; m < NRepeat; m++) { - for (let i = 0; i < layers.length; i++) { - const layer = layers[i]; - for (let j = 0; j < layer.filters.length; j++) { - const filter = layer.filters[j]; - for (let k = 0; k < layer.features.length; k++) { - const feature = layer.features[k]; - if (typeof filter(feature) !== 'boolean') { - evented.fire('error', {message: 'Expected boolean result from filter'}); - break; - } - ++applied; - } - results.push(['apply', applied, performance.now() - start]); - } - } - } - - evented.fire('log', {message: `Apply filter: ${formatNumber(performance.now() - start)}ms (${applied} iterations)`}); - - evented.fire('end', {message: 'Done', samples: results}); -} diff --git a/bench/benchmarks/filter_create.js b/bench/benchmarks/filter_create.js new file mode 100644 index 00000000000..393f9f1edc6 --- /dev/null +++ b/bench/benchmarks/filter_create.js @@ -0,0 +1,15 @@ +// @flow + +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const createFilter = require('../../src/style-spec/feature_filter'); +const filters = require('../data/filters.json'); + +module.exports = class FilterCreate extends Benchmark { + bench() { + for (const filter of filters) { + createFilter(filter.filter); + } + } +}; diff --git a/bench/benchmarks/filter_evaluate.js b/bench/benchmarks/filter_evaluate.js new file mode 100644 index 00000000000..77c83b02dbd --- /dev/null +++ b/bench/benchmarks/filter_evaluate.js @@ -0,0 +1,50 @@ +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const VectorTile = require('@mapbox/vector-tile').VectorTile; +const Pbf = require('pbf'); +const createFilter = require('../../src/style-spec/feature_filter'); +const filters = require('../data/filters.json'); +const assert = require('assert'); + +module.exports = class FilterEvaluate extends Benchmark { + setup() { + return fetch('/bench/data/785.vector.pbf') + .then(response => response.arrayBuffer()) + .then(data => { + const tile = new VectorTile(new Pbf(data)); + + this.layers = []; + for (const name in tile.layers) { + const layer = tile.layers[name]; + if (!layer.length) continue; + + const features = []; + for (let j = 0; j < layer.length; j++) { + features.push(layer.feature(j)); + } + + const layerFilters = []; + for (const filter of filters) { + if (filter.layer === name) { + layerFilters.push(createFilter(filter.filter)); + } + } + + this.layers.push({ features, filters: layerFilters }); + } + }); + } + + bench() { + for (const layer of this.layers) { + for (const filter of layer.filters) { + for (const feature of layer.features) { + if (typeof filter(feature) !== 'boolean') { + assert(false, 'Expected boolean result from filter'); + } + } + } + } + } +}; diff --git a/bench/benchmarks/fps.js b/bench/benchmarks/fps.js deleted file mode 100644 index 08f16755f03..00000000000 --- a/bench/benchmarks/fps.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const Evented = require('../../src/util/evented'); -const formatNumber = require('../lib/format_number'); -const measureFramerate = require('../lib/measure_framerate'); -const createMap = require('../lib/create_map'); - -const DURATION_MILLISECONDS = 5 * 1000; - -module.exports = function() { - const evented = new Evented(); - - const map = createMap({ - width: 1024, - height: 768, - zoom: 5, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/bright-v9' - }); - - map.on('load', () => { - map.repaint = true; - - evented.fire('log', { - message: `starting ${formatNumber(DURATION_MILLISECONDS / 1000)} second test`, - color: 'dark' - }); - - measureFramerate(DURATION_MILLISECONDS, (err, fps) => { - map.remove(); - if (err) { - evented.fire('error', { error: err }); - } else { - evented.fire('end', { - message: `${formatNumber(fps)} fps`, - score: 1 / fps - }); - } - }); - }); - - return evented; -}; diff --git a/bench/benchmarks/frame_duration.js b/bench/benchmarks/frame_duration.js deleted file mode 100644 index eea34b249df..00000000000 --- a/bench/benchmarks/frame_duration.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict'; - -const Evented = require('../../src/util/evented'); -const formatNumber = require('../lib/format_number'); -const createMap = require('../lib/create_map'); - -const DURATION_MILLISECONDS = 1 * 5000; - -const zooms = [4, 8, 11, 13, 15, 17]; -const results = []; - -module.exports = function(options) { - // The goal of this benchmark is to measure the time it takes to run the cpu - // part of rendering. While the gpu rendering happens asynchronously, sometimes - // when the gpu falls behind the cpu commands synchronously wait for the gpu to catch up. - // This ends up affecting the duration of the call on the cpu. - // - // Setting the devicePixelRatio to a small number makes the canvas very small. - // This greatly reduces the amount of work the gpu needs to do and reduces the - // impact the actual rendering has on this benchmark. - window.devicePixelRatio = 1 / 16; - - const evented = new Evented(); - - asyncSeries(zooms.length, runZoom, done); - - function runZoom(times, callback) { - const index = zooms.length - times; - - measureFrameTime(options, zooms[index], (err_, result) => { - results[index] = result; - evented.fire('log', { - message: `${formatNumber(result.sum / result.count * 10) / 10} ms, ${ - formatNumber(result.countAbove16 / result.count * 100)}% > 16 ms at zoom ${zooms[index]}` - }); - callback(); - }); - } - - function done() { - let sum = 0; - let count = 0; - let countAbove16 = 0; - const samples = []; - for (let i = 0; i < results.length; i++) { - const result = results[i]; - sum += result.sum; - count += result.count; - countAbove16 += result.countAbove16; - for (const value of result.samples) { - samples.push([zooms[i], value]); - } - } - evented.fire('end', { - message: `${formatNumber(sum / count * 10) / 10} ms, ${formatNumber(countAbove16 / count * 100)}% > 16ms`, - score: sum / count, - samples: [['zoom', 'frame']].concat(samples) - }); - } - - return evented; -}; - -function measureFrameTime(options, zoom, callback) { - - const map = createMap({ - width: 1024, - height: 768, - zoom: zoom, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/streets-v9' - }); - - map.on('load', () => { - - map.repaint = true; - - // adding a delay seems to make the results more consistent - window.setTimeout(() => { - let sum = 0; - let count = 0; - let countAbove16 = 0; - const samples = []; - const start = performance.now(); - - map._realrender = map._render; - map._render = function() { - map._styleDirty = true; - map._sourcesDirty = true; - - const frameStart = performance.now(); - map._realrender(); - const frameEnd = performance.now(); - const duration = frameEnd - frameStart; - - samples.push(duration); - sum += duration; - count++; - if (duration >= 16) countAbove16++; - - if (frameEnd - start > DURATION_MILLISECONDS) { - map.repaint = false; - map.remove(); - map.getContainer().remove(); - callback(undefined, { - sum: sum, - count: count, - countAbove16: countAbove16, - samples: samples - }); - } - }; - }, 100); - }); -} - -function asyncSeries(times, work, callback) { - if (times > 0) { - work(times, (err) => { - if (err) callback(err); - else asyncSeries(times - 1, work, callback); - }); - } else { - callback(); - } -} diff --git a/bench/benchmarks/geojson_setdata_large.js b/bench/benchmarks/geojson_setdata_large.js deleted file mode 100644 index 10f1df25541..00000000000 --- a/bench/benchmarks/geojson_setdata_large.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const Evented = require('../../src/util/evented'); -const formatNumber = require('../lib/format_number'); -const setDataPerf = require('../lib/set_data_perf'); -const setupGeoJSONMap = require('../lib/setup_geojson_map'); -const createMap = require('../lib/create_map'); -const ajax = require('../../src/util/ajax'); - -module.exports = function() { - const evented = new Evented(); - - setTimeout(() => { - evented.fire('log', {message: 'downloading large geojson'}); - }, 0); - - ajax.getJSON({ url: 'http://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_land.geojson' }, (err, data) => { - evented.fire('log', {message: 'starting test'}); - - if (err) return evented.fire('error', {error: err}); - - let map = createMap({ - width: 1024, - height: 768, - zoom: 5, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/bright-v9' - }); - - map.on('load', () => { - map = setupGeoJSONMap(map); - - setDataPerf(map.style.sourceCaches.geojson, data, (err, ms) => { - if (err) return evented.fire('error', {error: err}); - map.remove(); - evented.fire('end', {message: `${formatNumber(ms)} ms`, score: ms}); - }); - }); - }); - - return evented; -}; diff --git a/bench/benchmarks/geojson_setdata_small.js b/bench/benchmarks/geojson_setdata_small.js deleted file mode 100644 index 1e65065b584..00000000000 --- a/bench/benchmarks/geojson_setdata_small.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -const Evented = require('../../src/util/evented'); -const formatNumber = require('../lib/format_number'); -const setDataPerf = require('../lib/set_data_perf'); -const setupGeoJSONMap = require('../lib/setup_geojson_map'); -const createMap = require('../lib/create_map'); - -const featureCollection = { - 'type': 'FeatureCollection', - 'features': [{ - 'type': 'Feature', - 'properties': {}, - 'geometry': { - 'type': 'Point', - 'coordinates': [ -77.032194, 38.912753 ] - } - }] -}; - -module.exports = function() { - const evented = new Evented(); - - let map = createMap({ - width: 1024, - height: 768, - zoom: 5, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/bright-v9' - }); - - map.on('load', () => { - map = setupGeoJSONMap(map); - - setDataPerf(map.style.sourceCaches.geojson, featureCollection, (err, ms) => { - map.remove(); - if (err) return evented.fire('error', {error: err}); - evented.fire('end', {message: `${formatNumber(ms)} ms`, score: ms}); - }); - }); - - return evented; -}; diff --git a/bench/benchmarks/layout.js b/bench/benchmarks/layout.js new file mode 100644 index 00000000000..2763bd64886 --- /dev/null +++ b/bench/benchmarks/layout.js @@ -0,0 +1,138 @@ +// @flow + +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const createStyle = require('../lib/create_style'); + +const VT = require('@mapbox/vector-tile'); +const Protobuf = require('pbf'); +const assert = require('assert'); +const promisify = require('pify'); + +const WorkerTile = require('../../src/source/worker_tile'); +const StyleLayerIndex = require('../../src/style/style_layer_index'); +const deref = require('../../src/style-spec/deref'); +const TileCoord = require('../../src/source/tile_coord'); +const { + normalizeStyleURL, + normalizeSourceURL, + normalizeTileURL +} = require('../../src/util/mapbox'); + +// Note: this class is extended in turn by the LayoutDDS benchmark. +module.exports = class Layout extends Benchmark { + glyphs: Object; + icons: Object; + workerTile: WorkerTile; + layerIndex: StyleLayerIndex; + tiles: Array<{coord: TileCoord, buffer: ArrayBuffer}>; + + tileCoords(): Array { + return [ + new TileCoord(12, 655, 1583), + new TileCoord(8, 40, 98), + new TileCoord(4, 3, 6), + new TileCoord(0, 0, 0) + ]; + } + + sourceID(): string { + return 'composite'; + } + + fetchStyle(): Promise { + return fetch(normalizeStyleURL(`mapbox://styles/mapbox/streets-v9`)) + .then(response => response.json()); + } + + fetchTiles(styleJSON: StyleSpecification): Promise> { + const sourceURL: string = (styleJSON.sources[this.sourceID()]: any).url; + return fetch(normalizeSourceURL(sourceURL)) + .then(response => response.json()) + .then((tileJSON: TileJSON) => { + return Promise.all(this.tileCoords().map(coord => { + return fetch((normalizeTileURL(coord.url(tileJSON.tiles)))) + .then(response => response.arrayBuffer()) + .then(buffer => ({coord, buffer})); + })); + }); + } + + setup(): Promise { + return this.fetchStyle() + .then((styleJSON) => { + this.layerIndex = new StyleLayerIndex(deref(styleJSON.layers)); + return Promise.all([createStyle(styleJSON), this.fetchTiles(styleJSON)]); + }) + .then(([style, tiles]) => { + this.tiles = tiles; + this.glyphs = {}; + this.icons = {}; + + const preloadGlyphs = (params, callback) => { + style.getGlyphs('', params, (err, glyphs) => { + this.glyphs[JSON.stringify(params)] = glyphs; + callback(err, glyphs); + }); + }; + + const preloadImages = (params, callback) => { + style.getImages('', params, (err, icons) => { + this.icons[JSON.stringify(params)] = icons; + callback(err, icons); + }); + }; + + return this.bench(preloadGlyphs, preloadImages); + }); + } + + bench(getGlyphs: Function = (params, callback) => callback(null, this.glyphs[JSON.stringify(params)]), + getImages: Function = (params, callback) => callback(null, this.icons[JSON.stringify(params)])) { + + const actor = { + send(action, params, callback) { + setTimeout(() => { + if (action === 'getImages') { + getImages(params, callback); + } else if (action === 'getGlyphs') { + getGlyphs(params, callback); + } else assert(false); + }, 0); + } + }; + + let promise: Promise = Promise.resolve(); + + for (const {coord, buffer} of this.tiles) { + promise = promise.then(() => { + const workerTile = new WorkerTile({ + coord, + zoom: coord.z, + tileSize: 512, + overscaling: 1, + showCollisionBoxes: false, + source: this.sourceID(), + uid: '0', + maxZoom: 22, + pixelRatio: 1, + request: { + url: '' + }, + angle: 0, + pitch: 0, + cameraToCenterDistance: 0, + cameraToTileDistance: 0 + }); + + const tile = new VT.VectorTile(new Protobuf(buffer)); + const parse = promisify(workerTile.parse.bind(workerTile)); + + return parse(tile, this.layerIndex, actor); + }); + } + + return promise; + } +}; diff --git a/bench/benchmarks/layout_dds.js b/bench/benchmarks/layout_dds.js new file mode 100644 index 00000000000..bd87e57bb1e --- /dev/null +++ b/bench/benchmarks/layout_dds.js @@ -0,0 +1,97 @@ +// @flow + +'use strict'; + +const Layout = require('./layout'); +const TileCoord = require('../../src/source/tile_coord'); + +const LAYER_COUNT = 2; + +module.exports = class LayoutDDS extends Layout { + tileCoords(): Array { + return [ + new TileCoord(15, 9373, 12535) + ]; + } + + sourceID(): string { + return 'mapbox'; + } + + fetchStyle(): Promise { + const style = { + "version": 8, + "sources": { + "mapbox": { "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v7" } + }, + "layers": [] + }; + + const layers = [ + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-width": 3, + "line-color":{ + "type": "categorical", + "property": "class", + "stops":[ + [{"zoom": 0, "value": "motorway"}, "#0000FF"], + [{"zoom": 0, "value": "trunk"}, "#000FF0"], + [{"zoom": 0, "value": "primary"}, "#00FF00"], + [{"zoom": 0, "value": "secondary"}, "#0FF000"], + [{"zoom": 0, "value": "street"}, "#FF0000"], + [{"zoom": 17, "value": "motorway"}, "#000088"], + [{"zoom": 17, "value": "trunk"}, "#000880"], + [{"zoom": 17, "value": "primary"}, "#008800"], + [{"zoom": 17, "value": "secondary"}, "#088000"], + [{"zoom": 17, "value": "street"}, "#880000"] + ], + "default": "#444444" + } + } + }, + { + "id": "poi", + "type": "circle", + "source": "mapbox", + "source-layer": "poi_label", + "paint": { + "circle-radius": { + "base": 2, + "property": "scalerank", + "stops":[ + [{"zoom": 0, "value": 0}, 1], + [{"zoom": 0, "value": 10}, 5], + [{"zoom": 17, "value": 0}, 20], + [{"zoom": 17, "value": 10}, 50] + ] + }, + "circle-color": { + "base": 1.25, + "property": "localrank", + "stops":[ + [{"zoom": 0, "value": 0}, "#002222"], + [{"zoom": 0, "value": 10}, "#220022"], + [{"zoom": 17, "value": 0}, "#008888"], + [{"zoom": 17, "value": 10}, "#880088"] + ] + } + } + } + ]; + + while (style.layers.length < LAYER_COUNT) { + for (const layer of layers) { + style.layers.push(Object.assign(({}: any), layer, { + id: layer.id + style.layers.length + })); + } + } + + return Promise.resolve(style); + } +}; diff --git a/bench/benchmarks/map_load.js b/bench/benchmarks/map_load.js index 82870cdf01e..690c4b3ef06 100644 --- a/bench/benchmarks/map_load.js +++ b/bench/benchmarks/map_load.js @@ -1,49 +1,16 @@ 'use strict'; -const Evented = require('../../src/util/evented'); -const formatNumber = require('../lib/format_number'); +const Benchmark = require('../lib/benchmark'); const createMap = require('../lib/create_map'); -module.exports = function() { - const evented = new Evented(); - - const mapsOnPage = 6; - - evented.fire('log', { message: `Creating ${mapsOnPage} maps` }); - - let loaded = 0; - const maps = []; - const start = Date.now(); - for (let i = 0; i < mapsOnPage; i++) { - const map = maps[i] = createMap({ +module.exports = class MapLoad extends Benchmark { + bench() { + return createMap({ style: { version: 8, sources: {}, layers: [] } - }); - map.on('load', onload.bind(null, map)); - map.on('error', (err) => { - evented.fire('error', err); - }); + }).then(map => map.remove()); } - - function onload () { - if (++loaded >= mapsOnPage) { - const duration = Date.now() - start; - for (let i = 0; i < maps.length; i++) { - maps[i].remove(); - } - evented.fire('end', { - message: `${formatNumber(duration)} ms`, - score: duration - }); - done(); - } - } - - function done () { - } - - return evented; }; diff --git a/bench/benchmarks/paint.js b/bench/benchmarks/paint.js new file mode 100644 index 00000000000..cc3e35c0772 --- /dev/null +++ b/bench/benchmarks/paint.js @@ -0,0 +1,36 @@ +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const createMap = require('../lib/create_map'); + +const width = 1024; +const height = 768; +const zooms = [4, 8, 11, 13, 15, 17]; + +module.exports = class Paint extends Benchmark { + setup() { + return Promise.all(zooms.map(zoom => { + return createMap({ + zoom, + width, + height, + center: [-77.032194, 38.912753], + style: 'mapbox://styles/mapbox/streets-v9' + }); + })).then(maps => { this.maps = maps; }); + } + + bench() { + for (const map of this.maps) { + map._styleDirty = true; + map._sourcesDirty = true; + map._render(); + } + } + + teardown() { + for (const map of this.maps) { + map.remove(); + } + } +}; diff --git a/bench/benchmarks/query_box.js b/bench/benchmarks/query_box.js index 83f651d901e..8e8e480e4b5 100644 --- a/bench/benchmarks/query_box.js +++ b/bench/benchmarks/query_box.js @@ -1,83 +1,34 @@ 'use strict'; -const Evented = require('../../src/util/evented'); +const Benchmark = require('../lib/benchmark'); const createMap = require('../lib/create_map'); const width = 1024; const height = 768; - -const numSamples = 10; - -const zoomLevels = []; -for (let i = 4; i < 19; i++) { - zoomLevels.push(i); -} - -module.exports = function() { - const evented = new Evented(); - - let sum = 0; - let count = 0; - - asyncSeries(zoomLevels.length, (n, callback) => { - const zoomLevel = zoomLevels[zoomLevels.length - n]; - const map = createMap({ - width: width, - height: height, - zoom: zoomLevel, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/streets-v9' - }); - map.getContainer().style.display = 'none'; - - map.on('load', () => { - - let zoomSum = 0; - let zoomCount = 0; - asyncSeries(numSamples, (n, callback) => { - const start = performance.now(); - map.queryRenderedFeatures({}); - const duration = performance.now() - start; - sum += duration; - count++; - zoomSum += duration; - zoomCount++; - callback(); - }, () => { - evented.fire('log', { - message: `${(zoomSum / zoomCount).toFixed(2)} ms at zoom ${zoomLevel}` - }); - map.remove(); - callback(); +const zooms = [4, 8, 11, 13, 15, 17]; + +module.exports = class QueryBox extends Benchmark { + setup() { + return Promise.all(zooms.map(zoom => { + return createMap({ + zoom, + width, + height, + center: [-77.032194, 38.912753], + style: 'mapbox://styles/mapbox/streets-v9' }); - }); - }, done); - - - function done() { - const average = sum / count; - evented.fire('end', { - message: `${(average).toFixed(2)} ms`, - score: average - }); + })).then(maps => { this.maps = maps; }); } - setTimeout(() => { - evented.fire('log', { - message: 'loading assets', - color: 'dark' - }); - }, 0); - return evented; -}; + bench() { + for (const map of this.maps) { + map.queryRenderedFeatures({}); + } + } -function asyncSeries(times, work, callback) { - if (times > 0) { - work(times, (err) => { - if (err) callback(err); - else asyncSeries(times - 1, work, callback); - }); - } else { - callback(); + teardown() { + for (const map of this.maps) { + map.remove(); + } } -} +}; diff --git a/bench/benchmarks/query_point.js b/bench/benchmarks/query_point.js index e2284889357..82fc00ea618 100644 --- a/bench/benchmarks/query_point.js +++ b/bench/benchmarks/query_point.js @@ -1,93 +1,47 @@ 'use strict'; -const Evented = require('../../src/util/evented'); +const Benchmark = require('../lib/benchmark'); const createMap = require('../lib/create_map'); const width = 1024; const height = 768; +const zooms = [4, 8, 11, 13, 15, 17]; -const zoomLevels = []; -for (let i = 4; i < 19; i++) { - zoomLevels.push(i); -} - -const queryPoints = []; -const d = 20; +const points = []; +const d = 4; for (let x = 0; x < d; x++) { for (let y = 0; y < d; y++) { - queryPoints.push([ + points.push([ (x / d) * width, (y / d) * height ]); } } -module.exports = function() { - const evented = new Evented(); - - let sum = 0; - let count = 0; - - asyncSeries(zoomLevels.length, (n, callback) => { - const zoomLevel = zoomLevels[zoomLevels.length - n]; - const map = createMap({ - width: width, - height: height, - zoom: zoomLevel, - center: [-77.032194, 38.912753], - style: 'mapbox://styles/mapbox/streets-v9' - }); - map.getContainer().style.display = 'none'; - - map.on('load', () => { - - let zoomSum = 0; - let zoomCount = 0; - asyncSeries(queryPoints.length, (n, callback) => { - const queryPoint = queryPoints[queryPoints.length - n]; - const start = performance.now(); - map.queryRenderedFeatures(queryPoint, {}); - const duration = performance.now() - start; - sum += duration; - count++; - zoomSum += duration; - zoomCount++; - callback(); - }, () => { - map.remove(); - evented.fire('log', { - message: `${(zoomSum / zoomCount).toFixed(2)} ms at zoom ${zoomLevel}` - }); - callback(); +module.exports = class QueryPoint extends Benchmark { + setup() { + return Promise.all(zooms.map(zoom => { + return createMap({ + zoom, + width, + height, + center: [-77.032194, 38.912753], + style: 'mapbox://styles/mapbox/streets-v9' }); - }); - }, done); - - - function done() { - const average = sum / count; - evented.fire('end', { - message: `${(average).toFixed(2)} ms`, - score: average - }); + })).then(maps => { this.maps = maps; }); } - setTimeout(() => { - evented.fire('log', { - message: 'loading assets', - color: 'dark' - }); - }, 0); - return evented; -}; + bench() { + for (const map of this.maps) { + for (const point of points) { + map.queryRenderedFeatures(point, {}); + } + } + } -function asyncSeries(times, work, callback) { - if (times > 0) { - work(times, (err) => { - if (err) callback(err); - else asyncSeries(times - 1, work, callback); - }); - } else { - callback(); + teardown() { + for (const map of this.maps) { + map.remove(); + } } -} +}; diff --git a/bench/benchmarks/style_layer_create.js b/bench/benchmarks/style_layer_create.js new file mode 100644 index 00000000000..e9d63695c6f --- /dev/null +++ b/bench/benchmarks/style_layer_create.js @@ -0,0 +1,20 @@ +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const accessToken = require('../lib/access_token'); +const StyleLayer = require('../../src/style/style_layer'); +const deref = require('../../src/style-spec/deref'); + +module.exports = class StyleLayerCreate extends Benchmark { + setup() { + return fetch(`https://api.mapbox.com/styles/v1/mapbox/streets-v9?access_token=${accessToken}`) + .then(response => response.json()) + .then(json => { this.json = json; }); + } + + bench() { + for (const layer of deref(this.json.layers)) { + StyleLayer.create(layer); + } + } +}; diff --git a/bench/benchmarks/style_load.js b/bench/benchmarks/style_load.js deleted file mode 100644 index f08e527ca73..00000000000 --- a/bench/benchmarks/style_load.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -const Evented = require('../../src/util/evented'); -const ajax = require('../../src/util/ajax'); -const config = require('../../src/util/config'); -const Style = require('../../src/style/style'); -const formatNumber = require('../lib/format_number'); -const accessToken = require('../lib/access_token'); - -module.exports = function() { - config.ACCESS_TOKEN = accessToken; - - const evented = new Evented(); - - class StubMap extends Evented { - _transformRequest(url) { - return { url }; - } - } - const stylesheetURL = `https://api.mapbox.com/styles/v1/mapbox/streets-v9?access_token=${accessToken}`; - ajax.getJSON({ url: stylesheetURL }, (err, json) => { - if (err) { - return evented.fire('error', {error: err}); - } - - let timeSum = 0; - let timeCount = 0; - - asyncTimesSeries(20, (callback) => { - const timeStart = performance.now(); - new Style(json, new StubMap()) - .on('error', (err) => { - evented.fire('error', { error: err }); - }) - .on('style.load', () => { - const time = performance.now() - timeStart; - timeSum += time; - timeCount++; - callback(); - }); - }, () => { - const timeAverage = timeSum / timeCount; - evented.fire('end', { - message: `${formatNumber(timeAverage)} ms`, - score: timeAverage - }); - }); - }); - - return evented; -}; - -function asyncTimesSeries(times, work, callback) { - if (times > 0) { - work((err) => { - if (err) callback(err); - else asyncTimesSeries(times - 1, work, callback); - }); - } else { - callback(); - } -} diff --git a/bench/benchmarks/style_validate.js b/bench/benchmarks/style_validate.js new file mode 100644 index 00000000000..3f1e42c20f3 --- /dev/null +++ b/bench/benchmarks/style_validate.js @@ -0,0 +1,17 @@ +'use strict'; + +const Benchmark = require('../lib/benchmark'); +const accessToken = require('../lib/access_token'); +const validateStyle = require('../../src/style-spec/validate_style.min'); + +module.exports = class StyleValidate extends Benchmark { + setup() { + return fetch(`https://api.mapbox.com/styles/v1/mapbox/streets-v9?access_token=${accessToken}`) + .then(response => response.json()) + .then(json => { this.json = json; }); + } + + bench() { + validateStyle(this.json); + } +}; diff --git a/bench/benchmarks/tile_layout_dds.js b/bench/benchmarks/tile_layout_dds.js deleted file mode 100644 index 1377b1e6f7d..00000000000 --- a/bench/benchmarks/tile_layout_dds.js +++ /dev/null @@ -1,278 +0,0 @@ -'use strict'; - -const VT = require('@mapbox/vector-tile'); -const Protobuf = require('pbf'); -const assert = require('assert'); - -const WorkerTile = require('../../src/source/worker_tile'); -const Coordinate = require('../../src/geo/coordinate'); -const ajax = require('../../src/util/ajax'); -const Style = require('../../src/style/style'); -const StyleLayerIndex = require('../../src/style/style_layer_index'); -const util = require('../../src/util/util'); -const Evented = require('../../src/util/evented'); -const config = require('../../src/util/config'); -const formatNumber = require('../lib/format_number'); -const accessToken = require('../lib/access_token'); -const deref = require('../../src/style-spec/deref'); - -const SAMPLE_COUNT = 10; -const TILE_COUNT = 25; -const LAYER_COUNT = 30; - -const coordinates = []; -const xRange = [9370, 9375]; -const yRange = [12532, 12537]; -while (coordinates.length < TILE_COUNT) { - for (let x = xRange[0]; x < xRange[1]; x++) { - for (let y = yRange[0]; y < yRange[1]; y++) { - coordinates.push(new Coordinate(x, y, 15)); - } - } -} - -const stylesheet = { - "version": 8, - "sources": { - "mapbox": { "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v7" } - }, - "layers": [] -}; - -const layers = [ - { - "id": "road", - "type": "line", - "source": "mapbox", - "source-layer": "road", - "paint": { - "line-width": 3, - "line-color":{ - "type": "categorical", - "property": "class", - "stops":[ - [{"zoom": 0, "value": "motorway"}, "#0000FF"], - [{"zoom": 0, "value": "trunk"}, "#000FF0"], - [{"zoom": 0, "value": "primary"}, "#00FF00"], - [{"zoom": 0, "value": "secondary"}, "#0FF000"], - [{"zoom": 0, "value": "street"}, "#FF0000"], - [{"zoom": 17, "value": "motorway"}, "#000088"], - [{"zoom": 17, "value": "trunk"}, "#000880"], - [{"zoom": 17, "value": "primary"}, "#008800"], - [{"zoom": 17, "value": "secondary"}, "#088000"], - [{"zoom": 17, "value": "street"}, "#880000"] - ], - "default": "#444444" - } - } - }, - { - "id": "poi", - "type": "circle", - "source": "mapbox", - "source-layer": "poi_label", - "paint": { - "circle-radius": { - "base": 2, - "property": "scalerank", - "stops":[ - [{"zoom": 0, "value": 0}, 1], - [{"zoom": 0, "value": 10}, 5], - [{"zoom": 17, "value": 0}, 20], - [{"zoom": 17, "value": 10}, 50] - ] - }, - "circle-color": { - "base": 1.25, - "property": "localrank", - "stops":[ - [{"zoom": 0, "value": 0}, "#002222"], - [{"zoom": 0, "value": 10}, "#220022"], - [{"zoom": 17, "value": 0}, "#008888"], - [{"zoom": 17, "value": 10}, "#880088"] - ] - } - } - } -]; - -while (stylesheet.layers.length < LAYER_COUNT) { - for (const layer of layers) { - stylesheet.layers.push(Object.assign({}, layer, { - id: layer.id + stylesheet.layers.length - })); - } -} - -module.exports = function run() { - config.ACCESS_TOKEN = accessToken; - - const evented = new Evented(); - - setTimeout(() => { - evented.fire('log', { - message: 'preloading assets', - color: 'dark' - }); - - preloadAssets(stylesheet, (err, assets) => { - if (err) return evented.fire('error', {error: err}); - - evented.fire('log', { - message: 'starting first test', - color: 'dark' - }); - - function getGlyphs(params, callback) { - callback(null, assets.glyphs[JSON.stringify(params)]); - } - - function getIcons(params, callback) { - callback(null, assets.icons[JSON.stringify(params)]); - } - - function getTile(url, callback) { - callback(null, assets.tiles[url]); - } - - let timeSum = 0; - let timeCount = 0; - let samples = [['run', 'iteration', 'elapsed']]; - - asyncTimesSeries(SAMPLE_COUNT, (callback) => { - runSample(stylesheet, getGlyphs, getIcons, getTile, (err, result) => { - if (err) return evented.fire('error', { error: err }); - timeSum += result.time; - timeCount++; - samples = samples.concat(result.samples - .map((t, i) => [timeCount, i, t])); - evented.fire('log', { message: `${formatNumber(result.time)} ms` }); - callback(); - }); - }, (err) => { - if (err) { - evented.fire('error', { error: err }); - - } else { - const timeAverage = timeSum / timeCount; - evented.fire('end', { - message: `${formatNumber(timeAverage)} ms`, - score: timeAverage, - samples - }); - } - }); - }); - - }, 0); - - return evented; -}; - -class StubMap extends Evented { - _transformRequest(url) { - return { url }; - } -} - -function preloadAssets(stylesheet, callback) { - const assets = { - glyphs: {}, - icons: {}, - tiles: {} - }; - - const style = new Style(stylesheet, new StubMap()); - - style.on('style.load', () => { - function getGlyphs(params, callback) { - style.getGlyphs(0, params, (err, glyphs) => { - assets.glyphs[JSON.stringify(params)] = glyphs; - callback(err, glyphs); - }); - } - - function getIcons(params, callback) { - style.getIcons(0, params, (err, icons) => { - assets.icons[JSON.stringify(params)] = icons; - callback(err, icons); - }); - } - - function getTile(url, callback) { - ajax.getArrayBuffer({ url }, (err, response) => { - assets.tiles[url] = response.data; - callback(err, response.data); - }); - } - - runSample(stylesheet, getGlyphs, getIcons, getTile, (err) => { - style._remove(); - callback(err, assets); - }); - }); - - style.on('error', (event) => { - callback(event.error); - }); - -} - -function runSample(stylesheet, getGlyphs, getIcons, getTile, callback) { - const layerIndex = new StyleLayerIndex(deref(stylesheet.layers)); - - const timeStart = performance.now(); - const samples = []; - - util.asyncAll(coordinates, (coordinate, eachCallback) => { - const url = `https://a.tiles.mapbox.com/v4/mapbox.mapbox-streets-v7/${coordinate.zoom}/${coordinate.column}/${coordinate.row}.vector.pbf?access_token=${config.ACCESS_TOKEN}`; - - const workerTile = new WorkerTile({ - coord: coordinate, - zoom: coordinate.zoom, - tileSize: 512, - overscaling: 1, - angle: 0, - pitch: 0, - showCollisionBoxes: false, - source: 'mapbox', - uid: url - }); - - const actor = { - send: function(action, params, sendCallback) { - setTimeout(() => { - if (action === 'getIcons') { - getIcons(params, sendCallback); - } else if (action === 'getGlyphs') { - getGlyphs(params, sendCallback); - } else assert(false); - }, 0); - } - }; - - getTile(url, (err, response) => { - if (err) throw err; - const data = new VT.VectorTile(new Protobuf(response)); - workerTile.parse(data, layerIndex, actor, (err) => { - if (err) return callback(err); - samples.push(performance.now() - timeStart); - eachCallback(); - }); - }); - }, (err) => { - const timeEnd = performance.now(); - callback(err, { time: timeEnd - timeStart, samples }); - }); -} - -function asyncTimesSeries(times, work, callback) { - if (times > 0) { - setTimeout(work, 100, (err) => { - if (err) callback(err); - else asyncTimesSeries(times - 1, work, callback); - }); - } else { - callback(); - } -} diff --git a/bench/benchmarks_view.js b/bench/benchmarks_view.js index d51cb0c242a..b6480f43db8 100644 --- a/bench/benchmarks_view.js +++ b/bench/benchmarks_view.js @@ -1,233 +1,405 @@ 'use strict'; -/*eslint no-unused-vars: ["error", { "varsIgnorePattern": "BenchmarksView|clipboard" }]*/ - -const Clipboard = require('clipboard'); - -// Benchmark results seem to be more consistent with a warmup and cooldown -// period. These values are measured in milliseconds. -const benchmarkCooldownTime = 250; -const benchmarkWarmupTime = 250; - -const BenchmarksView = React.createClass({ - - render: function() { - return
-

- Benchmarks - - Copy Results - -

- - - - - {this.versions().map((v) => )} - - - - {Object.keys(this.state.results).map(this.renderBenchmark)} - -
Benchmark{v}
-
; - }, - - renderBenchmark: function(name) { - return - {name} - {Object.keys(this.state.results[name]).map(this.renderBenchmarkVersion.bind(this, name))} - ; - }, - - renderBenchmarkVersion: function(name, version) { - const results = this.state.results[name][version]; - const sampleData = results.samples ? results.samples.map(row => row.join(',')).join('\n') : null; - return ( - - {results.logs.map((log, index) => { - return
{log.message}
; - })} - {sampleData ? ( -
- - Sample Data - Copy - -
{sampleData}
-
- ) : ''} - - ); - }, - - versions: function() { - const versions = []; - for (const name in this.state.results) { - for (const version in this.state.results[name]) { - if (versions.indexOf(version) < 0) { - versions.push(version); - } - } - } - return versions; - }, - - renderTextBenchmarks: function() { - const versions = this.versions(); - let output = `benchmark | ${versions.join(' | ')}\n---`; - for (let i = 0; i < versions.length; i++) { - output += ' | ---'; - } - output += '\n'; - - for (const name in this.state.results) { - output += `**${name}**`; - for (const version of versions) { - const result = this.state.results[name][version]; - output += ` | ${result && result.message || 'n\/a'} `; - } - output += '\n'; - } - return output; - }, - - getInitialState: function() { - const results = {}; - - for (const name in this.props.benchmarks) { - for (const version in this.props.benchmarks[name]) { - if (!this.props.benchmarkFilter || this.props.benchmarkFilter(name, version)) { - results[name] = results[name] || {}; - results[name][version] = { - status: 'waiting', - logs: [] - }; - } - } - } - return { results: results }; - }, +/* global d3 */ - componentDidMount: function() { - const that = this; +const versionColor = d3.scaleOrdinal(d3.schemeCategory10); +versionColor(0); // Skip blue -- too similar to link color. - asyncSeries(Object.keys(that.state.results), (name, callback) => { - asyncSeries(Object.keys(that.state.results[name]), (version, callback) => { - that.runBenchmark(name, version, callback); - }, callback); - }, (err) => { - if (err) throw err; - }); - }, - - runBenchmark: function(name, version, outerCallback) { - const that = this; - const results = this.state.results[name][version]; - - function log(color, message) { - results.logs.push({ - color: color || 'blue', - message: message - }); - that.forceUpdate(); +const formatSample = d3.format(".3r"); +const Axis = require('./lib/axis'); + +class StatisticsPlot extends React.Component { + constructor(props) { + super(props); + this.state = {width: 100}; + } + + render() { + function kernelDensityEstimator(kernel, ticks) { + return function(samples) { + if (samples.length === 0) { + return []; + } + // https://en.wikipedia.org/wiki/Kernel_density_estimation#A_rule-of-thumb_bandwidth_estimator + const bandwidth = 1.06 * d3.deviation(samples) * Math.pow(samples.length, -0.2); + return ticks.map((x) => { + return [x, d3.mean(samples, (v) => kernel((x - v) / bandwidth)) / bandwidth]; + }); + }; } - function callback() { - setTimeout(outerCallback, benchmarkCooldownTime); + function kernelEpanechnikov(v) { + return Math.abs(v) <= 1 ? 0.75 * (1 - v * v) : 0; } - results.status = 'running'; - log('dark', 'starting'); + const margin = {top: 0, right: 20, bottom: 20, left: 0}; + const width = this.state.width - margin.left - margin.right; + const height = 400 - margin.top - margin.bottom; + const kdeWidth = 100; + + const t = d3.scaleLinear() + .domain([ + d3.min(this.props.versions.map(v => d3.min(v.samples))), + d3.max(this.props.versions.map(v => d3.max(v.samples))) + ]) + .range([height, 0]) + .nice(); + + const b = d3.scaleBand() + .domain(this.props.versions.map(v => v.name)) + .range([kdeWidth + 20, width]) + .paddingOuter(0.15) + .paddingInner(0.3); + + const kde = kernelDensityEstimator(kernelEpanechnikov, t.ticks(50)); + const versions = this.props.versions.map(v => ({ + name: v.name, + samples: v.samples, + density: kde(v.samples) + })); + + const p = d3.scaleLinear() + .domain([0, d3.max(versions.map(v => d3.max(v.density, d => d[1])))]) + .range([0, kdeWidth]); + + const line = d3.line() + .curve(d3.curveBasis) + .y(d => t(d[0])) + .x(d => p(d[1])); + + return ( + { this.ref = ref; }}> + + + + + Time (ms) + + {versions.map((v, i) => { + if (v.samples.length === 0) + return null; + + const bandwidth = b.bandwidth(); + const color = versionColor(v.name); + const scale = d3.scaleLinear() + .domain([0, v.samples.length]) + .range([0, bandwidth]); + + const sorted = v.samples.slice().sort(d3.ascending); + const [q1, q2, q3] = [.25, .5, .75].map((d) => d3.quantile(sorted, d)); + const mean = d3.mean(sorted); + + let min = [NaN, Infinity]; + let max = [NaN, -Infinity]; + for (let i = 0; i < v.samples.length; i++) { + const s = v.samples[i]; + if (s < min[1]) min = [i, s]; + if (s > max[1]) max = [i, s]; + } + + return + + + {v.samples.map((d, i) => + + )} + + + + {[mean].map((d, i) => + {formatSample(d)} + )} + {[min, max].map((d, i) => + {formatSample(d[1])} + )} + {[q1, q2, q3].map((d, i) => + {formatSample(d)} + )} + + ; + })} + + + ); + } + + componentDidMount() { + this.setState({ width: this.ref.clientWidth }); + } +} + +function regression(samples) { + const result = []; + for (let i = 0, n = 1; i + n < samples.length; i += n, n++) { + result.push([n, samples.slice(i, i + n).reduce(((sum, sample) => sum + sample), 0)]); + } + return result; +} - setTimeout(() => { - const emitter = that.props.benchmarks[name][version](); +class RegressionPlot extends React.Component { + constructor(props) { + super(props); + this.state = {width: 100}; + } - emitter.on('log', (event) => { - log(event.color, event.message); + render() { + const margin = {top: 10, right: 20, bottom: 30, left: 0}; + const width = this.state.width - margin.left - margin.right; + const height = 200 - margin.top - margin.bottom; + const versions = this.props.versions.filter(version => version.regression); - }); + const x = d3.scaleLinear() + .domain([0, d3.max(versions.map(version => d3.max(version.regression.data, d => d[0])))]) + .range([0, width]) + .nice(); - emitter.on('end', (event) => { - results.message = event.message; - results.status = 'ended'; - results.samples = event.samples; - log('green', event.message); - callback(); + const y = d3.scaleLinear() + .domain([0, d3.max(versions.map(version => d3.max(version.regression.data, d => d[1])))]) + .range([height, 0]) + .nice(); - }); + const line = d3.line() + .x(d => x(d[0])) + .y(d => y(d[1])); - emitter.on('error', (event) => { - results.status = 'errored'; - log('red', event.error); - callback(); - }); + return ( + { this.ref = ref; }}> + + + Iterations + + + Time (ms) + + {versions.map((v, i) => + + {v.regression.data.map(([a, b], i) => + + )} + [ + d[0], + d[0] * v.regression.slope + v.regression.intercept + ]))} /> + + )} + + + ); + } - }, benchmarkWarmupTime); - }, + componentDidMount() { + this.setState({ width: this.ref.clientWidth }); + } +} - getBenchmarkVersionStatus: function(name, version) { - return this.state.results[name][version].status; - }, +class BenchmarkStatistic extends React.Component { + render() { + switch (this.props.status) { + case 'waiting': + return

; + case 'running': + return

Running...

; + case 'error': + return

{this.props.error.message}

; + default: + return

{this.props.statistic(this.props)}

; + } + } +} - getBenchmarkStatus: function(name) { - return reduceStatuses(Object.keys(this.state.results[name]).map(function(version) { - return this.getBenchmarkVersionStatus(name, version); - }, this)); - }, +class BenchmarkRow extends React.Component { + render() { + const ended = this.props.versions.find(version => version.status === 'ended'); + return ( +
+

{this.props.name}

+
+ + {this.props.versions.map(version => )} + {this.renderStatistic('R² Slope / Correlation', + (version) => `${formatSample(version.regression.slope)} ms / ${version.regression.correlation.toFixed(3)} ${ + version.regression.correlation < 0.9 ? '\u2620\uFE0F' : + version.regression.correlation < 0.99 ? '\u26A0\uFE0F' : ''}`)} + {this.renderStatistic('Mean', + (version) => `${formatSample(d3.mean(version.samples))} ms`)} + {this.renderStatistic('Minimum', + (version) => `${formatSample(d3.min(version.samples))} ms`)} + {this.renderStatistic('Deviation', + (version) => `${formatSample(d3.deviation(version.samples))} ms`)} +
{version.name}
+ {ended && } + {ended && } +
+
+ ); + } - getStatus() { - return reduceStatuses(Object.keys(this.state.results).map(function(name) { - return this.getBenchmarkStatus(name); - }, this)); - }, + renderStatistic(title, statistic) { + return ( + + {title} + {this.props.versions.map(version => + + )} + + ); + } reload() { location.reload(); } -}); +} -function reduceStatuses(statuses) { - if (statuses.indexOf('running') !== -1) { - return 'running'; - } else if (statuses.indexOf('waiting') !== -1) { - return 'waiting'; - } else { - return 'ended'; +class BenchmarksTable extends React.Component { + render() { + return ( +
+

Mapbox GL JS Benchmarks – {this.props.finished ? 'Finished' : 'Running'}

+ {this.props.benchmarks.map(benchmark => )} +
+ ); } } -const clipboard = new Clipboard('.clipboard'); - -ReactDOM.render( - , - document.getElementById('benchmarks') -); - -function asyncSeries(array, iterator, callback) { - if (array.length) { - iterator(array[0], (err) => { - if (err) callback(err); - else asyncSeries(array.slice(1), iterator, callback); +const versions = window.mapboxglVersions; +const benchmarks = []; +const filter = window.location.hash.substr(1); + +let finished = false; +let promise = Promise.resolve(); + +for (const name in window.mapboxglBenchmarks) { + if (filter && name !== filter) + continue; + + const benchmark = { name, versions: [] }; + benchmarks.push(benchmark); + + for (const ver in window.mapboxglBenchmarks[name]) { + const version = { + name: ver, + status: 'waiting', + logs: [], + samples: [] + }; + + benchmark.versions.push(version); + + promise = promise.then(() => { + version.status = 'running'; + update(); + + return window.mapboxglBenchmarks[name][ver].run() + .then(samples => { + version.status = 'ended'; + version.samples = samples; + version.regression = leastSquaresRegression(regression(samples)); + update(); + }) + .catch(error => { + version.status = 'errored'; + version.error = error; + update(); + }); }); - } else { - callback(); } } + +promise = promise.then(() => { + finished = true; + update(); +}); + +function update() { + ReactDOM.render( + , + document.getElementById('benchmarks') + ); +} + +function leastSquaresRegression(data) { + const meanX = d3.sum(data, d => d[0]) / data.length; + const meanY = d3.sum(data, d => d[1]) / data.length; + const varianceX = d3.variance(data, d => d[0]); + const sdX = Math.sqrt(varianceX); + const sdY = d3.deviation(data, d => d[1]); + const covariance = d3.sum(data, ([x, y]) => + (x - meanX) * (y - meanY) + ) / (data.length - 1); + + const correlation = covariance / sdX / sdY; + const slope = covariance / varianceX; + const intercept = meanY - slope * meanX; + + return { correlation, slope, intercept, data }; +} diff --git a/bench/index.html b/bench/index.html index 526590f6af7..f061e8704b8 100644 --- a/bench/index.html +++ b/bench/index.html @@ -12,9 +12,10 @@
- - - + + + + diff --git a/bench/lib/axis.js b/bench/lib/axis.js new file mode 100644 index 00000000000..2c1bbffd9d8 --- /dev/null +++ b/bench/lib/axis.js @@ -0,0 +1,85 @@ +'use strict'; + +function identity(x) { + return x; +} + +function translateX(x) { + return `translate(${x + 0.5},0)`; +} + +function translateY(y) { + return `translate(0,${y + 0.5})`; +} + +function number(scale) { + return function(d) { + return +scale(d); + }; +} + +function center(scale) { + let offset = Math.max(0, scale.bandwidth() - 1) / 2; // Adjust for 0.5px offset. + if (scale.round()) offset = Math.round(offset); + return function(d) { + return +scale(d) + offset; + }; +} + +class Axis extends React.Component { + render() { + const scale = this.props.scale; + const orient = this.props.orientation || 'left'; + const tickArguments = this.props.ticks ? [].concat(this.props.ticks) : []; + const tickValues = this.props.tickValues || null; + const tickFormat = this.props.tickFormat || null; + const tickSizeInner = this.props.tickSize || this.props.tickSizeInner || 6; + const tickSizeOuter = this.props.tickSize || this.props.tickSizeOuter || 6; + const tickPadding = this.props.tickPadding || 3; + + const k = orient === 'top' || orient === 'left' ? -1 : 1; + const x = orient === 'left' || orient === 'right' ? 'x' : 'y'; + const transform = orient === 'top' || orient === 'bottom' ? translateX : translateY; + + const values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues; + const format = tickFormat == null ? (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : identity) : tickFormat; + const spacing = Math.max(tickSizeInner, 0) + tickPadding; + const range = scale.range(); + const range0 = +range[0] + 0.5; + const range1 = +range[range.length - 1] + 0.5; + const position = (scale.bandwidth ? center : number)(scale.copy()); + + return ( + + + {values.map((d, i) => + + + {format(d)} + + )} + {this.props.children} + + ); + } +} + +module.exports = Axis; diff --git a/bench/lib/benchmark.js b/bench/lib/benchmark.js new file mode 100644 index 00000000000..e5273e77167 --- /dev/null +++ b/bench/lib/benchmark.js @@ -0,0 +1,93 @@ +// @flow + +'use strict'; + +class Benchmark { + constructor() { + this._async = this._async.bind(this); + } + + /** + * The `setup` method is intended to be overridden by subclasses. It will be called once, prior to + * running any benchmark iterations, and may set state on `this` which the benchmark later accesses. + * If the setup involves an asynchronous step, `setup` may return a promise. + */ + setup(): Promise | void {} + + /** + * The `bench` method is intended to be overridden by subclasses. It should contain the code to be + * benchmarked. It may access state on `this` set by the `setup` function (but should not modify this + * state). It will be called multiple times, the total number to be determined by the harness. If + * the benchmark involves an asynchronous step, `bench` may return a promise. + */ + bench(): Promise | void {} + + /** + * The `teardown` method is intended to be overridden by subclasses. It will be called once, after + * running all benchmark iterations, and may perform any necessary cleanup. If cleaning up involves + * an asynchronous step, `teardown` may return a promise. + */ + teardown(): Promise | void {} + + _async: () => Promise>; + _elapsed: number; + _samples: Array; + _start: number; + + /** + * Run the benchmark by executing `setup` once, sampling the execution time of `bench` some number of + * times, and then executing `teardown`. Yields an array of execution times. + */ + run(): Promise> { + return Promise.resolve(this.setup()).then(() => this._begin()); + } + + _done() { + // 210 samples => 20 observations for regression + return this._elapsed >= 500 && this._samples.length > 210; + } + + _begin(): Promise> { + this._samples = []; + this._elapsed = 0; + this._start = performance.now(); + + const bench = this.bench(); + if (bench instanceof Promise) { + return bench.then(this._async); + } else { + return (this._sync(): any); + } + } + + _sync() { + // Avoid Promise overhead for sync benchmarks. + while (true) { + const sample = performance.now() - this._start; + this._samples.push(sample); + this._elapsed += sample; + if (this._done()) { + return this._end(); + } + this._start = performance.now(); + this.bench(); + } + } + + _async(): Promise> { + const sample = performance.now() - this._start; + this._samples.push(sample); + this._elapsed += sample; + if (this._done()) { + return this._end(); + } + this._start = performance.now(); + return ((this.bench(): any): Promise).then(this._async); + } + + _end(): Promise> { + return Promise.resolve(this.teardown()).then(() => this._samples); + } +} + +module.exports = Benchmark; diff --git a/bench/lib/coordinates.js b/bench/lib/coordinates.js deleted file mode 100644 index 5312e928b8a..00000000000 --- a/bench/lib/coordinates.js +++ /dev/null @@ -1,159 +0,0 @@ -'use strict'; - -// This file contains the set of coordinates of tiles used in a "flyTo" from -// the Mapbox SF office to the Mapbox DC office. - -const Coordinate = require('../../src/geo/coordinate'); - -module.exports = [ - new Coordinate(5242, 12665, 15), - new Coordinate(5242, 12666, 15), - new Coordinate(5242, 12664, 15), - new Coordinate(2620, 6332, 14), - new Coordinate(2620, 6333, 14), - new Coordinate(2621, 6332, 14), - new Coordinate(2621, 6333, 14), - new Coordinate(2620, 6331, 14), - new Coordinate(2621, 6331, 14), - new Coordinate(1309, 3166, 13), - new Coordinate(1309, 3167, 13), - new Coordinate(655, 1583, 12), - new Coordinate(655, 1582, 12), - new Coordinate(654, 1583, 12), - new Coordinate(654, 1582, 12), - new Coordinate(327, 790, 11), - new Coordinate(326, 791, 11), - new Coordinate(326, 790, 11), - new Coordinate(326, 792, 11), - new Coordinate(328, 791, 11), - new Coordinate(328, 790, 11), - new Coordinate(328, 792, 11), - new Coordinate(163, 395, 10), - new Coordinate(164, 395, 10), - new Coordinate(163, 396, 10), - new Coordinate(164, 396, 10), - new Coordinate(81, 197, 9), - new Coordinate(82, 197, 9), - new Coordinate(81, 198, 9), - new Coordinate(82, 198, 9), - new Coordinate(81, 196, 9), - new Coordinate(82, 196, 9), - new Coordinate(40, 98, 8), - new Coordinate(41, 98, 8), - new Coordinate(40, 99, 8), - new Coordinate(41, 99, 8), - new Coordinate(40, 97, 8), - new Coordinate(41, 97, 8), - new Coordinate(20, 49, 7), - new Coordinate(20, 48, 7), - new Coordinate(19, 49, 7), - new Coordinate(19, 48, 7), - new Coordinate(20, 50, 7), - new Coordinate(19, 50, 7), - new Coordinate(10, 24, 6), - new Coordinate(9, 24, 6), - new Coordinate(10, 25, 6), - new Coordinate(9, 25, 6), - new Coordinate(10, 23, 6), - new Coordinate(9, 23, 6), - new Coordinate(5, 12, 5), - new Coordinate(4, 12, 5), - new Coordinate(5, 11, 5), - new Coordinate(4, 11, 5), - new Coordinate(2, 6, 4), - new Coordinate(2, 5, 4), - new Coordinate(1, 5, 4), - new Coordinate(1, 6, 4), - new Coordinate(3, 5, 4), - new Coordinate(3, 6, 4), - new Coordinate(1, 2, 3), - new Coordinate(1, 3, 3), - new Coordinate(0, 2, 3), - new Coordinate(0, 3, 3), - new Coordinate(0, 1, 2), - new Coordinate(0, 0, 2), - new Coordinate(0, 2, 2), - new Coordinate(1, 1, 2), - new Coordinate(1, 0, 2), - new Coordinate(1, 2, 2), - new Coordinate(0, 0, 1), - new Coordinate(0, 1, 1), - new Coordinate(1, 0, 1), - new Coordinate(1, 1, 1), - new Coordinate(0, 0, 0), - new Coordinate(1, 1, 2), - new Coordinate(1, 2, 2), - new Coordinate(1, 0, 2), - new Coordinate(2, 3, 3), - new Coordinate(2, 2, 3), - new Coordinate(1, 2, 3), - new Coordinate(4, 6, 4), - new Coordinate(4, 5, 4), - new Coordinate(9, 12, 5), - new Coordinate(8, 12, 5), - new Coordinate(9, 11, 5), - new Coordinate(8, 11, 5), - new Coordinate(9, 13, 5), - new Coordinate(8, 13, 5), - new Coordinate(18, 24, 6), - new Coordinate(18, 23, 6), - new Coordinate(19, 24, 6), - new Coordinate(17, 24, 6), - new Coordinate(18, 25, 6), - new Coordinate(19, 23, 6), - new Coordinate(17, 23, 6), - new Coordinate(19, 25, 6), - new Coordinate(17, 25, 6), - new Coordinate(36, 48, 7), - new Coordinate(37, 48, 7), - new Coordinate(36, 49, 7), - new Coordinate(37, 49, 7), - new Coordinate(36, 47, 7), - new Coordinate(37, 47, 7), - new Coordinate(73, 97, 8), - new Coordinate(73, 98, 8), - new Coordinate(72, 97, 8), - new Coordinate(73, 96, 8), - new Coordinate(72, 98, 8), - new Coordinate(72, 96, 8), - new Coordinate(146, 195, 9), - new Coordinate(147, 195, 9), - new Coordinate(146, 196, 9), - new Coordinate(146, 194, 9), - new Coordinate(147, 196, 9), - new Coordinate(147, 194, 9), - new Coordinate(293, 391, 10), - new Coordinate(292, 391, 10), - new Coordinate(293, 390, 10), - new Coordinate(292, 390, 10), - new Coordinate(293, 392, 10), - new Coordinate(292, 392, 10), - new Coordinate(585, 783, 11), - new Coordinate(586, 783, 11), - new Coordinate(585, 782, 11), - new Coordinate(586, 782, 11), - new Coordinate(585, 784, 11), - new Coordinate(586, 784, 11), - new Coordinate(1171, 1566, 12), - new Coordinate(1172, 1566, 12), - new Coordinate(1171, 1567, 12), - new Coordinate(1171, 1565, 12), - new Coordinate(1172, 1567, 12), - new Coordinate(1172, 1565, 12), - new Coordinate(2343, 3133, 13), - new Coordinate(2342, 3133, 13), - new Coordinate(2343, 3132, 13), - new Coordinate(2342, 3132, 13), - new Coordinate(2343, 3134, 13), - new Coordinate(2342, 3134, 13), - new Coordinate(4686, 6266, 14), - new Coordinate(4686, 6267, 14), - new Coordinate(4685, 6266, 14), - new Coordinate(4685, 6267, 14), - new Coordinate(9372, 12533, 15), - new Coordinate(9372, 12534, 15), - new Coordinate(9373, 12533, 15), - new Coordinate(9373, 12534, 15), - new Coordinate(9372, 12532, 15), - new Coordinate(9373, 12532, 15) -]; diff --git a/bench/lib/create_map.js b/bench/lib/create_map.js index 3c0a600f60d..f399920a298 100644 --- a/bench/lib/create_map.js +++ b/bench/lib/create_map.js @@ -1,28 +1,26 @@ -'use strict'; - -const util = require('../../src/util/util'); -const mapboxgl = require('../../src'); +// @flow -module.exports = function createMap(options) { - options = util.extend({width: 512, height: 512}, options); +'use strict'; - const 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); +const Map = require('../../src/ui/map'); - mapboxgl.accessToken = require('./access_token'); +module.exports = function (options: any): Promise { + return new Promise((resolve, reject) => { + const container = document.createElement('div'); + container.style.width = `${options.width || 512}px`; + container.style.height = `${options.width || 512}px`; + container.style.margin = '0 auto'; + container.style.display = 'none'; + (document.body: any).appendChild(container); - const map = new mapboxgl.Map(util.extend({ - container: element, - style: 'mapbox://styles/mapbox/streets-v9', - interactive: false - }, options)); + const map = new Map(Object.assign({ + container, + style: 'mapbox://styles/mapbox/streets-v9' + }, options)); - map.on('remove', () => { - map.getContainer().remove(); + map + .on('load', () => resolve(map)) + .on('error', (e) => reject(e.error)) + .on('remove', () => container.remove()); }); - - return map; }; diff --git a/bench/lib/create_style.js b/bench/lib/create_style.js new file mode 100644 index 00000000000..433a4bb7184 --- /dev/null +++ b/bench/lib/create_style.js @@ -0,0 +1,22 @@ +// @flow + +'use strict'; + +const Style = require('../../src/style/style'); +const Evented = require('../../src/util/evented'); + +class StubMap extends Evented { + _transformRequest(url) { + return { url }; + } +} + +module.exports = function (styleJSON: StyleSpecification): Promise