ui: make sure data fetch bounds are aligned to resolution

This CL changes the TimelineFetcher to ensure that the bounds it requests
is a multiple of the resolution. If this is not the case, it can led to
very subtle rendering artifacts: this is especially bad if the no row is
shown when it should be.

Change-Id: I01e088d6cd8d886d6b0b8e8a6285d4e0ff53f67f
Bug: 338126907
diff --git a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256 b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
index aa136cd..1375a4a 100644
--- a/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
+++ b/test/data/ui-screenshots/ui-android_trace_30s_load.png.sha256
@@ -1 +1 @@
-b2c625bbdeb611de9a4bfbc3497353ee91322410bb867eee603bf2635353a21f
\ No newline at end of file
+3194c35d4ec8eaf318fa2edcbf8ba04a9d29b0093fd60248ee0c73e982a1766e
\ No newline at end of file
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 4adeee6..0b58e01 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {BigintMath} from '../base/bigint_math';
 import {Disposable} from '../base/disposable';
 import {duration, Time, time, TimeSpan} from '../base/time';
 export {Store} from '../base/store';
@@ -62,9 +63,14 @@
   async requestData(timespan: TimeSpan, resolution: duration): Promise<void> {
     if (this.shouldLoadNewData(timespan, resolution)) {
       // Over request data, one page worth to the left and right.
-      const start = Time.sub(timespan.start, timespan.duration);
-      const end = Time.add(timespan.end, timespan.duration);
-      this.latestTimespan = new TimeSpan(start, end);
+      const start = timespan.start - timespan.duration;
+      const end = timespan.end + timespan.duration;
+
+      // Quantize up and down to the bounds of |resolution|.
+      const startQ = Time.fromRaw(BigintMath.quantFloor(start, resolution));
+      const endQ = Time.fromRaw(BigintMath.quantCeil(end, resolution));
+
+      this.latestTimespan = new TimeSpan(startQ, endQ);
       this.latestResolution = resolution;
       await this.loadData();
     }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 39c38e5..7e013cc 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -687,11 +687,12 @@
       );
     }
 
+    const resolution = rawSlicesKey.bucketSize;
     const extraCols = this.extraSqlColumns.join(',');
     const queryRes = await this.engine.query(`
       SELECT
-        (z.ts / ${rawSlicesKey.bucketSize}) * ${rawSlicesKey.bucketSize} as tsQ,
-        max(z.dur, ${rawSlicesKey.bucketSize}) as durQ,
+        (z.ts / ${resolution}) * ${resolution} as tsQ,
+        ((z.dur / ${resolution}) + 1) * ${resolution} as durQ,
         s.ts as ts,
         s.dur as dur,
         s.id,
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 5c042dc..139bac5 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -47,8 +47,8 @@
 export interface Data extends TrackData {
   // Slices are stored in a columnar fashion. All fields have the same length.
   ids: Float64Array;
-  starts: BigInt64Array;
-  ends: BigInt64Array;
+  startQs: BigInt64Array;
+  endQs: BigInt64Array;
   utids: Uint32Array;
   flags: Uint8Array;
   lastRowId: number;
@@ -112,8 +112,8 @@
 
     const queryRes = await this.engine.query(`
       select
-        (z.ts / ${resolution}) * ${resolution} as ts,
-        max(z.dur, ${resolution}) as dur,
+        (z.ts / ${resolution}) * ${resolution} as tsQ,
+        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
         s.utid,
         s.id,
         s.dur = -1 as isIncomplete,
@@ -130,26 +130,23 @@
       length: numRows,
       lastRowId: this.lastRowId,
       ids: new Float64Array(numRows),
-      starts: new BigInt64Array(numRows),
-      ends: new BigInt64Array(numRows),
+      startQs: new BigInt64Array(numRows),
+      endQs: new BigInt64Array(numRows),
       utids: new Uint32Array(numRows),
       flags: new Uint8Array(numRows),
     };
 
     const it = queryRes.iter({
-      ts: LONG,
-      dur: LONG,
+      tsQ: LONG,
+      tsEndQ: LONG,
       utid: NUM,
       id: NUM,
       isIncomplete: NUM,
       isRealtime: NUM,
     });
     for (let row = 0; it.valid(); it.next(), row++) {
-      const start = it.ts;
-      const dur = it.dur;
-
-      slices.starts[row] = start;
-      slices.ends[row] = start + dur;
+      slices.startQs[row] = it.tsQ;
+      slices.endQs[row] = it.tsEndQ;
       slices.utids[row] = it.utid;
       slices.ids[row] = it.id;
 
@@ -201,8 +198,8 @@
   renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
     const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
       globals.timeline;
-    assertTrue(data.starts.length === data.ends.length);
-    assertTrue(data.starts.length === data.utids.length);
+    assertTrue(data.startQs.length === data.endQs.length);
+    assertTrue(data.startQs.length === data.utids.length);
 
     const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
 
@@ -213,15 +210,15 @@
     const startTime = visibleTimeSpan.start;
     const endTime = visibleTimeSpan.end;
 
-    const rawStartIdx = data.ends.findIndex((end) => end >= startTime);
+    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
     const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
 
-    const [, rawEndIdx] = searchSegment(data.starts, endTime);
-    const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx;
+    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
+    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
 
     for (let i = startIdx; i < endIdx; i++) {
-      const tStart = Time.fromRaw(data.starts[i]);
-      let tEnd = Time.fromRaw(data.ends[i]);
+      const tStart = Time.fromRaw(data.startQs[i]);
+      let tEnd = Time.fromRaw(data.endQs[i]);
       const utid = data.utids[i];
 
       // If the last slice is incomplete, it should end with the end of the
@@ -320,8 +317,8 @@
     if (selection !== null && selection.kind === 'SLICE') {
       const [startIndex, endIndex] = searchEq(data.ids, selection.id);
       if (startIndex !== endIndex) {
-        const tStart = Time.fromRaw(data.starts[startIndex]);
-        const tEnd = Time.fromRaw(data.ends[startIndex]);
+        const tStart = Time.fromRaw(data.startQs[startIndex]);
+        const tEnd = Time.fromRaw(data.endQs[startIndex]);
         const utid = data.utids[startIndex];
         const color = colorForThread(globals.threads.get(utid));
         const rectStart = visibleTimeScale.timeToPx(tStart);
@@ -413,9 +410,9 @@
     const t = visibleTimeScale.pxToHpTime(pos.x);
     let hoveredUtid = -1;
 
-    for (let i = 0; i < data.starts.length; i++) {
-      const tStart = Time.fromRaw(data.starts[i]);
-      const tEnd = Time.fromRaw(data.ends[i]);
+    for (let i = 0; i < data.startQs.length; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      const tEnd = Time.fromRaw(data.endQs[i]);
       const utid = data.utids[i];
       if (t.gte(tStart) && t.lt(tEnd)) {
         hoveredUtid = utid;
@@ -442,7 +439,7 @@
     if (data === undefined) return false;
     const {visibleTimeScale} = globals.timeline;
     const time = visibleTimeScale.pxToHpTime(x);
-    const index = search(data.starts, time.toTime());
+    const index = search(data.startQs, time.toTime());
     const id = index === -1 ? undefined : data.ids[index];
     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
     if (!id || this.utidHoveredInThisTrack === -1) return false;