From f23b13dc8fb5dae5ead4eb3fcda668d95c5ad228 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 5 Jun 2020 12:03:53 -0400 Subject: [PATCH] Speed up item list annotation counts. When we show an item list in Girder with annotations, we show a badge on each thumbnail indicating how many annotations are on that large image. Before, this made one REST request per call without any sort of throttling, which could effectively block navigation until enough of the calls were resolved. This gets all needed values in a single REST request. --- .../rest/annotation.py | 20 +++++- .../web_client/views/itemList.js | 68 +++++++++++-------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/girder_annotation/girder_large_image_annotation/rest/annotation.py b/girder_annotation/girder_large_image_annotation/rest/annotation.py index ad29283b5..5e8f1a455 100644 --- a/girder_annotation/girder_large_image_annotation/rest/annotation.py +++ b/girder_annotation/girder_large_image_annotation/rest/annotation.py @@ -26,7 +26,7 @@ from girder.api import access from girder.api.describe import describeRoute, autoDescribeRoute, Description from girder.api.rest import Resource, loadmodel, filtermodel, setResponseHeader -from girder.constants import AccessType, SortDir +from girder.constants import AccessType, SortDir, TokenScope from girder.exceptions import ValidationException, RestException, AccessException from girder.models.item import Item from girder.models.user import User @@ -60,6 +60,7 @@ def __init__(self): self.route('DELETE', ('item', ':id'), self.deleteItemAnnotations) self.route('GET', ('old',), self.getOldAnnotations) self.route('DELETE', ('old',), self.deleteOldAnnotations) + self.route('GET', ('counts',), self.getItemListAnnotationCounts) @describeRoute( Description('Search for annotations.') @@ -565,3 +566,20 @@ def getOldAnnotations(self, age, versions): def deleteOldAnnotations(self, age, versions): setResponseTimeLimit(86400) return Annotation().removeOldAnnotations(True, age, versions) + + @access.public(scope=TokenScope.DATA_READ) + @autoDescribeRoute( + Description('Get annotation counts for a list of items.') + .param('items', 'A comma-separated list of item ids.') + .errorResponse() + ) + def getItemListAnnotationCounts(self, items): + user = self.getCurrentUser() + results = {} + for itemId in items.split(','): + item = Item().load(itemId, level=AccessType.READ, user=user) + annotations = Annotation().findWithPermissions( + {'_active': {'$ne': False}, 'itemId': item['_id']}, + user=self.getCurrentUser(), level=AccessType.READ, limit=-1) + results[itemId] = annotations.count() + return results diff --git a/girder_annotation/girder_large_image_annotation/web_client/views/itemList.js b/girder_annotation/girder_large_image_annotation/web_client/views/itemList.js index 527346a85..7bb1c1b2d 100644 --- a/girder_annotation/girder_large_image_annotation/web_client/views/itemList.js +++ b/girder_annotation/girder_large_image_annotation/web_client/views/itemList.js @@ -12,29 +12,18 @@ import '../stylesheets/itemList.styl'; wrap(ItemListWidget, 'render', function (render) { render.apply(this, _.rest(arguments)); - function addLargeImageAnnotationBadge(item, parent) { - restRequest({ - type: 'GET', - url: 'annotation', - data: { - itemId: item.id, - limit: -1 - }, - error: null - }).done((result, status, jqxhr) => { - const numAnnotations = +jqxhr.getResponseHeader('Girder-Total-Count') || 0; - const thumbnail = $('a[g-item-cid="' + item.cid + '"] .large_image_thumbnail', parent).first(); - - let badge = thumbnail.find('.large_image_annotation_badge'); - if (badge.length === 0) { - badge = $(`
`).appendTo(thumbnail); - } - // update badge - badge - .attr('title', `${numAnnotations} annotation${numAnnotations === 1 ? '' : 's'}`) - .text(numAnnotations) - .toggleClass('hidden', numAnnotations === 0); - }); + function addLargeImageAnnotationBadge(item, parent, numAnnotations) { + const thumbnail = $('a[g-item-cid="' + item.cid + '"] .large_image_thumbnail', parent).first(); + + let badge = thumbnail.find('.large_image_annotation_badge'); + if (badge.length === 0) { + badge = $(``).appendTo(thumbnail); + } + // update badge + badge + .attr('title', `${numAnnotations} annotation${numAnnotations === 1 ? '' : 's'}`) + .text(numAnnotations) + .toggleClass('hidden', !numAnnotations); } largeImageAnnotationConfig.getSettings((settings) => { @@ -50,12 +39,35 @@ wrap(ItemListWidget, 'render', function (render) { return; } - _.each(items, (item) => { - if (item.get('largeImage')) { - item.getAccessLevel(function () { - addLargeImageAnnotationBadge(item, parent); + const needCounts = items.filter((item) => item._annotationCount === undefined && item.has('largeImage')).map((item) => { + item._annotationCount = null; // pending + return item.id; + }); + let promise; + if (!needCounts.length) { + promise = $.Deferred().resolve({}); + } else { + promise = restRequest({ + type: 'POST', + url: 'annotation/counts', + data: { + items: needCounts.join(',') + }, + headers: { 'X-HTTP-Method-Override': 'GET' }, + error: null + }).done((resp) => { + Object.entries(resp).forEach(([id, count]) => { + this.collection.get(id)._annotationCount = count; }); - } + }); + } + promise.then(() => { + this.collection.forEach((item) => { + if (item._annotationCount !== undefined) { + addLargeImageAnnotationBadge(item, parent, item._annotationCount); + } + }); + return null; }); }); });