Merge pull request #4259 from openstreetmap/performance

Performance: use requestIdleCallback for lazy execution
This commit is contained in:
Bryan Housel
2017-08-24 22:12:35 -04:00
committed by GitHub
9 changed files with 176 additions and 40 deletions
+5 -4
View File
@@ -13,6 +13,7 @@ import { services } from '../services/index';
import { uiInit } from '../ui/init';
import { utilDetect } from '../util/detect';
import { utilRebind } from '../util/rebind';
import { utilCallWhenIdle } from '../util/index';
export var areaKeys = {};
@@ -83,9 +84,9 @@ export function coreContext() {
/* Connection */
function entitiesLoaded(err, result) {
var entitiesLoaded = utilCallWhenIdle(function entitiesLoaded(err, result) {
if (!err) history.merge(result.data, result.extent);
}
});
context.preauth = function(options) {
if (connection) {
@@ -94,7 +95,7 @@ export function coreContext() {
return context;
};
context.loadTiles = function(projection, dimensions, callback) {
context.loadTiles = utilCallWhenIdle(function(projection, dimensions, callback) {
function done(err, result) {
entitiesLoaded(err, result);
if (callback) callback(err, result);
@@ -102,7 +103,7 @@ export function coreContext() {
if (connection) {
connection.loadTiles(projection, dimensions, done);
}
};
});
context.loadEntity = function(id, callback) {
function done(err, result) {
+26 -9
View File
@@ -64,8 +64,27 @@ export function rendererMap(context) {
.on('zoom', zoomPan);
var _selection = d3.select(null);
var isRedrawScheduled = false;
var pendingRedrawCall;
function scheduleRedraw() {
// Only schedule the redraw if one has not already been set.
if (isRedrawScheduled) return;
isRedrawScheduled = true;
var that = this;
var args = arguments;
pendingRedrawCall = requestIdleCallback(function () {
// Reset the boolean so future redraws can be set.
isRedrawScheduled = false;
redraw.apply(that, args);
}, { timeout: 1400 });
}
function cancelPendingRedraw() {
isRedrawScheduled = false;
window.cancelIdleCallback(pendingRedrawCall);
}
function map(selection) {
_selection = selection;
@@ -323,7 +342,7 @@ export function rendererMap(context) {
surface.interrupt();
uiFlash().text(t('cannot_zoom'));
setZoom(context.minEditableZoom(), true);
queueRedraw();
scheduleRedraw();
dispatch.call('move', this, map);
return;
}
@@ -346,7 +365,7 @@ export function rendererMap(context) {
transformed = true;
transformLast = eventTransform;
utilSetTransform(supersurface, tX, tY, scale);
queueRedraw();
scheduleRedraw();
dispatch.call('move', this, map);
}
@@ -403,11 +422,9 @@ export function rendererMap(context) {
}
var queueRedraw = _.throttle(redraw, 750);
var immediateRedraw = function(difference, extent) {
if (!difference && !extent) queueRedraw.cancel();
if (!difference && !extent) cancelPendingRedraw();
redraw(difference, extent);
};
@@ -576,7 +593,7 @@ export function rendererMap(context) {
mouse = utilFastMouse(supersurface.node());
setCenter(center);
queueRedraw();
scheduleRedraw();
return map;
};
@@ -605,7 +622,7 @@ export function rendererMap(context) {
dispatch.call('move', this, map);
}
queueRedraw();
scheduleRedraw();
return map;
};
@@ -625,7 +642,7 @@ export function rendererMap(context) {
dispatch.call('move', this, map);
}
queueRedraw();
scheduleRedraw();
return map;
};
@@ -648,7 +665,7 @@ export function rendererMap(context) {
dispatch.call('move', this, map);
}
queueRedraw();
scheduleRedraw();
return map;
};
+44 -23
View File
@@ -11,7 +11,7 @@ import {
osmWay
} from '../osm';
import { utilRebind } from '../util';
import { utilRebind, utilIdleWorker } from '../util';
var dispatch = d3.dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'),
@@ -19,6 +19,7 @@ var dispatch = d3.dispatch('authLoading', 'authDone', 'change', 'loading', 'load
blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'],
inflight = {},
loadedTiles = {},
entityCache = {},
tileZoom = 16,
oauth = osmAuth({
url: urlroot,
@@ -100,10 +101,10 @@ function getVisible(attrs) {
var parsers = {
node: function nodeData(obj) {
node: function nodeData(obj, uid) {
var attrs = obj.attributes;
return new osmNode({
id: osmEntity.id.fromOSM('node', attrs.id.value),
id:uid,
visible: getVisible(attrs),
version: attrs.version.value,
changeset: attrs.changeset && attrs.changeset.value,
@@ -115,10 +116,10 @@ var parsers = {
});
},
way: function wayData(obj) {
way: function wayData(obj, uid) {
var attrs = obj.attributes;
return new osmWay({
id: osmEntity.id.fromOSM('way', attrs.id.value),
id: uid,
visible: getVisible(attrs),
version: attrs.version.value,
changeset: attrs.changeset && attrs.changeset.value,
@@ -130,10 +131,10 @@ var parsers = {
});
},
relation: function relationData(obj) {
relation: function relationData(obj, uid) {
var attrs = obj.attributes;
return new osmRelation({
id: osmEntity.id.fromOSM('relation', attrs.id.value),
id: uid,
visible: getVisible(attrs),
version: attrs.version.value,
changeset: attrs.changeset && attrs.changeset.value,
@@ -147,22 +148,25 @@ var parsers = {
};
function parse(xml) {
function parse(xml, callback, options) {
options = _.extend({ cache: true }, options);
if (!xml || !xml.childNodes) return;
var root = xml.childNodes[0],
children = root.childNodes,
entities = [];
children = root.childNodes;
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i],
parser = parsers[child.nodeName];
function parseChild(child) {
var parser = parsers[child.nodeName];
if (parser) {
entities.push(parser(child));
var uid = osmEntity.id.fromOSM(child.nodeName, child.attributes.id.value);
if (options.cache && entityCache[uid]) {
return null;
}
return parser(child, uid);
}
}
return entities;
utilIdleWorker(children, parseChild, callback);
}
@@ -178,6 +182,7 @@ export default {
userDetails = undefined;
rateLimitError = undefined;
_.forEach(inflight, abortRequest);
entityCache = {};
loadedTiles = {};
inflight = {};
return this;
@@ -213,7 +218,8 @@ export default {
},
loadFromAPI: function(path, callback) {
loadFromAPI: function(path, callback, options) {
options = _.extend({ cache: true }, options);
var that = this;
function done(err, xml) {
@@ -237,7 +243,15 @@ export default {
}
if (callback) {
callback(err, parse(xml));
if (err) return callback(err, null);
parse(xml, function (entities) {
if (options.cache) {
for (var i in entities) {
entityCache[entities[i].id] = true;
}
}
callback(null, entities);
}, options);
}
}
}
@@ -253,42 +267,49 @@ export default {
loadEntity: function(id, callback) {
var type = osmEntity.id.type(id),
osmID = osmEntity.id.toOSM(id);
osmID = osmEntity.id.toOSM(id),
options = { cache: false };
this.loadFromAPI(
'/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : ''),
function(err, entities) {
if (callback) callback(err, { data: entities });
}
},
options
);
},
loadEntityVersion: function(id, version, callback) {
var type = osmEntity.id.type(id),
osmID = osmEntity.id.toOSM(id);
osmID = osmEntity.id.toOSM(id),
options = { cache: false };
this.loadFromAPI(
'/api/0.6/' + type + '/' + osmID + '/' + version,
function(err, entities) {
if (callback) callback(err, { data: entities });
}
},
options
);
},
loadMultiple: function(ids, callback) {
var that = this;
_.each(_.groupBy(_.uniq(ids), osmEntity.id.type), function(v, k) {
var type = k + 's',
osmIDs = _.map(v, osmEntity.id.toOSM);
osmIDs = _.map(v, osmEntity.id.toOSM),
options = { cache: false };
_.each(_.chunk(osmIDs, 150), function(arr) {
that.loadFromAPI(
'/api/0.6/' + type + '?' + type + '=' + arr.join(),
function(err, entities) {
if (callback) callback(err, { data: entities });
}
},
options
);
});
});
+3 -2
View File
@@ -18,7 +18,8 @@ import { utilDetect } from '../util/detect';
import {
utilDisplayName,
utilDisplayNameForPath,
utilEntitySelector
utilEntitySelector,
utilCallWhenIdle
} from '../util/index';
@@ -652,7 +653,7 @@ export function svgLabels(projection, context) {
}
var throttleFilterLabels = _.throttle(filterLabels, 100);
var throttleFilterLabels = _.throttle(utilCallWhenIdle(filterLabels), 100);
drawLabels.observe = function(selection) {
+2 -2
View File
@@ -4,7 +4,7 @@ import { d3keybinding } from '../lib/d3.keybinding.js';
import { t, textDirection } from '../util/locale';
import { geoMetersToOffset, geoOffsetToMeters } from '../geo/index';
import { utilDetect } from '../util/detect';
import { utilSetTransform } from '../util/index';
import { utilSetTransform, utilCallWhenIdle } from '../util/index';
import { svgIcon } from '../svg/index';
import { uiMapInMap } from './map_in_map';
import { uiCmd } from './cmd';
@@ -540,7 +540,7 @@ export function uiBackground(context) {
);
context.map()
.on('move.background-update', _.debounce(update, 1000));
.on('move.background-update', _.debounce(utilCallWhenIdle(update), 1000));
context.background()
.on('change.background-update', update);
+11
View File
@@ -0,0 +1,11 @@
// note the function should be of low priority
// and should not be returning a value.
export function utilCallWhenIdle(func, timeout) {
return function() {
var args = arguments;
var that = this;
window.requestIdleCallback(function() {
func.apply(that, args);
}, {timeout: timeout});
};
}
+46
View File
@@ -0,0 +1,46 @@
export function utilIdleWorker(tasks, processor, callback) {
var processed = [];
var currentPos = 0;
var totalTasks = tasks.length;
function worker(deadline) {
while (deadline.timeRemaining() > 0 && currentPos < totalTasks) {
var result = processor(tasks[currentPos]);
// if falsy dont add to the processed list
if (result) processed.push(result);
currentPos++;
}
// more tasks are left, we might need more idleCallbacks
if (currentPos < totalTasks) {
return window.requestIdleCallback(function(deadline) {worker(deadline);});
}
// tasks are completed
return callback(processed);
}
window.requestIdleCallback(function(deadline) {worker(deadline);});
}
// shim
window.requestIdleCallback =
window.requestIdleCallback ||
function(cb) {
var start = Date.now();
return setTimeout(function() {
cb({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
window.cancelIdleCallback =
window.cancelIdleCallback ||
function(id) {
clearTimeout(id);
};
+2
View File
@@ -22,3 +22,5 @@ export { utilSuggestNames } from './suggest_names';
export { utilTagText } from './util';
export { utilTriggerEvent } from './trigger_event';
export { utilWrap } from './util';
export { utilIdleWorker} from './idle_worker';
export { utilCallWhenIdle } from './call_when_idle';
+37
View File
@@ -320,6 +320,24 @@ describe('iD.serviceOsm', function () {
[200, { 'Content-Type': 'text/xml' }, wayXML]);
server.respond();
});
it('does not ignore repeat requests', function(done) {
var id = 'n1';
connection.loadEntity(id, function(err1, result1) {
var entity1 = _.find(result1.data, function(e1) { return e1.id === id; });
expect(entity1).to.be.an.instanceOf(iD.Node);
connection.loadEntity(id, function(err2, result2) {
var entity2 = _.find(result2.data, function(e2) { return e2.id === id; });
expect(entity2).to.be.an.instanceOf(iD.Node);
done();
});
server.respond();
});
server.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1',
[200, { 'Content-Type': 'text/xml' }, nodeXML]);
server.respond();
});
});
describe('#loadEntityVersion', function () {
@@ -363,6 +381,24 @@ describe('iD.serviceOsm', function () {
[200, { 'Content-Type': 'text/xml' }, wayXML]);
server.respond();
});
it('does not ignore repeat requests', function(done) {
var id = 'n1';
connection.loadEntityVersion(id, 1, function(err1, result1) {
var entity1 = _.find(result1.data, function(e1) { return e1.id === id; });
expect(entity1).to.be.an.instanceOf(iD.Node);
connection.loadEntityVersion(id, 1, function(err2, result2) {
var entity2 = _.find(result2.data, function(e2) { return e2.id === id; });
expect(entity2).to.be.an.instanceOf(iD.Node);
done();
});
server.respond();
});
server.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1/1',
[200, { 'Content-Type': 'text/xml' }, nodeXML]);
server.respond();
});
});
describe('#loadMultiple', function () {
@@ -376,6 +412,7 @@ describe('iD.serviceOsm', function () {
it('loads nodes');
it('loads ways');
it('does not ignore repeat requests');
});