ui: add tracks to list of searchable items

Change-Id: I128a0a04fb8714f51caa09ef8b749f5c74baae72
diff --git a/ui/src/common/search_data.ts b/ui/src/common/search_data.ts
index b81dfb2..7209c04 100644
--- a/ui/src/common/search_data.ts
+++ b/ui/src/common/search_data.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+export type SearchSource = 'cpu' | 'log' | 'slice' | 'track';
+
 export interface SearchSummary {
   tsStarts: BigInt64Array;
   tsEnds: BigInt64Array;
@@ -19,10 +21,10 @@
 }
 
 export interface CurrentSearchResults {
-  sliceIds: Float64Array;
-  tsStarts: BigInt64Array;
+  eventIds: Float64Array;
+  tses: BigInt64Array;
   utids: Float64Array;
   trackKeys: string[];
-  sources: string[];
+  sources: SearchSource[];
   totalResults: number;
 }
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 5caff6e..61ada93 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -15,7 +15,11 @@
 import {sqliteString} from '../base/string_utils';
 import {Duration, duration, Span, time, Time, TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
-import {CurrentSearchResults, SearchSummary} from '../common/search_data';
+import {
+  CurrentSearchResults,
+  SearchSource,
+  SearchSummary,
+} from '../common/search_data';
 import {OmniboxState} from '../common/state';
 import {globals} from '../frontend/globals';
 import {publishSearch, publishSearchResult} from '../frontend/publish';
@@ -100,8 +104,8 @@
         count: new Uint8Array(0),
       });
       publishSearchResult({
-        sliceIds: new Float64Array(0),
-        tsStarts: new BigInt64Array(0),
+        eventIds: new Float64Array(0),
+        tses: new BigInt64Array(0),
         utids: new Float64Array(0),
         sources: [],
         trackKeys: [],
@@ -203,11 +207,9 @@
     // easier once the track table has entries for all the tracks.
     const cpuToTrackId = new Map();
     for (const track of Object.values(globals.state.tracks)) {
-      if (exists(track?.uri)) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key);
-        }
+      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
+      if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.cpu) && cpuToTrackId.set(trackInfo.cpu, track.key);
       }
     }
 
@@ -220,60 +222,83 @@
       utids.push(it.utid);
     }
 
