[current results] Add support for paging to getResults query

Change-Id: Ie0e69550ace2647d30a0d5f198dff2c656825197
Reviewed-on: https://dart-review.googlesource.com/c/dart_ci/+/160560
Reviewed-by: Karl Klose <karlklose@google.com>
diff --git a/current_results/bin/client.dart b/current_results/bin/client.dart
index 6fc7975..5f7fb2c 100644
--- a/current_results/bin/client.dart
+++ b/current_results/bin/client.dart
@@ -43,6 +43,7 @@
             'comma-separated, to fetch results for');
     argParser.addOption('limit',
         abbr: 'l', help: 'number of results to return');
+    argParser.addOption('page', help: 'page token returned from previous call');
   }
   String get name => 'getResults';
   String get description => 'Send a GetResults gRPC request to the server';
@@ -53,6 +54,9 @@
     if (argResults['limit'] != null) {
       request.pageSize = int.parse(argResults['limit']);
     }
+    if (argResults['page'] != null) {
+      request.pageToken = argResults['page'];
+    }
 
     final result = await QueryClient(channel).getResults(request);
     print(result.toProto3Json());
diff --git a/current_results/lib/src/slice.dart b/current_results/lib/src/slice.dart
index 1b6ae30..6ddd550 100644
--- a/current_results/lib/src/slice.dart
+++ b/current_results/lib/src/slice.dart
@@ -92,6 +92,11 @@
   query_api.GetResultsResponse results(query_api.GetResultsRequest query) {
     final response = query_api.GetResultsResponse();
     final limit = min(100000, query.pageSize == 0 ? 100000 : query.pageSize);
+    // TODO(whesse): Change the implementation to return results sorted by test name first.
+    // This will be less efficient, but it is much better for the clients.
+    // The page token will change to a test name and configuration when this is done.
+    final skip =
+        max(0, query.pageToken.isEmpty ? 0 : int.parse(query.pageToken));
     final filterTerms =
         query.filter.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty);
     final configurationSet = Set<String>();
@@ -105,6 +110,13 @@
         configurationSet.addAll(matchingConfigurations);
       }
     }
+    testPrefixes..sort();
+    for (int i = 0; i < testPrefixes.length; ++i) {
+      while (i + 1 < testPrefixes.length &&
+          testPrefixes[i + 1].startsWith(testPrefixes[i])) {
+        testPrefixes.removeAt(i + 1);
+      }
+    }
     final configurations =
         (configurationSet.isEmpty ? _stored.keys : configurationSet).toList()
           ..sort();
@@ -113,17 +125,22 @@
       response.results.addAll(configurations
           .expand((configuration) => _stored[configuration])
           .map(Result.toApi)
+          .skip(skip)
           .take(limit));
+      if (response.results.length >= limit) {
+        response.nextPageToken = (skip + response.results.length).toString();
+      }
       return response;
     }
 
+    var skipped = 0;
     for (final configuration in configurations) {
       final sorted = _stored[configuration];
       for (final testNamePrefix in testPrefixes) {
         if (response.results.length >= limit) break;
         final prefixResult =
             Result(testNamePrefix, null, null, null, null, null, null);
-        final start =
+        var start =
             lowerBound<Result>(sorted, prefixResult, compare: compareNames);
         if (start < sorted.length &&
             sorted[start].name.startsWith(testNamePrefix)) {
@@ -133,13 +150,23 @@
             end = lowerBound<Result>(sorted, prefixResult,
                 compare: compareNamesPrefixMatchesBelow);
           }
-          response.results.addAll(sorted
-              .getRange(start, end)
-              .map(Result.toApi)
-              .take(limit - response.results.length));
+          if (end - start > skip - skipped) {
+            // No-op if skipped == skip
+            start += skip - skipped;
+            skipped = skip;
+            response.results.addAll(sorted
+                .getRange(start, end)
+                .map(Result.toApi)
+                .take(limit - response.results.length));
+          } else {
+            skipped += end - start;
+          }
         }
       }
     }
+    if (response.results.length >= limit) {
+      response.nextPageToken = (skip + response.results.length).toString();
+    }
     return response;
   }