LP#1777207: teach egGrid how to prepend rows more efficiently
authorGalen Charlton <gmc@equinoxinitiative.org>
Wed, 3 Jul 2019 21:53:01 +0000 (17:53 -0400)
committerDan Wells <dbw2@calvin.edu>
Thu, 18 Jul 2019 16:36:10 +0000 (12:36 -0400)
The checkin and checkout grids in the AngularJS client have
been doing full grid refreshes when adding a checkin or
checkout to their respective grids. While this does not
result in re-fetching data for the loans that were already
processed, as more entries get added to the grid the time
it takes to do a full digest of the grid contents during a
egGrid.collect() (which empties the list of displayed rows,
then refills it), gets progressively longer. Grids that have
only ~40 entries have been observed to take several seconds
purely on the AngularJS rendering phase.

This patch teaches egGrid a new prepend() method that
takes the first element from the underlying data source and
unshifts it onto the list of displayed grid rows, saving much
rendering time. The prepend() method will also force the
grid offset back to 0 if it isn't already. Note that if
an item that would be added via prepend() might duplicate an
existing row entry, prepend() will do a full collect() instead.

If the data source has sort options set, the prepend() will
remove them. For arrayNotifier-based data sources, as are used
in the checkin and checkout grids, this means that if the user
sorts the contents of the grid, then does a circ transaction,
the new transaction will still appear at the top of the list.
Due to the way arrayNotifier currently works, the remaining
entries will retain their previous ordering.

As an implementation note, prepend() is likely going to work
/only/ for arrayNotifier grid data sources.

To test
-------
[1] In the checkin grid, check in a large number of items.
    Note that the time it takes to each each item gets
    progressively longer.
[2] Apply the patch and repeat step 1. This time, the time
    for each checkin should not significantly vary.
[3] Verify that column sorting works as expected.
[4] Upon sorting the grid, do more checkins and note that
    the new transactions show up at the top.
[5] Verify that the checkout grid continues to behave as expected.

Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
Open-ILS/web/js/ui/default/staff/services/grid.js

index 6d71f6c..965529b 100644 (file)
@@ -233,9 +233,13 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
                     {already_checked_in : final_resp.evt.copy_barcode};
             }
 
-            if ($scope.trim_list && checkinSvc.checkins.length > 20)
+            if ($scope.trim_list && checkinSvc.checkins.length > 20) {
                 //cut array short at 20 items
                 checkinSvc.checkins.length = 20;
+                checkinGrid.prepend(20);
+            } else {
+                checkinGrid.prepend();
+            }
         },
         function() {
             // Checkin was rejected somewhere along the way.
@@ -249,9 +253,7 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
             checkinSvc.checkins.splice(pos, 1);
 
         })['finally'](function() {
-
-            // when all is said and done, refresh the grid and refocus
-            checkinGrid.refresh();
+            // when all is said and done, refocus
             $scope.focusMe = true;
         });
     }
index f3df2d8..d79811c 100644 (file)
@@ -189,7 +189,7 @@ function($scope , $q , $routeParams , egCore , egUser , patronSvc ,
         };
 
         $scope.checkouts.unshift(row_item);
-        $scope.gridDataProvider.refresh();
+        $scope.gridDataProvider.prepend();
 
         egCore.hatch.setItem('circ.checkout.strict_barcode', $scope.strict_barcode);
         var options = {check_barcode : $scope.strict_barcode};
index 9ee646a..3998abb 100644 (file)
@@ -339,6 +339,10 @@ angular.module('egGridMod',
                     grid.collect();
                 }
 
+                controls.prepend = function(limit) {
+                    grid.prepend(limit);
+                }
+
                 controls.setLimit = function(limit,forget) {
                     grid.limit = limit;
                     if (!forget && grid.persistKey) {
@@ -360,6 +364,7 @@ angular.module('egGridMod',
                 }
 
                 grid.dataProvider.refresh = controls.refresh;
+                grid.dataProvider.prepend = controls.prepend;
                 grid.controls = controls;
             }
 
@@ -1322,6 +1327,72 @@ angular.module('egGridMod',
                 });
             }
 
+            grid.prepend = function(limit) {
+                var ran_into_duplicate = false;
+                var sort = grid.dataProvider.sort;
+                if (sort && sort.length) {
+                    // If sorting is in effect, we have no way
+                    // of knowing that the new item should be
+                    // visible _if the sort order is retained_.
+                    // However, since the grids that do prepending in
+                    // the first place are ones where we always
+                    // want the new row to show up on top, we'll
+                    // remove the current sort options.
+                    grid.dataProvider.sort = [];
+                }
+                if (grid.offset > 0) {
+                    // if we're prepending, we're forcing the
+                    // offset back to zero to display the top
+                    // of the list
+                    grid.offset = 0;
+                    grid.collect();
+                    return;
+                }
+                if (grid.collecting) return; // avoid parallel collect() or prepend()
+                grid.collecting = true;
+                console.debug('egGrid.prepend() starting');
+                // Note that we can count on the most-recently added
+                // item being at offset 0 in the data provider only
+                // for arrayNotifier data sources that do not have
+                // sort options currently set.
+                grid.dataProvider.get(0, 1).then(
+                null,
+                null,
+                function(item) {
+                    if (item) {
+                        var newIdx = grid.indexValue(item);
+                        angular.forEach($scope.items, function(existing) {
+                            if (grid.indexValue(existing) == newIdx) {
+                                console.debug('egGrid.prepend(): refusing to add duplicate item ' + newIdx);
+                                ran_into_duplicate = true;
+                                return;
+                            }
+                        });
+                        $scope.items.unshift(item);
+                        if (limit && $scope.items.length > limit) {
+                            // this accommodates the checkin grid that
+                            // allows the user to set a definite limit
+                            // without requiring that entire collect()
+                            $scope.items.length = limit;
+                        }
+                        if ($scope.items.length > grid.limit) {
+                            $scope.items.length = grid.limit;
+                        }
+                        if (grid.controls.itemRetrieved)
+                            grid.controls.itemRetrieved(item);
+                        if ($scope.selectAll)
+                            $scope.selected[grid.indexValue(item)] = true
+                    }
+                }).finally(function() {
+                    console.debug('egGrid.prepend() complete');
+                    grid.collecting = false;
+                    $scope.selected = angular.copy($scope.selected);
+                    if (ran_into_duplicate) {
+                        grid.collect();
+                    }
+                });
+            }
+
             grid.init();
         }]
     };
@@ -1912,6 +1983,7 @@ angular.module('egGridMod',
             // Calls the grid refresh function.  Once instantiated, the
             // grid will replace this function with it's own refresh()
             gridData.refresh = function(noReset) { }
+            gridData.prepend = function(limit) { }
 
             if (!gridData.get) {
                 // returns a promise whose notify() delivers items