Speed up emptying trash

Shows a progress meter, which allows for larger chunks and fewer
refreshes, avoids unnecessary updating of parent items that are
being deleted anyway, and skip re-sorting of modified items in the
trash.

Closes #1292, Emptying trash is slow
This commit is contained in:
Dan Stillman 2017-09-15 20:20:41 -04:00
parent 7935d01a1c
commit 3872e646ac
7 changed files with 101 additions and 53 deletions

View file

@ -1160,7 +1160,7 @@ Zotero.DataObject.prototype.updateSynced = Zotero.Promise.coroutine(function* (s
*/ */
Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) { Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) {
if (!options || typeof options != 'object') { if (!options || typeof options != 'object') {
throw new Error("'options' must be an object"); throw new Error("'options' must be an object (" + typeof options + ")");
} }
var env = { var env = {

View file

@ -909,6 +909,7 @@ Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
* *
* @param {Integer|Integer[]} ids - Object ids * @param {Integer|Integer[]} ids - Object ids
* @param {Object} [options] - See Zotero.DataObject.prototype.erase * @param {Object} [options] - See Zotero.DataObject.prototype.erase
* @param {Function} [options.onProgress] - f(progress, progressMax)
* @return {Promise} * @return {Promise}
*/ */
Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) { Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) {
@ -920,6 +921,9 @@ Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, op
continue; continue;
} }
yield obj.erase(options); yield obj.erase(options);
if (options.onProgress) {
options.onProgress(i + 1, ids.length);
}
} }
this.unload(ids); this.unload(ids);
}.bind(this)); }.bind(this));

View file

