Optimize startup: coalesce & debounce prefs.set

Previously prefs.set broadcast many messages per each changed pref value to all open tabs, background page, popups. This lead to repeated and needless updates of various things like the toolbar icon, reapplying of styles, and whatnot. It could easily take more than 100ms on an average computer with many tabs open.

Now we debounce the broadcast & sync.set and coalesce all values in one object which is then sent just once per destination.
This commit is contained in:
tophf 2017-03-31 11:46:18 +03:00
parent f8d13d8dec
commit 26802e36df
7 changed files with 109 additions and 116 deletions

View File

@ -47,8 +47,7 @@ function applyOnMessage(request, sender, sendResponse) {
applyOnMessage(Object.assign(request, {styles}))); applyOnMessage(Object.assign(request, {styles})));
return; return;
} }
// Also handle special request just for the pop-up switch (request.method) {
switch (request.method == 'updatePopup' ? request.reason : request.method) {
case 'styleDeleted': case 'styleDeleted':
removeStyle(request.id, document); removeStyle(request.id, document);
@ -80,8 +79,10 @@ function applyOnMessage(request, sender, sendResponse) {
replaceAll(request.styles, document); replaceAll(request.styles, document);
break; break;
case 'styleDisableAll': case 'prefChanged':
doDisableAll(request.disableAll); if ('disableAll' in request.prefs) {
doDisableAll(request.prefs.disableAll);
}
break; break;
case 'ping': case 'ping':

View File

@ -72,14 +72,13 @@ function onBackgroundMessage(request, sender, sendResponse) {
() => sendResponse(false)); () => sendResponse(false));
return KEEP_CHANNEL_OPEN; return KEEP_CHANNEL_OPEN;
case 'styleDisableAll':
request = {prefName: 'disableAll', value: request.disableAll};
// fallthrough to prefChanged
case 'prefChanged': case 'prefChanged':
// eslint-disable-next-line no-use-before-define for (var prefName in request.prefs) { // eslint-disable-line no-var
if (typeof request.value == 'boolean' && contextMenus[request.prefName]) { if (prefName in contextMenus) { // eslint-disable-line no-use-before-define
chrome.contextMenus.update(request.prefName, {checked: request.value}, ignoreChromeError); chrome.contextMenus.update(prefName, {
checked: request.prefs[prefName],
}, ignoreChromeError);
}
} }
break; break;
} }

View File

@ -1824,8 +1824,8 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
} }
break; break;
case "prefChanged": case "prefChanged":
if (request.prefName == "editor.smartIndent") { if ('editor.smartIndent' in request.prefs) {
CodeMirror.setOption("smartIndent", request.value); CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
} }
break; break;
case 'editDeleteText': case 'editDeleteText':

View File

@ -20,17 +20,14 @@ function notifyAllTabs(request) {
updateIcon(tab); updateIcon(tab);
} }
}); });
// notify all open popups
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
chrome.runtime.sendMessage(reqPopup);
// notify self: the message no longer is sent to the origin in new Chrome // notify self: the message no longer is sent to the origin in new Chrome
if (typeof applyOnMessage !== 'undefined') { if (window.applyOnMessage) {
applyOnMessage(reqPopup); applyOnMessage(request);
} } else if (window.onBackgroundMessage) {
// notify self: pref changed by background page
if (request.method == 'prefChanged' && typeof onBackgroundMessage !== 'undefined') {
onBackgroundMessage(request); onBackgroundMessage(request);
} }
// notify background page and all open popups
chrome.runtime.sendMessage(request);
} }

View File

