Improve downloaded object processing

- Use an increasing notifier batch size, so objects initially appear one by one
  but then start showing up in batches, up to 50. (UI updates are expensive, so
  for larger syncs we don't want to update after each object.)
- Avoid separate save to update attachment file sync state, which was also
  happening outside of notifier batches (causing individual updates regardless
  of the batch size)
- Add a 10ms delay after processing each object, which keeps the UI responsive
  during downloads. #989 could reduce this to 1 during idle, to save a few
  minutes when downloading very large libraries.
This commit is contained in:
Dan Stillman 2016-05-05 04:34:19 -04:00
parent 4ac2da42d1
commit 09c3a95a7e
2 changed files with 69 additions and 38 deletions

View file

@ -591,8 +591,9 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
); );
var conflicts = []; var conflicts = [];
var num = 0;
// Process batches as soon as they're available // Process batches when they're available, one at a time
yield Zotero.Promise.map( yield Zotero.Promise.map(
json, json,
function (batch) { function (batch) {
@ -618,9 +619,31 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
objectType, objectType,
this.libraryID, this.libraryID,
batch, batch,
this._getOptions() this._getOptions({
onObjectProcessed: () => {
num++;
},
// Increase the notifier batch size as we go
getNotifierBatchSize: () => {
var size;
if (num < 10) {
size = 1;
}
else if (num < 50) {
size = 5;
}
else if (num < 150) {
size = 25;
}
else {
size = 50;
}
return Math.min(size, batch.length);
}
})
) )
.then(function (results) { .then(function (results) {
num += results.length;
let processedKeys = []; let processedKeys = [];
let conflictResults = []; let conflictResults = [];
results.forEach(x => { results.forEach(x => {
@ -639,7 +662,10 @@ Zotero.Sync.Data.Engine.prototype._downloadObjects = Zotero.Promise.coroutine(fu
keys = Zotero.Utilities.arrayDiff(keys, processedKeys); keys = Zotero.Utilities.arrayDiff(keys, processedKeys);
conflicts.push(...conflictResults); conflicts.push(...conflictResults);
}.bind(this)); }.bind(this));
}.bind(this) }.bind(this),
{
concurrency: 1
}
); );
if (!keys.length || keys.length == lastLength) { if (!keys.length || keys.length == lastLength) {
@ -1433,9 +1459,12 @@ Zotero.Sync.Data.Engine.prototype._fullSync = Zotero.Promise.coroutine(function*
}); });
Zotero.Sync.Data.Engine.prototype._getOptions = function () { Zotero.Sync.Data.Engine.prototype._getOptions = function (additionalOpts = {}) {
var options = {}; var options = {};
this.optionNames.forEach(x => options[x] = this[x]); this.optionNames.forEach(x => options[x] = this[x]);
for (let opt in additionalOpts) {
options[opt] = additionalOpts[opt];
}
return options; return options;
} }

View file

@ -535,14 +535,19 @@ Zotero.Sync.Data.Local = {
}); });
} }
var batchSize = 10; var batchSize = options.getNotifierBatchSize ? options.getNotifierBatchSize() : json.length;
var notifierQueues = []; var notifierQueues = [];
try { try {
for (let i = 0; i < json.length; i++) { for (let i = 0; i < json.length; i++) {
// Batch notifier updates // Batch notifier updates
if (notifierQueues.length == batchSize) { if (notifierQueues.length == batchSize) {
yield Zotero.Notifier.commit(notifierQueues); yield Zotero.Notifier.commit(notifierQueues);
notifierQueues = []; notifierQueues = [];
// Get the current batch size, which might have increased
if (options.getNotifierBatchSize) {
batchSize = options.getNotifierBatchSize()
}
} }
let notifierQueue = new Zotero.Notifier.Queue; let notifierQueue = new Zotero.Notifier.Queue;
@ -786,6 +791,13 @@ Zotero.Sync.Data.Local = {
throw e; throw e;
} }
} }
finally {
if (options.onObjectProcessed) {
options.onObjectProcessed();
}
}
yield Zotero.Promise.delay(10);
} }
} }
finally { finally {
@ -825,32 +837,10 @@ Zotero.Sync.Data.Local = {
}, },
_onObjectProcessed: Zotero.Promise.coroutine(function* (obj, jsonObject, isNewObject, storageDetailsChanged) { /**
var jsonData = jsonObject.data; * Check whether an attachment's file mod time matches the given mod time, and mark the file
* for download if not (or if this is a new attachment)
// Delete older versions of the object in the cache */
yield this.deleteCacheObjectVersions(
obj.objectType, obj.libraryID, jsonData.key, null, jsonData.version - 1
);
// Delete from sync queue
yield this._removeObjectFromSyncQueue(obj.objectType, obj.libraryID, jsonData.key);
// Mark updated attachments for download
if (obj.objectType == 'item' && obj.isImportedAttachment()) {
// If storage changes were made (attachment mtime or hash), mark
// library as requiring download
if (isNewObject || storageDetailsChanged) {
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true;
}
yield this._checkAttachmentForDownload(
obj, jsonData.mtime, isNewObject
);
}
}),
_checkAttachmentForDownload: Zotero.Promise.coroutine(function* (item, mtime, isNewObject) { _checkAttachmentForDownload: Zotero.Promise.coroutine(function* (item, mtime, isNewObject) {
var markToDownload = false; var markToDownload = false;
if (!isNewObject) { if (!isNewObject) {
@ -882,7 +872,6 @@ Zotero.Sync.Data.Local = {
} }
if (markToDownload) { if (markToDownload) {
item.attachmentSyncState = "to_download"; item.attachmentSyncState = "to_download";
yield item.save({ skipAll: true });
} }
}), }),
@ -956,7 +945,7 @@ Zotero.Sync.Data.Local = {
Zotero.debug("Processing resolved conflicts"); Zotero.debug("Processing resolved conflicts");
let batchSize = 50; let batchSize = mergeData.length;
let notifierQueues = []; let notifierQueues = [];
try { try {
for (let i = 0; i < mergeData.length; i++) { for (let i = 0; i < mergeData.length; i++) {
@ -1125,6 +1114,9 @@ Zotero.Sync.Data.Local = {
if (!options.skipData) { if (!options.skipData) {
obj.fromJSON(json.data); obj.fromJSON(json.data);
} }
if (obj.objectType == 'item' && obj.isImportedAttachment()) {
this._checkAttachmentForDownload(obj, json.data.mtime, options.isNewObject);
}
obj.version = json.data.version; obj.version = json.data.version;
if (!options.saveAsChanged) { if (!options.saveAsChanged) {
obj.synced = true; obj.synced = true;
@ -1143,12 +1135,22 @@ Zotero.Sync.Data.Local = {
yield this.saveCacheObject(obj.objectType, obj.libraryID, json.data); yield this.saveCacheObject(obj.objectType, obj.libraryID, json.data);
results.processed = true; results.processed = true;
yield this._onObjectProcessed( // Delete older versions of the object in the cache
obj, yield this.deleteCacheObjectVersions(
json, obj.objectType, obj.libraryID, json.key, null, json.version - 1
options.isNewObject,
options.storageDetailsChanged
); );
// Delete from sync queue
yield this._removeObjectFromSyncQueue(obj.objectType, obj.libraryID, json.key);
// Mark updated attachments for download
if (obj.objectType == 'item' && obj.isImportedAttachment()) {
// If storage changes were made (attachment mtime or hash), mark
// library as requiring download
if (options.isNewObject || options.storageDetailsChanged) {
Zotero.Libraries.get(obj.libraryID).storageDownloadNeeded = true;
}
}
} }
catch (e) { catch (e) {
// For now, allow sync to proceed after all errors // For now, allow sync to proceed after all errors