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 = {}) {
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 = {

View file

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

View file

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

View file

@ -869,32 +869,56 @@ Zotero.Items = function() {
/**
* @param {Integer} libraryID - Library to delete from
* @param {Integer} [days] - Only delete items deleted more than this many days ago
* @param {Integer} [limit]
* @param {Object} [options]
* @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) {
throw new Error("Library ID not provided");
}
var t = new Date();
var deletedIDs = [];
var deleted = await this.getDeleted(libraryID, false, options.days);
var processed = 0;
if (deleted.length) {
let toDelete = {
top: [],
child: []
};
deleted.forEach((item) => {
item.isTopLevelItem() ? toDelete.top.push(item.id) : toDelete.child.push(item.id)
});
deletedIDs = yield this.getDeleted(libraryID, true, days);
if (deletedIDs.length) {
yield Zotero.Utilities.Internal.forEachChunkAsync(deletedIDs, 50, Zotero.Promise.coroutine(function* (chunk) {
yield this.erase(chunk);
yield Zotero.Notifier.trigger('refresh', 'trash', libraryID);
}.bind(this)));
// 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) {
Zotero.debug("Emptied " + deletedIDs.length + " item(s) from trash in " + (new Date() - t) + " ms");
}
return deletedIDs.length;
});
return deleted.length;
};
/**

View file

@ -702,7 +702,11 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
yield this.refresh(skipExpandMatchParents);
refreshed = 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()) {

View file

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