@ -3972,13 +3972,13 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem)) ? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem))
: null; : null;
if (parentItem) { if (parentItem && !env.options.skipParentRefresh) {
Zotero.Notifier.queue('refresh', 'item', parentItem.id); Zotero.Notifier.queue('refresh', 'item', parentItem.id);
} }
// // Delete associated attachment files // // Delete associated attachment files
if (this.isAttachment()) { if (this.isAttachment()) {
let linkMode = this.getAttachmentLinkMode(); let linkMode = this.attachmentLinkMode;
// If link only, nothing to delete // If link only, nothing to delete
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
try { try {
@ -4005,7 +4005,9 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
for (let i=0; i<toDelete.length; i++) { for (let i=0; i<toDelete.length; i++) {
let obj = yield this.ObjectsClass.getAsync(toDelete[i]); let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
// Copy all options other than 'tx', which would cause a deadlock // Copy all options other than 'tx', which would cause a deadlock
let options = {}; let options = {
skipParentRefresh: true
};
Object.assign(options, env.options); Object.assign(options, env.options);
delete options.tx; delete options.tx;
yield obj.erase(options); yield obj.erase(options);
@ -4029,7 +4031,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id); yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id);
if (parentItem) { if (parentItem && !env.options.skipParentRefresh) {
yield parentItem.reload(['primaryData', 'childItems'], true); yield parentItem.reload(['primaryData', 'childItems'], true);
parentItem.clearBestAttachmentState(); parentItem.clearBestAttachmentState();
} }

View file

@ -869,32 +869,56 @@ Zotero.Items = function() {
/** /**
* @param {Integer} libraryID - Library to delete from * @param {Integer} libraryID - Library to delete from
* @param {Integer} [days] - Only delete items deleted more than this many days ago * @param {Object} [options]
* @param {Integer} [limit] * @param {Function} [options.onProgress] - fn(progress, progressMax)
* @param {Integer} [options.days] - Only delete items deleted more than this many days ago
*/ */
this.emptyTrash = Zotero.Promise.coroutine(function* (libraryID, days, limit) { this.emptyTrash = async function (libraryID, options = {}) {
if (typeof arguments[1] == 'number') {
Zotero.warn("Zotero.Items.emptyTrash() has changed -- update your code");
options.days = arguments[1];
}
if (!libraryID) { if (!libraryID) {
throw new Error("Library ID not provided"); throw new Error("Library ID not provided");
} }
var t = new Date(); var t = new Date();
var deletedIDs = []; var deleted = await this.getDeleted(libraryID, false, options.days);
var processed = 0;
deletedIDs = yield this.getDeleted(libraryID, true, days); if (deleted.length) {
if (deletedIDs.length) { let toDelete = {
yield Zotero.Utilities.Internal.forEachChunkAsync(deletedIDs, 50, Zotero.Promise.coroutine(function* (chunk) { top: [],
yield this.erase(chunk); child: []
yield Zotero.Notifier.trigger('refresh', 'trash', libraryID); };
}.bind(this))); deleted.forEach((item) => {
item.isTopLevelItem() ? toDelete.top.push(item.id) : toDelete.child.push(item.id)
});
// Show progress meter during deletions
let eraseOptions = options.onProgress
? {
onProgress: function (progress, progressMax) {
options.onProgress(processed + progress, deleted.length);
}
}
: undefined;
for (let x of ['top', 'child']) {
await Zotero.Utilities.Internal.forEachChunkAsync(
toDelete[x],
1000,
async function (chunk) {
await this.erase(chunk, eraseOptions);
processed += chunk.length;
}.bind(this)
);
}
Zotero.debug("Emptied " + deleted.length + " item(s) from trash in " + (new Date() - t) + " ms");
} }
if (deletedIDs.length) { return deleted.length;
Zotero.debug("Emptied " + deletedIDs.length + " item(s) from trash in " + (new Date() - t) + " ms"); };
}
return deletedIDs.length;
});
/** /**

View file

@ -702,7 +702,11 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
yield this.refresh(skipExpandMatchParents); yield this.refresh(skipExpandMatchParents);
refreshed = true; refreshed = true;
madeChanges = true; madeChanges = true;
sort = true; // Don't bother re-sorting in trash, since it's probably just a modification of a parent
// item that's about to be deleted
if (!collectionTreeRow.isTrash()) {
sort = true;
}
} }
else if (collectionTreeRow.isFeed()) { else if (collectionTreeRow.isFeed()) {

View file

@ -39,41 +39,30 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
return; return;
} }
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) " var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) VALUES ";
+ "VALUES (?, ?, ?)"; var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES ";
var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES (?, ?)";
var storageForLibrary = {}; var storageForLibrary = {};
return Zotero.Utilities.Internal.forEachChunkAsync( return Zotero.Utilities.Internal.forEachChunkAsync(
ids, ids,
100, 100,
function (chunk) { async function (chunk) {
return Zotero.DB.executeTransaction(function* () { var syncSets = [];
for (let id of chunk) { var storageSets = [];
if (extraData[id] && extraData[id].skipDeleteLog) { chunk
continue; .filter(id => !extraData[id] || !extraData[id].skipDeleteLog)
} .forEach(id => {
if (type == 'setting') { if (type == 'setting') {
var [libraryID, key] = id.split("/"); var [libraryID, key] = id.split("/");
} }
else { else {
var { libraryID, key } = extraData[id]; var { libraryID, key } = extraData[id];
} }
if (!key) { if (!key) {
throw new Error("Key not provided in notifier object"); throw new Error("Key not provided in notifier object");
} }
syncSets.push(syncObjectTypeID, libraryID, key);
yield Zotero.DB.queryAsync(
syncSQL,
[
syncObjectTypeID,
libraryID,
key
]
);
if (type == 'item') { if (type == 'item') {
if (storageForLibrary[libraryID] === undefined) { if (storageForLibrary[libraryID] === undefined) {
@ -81,17 +70,28 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
Zotero.Sync.Storage.Local.getModeForLibrary(libraryID) == 'webdav'; Zotero.Sync.Storage.Local.getModeForLibrary(libraryID) == 'webdav';
} }
if (storageForLibrary[libraryID] && extraData[id].storageDeleteLog) { if (storageForLibrary[libraryID] && extraData[id].storageDeleteLog) {
yield Zotero.DB.queryAsync( storageSets.push(libraryID, key);
storageSQL,
[
libraryID,
key
]
);
} }
} }
} });
});
if (storageSets.length) {
return Zotero.DB.executeTransaction(function* () {
yield Zotero.DB.queryAsync(
syncSQL + Array(syncSets.length / 3).fill('(?, ?, ?)').join(', '),
syncSets
);
yield Zotero.DB.queryAsync(
storageSQL + Array(storageSets.length / 3).fill('(?, ?)').join(', '),
storageSets
);
});
}
else if (syncSets.length) {
await Zotero.DB.queryAsync(
syncSQL + Array(syncSets.length / 3).fill('(?, ?, ?)').join(', '), syncSets
);
}
} }
); );
}); });

View file

@ -2042,7 +2042,21 @@ var ZoteroPane = new function()
+ Zotero.getString('general.actionCannotBeUndone') + Zotero.getString('general.actionCannotBeUndone')
); );
if (result) { if (result) {
let deleted = yield Zotero.Items.emptyTrash(libraryID); Zotero.showZoteroPaneProgressMeter(null, true);
try {
let deleted = yield Zotero.Items.emptyTrash(
libraryID,
{
onProgress: (progress, progressMax) => {
var percentage = Math.round((progress / progressMax) * 100);
Zotero.updateZoteroPaneProgressMeter(percentage);
}
}
);
}
finally {
Zotero.hideZoteroPaneOverlays();
}
yield Zotero.purgeDataObjects(); yield Zotero.purgeDataObjects();
} }
}); });