perfetto-ui: Quantize small chrome slices

To avoid having to ignore small chrome slices we instead quantize
them into 1px width slices. Larger slices are displayed as usual.

When zoomed enough all slices will be displayed.

Open chrome example on https://taylori-dot-perfetto-ui.appspot.com

Bug:130829957
Change-Id: Ic179df56a25c39a045cf23018656e73689f8a83d
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index 186491a..55fb7e9 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -23,68 +23,139 @@
 class ChromeSliceTrackController extends TrackController<Config, Data> {
   static readonly kind = SLICE_TRACK_KIND;
   private busy = false;
+  private setup = false;
 
-  onBoundsChange(start: number, end: number, resolution: number) {
+  onBoundsChange(start: number, end: number, resolution: number): void {
+    this.update(start, end, resolution);
+  }
+
+  private async update(start: number, end: number, resolution: number) {
     // TODO: we should really call TraceProcessor.Interrupt() at this point.
     if (this.busy) return;
+
+    const startNs = Math.round(start * 1e9);
+    const endNs = Math.round(end * 1e9);
+    // Ns in 1px width. We want all slices smaller than 1px to be grouped.
+    const minNs = Math.round(resolution * 1e9);
     const LIMIT = 10000;
-
-    // TODO: "ts >= x - dur" below is inefficient because doesn't allow to use
-    // any index. We need to introduce ts_lower_bound also for the slices table
-    // (see sched table).
-    const query = `select ts,dur,depth,cat,name from slices ` +
-        `where utid = ${this.config.utid} ` +
-        `and ts >= ${Math.round(start * 1e9)} - dur ` +
-        `and ts <= ${Math.round(end * 1e9)} ` +
-        `and dur >= ${Math.round(resolution * 1e9)} ` +
-        `order by ts ` +
-        `limit ${LIMIT};`;
-
     this.busy = true;
-    this.engine.query(query).then(rawResult => {
-      this.busy = false;
-      if (rawResult.error) {
-        throw new Error(`Query error "${query}": ${rawResult.error}`);
-      }
 
-      const numRows = +rawResult.numRecords;
+    if (!this.setup) {
+      await this.query(
+          `create virtual table ${this.tableName('window')} using window;`);
 
-      const slices: Data = {
-        start,
-        end,
-        resolution,
-        strings: [],
-        starts: new Float64Array(numRows),
-        ends: new Float64Array(numRows),
-        depths: new Uint16Array(numRows),
-        titles: new Uint16Array(numRows),
-        categories: new Uint16Array(numRows),
-      };
+      await this.query(
+          `create view ${this.tableName('small')} as ` +
+          `select ts,dur,depth,cat,name from slices ` +
+          `where utid = ${this.config.utid} ` +
+          `and ts >= ${startNs} - dur ` +
+          `and ts <= ${endNs} ` +
+          `and dur < ${minNs} ` +
+          `order by ts ` +
+          `limit ${LIMIT};`);
 
-      const stringIndexes = new Map<string, number>();
-      function internString(str: string) {
-        let idx = stringIndexes.get(str);
-        if (idx !== undefined) return idx;
-        idx = slices.strings.length;
-        slices.strings.push(str);
-        stringIndexes.set(str, idx);
-        return idx;
-      }
+      await this.query(`create virtual table ${this.tableName('span')} using
+      span_join(${this.tableName('small')},
+      ${this.tableName('window')});`);
 
-      for (let row = 0; row < numRows; row++) {
-        const cols = rawResult.columns;
-        const startSec = fromNs(+cols[0].longValues![row]);
-        slices.starts[row] = startSec;
-        slices.ends[row] = startSec + fromNs(+cols[1].longValues![row]);
-        slices.depths[row] = +cols[2].longValues![row];
-        slices.categories[row] = internString(cols[3].stringValues![row]);
-        slices.titles[row] = internString(cols[4].stringValues![row]);
-      }
-      if (numRows === LIMIT) {
-        slices.end = slices.ends[slices.ends.length - 1];
-      }
-      this.publish(slices);
-    });
+      this.setup = true;
+    }
+
+    const windowDurNs = Math.max(1, endNs - startNs);
+
+    this.query(`update ${this.tableName('window')} set
+    window_start=${startNs},
+    window_dur=${windowDurNs},
+    quantum=${minNs}`);
+
+    await this.query(`drop view if exists ${this.tableName('small')}`);
+    await this.query(`drop view if exists ${this.tableName('big')}`);
+    await this.query(`drop view if exists ${this.tableName('summary')}`);
+
+    await this.query(
+        `create view ${this.tableName('small')} as ` +
+        `select ts,dur,depth,cat,name from slices ` +
+        `where utid = ${this.config.utid} ` +
+        `and ts >= ${startNs} - dur ` +
+        `and ts <= ${endNs} ` +
+        `and dur < ${minNs} ` +
+        `order by ts `);
+
+    await this.query(
+        `create view ${this.tableName('big')} as ` +
+        `select ts,dur,depth,cat,name from slices ` +
+        `where utid = ${this.config.utid} ` +
+        `and ts >= ${startNs} - dur ` +
+        `and ts <= ${endNs} ` +
+        `and dur >= ${minNs} ` +
+        `order by ts `);
+
+    await this.query(`create view ${this.tableName('summary')} as select
+      min(ts) as ts,
+      ${minNs} as dur,
+      depth,
+      cat,
+      'Busy' as name
+      from ${this.tableName('span')}
+      group by cat, depth, quantum_ts
+      limit ${LIMIT};`);
+
+    const query = `select * from ${this.tableName('summary')} UNION ` +
+        `select * from ${this.tableName('big')} order by ts`;
+
+    const rawResult = await this.query(query);
+    this.busy = false;
+
+    if (rawResult.error) {
+      throw new Error(`Query error "${query}": ${rawResult.error}`);
+    }
+
+    const numRows = +rawResult.numRecords;
+
+    const slices: Data = {
+      start,
+      end,
+      resolution,
+      strings: [],
+      starts: new Float64Array(numRows),
+      ends: new Float64Array(numRows),
+      depths: new Uint16Array(numRows),
+      titles: new Uint16Array(numRows),
+      categories: new Uint16Array(numRows),
+    };
+
+    const stringIndexes = new Map<string, number>();
+    function internString(str: string) {
+      let idx = stringIndexes.get(str);
+      if (idx !== undefined) return idx;
+      idx = slices.strings.length;
+      slices.strings.push(str);
+      stringIndexes.set(str, idx);
+      return idx;
+    }
+
+    for (let row = 0; row < numRows; row++) {
+      const cols = rawResult.columns;
+      const startSec = fromNs(+cols[0].longValues![row]);
+      slices.starts[row] = startSec;
+      slices.ends[row] = startSec + fromNs(+cols[1].longValues![row]);
+      slices.depths[row] = +cols[2].longValues![row];
+      slices.categories[row] = internString(cols[3].stringValues![row]);
+      slices.titles[row] = internString(cols[4].stringValues![row]);
+    }
+    if (numRows === LIMIT) {
+      slices.end = slices.ends[slices.ends.length - 1];
+    }
+    this.publish(slices);
+  }
+
+  private async query(query: string) {
+    const result = await this.engine.query(query);
+    if (result.error) {
+      console.error(`Query error "${query}": ${result.error}`);
+      throw new Error(`Query error "${query}": ${result.error}`);
+    }
+    return result;
   }
 }
 
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index d83da53..1e01e31 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -91,7 +91,6 @@
       const rectXStart = Math.max(timeScale.timeToPx(tStart), 0);
       const rectXEnd = Math.min(timeScale.timeToPx(tEnd), pxEnd);
       const rectWidth = rectXEnd - rectXStart;
-      if (rectWidth < 0.1) continue;
       const rectYStart = TRACK_PADDING + depth * SLICE_HEIGHT;
 
       const hovered = titleId === this.hoveredTitleId;