perfetto-ui: Group together short thread states

- Select all long thread states and display everything else as 'Busy'.
- When zoomed enough, show all detail.
- When grouping states, show any rect with a width < 1px as 1px width.

https://taylori-dot-perfetto-ui.appspot.com/#!/?s=a85d21e477f26c1d5a9c3a20ef5c1296bcd645f9deac26305aba8f7895d32dc6

Bug:130830681
Change-Id: I13d770cb5daa556dcb15a6719c67ea3c3563d97b
diff --git a/ui/src/common/thread_state.ts b/ui/src/common/thread_state.ts
index 6008cf8..1215f4f 100644
--- a/ui/src/common/thread_state.ts
+++ b/ui/src/common/thread_state.ts
@@ -30,7 +30,9 @@
 
 export function translateState(state: string|undefined) {
   if (state === undefined) return '';
-  if (state === 'Running' || state === 'Runnable') return state;
+  if (state === 'Running' || state === 'Runnable' || state === 'Busy') {
+    return state;
+  }
   let result = states[state[0]];
   for (let i = 1; i < state.length; i++) {
     result += state[i] === '+' ? ' ' : ' + ';
diff --git a/ui/src/frontend/colorizer.ts b/ui/src/frontend/colorizer.ts
index 7522e7f..e42f42c 100644
--- a/ui/src/frontend/colorizer.ts
+++ b/ui/src/frontend/colorizer.ts
@@ -65,6 +65,7 @@
 export function colorForState(state: string): Color {
   switch (state) {
     case 'Running':
+    case 'Busy':
       return {c: 'dark green', h: 120, s: 44, l: 34};
     case 'Runnable':
     case 'R':
diff --git a/ui/src/tracks/thread_state/common.ts b/ui/src/tracks/thread_state/common.ts
index 32b32c4..fe6def0 100644
--- a/ui/src/tracks/thread_state/common.ts
+++ b/ui/src/tracks/thread_state/common.ts
@@ -26,3 +26,7 @@
 }
 
 export interface Config { utid: number; }
+
+export function groupBusyStates(resolution: number) {
+  return resolution >= 0.0001;
+}
diff --git a/ui/src/tracks/thread_state/controller.ts b/ui/src/tracks/thread_state/controller.ts
index 7b3e74e..c936ec5 100644
--- a/ui/src/tracks/thread_state/controller.ts
+++ b/ui/src/tracks/thread_state/controller.ts
@@ -21,6 +21,7 @@
 import {
   Config,
   Data,
+  groupBusyStates,
   THREAD_STATE_TRACK_KIND,
 } from './common';
 
@@ -40,17 +41,23 @@
 
     const startNs = Math.round(start * 1e9);
     const endNs = Math.round(end * 1e9);
+    let minNs = 0;
+    if (groupBusyStates(resolution)) {
+      // Ns for 20px (the smallest state to display)
+      minNs = Math.round(resolution * 20 * 1e9);
+    }
+
 
     if (this.setup === false) {
       await this.query(`create view ${this.tableName('sched_wakeup')} AS
-        select
-          ts,
-          lead(ts, 1, (select end_ts from trace_bounds))
-            OVER(order by ts) - ts as dur,
-          ref as utid
-        from instants
-        where name = 'sched_wakeup'
-        and utid = ${this.config.utid}`);
+      select
+        ts,
+        lead(ts, 1, (select end_ts from trace_bounds))
+          OVER(order by ts) - ts as dur,
+        ref as utid
+      from instants
+      where name = 'sched_wakeup'
+      and utid = ${this.config.utid}`);
 
       await this.query(
           `create virtual table ${this.tableName('window')} using window;`);
@@ -60,14 +67,11 @@
       await this.query(`create view ${this.tableName('start')} as
       select min(ts) as ts from
         (select ts from ${this.tableName('sched_wakeup')} UNION
-         select ts from sched where utid = ${this.config.utid})`);
+        select ts from sched where utid = ${this.config.utid})`);
 
       // Create an entry from first ts to either the first sched_wakeup
       // or to the end if there are no sched wakeups. This means
       // we will show all information we have even with no sched_wakeup events.
-      // TODO(taylori): Once span outer join exists I should simplify this
-      // by outer joining sched_wakeup and sched and then left joining with
-      // window.
       await this.query(`create view ${this.tableName('fill')} AS
         select
         (select ts from ${this.tableName('start')}),
@@ -103,23 +107,56 @@
         where utid = ${this.config.utid}
         window ${this.tableName('ordered')} as (order by ts)`);
 
+      await this.query(`create view ${this.tableName('long_states')} as
+      select * from ${this.tableName('span_view')} where dur >= ${minNs}`);
+
+      // Create a slice from the first ts to the end of the trace. To
+      // be span joined with the long states - This effectively combines all
+      // of the short states into a single 'Busy' state.
+      await this.query(`create view ${this.tableName('fill_gaps')} as select
+      (select min(ts) from ${this.tableName('span_view')}) as ts,
+      (select end_ts from trace_bounds) -
+      (select min(ts) from ${this.tableName('span_view')}) as dur,
+      ${this.config.utid} as utid`);
+
+      await this.query(`create virtual table ${this.tableName('summarized')}
+      using span_left_join(${this.tableName('fill_gaps')} partitioned utid,
+      ${this.tableName('long_states')} partitioned utid)`);
+
       await this.query(`create virtual table ${this.tableName('current')}
-        using span_join(
-          ${this.tableName('window')},
-          ${this.tableName('span_view')} partitioned utid)`);
+      using span_join(
+        ${this.tableName('window')},
+        ${this.tableName('summarized')} partitioned utid)`);
+
 
       this.setup = true;
     }
 
-    const windowDur = Math.max(1, endNs - startNs);
+    const windowDurNs = Math.max(1, endNs - startNs);
 
     this.query(`update ${this.tableName('window')} set
-      window_start = ${startNs},
-      window_dur = ${windowDur},
-      quantum = 0`);
+     window_start=${startNs},
+     window_dur=${windowDurNs},
+     quantum=0`);
+
+    this.query(`drop view if exists ${this.tableName('long_states')}`);
+    this.query(`drop view if exists ${this.tableName('fill_gaps')}`);
+
+    await this.query(`create view ${this.tableName('long_states')} as
+     select * from ${this.tableName('span_view')} where dur > ${minNs}`);
+
+    await this.query(`create view ${this.tableName('fill_gaps')} as select
+     (select min(ts) from ${this.tableName('span_view')}) as ts,
+     (select end_ts from trace_bounds) - (select min(ts) from ${
+                                                                this.tableName(
+                                                                    'span_view')
+                                                              }) as dur,
+     ${this.config.utid} as utid`);
 
     const query = `select ts, cast(dur as double), utid,
-      state from ${this.tableName('current')}`;
+    case when state is not null then state else 'Busy' end as state
+    from ${this.tableName('current')}`;
+
 
     const result = await this.query(query);
 
@@ -171,10 +208,13 @@
       this.query(`drop table ${this.tableName('window')}`);
       this.query(`drop table ${this.tableName('span')}`);
       this.query(`drop table ${this.tableName('current')}`);
+      this.query(`drop table ${this.tableName('summarized')}`);
       this.query(`drop view ${this.tableName('sched_wakeup')}`);
       this.query(`drop view ${this.tableName('fill')}`);
       this.query(`drop view ${this.tableName('full_sched_wakeup')}`);
       this.query(`drop view ${this.tableName('span_view')}`);
+      this.query(`drop view ${this.tableName('long_states')}`);
+      this.query(`drop view ${this.tableName('fill_gaps')}`);
       this.setup = false;
     }
   }
diff --git a/ui/src/tracks/thread_state/frontend.ts b/ui/src/tracks/thread_state/frontend.ts
index 78284e4..bed5937 100644
--- a/ui/src/tracks/thread_state/frontend.ts
+++ b/ui/src/tracks/thread_state/frontend.ts
@@ -26,6 +26,7 @@
 import {
   Config,
   Data,
+  groupBusyStates,
   THREAD_STATE_TRACK_KIND,
 } from './common';
 
@@ -76,7 +77,10 @@
         const rectEnd = timeScale.timeToPx(tEnd);
         const color = colorForState(state);
         ctx.fillStyle = `hsl(${color.h},${color.s}%,${color.l}%)`;
-        const rectWidth = rectEnd - rectStart;
+        let rectWidth = rectEnd - rectStart;
+        if (groupBusyStates(data.resolution) && rectWidth < 1) {
+          rectWidth = 1;
+        }
         ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
 
         // Don't render text when we have less than 5px to play with.