From 179214d4ea031b2e61ac72a7b64a90a48e7ab02e Mon Sep 17 00:00:00 2001 From: Tom Lee Date: Fri, 18 May 2018 17:42:52 -0400 Subject: [PATCH] when web worker perf.getEntries() is stubbed, use fallback method --- src/source/geojson_worker_source.js | 10 ++- src/source/vector_tile_worker_source.js | 10 ++- src/util/performance.js | 89 +++++++++++++++++-- .../unit/source/geojson_worker_source.test.js | 32 +++++++ .../source/vector_tile_worker_source.test.js | 54 +++++++++++ 5 files changed, 180 insertions(+), 15 deletions(-) diff --git a/src/source/geojson_worker_source.js b/src/source/geojson_worker_source.js index 2d0fdfe6a39..ed0d55a1141 100644 --- a/src/source/geojson_worker_source.js +++ b/src/source/geojson_worker_source.js @@ -2,7 +2,7 @@ import { getJSON } from '../util/ajax'; -import perf from '../util/performance'; +import performance from '../util/performance'; import rewind from 'geojson-rewind'; import GeoJSONWrapper from './geojson_wrapper'; import vtpbf from 'vt-pbf'; @@ -152,6 +152,10 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { const params = this._pendingLoadDataParams; delete this._pendingCallback; delete this._pendingLoadDataParams; + + const perf = (params && params.request && params.request.collectResourceTiming) ? + new performance.Performance(params.request) : false; + this.loadGeoJSON(params, (err, data) => { if (err || !data) { return callback(err); @@ -171,8 +175,8 @@ class GeoJSONWorkerSource extends VectorTileWorkerSource { this.loaded = {}; const result = {}; - if (params.request && params.request.collectResourceTiming) { - const resourceTimingData = perf.getEntriesByName(params.request.url); + if (perf) { + const resourceTimingData = perf.finish(); // it's necessary to eval the result of getEntriesByName() here via parse/stringify // late evaluation in the main thread causes TypeError: illegal invocation if (resourceTimingData) { diff --git a/src/source/vector_tile_worker_source.js b/src/source/vector_tile_worker_source.js index b02cdf4d94b..11baf5ecfaa 100644 --- a/src/source/vector_tile_worker_source.js +++ b/src/source/vector_tile_worker_source.js @@ -6,7 +6,7 @@ import vt from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import WorkerTile from './worker_tile'; import { extend } from '../util/util'; -import perf from '../util/performance'; +import performance from '../util/performance'; import type { WorkerSource, @@ -102,6 +102,9 @@ class VectorTileWorkerSource implements WorkerSource { if (!this.loading) this.loading = {}; + const perf = (params && params.request && params.request.collectResourceTiming) ? + new performance.Performance(params.request) : false; + const workerTile = this.loading[uid] = new WorkerTile(params); workerTile.abort = this.loadVectorData(params, (err, response) => { delete this.loading[uid]; @@ -114,9 +117,10 @@ class VectorTileWorkerSource implements WorkerSource { const cacheControl = {}; if (response.expires) cacheControl.expires = response.expires; if (response.cacheControl) cacheControl.cacheControl = response.cacheControl; + const resourceTiming = {}; - if (params.request && params.request.collectResourceTiming) { - const resourceTimingData = perf.getEntriesByName(params.request.url); + if (perf) { + const resourceTimingData = perf.finish(); // it's necessary to eval the result of getEntriesByName() here via parse/stringify // late evaluation in the main thread causes TypeError: illegal invocation if (resourceTimingData) diff --git a/src/util/performance.js b/src/util/performance.js index 26d9ad2a288..053ed28d8f8 100644 --- a/src/util/performance.js +++ b/src/util/performance.js @@ -1,14 +1,85 @@ // @flow -// Wraps performance.getEntriesByName to facilitate testing +import type {RequestParameters} from '../util/ajax'; + +// Wraps performance to facilitate testing // Not incorporated into browser.js because the latter is poisonous when used outside the main thread -const exported = { - getEntriesByName: (url: string) => { - if ((typeof performance !== 'undefined') && performance && performance.getEntriesByName) - return performance.getEntriesByName(url); - else - return false; - } +const performanceExists = typeof performance !== 'undefined'; +const wrapper = {}; + +wrapper.getEntriesByName = (url: string) => { + if (performanceExists && performance && performance.getEntriesByName) + return performance.getEntriesByName(url); + else + return false; +}; + +wrapper.mark = (name: string) => { + if (performanceExists && performance && performance.mark) + return performance.mark(name); + else + return false; +}; + +wrapper.measure = (name: string, startMark: string, endMark: string) => { + if (performanceExists && performance && performance.measure) + return performance.measure(name, startMark, endMark); + else + return false; +}; + +wrapper.clearMarks = (name: string) => { + if (performanceExists && performance && performance.clearMarks) + return performance.clearMarks(name); + else + return false; +}; + +wrapper.clearMeasures = (name: string) => { + if (performanceExists && performance && performance.clearMeasures) + return performance.clearMeasures(name); + else + return false; }; -export default exported; +/** + * Safe wrapper for the performance resource timing API in web workers with graceful degradation + * + * @param {RequestParameters} request + * @private + */ +class Performance { + _marks: {start: string, end: string, measure: string}; + + constructor (request: RequestParameters) { + this._marks = { + start: [request.url, 'start'].join('#'), + end: [request.url, 'end'].join('#'), + measure: request.url.toString() + }; + + wrapper.mark(this._marks.start); + } + + finish() { + wrapper.mark(this._marks.end); + let resourceTimingData = wrapper.getEntriesByName(this._marks.measure); + + // fallback if web worker implementation of perf.getEntriesByName returns empty + if (resourceTimingData.length === 0) { + wrapper.measure(this._marks.measure, this._marks.start, this._marks.end); + resourceTimingData = wrapper.getEntriesByName(this._marks.measure); + + // cleanup + wrapper.clearMarks(this._marks.start); + wrapper.clearMarks(this._marks.end); + wrapper.clearMeasures(this._marks.measure); + } + + return resourceTimingData; + } +} + +wrapper.Performance = Performance; + +export default wrapper; diff --git a/test/unit/source/geojson_worker_source.test.js b/test/unit/source/geojson_worker_source.test.js index e5256ba42e0..eda04e2d39f 100644 --- a/test/unit/source/geojson_worker_source.test.js +++ b/test/unit/source/geojson_worker_source.test.js @@ -135,6 +135,38 @@ test('resourceTiming', (t) => { }); }); + t.test('loadData - url (resourceTiming fallback method)', (t) => { + const sampleMarks = [100, 350]; + const marks = {}; + const measures = {}; + t.stub(perf, 'getEntriesByName').callsFake((name) => { return measures[name] || []; }); + t.stub(perf, 'mark').callsFake((name) => { + marks[name] = sampleMarks.shift(); + return null; + }); + t.stub(perf, 'measure').callsFake((name, start, end) => { + measures[name] = measures[name] || []; + measures[name].push({ + duration: marks[end] - marks[start], + entryType: 'measure', + name: name, + startTime: marks[start] + }); + return null; + }); + t.stub(perf, 'clearMarks').callsFake(() => { return null; }); + t.stub(perf, 'clearMeasures').callsFake(() => { return null; }); + + const layerIndex = new StyleLayerIndex(layers); + const source = new GeoJSONWorkerSource(null, layerIndex, (params, callback) => { return callback(null, geoJson); }); + + source.loadData({ source: 'testSource', request: { url: 'http://localhost/nonexistent', collectResourceTiming: true } }, (err, result) => { + t.equal(err, null); + t.deepEquals(result.resourceTiming.testSource, [{"duration": 250, "entryType": "measure", "name": "http://localhost/nonexistent", "startTime": 100 }], 'got expected resource timing'); + t.end(); + }); + }); + t.test('loadData - data', (t) => { const layerIndex = new StyleLayerIndex(layers); const source = new GeoJSONWorkerSource(null, layerIndex); diff --git a/test/unit/source/vector_tile_worker_source.test.js b/test/unit/source/vector_tile_worker_source.test.js index 90787a68bc7..6b0a310bd7a 100644 --- a/test/unit/source/vector_tile_worker_source.test.js +++ b/test/unit/source/vector_tile_worker_source.test.js @@ -207,3 +207,57 @@ test('VectorTileWorkerSource provides resource timing information', (t) => { t.end(); }); }); + +test('VectorTileWorkerSource provides resource timing information (fallback method)', (t) => { + const rawTileData = fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf')); + + function loadVectorData(params, callback) { + return callback(null, { + vectorTile: new vt.VectorTile(new Protobuf(rawTileData)), + rawData: rawTileData, + cacheControl: null, + expires: null + }); + } + + const layerIndex = new StyleLayerIndex([{ + id: 'test', + source: 'source', + 'source-layer': 'test', + type: 'fill' + }]); + + const source = new VectorTileWorkerSource(null, layerIndex, loadVectorData); + + const sampleMarks = [100, 350]; + const marks = {}; + const measures = {}; + t.stub(perf, 'getEntriesByName').callsFake((name) => { return measures[name] || []; }); + t.stub(perf, 'mark').callsFake((name) => { + marks[name] = sampleMarks.shift(); + return null; + }); + t.stub(perf, 'measure').callsFake((name, start, end) => { + measures[name] = measures[name] || []; + measures[name].push({ + duration: marks[end] - marks[start], + entryType: 'measure', + name: name, + startTime: marks[start] + }); + return null; + }); + t.stub(perf, 'clearMarks').callsFake(() => { return null; }); + t.stub(perf, 'clearMeasures').callsFake(() => { return null; }); + + source.loadTile({ + source: 'source', + uid: 0, + tileID: { overscaledZ: 0, wrap: 0, canonical: {x: 0, y: 0, z: 0, w: 0} }, + request: { url: 'http://localhost:2900/faketile.pbf', collectResourceTiming: true } + }, (err, res) => { + t.false(err); + t.deepEquals(res.resourceTiming[0], {"duration": 250, "entryType": "measure", "name": "http://localhost:2900/faketile.pbf", "startTime": 100 }, 'resourceTiming resp is expected'); + t.end(); + }); +});