@ -18,8 +18,7 @@ getActiveTabRealURL().then(url => {
chrome.runtime.onMessage.addListener(msg => { chrome.runtime.onMessage.addListener(msg => {
if (msg.method == 'updatePopup') { switch (msg.method) {
switch (msg.reason) {
case 'styleAdded': case 'styleAdded':
case 'styleUpdated': case 'styleUpdated':
handleUpdate(msg.style); handleUpdate(msg.style);
@ -27,7 +26,14 @@ chrome.runtime.onMessage.addListener(msg => {
case 'styleDeleted': case 'styleDeleted':
handleDelete(msg.id); handleDelete(msg.id);
break; break;
case 'prefChanged':
if ('popup.stylesFirst' in msg.prefs) {
const stylesFirst = msg.prefs['popup.stylesFirst'];
const actions = $('body > .actions');
const before = stylesFirst ? actions : actions.nextSibling;
document.body.insertBefore(installed, before);
} }
break;
} }
}); });
@ -53,7 +59,6 @@ function initPopup(url) {
$('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage(); $('#popup-options-button').onclick = () => chrome.runtime.openOptionsPage();
$('#popup-shortcuts-button').onclick = configureCommands.open; $('#popup-shortcuts-button').onclick = configureCommands.open;
// styles first?
if (!prefs.get('popup.stylesFirst')) { if (!prefs.get('popup.stylesFirst')) {
document.body.insertBefore( document.body.insertBefore(
$('body > .actions'), $('body > .actions'),

View File

@ -497,7 +497,7 @@ function getApplicableSections({style, matchUrl, strictRegexp = true, stopOnFirs
// and as Map in case the string is not the same reference used to add the item // and as Map in case the string is not the same reference used to add the item
//const t0start = performance.now(); //const t0start = performance.now();
const code = section.code; const code = section.code;
let isEmpty = code.length < 1000 && cachedStyles.emptyCode.get(code); let isEmpty = code !== null && code.length < 1000 && cachedStyles.emptyCode.get(code);
if (isEmpty === undefined) { if (isEmpty === undefined) {
isEmpty = !code || !code.trim() isEmpty = !code || !code.trim()
|| code.indexOf('@namespace') >= 0 || code.indexOf('@namespace') >= 0
@ -540,9 +540,18 @@ function tryRegExp(regexp) {
} }
prefs = prefs || new function Prefs() { function debounce(fn, ...args) {
const me = this; const timers = debounce.timers = debounce.timers || new Map();
debounce.run = debounce.run || ((fn, ...args) => {
timers.delete(fn);
fn(...args);
});
clearTimeout(timers.get(fn));
timers.set(fn, setTimeout(debounce.run, 0, fn, ...args));
}
prefs = prefs || new function Prefs() {
const defaults = { const defaults = {
'openEditInWindow': false, // new editor opens in a own browser window 'openEditInWindow': false, // new editor opens in a own browser window
'windowPosition': {}, // detached window position 'windowPosition': {}, // detached window position
@ -589,11 +598,23 @@ prefs = prefs || new function Prefs() {
}; };
const values = deepCopy(defaults); const values = deepCopy(defaults);
let syncTimeout; // see broadcast() function below // coalesce multiple pref changes in broadcast
let broadcastPrefs = {};
function doBroadcast() {
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs});
broadcastPrefs = {};
}
function doSyncSet() {
getSync().set({'settings': values});
}
Object.defineProperty(this, 'readOnlyValues', {value: {}}); Object.defineProperty(this, 'readOnlyValues', {value: {}});
Prefs.prototype.get = function(key, defaultValue) { Object.assign(Prefs.prototype, {
get(key, defaultValue) {
if (key in values) { if (key in values) {
return values[key]; return values[key];
} }
@ -604,70 +625,58 @@ prefs = prefs || new function Prefs() {
return defaults[key]; return defaults[key];
} }
console.warn("No default preference for '%s'", key); console.warn("No default preference for '%s'", key);
}; },
Prefs.prototype.getAll = function() { getAll() {
return deepCopy(values); return deepCopy(values);
}; },
Prefs.prototype.set = function(key, value, options) { set(key, value, {noBroadcast, noSync} = {}) {
const oldValue = deepCopy(values[key]); const oldValue = deepCopy(values[key]);
values[key] = value; values[key] = value;
defineReadonlyProperty(this.readOnlyValues, key, value); defineReadonlyProperty(this.readOnlyValues, key, value);
if ((!options || !options.noBroadcast) && !equal(value, oldValue)) { if (!noBroadcast && !equal(value, oldValue)) {
me.broadcast(key, value, options); this.broadcast(key, value, {noSync});
} }
}; },
Prefs.prototype.remove = key => me.set(key, undefined); remove: key => this.set(key, undefined),
Prefs.prototype.broadcast = function(key, value, options) { broadcast(key, value, {noSync} = {}) {
const message = {method: 'prefChanged', prefName: key, value: value}; broadcastPrefs[key] = value;
notifyAllTabs(message); debounce(doBroadcast);
chrome.runtime.sendMessage(message); if (!noSync) {
if (key == 'disableAll') { debounce(doSyncSet);
notifyAllTabs({method: 'styleDisableAll', disableAll: value});
} }
if (!options || !options.noSync) { },
clearTimeout(syncTimeout);
syncTimeout = setTimeout(function() {
getSync().set({'settings': values});
}, 0);
}
};
Object.keys(defaults).forEach(function(key) {
me.set(key, defaults[key], {noBroadcast: true});
}); });
getSync().get('settings', function(result) { Object.keys(defaults).forEach(key => {
const synced = result.settings; this.set(key, defaults[key], {noBroadcast: true});
});
getSync().get('settings', ({settings: synced}) => {
for (const key in defaults) { for (const key in defaults) {
if (synced && (key in synced)) { if (synced && (key in synced)) {
me.set(key, synced[key], {noSync: true}); this.set(key, synced[key], {noSync: true});
} else {
const value = tryMigrating(key);
if (value !== undefined) {
me.set(key, value);
}
} }
} }
if (typeof contextMenus !== 'undefined') { if (typeof contextMenus !== 'undefined') {
for (const id in contextMenus) { for (const id in contextMenus) {
if (typeof values[id] == 'boolean') { if (typeof values[id] == 'boolean') {
me.broadcast(id, values[id], {noSync: true}); this.broadcast(id, values[id], {noSync: true});
} }
} }
} }
}); });
chrome.storage.onChanged.addListener(function(changes, area) { chrome.storage.onChanged.addListener((changes, area) => {
if (area == 'sync' && 'settings' in changes) { if (area == 'sync' && 'settings' in changes) {
const synced = changes.settings.newValue; const synced = changes.settings.newValue;
if (synced) { if (synced) {
for (const key in defaults) { for (const key in defaults) {
if (key in synced) { if (key in synced) {
me.set(key, synced[key], {noSync: true}); this.set(key, synced[key], {noSync: true});
} }
} }
} else { } else {
@ -676,29 +685,6 @@ prefs = prefs || new function Prefs() {
} }
} }
}); });
function tryMigrating(key) {
if (!(key in localStorage)) {
return undefined;
}
const value = localStorage[key];
delete localStorage[key];
localStorage['DEPRECATED: ' + key] = value;
switch (typeof defaults[key]) {
case 'boolean':
return value.toLowerCase() === 'true';
case 'number':
return Number(value);
case 'object':
try {
return JSON.parse(value);
} catch (e) {
console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e);
return undefined;
}
}
return value;
}
}(); }();
@ -712,14 +698,18 @@ function setupLivePrefs(IDs) {
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value); prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
}); });
}); });
chrome.runtime.onMessage.addListener(function(request) { chrome.runtime.onMessage.addListener(msg => {
if (request.prefName in localIDs) { if (msg.prefs) {
updateElement(request.prefName); for (const prefName in msg.prefs) {
if (prefName in localIDs) {
updateElement(prefName, msg.prefs[prefName]);
}
}
} }
}); });
function updateElement(id) { function updateElement(id, value) {
const el = document.getElementById(id); const el = document.getElementById(id);
el[isCheckbox(el) ? 'checked' : 'value'] = prefs.get(id); el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true})); el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
return el; return el;
} }
@ -818,6 +808,7 @@ function defineReadonlyProperty(obj, key, value) {
Object.defineProperty(obj, key, {value: copy, configurable: true}); Object.defineProperty(obj, key, {value: copy, configurable: true});
} }
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494 // Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
function getSync() { function getSync() {
if ('sync' in chrome.storage) { if ('sync' in chrome.storage) {

View File

@ -106,7 +106,7 @@ window.setTimeout(function () {
} }
chrome.runtime.onMessage.addListener(request => { chrome.runtime.onMessage.addListener(request => {
// when user has changed the predefined time interval in the settings page // when user has changed the predefined time interval in the settings page
if (request.method === 'prefChanged' && request.prefName === 'updateInterval') { if (request.method === 'prefChanged' && 'updateInterval' in request.prefs) {
reset(); reset();
} }
// when user just manually checked for updates // when user just manually checked for updates