-    const queryRes = await this.query(`
-    select
-      id as sliceId,
-      ts,
-      'cpu' as source,
-      cpu as sourceId,
-      utid
-    from sched where utid in (${utids.join(',')})
-    union
-    select
-      slice_id as sliceId,
-      ts,
-      'track' as source,
-      track_id as sourceId,
-      0 as utid
-      from slice
-      where slice.name glob ${searchLiteral}
-        or (
-          0 != CAST(${sqliteString(search)} AS INT) and
-          sliceId = CAST(${sqliteString(search)} AS INT)
-        )
-    union
-    select
-      slice_id as sliceId,
-      ts,
-      'track' as source,
-      track_id as sourceId,
-      0 as utid
-      from slice
-      join args using(arg_set_id)
-      where string_value glob ${searchLiteral} or key glob ${searchLiteral}
-    union
-    select
-      id as sliceId,
-      ts,
-      'log' as source,
-      0 as sourceId,
-      utid
-    from android_logs where msg glob ${searchLiteral}
-    order by ts
-
+    const res = await this.query(`
+      select
+        id as sliceId,
+        ts,
+        'cpu' as source,
+        cpu as sourceId,
+        utid
+      from sched where utid in (${utids.join(',')})
+      union all
+      select *
+      from (
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+          from slice
+          where slice.name glob ${searchLiteral}
+            or (
+              0 != CAST(${sqliteString(search)} AS INT) and
+              sliceId = CAST(${sqliteString(search)} AS INT)
+            )
+        union
+        select
+          slice_id as sliceId,
+          ts,
+          'slice' as source,
+          track_id as sourceId,
+          0 as utid
+        from slice
+        join args using(arg_set_id)
+        where string_value glob ${searchLiteral} or key glob ${searchLiteral}
+      )
+      union all
+      select
+        id as sliceId,
+        ts,
+        'log' as source,
+        0 as sourceId,
+        utid
+      from android_logs where msg glob ${searchLiteral}
+      order by ts
     `);
 
-    const rows = queryRes.numRows();
     const searchResults: CurrentSearchResults = {
-      sliceIds: new Float64Array(rows),
-      tsStarts: new BigInt64Array(rows),
-      utids: new Float64Array(rows),
-      trackKeys: [],
+      eventIds: new Float64Array(0),
+      tses: new BigInt64Array(0),
+      utids: new Float64Array(0),
       sources: [],
+      trackKeys: [],
       totalResults: 0,
     };
 
-    const it = queryRes.iter({
+    const lowerSearch = search.toLowerCase();
+    for (const track of Object.values(globals.state.tracks)) {
+      if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
+        continue;
+      }
+      searchResults.totalResults++;
+      searchResults.sources.push('track');
+      searchResults.trackKeys.push(track.key);
+    }
+
+    const rows = res.numRows();
+    searchResults.eventIds = new Float64Array(
+      searchResults.totalResults + rows,
+    );
+    searchResults.tses = new BigInt64Array(searchResults.totalResults + rows);
+    searchResults.utids = new Float64Array(searchResults.totalResults + rows);
+    for (let i = 0; i < searchResults.totalResults; ++i) {
+      searchResults.eventIds[i] = -1;
+      searchResults.tses[i] = -1n;
+      searchResults.utids[i] = -1;
+    }
+
+    const it = res.iter({
       sliceId: NUM,
       ts: LONG,
       source: STR,
@@ -284,7 +309,7 @@
       let trackId = undefined;
       if (it.source === 'cpu') {
         trackId = cpuToTrackId.get(it.sourceId);
-      } else if (it.source === 'track') {
+      } else if (it.source === 'slice') {
         trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
       } else if (it.source === 'log') {
         const logTracks = Object.values(globals.state.tracks).filter(
@@ -305,9 +330,9 @@
 
       const i = searchResults.totalResults++;
       searchResults.trackKeys.push(trackId);
-      searchResults.sources.push(it.source);
-      searchResults.sliceIds[i] = it.sliceId;
-      searchResults.tsStarts[i] = it.ts;
+      searchResults.sources.push(it.source as SearchSource);
+      searchResults.eventIds[i] = it.sliceId;
+      searchResults.tses[i] = it.ts;
       searchResults.utids[i] = it.utid;
     }
     return searchResults;
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 8fc392d..68e16a5 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -276,8 +276,8 @@
   private _publishRedraw?: () => void = undefined;
 
   private _currentSearchResults: CurrentSearchResults = {
-    sliceIds: new Float64Array(0),
-    tsStarts: new BigInt64Array(0),
+    eventIds: new Float64Array(0),
+    tses: new BigInt64Array(0),
     utids: new Float64Array(0),
     trackKeys: [],
     sources: [],
@@ -648,8 +648,8 @@
     this._numQueriesQueued = 0;
     this._metricResult = undefined;
     this._currentSearchResults = {
-      sliceIds: new Float64Array(0),
-      tsStarts: new BigInt64Array(0),
+      eventIds: new Float64Array(0),
+      tses: new BigInt64Array(0),
       utids: new Float64Array(0),
       trackKeys: [],
       sources: [],
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index d9e3f0d..3fcc178 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -13,8 +13,10 @@
 // limitations under the License.
 
 import {searchSegment} from '../base/binary_search';
+import {assertUnreachable} from '../base/logging';
 import {Actions} from '../common/actions';
 import {globals} from './globals';
+import {verticalScrollToTrack} from './scroll_helper';
 
 function setToPrevious(current: number) {
   let index = current - 1;
@@ -34,7 +36,7 @@
   const vizWindow = globals.stateVisibleTime();
   const startNs = vizWindow.start;
   const endNs = vizWindow.end;
-  const currentTs = globals.currentSearchResults.tsStarts[index];
+  const currentTs = globals.currentSearchResults.tses[index];
 
   // If the value of |globals.currentSearchResults.totalResults| is 0,
   // it means that the query is in progress or no results are found.
@@ -44,12 +46,12 @@
 
   // If this is a new search or the currentTs is not in the viewport,
   // select the first/last item in the viewport.
-  if (index === -1 || currentTs < startNs || currentTs > endNs) {
+  if (
+    index === -1 ||
+    (currentTs !== -1n && (currentTs < startNs || currentTs > endNs))
+  ) {
     if (reverse) {
-      const [smaller] = searchSegment(
-        globals.currentSearchResults.tsStarts,
-        endNs,
-      );
+      const [smaller] = searchSegment(globals.currentSearchResults.tses, endNs);
       // If there is no item in the viewport just go to the previous.
       if (smaller === -1) {
         setToPrevious(index);
@@ -58,7 +60,7 @@
       }
     } else {
       const [, larger] = searchSegment(
-        globals.currentSearchResults.tsStarts,
+        globals.currentSearchResults.tses,
         startNs,
       );
       // If there is no item in the viewport just go to the next.
@@ -82,52 +84,61 @@
 function selectCurrentSearchResult() {
   const searchIndex = globals.state.searchIndex;
   const source = globals.currentSearchResults.sources[searchIndex];
-  const currentId = globals.currentSearchResults.sliceIds[searchIndex];
+  const currentId = globals.currentSearchResults.eventIds[searchIndex];
   const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
 
   if (currentId === undefined) return;
 
-  if (source === 'cpu') {
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id: currentId,
-        trackKey,
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-  } else if (source === 'log') {
-    globals.setLegacySelection(
-      {
-        kind: 'LOG',
-        id: currentId,
-        trackKey,
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-  } else {
-    // Search results only include slices from the slice table for now.
-    // When we include annotations we need to pass the correct table.
-    globals.setLegacySelection(
-      {
-        kind: 'CHROME_SLICE',
-        id: currentId,
-        trackKey,
-        table: 'slice',
-      },
-      {
-        clearSearch: false,
-        pendingScrollId: currentId,
-        switchToCurrentSelectionTab: true,
-      },
-    );
+  switch (source) {
+    case 'track':
+      verticalScrollToTrack(trackKey, true);
+      break;
+    case 'cpu':
+      globals.setLegacySelection(
+        {
+          kind: 'SLICE',
+          id: currentId,
+          trackKey,
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    case 'log':
+      globals.setLegacySelection(
+        {
+          kind: 'LOG',
+          id: currentId,
+          trackKey,
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    case 'slice':
+      // Search results only include slices from the slice table for now.
+      // When we include annotations we need to pass the correct table.
+      globals.setLegacySelection(
+        {
+          kind: 'CHROME_SLICE',
+          id: currentId,
+          trackKey,
+          table: 'slice',
+        },
+        {
+          clearSearch: false,
+          pendingScrollId: currentId,
+          switchToCurrentSelectionTab: true,
+        },
+      );
+      break;
+    default:
+      assertUnreachable(source);
   }
 }
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 2703720..90f3e4c 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -84,19 +84,21 @@
       );
     }
     const index = globals.state.searchIndex;
-    if (index !== -1 && index < globals.currentSearchResults.tsStarts.length) {
-      const start = globals.currentSearchResults.tsStarts[index];
-      const triangleStart =
-        Math.max(visibleTimeScale.timeToPx(Time.fromRaw(start)), 0) +
-        TRACK_SHELL_WIDTH;
-      ctx.fillStyle = '#000';
-      ctx.beginPath();
-      ctx.moveTo(triangleStart, size.height);
-      ctx.lineTo(triangleStart - 3, 0);
-      ctx.lineTo(triangleStart + 3, 0);
-      ctx.lineTo(triangleStart, size.height);
-      ctx.fill();
-      ctx.closePath();
+    if (index !== -1 && index < globals.currentSearchResults.tses.length) {
+      const start = globals.currentSearchResults.tses[index];
+      if (start !== -1n) {
+        const triangleStart =
+          Math.max(visibleTimeScale.timeToPx(Time.fromRaw(start)), 0) +
+          TRACK_SHELL_WIDTH;
+        ctx.fillStyle = '#000';
+        ctx.beginPath();
+        ctx.moveTo(triangleStart, size.height);
+        ctx.lineTo(triangleStart - 3, 0);
+        ctx.lineTo(triangleStart + 3, 0);
+        ctx.lineTo(triangleStart, size.height);
+        ctx.fill();
+        ctx.closePath();
+      }
     }
 
     ctx.restore();