Merge "tracing: Add support for track event timestamps using custom clocks"
diff --git a/CHANGELOG b/CHANGELOG
index 67b6c6b..c01f292 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,7 +4,7 @@
   Trace Processor:
     *
   UI:
-    *
+    * Fixed ADB connection issues ("unable to reset device") on Windows and Mac.
   SDK:
     * Added support for writing track events using custom clock timestamps.
 
diff --git a/docs/toc.md b/docs/toc.md
index 15c1cae..a27779d 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -40,6 +40,7 @@
 * [Trace visualization](#)
   * [Perfetto UI](visualization/perfetto-ui.md)
   * [Visualising large traces](visualization/large-traces.md)
+  * [Deep linking to Perfetto UI](visualization/deep-linking-to-perfetto-ui.md)
 
 * [Core concepts](#)
   * [Trace configuration](concepts/config.md)
diff --git a/docs/visualization/deep-linking-to-perfetto-ui.md b/docs/visualization/deep-linking-to-perfetto-ui.md
new file mode 100644
index 0000000..f6a9225
--- /dev/null
+++ b/docs/visualization/deep-linking-to-perfetto-ui.md
@@ -0,0 +1,142 @@
+# Deep linking to the Perfetto UI
+
+This document describes how to open traces hosted on external servers with the
+Perfetto UI. This can help integrating the Perfetto UI with custom dashboards
+and implement _'Open with Perfetto UI'_-like features.
+
+## Using window.open and postMessage
+
+The supported way of doing this is to _inject_ the trace as an ArrayBuffer
+via `window.open('https://ui.perfetto.dev')` and `postMessage()`.
+In order to do this you need some minimal JavaScript code running on some
+hosting infrastructure you control which can access the trace file. In most
+cases this is some dashboard which you want to deep-link to the Perfetto UI.
+
+#### Open ui.perfetto.dev via window.open
+
+The source dashboard, the one that knows how to locate a trace and deal with
+ACL checking / oauth authentication and the like, creates a new tab by doing
+
+```js
+var handle = window.open('https://ui.perfetto.dev');
+```
+
+The window handle allows bidirectional communication using `postMessage()`
+between the source dashboard and the Perfetto UI.
+
+#### Wait for the UI to be ready via PING/PONG
+
+Wait for the UI to be ready. The `window.open()` message channel is not
+buffered. If you send a message before the opened page has registered an
+`onmessage` listener the messagge will be dropped on the floor.
+In order to avoid this race, you can use a very basic PING/PONG protocol: keep
+sending a 'PING' message until the opened window replies with a 'PONG'.
+When this happens, that is the signal that the Perfetto UI is ready to open
+traces.
+
+#### Post a message the following JavaScript object
+
+```js
+  {
+    'perfetto': {
+      buffer: ArrayBuffer;
+      title: string;
+      fileName?: string;  // Optional
+      url?: string;       // Optional
+    }
+  }
+```
+
+`buffer` is the ArrayBuffer with the actual trace file content. This is
+typically something that you obtain by doing a `fetch()` on your backend
+storage.
+
+`title` is the human friendly trace title that will be shown in the
+sidebar. This can help people to disambiguate traces from several tabs.
+
+`fileName` will be used if the user clicks on "Download". A generic name will
+be used if omitted.
+
+`url` is used if the user clicks on the "Share" link in the sidebar. This should
+print to a URL owned by you that would cause your dashboard to re-open the
+current trace, by re-kicking-off the window.open() process herein described.
+If omitted traces won't be shareable.
+
+### Code samples
+
+See https://jsfiddle.net/primiano/1hd0a4wj/68/ (also mirrored on
+[this GitHub gist](https://gist.github.com/primiano/e164868b617844ef8fa4770eb3b323b9)
+)
+
+Googlers: take a look at the
+[existing examples in the internal codesearch](http://go/perfetto-ui-deeplink-cs)
+
+### Common pitfalls
+
+Many browsers sometimes block window.open() requests prompting the user to allow
+popups for the site. This usually happens if:
+
+- The window.open() is NOT initiated by a user gesture.
+- Too much time is passed from the user gesture to the window.open()
+
+If the trace file is big enough, the fetch() might take long time and pass the
+user gesture threshold. This can be detected by observing that the window.open()
+returned `null`. When this happens the best option is to show another clickable
+element and bind the fetched trace ArrayBuffer to the new onclick handler, like
+the code in the example above does.
+
+Some browser can have a variable time threshold for the user gesture timeout
+which depends on the website engagement score (how much the user has visited
+the page that does the window.open() before). It's quite common when testing
+this code to see a popup blocker the first time the new feature is used and
+then not see it again.
+
+### Where does the posted trace go?
+
+The Perfetto UI is client-only and doesn't require any server-side interaction.
+Traces pushed via postMessage() are kept only in the browser memory/cache and
+are not sent to any server.
+
+## Why can't I just pass a URL?
+
+_"Why you don't let me just pass a URL to the Perfetto UI (e.g. ui.perfetto.dev?url=...) and you deal with all this?"_
+
+The answer to this is manifold and boils down to security.
+
+#### Cross origin requests blocking
+
+If ui.perfetto.dev had to do a `fetch('https://yourwebsite.com/trace')` that
+would be a cross-origin request. Browsers disallow by default cross-origin
+fetch requests.
+In order for this to work, the web server that hosts yourwebsite.com would have
+to expose a custom HTTP response header
+ (`Access-Control-Allow-Origin: https://ui.perfetto.dev`) to allow the fetch.
+In most cases customizing the HTTP response headers is outside of dashboard's
+owners control.
+
+You can learn more about CORS at
+https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
+
+#### Content Security Policy
+
+Perfetto UI uses a strict Content Security Policy which disallows foreign
+fetches and subresources, as a security mitigation about common attacks.
+Even assuming that CORS headers are properly set and your trace files are
+publicly accessible, fetching the trace from the Perfetto UI would require
+allow-listing your origin in our CSP policy. This is not scalable.
+
+You can learn more about CSP at
+https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
+
+#### Dealing with OAuth2 or other authentication mechanisms
+
+Even ignoring CORS, the Perfetto UI would have to deal with OAuth2 or other
+authentication mechanisms to fetch the trace file. Even if all the dashboards
+out there used OAuth2, that would still mean that Perfetto UI would have to know
+about all the possible OAuth2 scopes, one for each dashboard. This is not
+scalable.
+
+## Source links
+
+The source code that deals with the postMessage() in the Perfetto UI is
+[`post_message_handler.ts`](/ui/src/frontend/post_message_handler.ts)
diff --git a/src/profiling/memory/client_api.cc b/src/profiling/memory/client_api.cc
index 9c3f6d1..9444293 100644
--- a/src/profiling/memory/client_api.cc
+++ b/src/profiling/memory/client_api.cc
@@ -539,11 +539,8 @@
     return false;
   }
 
-  uint64_t heap_intervals[perfetto::base::ArraySize(g_heaps)] = {};
   uint32_t max_heap = g_next_heap_id.load();
-  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
-    heap_intervals[i] = MaybeToggleHeap(i, client.get());
-  }
+  bool heaps_enabled[perfetto::base::ArraySize(g_heaps)] = {};
 
   PERFETTO_LOG("%s: heapprofd_client initialized.", getprogname());
   {
@@ -557,15 +554,36 @@
     // random engine.
     for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
       AHeapInfo& heap = GetHeap(i);
-      if (heap_intervals[i]) {
-        heap.sampler.SetSamplingInterval(heap_intervals[i]);
+      if (!heap.ready.load(std::memory_order_acquire)) {
+        continue;
+      }
+      const uint64_t interval =
+          GetHeapSamplingInterval(client->client_config(), heap.heap_name);
+      if (interval) {
+        heaps_enabled[i] = true;
+        heap.sampler.SetSamplingInterval(interval);
       }
     }
 
     // This cannot have been set in the meantime. There are never two concurrent
     // calls to this function, as Bionic uses atomics to guard against that.
     PERFETTO_DCHECK(*GetClientLocked() == nullptr);
-    *GetClientLocked() = std::move(client);
+    *GetClientLocked() = client;
   }
+
+  // We want to run MaybeToggleHeap last to make sure we never enable a heap
+  // but subsequently return `false` from this function, which indicates to the
+  // caller that we did not enable anything.
+  //
+  // For startup profiles, `false` is used by Bionic to signal it can unload
+  // the library again.
+  for (uint32_t i = kMinHeapId; i < max_heap; ++i) {
+    if (!heaps_enabled[i]) {
+      continue;
+    }
+    auto interval = MaybeToggleHeap(i, client.get());
+    PERFETTO_DCHECK(interval > 0);
+  }
+
   return true;
 }
diff --git a/src/trace_processor/util/proto_to_args_parser.h b/src/trace_processor/util/proto_to_args_parser.h
index e6934a6..629ab1f 100644
--- a/src/trace_processor/util/proto_to_args_parser.h
+++ b/src/trace_processor/util/proto_to_args_parser.h
@@ -100,9 +100,12 @@
       static_assert(std::is_same<typename FieldMetadata::message_type,
                                  protos::pbzero::InternedData>::value,
                     "Field should belong to InternedData proto");
-      return GetInternedMessageView(FieldMetadata::kFieldId, iid)
-          ->template GetOrCreateDecoder<
-              typename FieldMetadata::cpp_field_type>();
+      auto* interned_message_view =
+          GetInternedMessageView(FieldMetadata::kFieldId, iid);
+      if (!interned_message_view)
+        return nullptr;
+      return interned_message_view->template GetOrCreateDecoder<
+          typename FieldMetadata::cpp_field_type>();
     }
 
    protected:
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 110e429..6d77b0f 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -13,17 +13,23 @@
 // limitations under the License.
 
 import {defer, Deferred} from '../base/deferred';
-import {assertExists} from '../base/logging';
+import {assertExists, assertTrue} from '../base/logging';
 import {perfetto} from '../gen/protos';
 
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   ComputeMetricArgs,
   ComputeMetricResult,
+  QueryArgs,
   RawQueryArgs,
   RawQueryResult
 } from './protos';
-import {iter, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
+import {NUM, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
+import {
+  createQueryResult,
+  QueryResult,
+  WritableQueryResult
+} from './query_result';
 import {TimeSpan} from './time';
 
 import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
@@ -42,6 +48,12 @@
 
 export class QueryError extends Error {}
 
+// This is used to skip the decoding of queryResult from protobufjs and deal
+// with it ourselves. See the comment below around `QueryResult.decode = ...`.
+interface QueryResultBypass {
+  rawQueryResult: Uint8Array;
+}
+
 /**
  * Abstract interface of a trace proccessor.
  * This is the TypeScript equivalent of src/trace_processor/rpc.h.
@@ -65,6 +77,7 @@
   private rxBuf = new ProtoRingBuffer();
   private pendingParses = new Array<Deferred<void>>();
   private pendingEOFs = new Array<Deferred<void>>();
+  private pendingQueries = new Array<WritableQueryResult>();
   private pendingRawQueries = new Array<Deferred<RawQueryResult>>();
   private pendingRestoreTables = new Array<Deferred<void>>();
   private pendingComputeMetrics = new Array<Deferred<ComputeMetricResult>>();
@@ -103,6 +116,37 @@
    * proto-encoded message (without the proto preamble and varint size).
    */
   private onRpcResponseMessage(rpcMsgEncoded: Uint8Array) {
+    // Here we override the protobufjs-generated code to skip the parsing of the
+    // new streaming QueryResult and instead passing it through like a buffer.
+    // This is the overall problem: All trace processor responses are wrapped
+    // into a perfetto.protos.TraceProcessorRpc proto message. In all cases %
+    // TPM_QUERY_STREAMING, we want protobufjs to decode the proto bytes and
+    // give us a structured object. In the case of TPM_QUERY_STREAMING, instead,
+    // we want to deal with the proto parsing ourselves using the new
+    // QueryResult.appendResultBatch() method, because that handled streaming
+    // results more efficiently and skips several copies.
+    // By overriding the decode method below, we achieve two things:
+    // 1. We avoid protobufjs decoding the TraceProcessorRpc.query_result field.
+    // 2. We stash (a view of) the original buffer into the |rawQueryResult| so
+    //    the `case TPM_QUERY_STREAMING` below can take it.
+    perfetto.protos.QueryResult.decode =
+        (reader: protobuf.Reader, length: number) => {
+          const res =
+              perfetto.protos.QueryResult.create() as {} as QueryResultBypass;
+          res.rawQueryResult =
+              reader.buf.subarray(reader.pos, reader.pos + length);
+          // All this works only if protobufjs returns the original ArrayBuffer
+          // from |rpcMsgEncoded|. It should be always the case given the
+          // current implementation. This check mainly guards against future
+          // behavioral changes of protobufjs. We don't want to accidentally
+          // hold onto some internal protobufjs buffer. We are fine holding
+          // onto |rpcMsgEncoded| because those come from ProtoRingBuffer which
+          // is buffer-retention-friendly.
+          assertTrue(res.rawQueryResult.buffer === rpcMsgEncoded.buffer);
+          reader.pos += length;
+          return res as {} as perfetto.protos.QueryResult;
+        };
+
     const rpc = TraceProcessorRpc.decode(rpcMsgEncoded);
     this.loadingTracker.endLoading();
 
@@ -137,7 +181,12 @@
         assertExists(this.pendingRestoreTables.shift()).resolve();
         break;
       case TPM.TPM_QUERY_STREAMING:
-        // TODO(primiano): In the next CLs wire up the streaming query decoder.
+        const qRes = assertExists(rpc.queryResult) as {} as QueryResultBypass;
+        const pendingQuery = assertExists(this.pendingQueries[0]);
+        pendingQuery.appendResultBatch(qRes.rawQueryResult);
+        if (pendingQuery.isComplete()) {
+          this.pendingQueries.shift();
+        }
         break;
       case TPM.TPM_QUERY_RAW_DEPRECATED:
         const queryRes = assertExists(rpc.rawQueryResult) as RawQueryResult;
@@ -266,6 +315,38 @@
     return res;
   }
 
+  /*
+   * Issues a streaming query and retrieve results in batches.
+   * The returned QueryResult object will be populated over time with batches
+   * of rows (each batch conveys ~128KB of data and a variable number of rows).
+   * The caller can decide whether to wait that all batches have been received
+   * (by awaiting the returned object or calling result.waitAllRows()) or handle
+   * the rows incrementally.
+   *
+   * Example usage:
+   * const res = engine.queryV2('SELECT foo, bar FROM table');
+   * console.log(res.numRows());  // Will print 0 because we didn't await.
+   * await(res.waitAllRows());
+   * console.log(res.numRows());  // Will print the total number of rows.
+   *
+   * for (const it = res.iter({foo: NUM, bar:STR}); it.valid(); it.next()) {
+   *   console.log(it.foo, it.bar);
+   * }
+   * TODO(primiano): in next CLs move everything on queryV2, then rename it to
+   * just query(), and delete the old (columnar, non-streaming) query() method.
+   */
+  queryV2(sqlQuery: string): Promise<QueryResult>&QueryResult {
+    const rpc = TraceProcessorRpc.create();
+    rpc.request = TPM.TPM_QUERY_STREAMING;
+    rpc.queryArgs = new QueryArgs();
+    rpc.queryArgs.sqlQuery = sqlQuery;
+    rpc.queryArgs.timeQueuedNs = Math.floor(performance.now() * 1e6);
+    const result = createQueryResult();
+    this.pendingQueries.push(result);
+    this.rpcSendRequest(rpc);
+    return result;
+  }
+
   /**
    * Marshals the TraceProcessorRpc request arguments and sends the request
    * to the concrete Engine (Wasm or HTTP).
@@ -284,10 +365,13 @@
   // TODO(hjd): When streaming must invalidate this somehow.
   async getCpus(): Promise<number[]> {
     if (!this._cpus) {
-      const result =
-          await this.query('select distinct(cpu) from sched order by cpu;');
-      if (slowlyCountRows(result) === 0) return [];
-      this._cpus = result.columns[0].longValues!.map(n => +n);
+      const cpus = [];
+      const queryRes = await this.queryV2(
+          'select distinct(cpu) as cpu from sched order by cpu;');
+      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+        cpus.push(it.cpu);
+      }
+      this._cpus = cpus;
     }
     return this._cpus;
   }
@@ -307,8 +391,8 @@
   // TODO: This should live in code that's more specific to chrome, instead of
   // in engine.
   async getNumberOfProcesses(): Promise<number> {
-    const result = await this.query('select count(*) from process;');
-    return +result.columns[0].longValues![0];
+    const result = await this.queryV2('select count(*) as cnt from process;');
+    return result.firstRow({cnt: NUM}).cnt;
   }
 
   async getTraceTimeBounds(): Promise<TimeSpan> {
@@ -318,15 +402,15 @@
   }
 
   async getTracingMetadataTimeBounds(): Promise<TimeSpan> {
-    const query = await this.query(`select name, int_value from metadata
+    const queryRes = await this.queryV2(`select name, int_value from metadata
          where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
          or name = 'all_data_source_started_ns'`);
     let startBound = -Infinity;
     let endBound = Infinity;
-    const it = iter({'name': STR, 'int_value': NUM_NULL}, query);
+    const it = queryRes.iter({'name': STR, 'int_value': NUM_NULL});
     for (; it.valid(); it.next()) {
-      const columnName = it.row.name;
-      const timestamp = it.row.int_value;
+      const columnName = it.name;
+      const timestamp = it.int_value;
       if (timestamp === null) continue;
       if (columnName === 'tracing_disabled_ns') {
         endBound = Math.min(endBound, timestamp / 1e9);
diff --git a/ui/src/common/protos.ts b/ui/src/common/protos.ts
index 8b0a5fd..832c838 100644
--- a/ui/src/common/protos.ts
+++ b/ui/src/common/protos.ts
@@ -47,6 +47,7 @@
 
 // Trace Processor protos.
 import IRawQueryArgs = protos.perfetto.protos.IRawQueryArgs;
+import QueryArgs = protos.perfetto.protos.QueryArgs;
 import RawQueryArgs = protos.perfetto.protos.RawQueryArgs;
 import RawQueryResult = protos.perfetto.protos.RawQueryResult;
 import StatusResult = protos.perfetto.protos.StatusResult;
@@ -138,6 +139,7 @@
   MeminfoCounters,
   NativeContinuousDumpConfig,
   ProcessStatsConfig,
+  QueryArgs,
   RawQueryArgs,
   RawQueryResult,
   StatCounters,
diff --git a/ui/src/common/query_iterator.ts b/ui/src/common/query_iterator.ts
index 10029a5..8e1fa81 100644
--- a/ui/src/common/query_iterator.ts
+++ b/ui/src/common/query_iterator.ts
@@ -16,40 +16,70 @@
 
 import {RawQueryResult} from './protos';
 
-// Union of all the query result formats that we can turn into forward
-// iterators.
-// TODO(hjd): Replace someOtherEncoding place holder with the real new
-// format.
-type QueryResult = RawQueryResult|{someOtherEncoding: string};
-
-// One row extracted from an SQL result:
-interface Row {
-  [key: string]: string|number|null;
-}
-
-// API:
-// const result = await engine.query("select 42 as n;");
-// const it = iter({"answer": NUM}, result);
-// for (; it.valid(); it.next()) {
-//   console.log(it.row.answer);
-// }
-export interface RowIterator<T extends Row> {
-  valid(): boolean;
-  next(): void;
-  row: T;
-}
+// These types are used both for the new streaming query iterator and the old
+// columnar RawQueryResult.
 
 export const NUM = 0;
 export const STR = 'str';
 export const NUM_NULL: number|null = 1;
 export const STR_NULL: string|null = 'str_null';
-export type ColumnType =
-    (typeof NUM)|(typeof STR)|(typeof NUM_NULL)|(typeof STR_NULL);
+
+export type ColumnType = string|number|null;
+
+// One row extracted from an SQL result:
+export interface Row {
+  [key: string]: ColumnType;
+}
+
+// The methods that any iterator has to implement.
+export interface RowIteratorBase {
+  valid(): boolean;
+  next(): void;
+}
+
+// A RowIterator is a type that has all the fields defined in the query spec
+// plus the valid() and next() operators. This is to ultimately allow the
+// clients to do:
+// const result = await engine.queryV2("select name, surname, id from people;");
+// const iter = queryResult.iter({name: STR, surname: STR, id: NUM});
+// for (; iter.valid(); iter.next())
+//  console.log(iter.name, iter.surname);
+export type RowIterator<T extends Row> = RowIteratorBase&T;
+
+// The old iterator for non-batched queries. Going away. Usage.
+//   const result = await engine.query("select 42 as n;");
+//   const it = getRowIterator({"answer": NUM}, result);
+//   for (; it.valid(); it.next()) {
+//     console.log(it.row.answer);
+//   }
+export interface LegacyRowIterator<T extends Row> {
+  valid(): boolean;
+  next(): void;
+  row: T;
+}
+
+export function columnTypeToString(t: ColumnType): string {
+  switch (t) {
+    case NUM:
+      return 'NUM';
+    case NUM_NULL:
+      return 'NUM_NULL';
+    case STR:
+      return 'STR';
+    case STR_NULL:
+      return 'STR_NULL';
+    default:
+      return `INVALID(${t})`;
+  }
+}
+
+// TODO(primiano): the types and helpers in the rest of this file are
+// transitional and will be removed once we migrate everything to the streaming
+// query API.
 
 // Exported for testing
 export function findColumnIndex(
-    result: RawQueryResult, name: string, columnType: number|null|string):
-    number {
+    result: RawQueryResult, name: string, columnType: ColumnType): number {
   let matchingDescriptorIndex = -1;
   const disallowNulls = columnType === STR || columnType === NUM;
   const expectsStrings = columnType === STR || columnType === STR_NULL;
@@ -164,14 +194,15 @@
 // Deliberately not exported, use iter() below to make code easy to switch
 // to other queryResult formats.
 function iterFromColumns<T extends Row>(
-    querySpec: T, queryResult: RawQueryResult): RowIterator<T> {
+    querySpec: T, queryResult: RawQueryResult): LegacyRowIterator<T> {
   const iter = new ColumnarRowIterator(querySpec, queryResult);
-  return iter as unknown as RowIterator<T>;
+  return iter as unknown as LegacyRowIterator<T>;
 }
 
 // Deliberately not exported, use iterUntyped() below to make code easy to
 // switch to other queryResult formats.
-function iterUntypedFromColumns(result: RawQueryResult): RowIterator<Row> {
+function iterUntypedFromColumns(result: RawQueryResult):
+    LegacyRowIterator<Row> {
   const spec: Row = {};
   const desc = result.columnDescriptors;
   for (let i = 0; i < desc.length; ++i) {
@@ -182,41 +213,25 @@
     spec[name] = desc[i].type === 3 ? STR_NULL : NUM_NULL;
   }
   const iter = new ColumnarRowIterator(spec, result);
-  return iter as unknown as RowIterator<Row>;
+  return iter as unknown as LegacyRowIterator<Row>;
 }
 
-function isColumnarQueryResult(result: QueryResult): result is RawQueryResult {
-  return (result as RawQueryResult).columnDescriptors !== undefined;
-}
-
-export function iterUntyped(result: QueryResult): RowIterator<Row> {
-  if (isColumnarQueryResult(result)) {
-    return iterUntypedFromColumns(result);
-  } else {
-    throw new Error('Unsuported format');
-  }
+export function iterUntyped(result: RawQueryResult): LegacyRowIterator<Row> {
+  return iterUntypedFromColumns(result);
 }
 
 export function iter<T extends Row>(
-    spec: T, result: QueryResult): RowIterator<T> {
-  if (isColumnarQueryResult(result)) {
-    return iterFromColumns(spec, result);
-  } else {
-    throw new Error('Unsuported format');
-  }
+    spec: T, result: RawQueryResult): LegacyRowIterator<T> {
+  return iterFromColumns(spec, result);
 }
 
-export function slowlyCountRows(result: QueryResult): number {
-  if (isColumnarQueryResult(result)) {
-    // This isn't actually slow for columnar data but it might be for other
-    // formats.
-    return +result.numRecords;
-  } else {
-    throw new Error('Unsuported format');
-  }
+export function slowlyCountRows(result: RawQueryResult): number {
+  // This isn't actually slow for columnar data but it might be for other
+  // formats.
+  return +result.numRecords;
 }
 
-export function singleRow<T extends Row>(spec: T, result: QueryResult): T|
+export function singleRow<T extends Row>(spec: T, result: RawQueryResult): T|
     undefined {
   const numRows = slowlyCountRows(result);
   if (numRows === 0) {
@@ -231,7 +246,7 @@
   return it.row;
 }
 
-export function singleRowUntyped(result: QueryResult): Row|undefined {
+export function singleRowUntyped(result: RawQueryResult): Row|undefined {
   const numRows = slowlyCountRows(result);
   if (numRows === 0) {
     return undefined;
diff --git a/ui/src/common/query_result.ts b/ui/src/common/query_result.ts
new file mode 100644
index 0000000..82afc75
--- /dev/null
+++ b/ui/src/common/query_result.ts
@@ -0,0 +1,677 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file deals with deserialization and iteration of the proto-encoded
+// byte buffer that is returned by TraceProcessor when invoking the
+// TPM_QUERY_STREAMING method. The returned |query_result| buffer is optimized
+// for being moved cheaply across workers and decoded on-the-flight as we step
+// through the iterator.
+// See comments around QueryResult in trace_processor.proto for more details.
+
+// The classes in this file are organized as follows:
+//
+// QueryResultImpl:
+// The object returned by the Engine.queryV2(sql) method.
+// This object is a holder of row data. Batches of raw get appended
+// incrementally as they are received by the remote TraceProcessor instance.
+// QueryResultImpl also deals with asynchronicity of queries and allows callers
+// to obtain a promise that waits for more (or all) rows.
+// At any point in time the following objects hold a reference to QueryResult:
+// - The Engine: for appending row batches.
+// - UI code, typically controllers, who make queries.
+//
+// ResultBatch:
+// Hold the data, returned by the remote TraceProcessor instance, for a number
+// of rows (TP typically chunks the results in batches of 128KB).
+// A QueryResultImpl holds exclusively ResultBatches for a given query.
+// ResultBatch is not exposed externally, it's just an internal representation
+// that helps with proto decoding. ResultBatch is immutable after it gets
+// appended and decoded. The iteration state is held by the RowIteratorImpl.
+//
+// RowIteratorImpl:
+// Decouples the data owned by QueryResultImpl (and its ResultBatch(es)) from
+// the iteration state. The iterator effectively is the union of a ResultBatch
+// and the row number in it. Rows within the batch are decoded as the user calls
+// next(). When getting at the end of the batch, it takes care of switching to
+// the next batch (if any) within the QueryResultImpl.
+// This object is part of the API exposed to tracks / controllers.
+
+import * as protobuf from 'protobufjs/minimal';
+
+import {defer, Deferred} from '../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../base/logging';
+import {utf8Decode} from '../base/string_utils';
+
+import {
+  columnTypeToString,
+  NUM,
+  NUM_NULL,
+  Row,
+  RowIterator,
+  RowIteratorBase,
+  STR,
+  STR_NULL
+} from './query_iterator';
+
+// Disable Long.js support in protobuf. This seems to be enabled only in tests
+// but not in production code. In any case, for now we want casting to number
+// accepting the 2**53 limitation. This is consistent with passing
+// --force-number in the protobuf.js codegen invocation in //ui/BUILD.gn .
+// See also https://github.com/protobufjs/protobuf.js/issues/1253 .
+(protobuf.util as {} as {Long: undefined}).Long = undefined;
+protobuf.configure();
+
+// This has to match CellType in trace_processor.proto.
+enum CellType {
+  CELL_NULL = 1,
+  CELL_VARINT = 2,
+  CELL_FLOAT64 = 3,
+  CELL_STRING = 4,
+  CELL_BLOB = 5,
+}
+
+const CELL_TYPE_NAMES =
+    ['UNKNOWN', 'NULL', 'VARINT', 'FLOAT64', 'STRING', 'BLOB'];
+
+const TAG_LEN_DELIM = 2;
+
+// This is the interface exposed to readers (e.g. tracks). The underlying object
+// (QueryResultImpl) owns the result data. This allows to obtain iterators on
+// that. In future it will allow to wait for incremental updates (new rows being
+// fetched) for streaming queries.
+export interface QueryResult {
+  // Obtains an iterator.
+  // TODO(primiano): this should have an option to destruct data as we read. In
+  // the case of a long query (e.g. `SELECT * FROM sched` in the query prompt)
+  // we don't want to accumulate everything in memory. OTOH UI tracks want to
+  // keep the data around so they can redraw them on each animation frame. For
+  // now we keep everything in memory in the QueryResultImpl object.
+  // iter<T extends Row>(spec: T): RowIterator<T>;
+  iter<T extends Row>(spec: T): RowIterator<T>;
+
+  // Like iter() for queries that expect only one row. It embeds the valid()
+  // check (i.e. throws if no rows are available) and returns directly the
+  // first result.
+  firstRow<T extends Row>(spec: T): T;
+
+  // If != undefined the query errored out and error() contains the message.
+  error(): string|undefined;
+
+  // Returns the number of rows accumulated so far. Note that this number can
+  // change over time as more batches are received. It becomes stable only
+  // when isComplete() returns true or after waitAllRows() is resolved.
+  numRows(): number;
+
+  // If true all rows have been fetched. Calling iter() will iterate through the
+  // last row. If false, iter() will return an iterator which might iterate
+  // through some rows (or none) but will surely not reach the end.
+
+  isComplete(): boolean;
+
+  // Returns a promise that is resolved only when all rows (i.e. all batches)
+  // have been fetched. The promise return value is always the object iself.
+  waitAllRows(): Promise<QueryResult>;
+
+  // TODO(primiano): next CLs will introduce a waitMoreRows() to allow tracks
+  // to await until some more data (but not necessarily all) is available. For
+  // now everything uses waitAllRows().
+}
+
+// Interface exposed to engine.ts to pump in the data as new row batches arrive.
+export interface WritableQueryResult extends QueryResult {
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  //  The overall flow looks as follows:
+  // - The user calls engine.queryV2('select ...') and gets a QueryResult back.
+  // - The query call posts a message to the worker that runs the SQL engine (
+  //   or sends a HTTP request in case of the RPC+HTTP interface).
+  // - The returned QueryResult object is initially empty.
+  // - Over time, the sql engine will postMessage() back results in batches.
+  // - Each bach will end up calling this appendResultBatch() method.
+  // - If there is any pending promise (e.g. the caller called
+  //   queryResult.waitAllRows()), this call will awake them (if this is the
+  //   last batch).
+  appendResultBatch(resBytes: Uint8Array): void;
+}
+
+// The actual implementation, which bridges together the reader side and the
+// writer side (the one exposed to the Engine). This is the same object so that
+// when the engine pumps new row batches we can resolve pending promises that
+// readers (e.g. track code) are waiting for.
+class QueryResultImpl implements QueryResult, WritableQueryResult {
+  columnNames: string[] = [];
+  private _error?: string;
+  private _numRows = 0;
+  private _isComplete = false;
+
+  // --- QueryResult implementation.
+
+  // TODO(primiano): for the moment new batches are appended but old batches
+  // are never removed. This won't work with abnormally large result sets, as
+  // it will stash all rows in memory. We could switch to a model where the
+  // iterator is destructive and deletes batch objects once iterating past the
+  // end of each batch. If we do that, than we need to assign monotonic IDs to
+  // batches. Also if we do that, we should prevent creating more than one
+  // iterator for a QueryResult.
+  batches: ResultBatch[] = [];
+
+  // Promise awaiting on waitAllRows(). This should be resolved only when the
+  // last result batch has been been retrieved.
+  private allRowsPromise?: Deferred<QueryResult>;
+
+  isComplete(): boolean {
+    return this._isComplete;
+  }
+  numRows(): number {
+    return this._numRows;
+  }
+  error(): string|undefined {
+    return this._error;
+  }
+
+  iter<T extends Row>(spec: T): RowIterator<T> {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    return impl as {} as RowIterator<T>;
+  }
+
+  firstRow<T extends Row>(spec: T): T {
+    const impl = new RowIteratorImplWithRowData(spec, this);
+    assertTrue(impl.valid());
+    return impl as {} as RowIterator<T>as T;
+  }
+
+  // Can be called only once.
+  waitAllRows(): Promise<QueryResult> {
+    assertTrue(this.allRowsPromise === undefined);
+    this.allRowsPromise = defer<QueryResult>();
+    if (this._isComplete) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+    return this.allRowsPromise;
+  }
+
+  // --- WritableQueryResult implementation.
+
+  // Called by the engine when a new QueryResult is available. Note that a
+  // single Query() call can yield >1 QueryResult due to result batching
+  // if more than ~64K of data are returned, e.g. when returning O(M) rows.
+  // |resBytes| is a proto-encoded trace_processor.QueryResult message.
+  // It is fine to retain the resBytes without slicing a copy, because
+  // ProtoRingBuffer does the slice() for us (or passes through the buffer
+  // coming from postMessage() (Wasm case) of fetch() (HTTP+RPC case).
+  appendResultBatch(resBytes: Uint8Array) {
+    const reader = protobuf.Reader.create(resBytes);
+    assertTrue(reader.pos === 0);
+    const columnNamesEmptyAtStartOfBatch = this.columnNames.length === 0;
+    while (reader.pos < reader.len) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // column_names
+          // Only the first batch should contain the column names. If this fires
+          // something is going wrong in the handling of the batch stream.
+          assertTrue(columnNamesEmptyAtStartOfBatch);
+          this.columnNames.push(reader.string());
+          break;
+        case 2:  // error
+          // The query has errored only if the |error| field is non-empty.
+          // In protos, we don't distinguish between non-present and empty.
+          // Make sure we don't propagate ambiguous empty strings to JS.
+          const err = reader.string();
+          this._error = (err !== undefined && err.length) ? err : undefined;
+          break;
+        case 3:  // batch
+          const batchLen = reader.uint32();
+          const batchRaw = resBytes.subarray(reader.pos, reader.pos + batchLen);
+          reader.pos += batchLen;
+
+          // The ResultBatch ctor parses the CellsBatch submessage.
+          const parsedBatch = new ResultBatch(batchRaw);
+          this.batches.push(parsedBatch);
+          this._isComplete = parsedBatch.isLastBatch;
+
+          // In theory one could construct a valid proto serializing the column
+          // names after the cell batches. In practice the QueryResultSerializer
+          // doesn't do that so it's not worth complicating the code.
+          const numColumns = this.columnNames.length;
+          if (numColumns !== 0) {
+            assertTrue(parsedBatch.numCells % numColumns === 0);
+            this._numRows += parsedBatch.numCells / numColumns;
+          } else {
+            // numColumns == 0 is  plausible for queries like CREATE TABLE ... .
+            assertTrue(parsedBatch.numCells === 0);
+          }
+          break;
+        default:
+          console.warn(`Unexpected QueryResult field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch (tag)
+    }    // while (pos < end)
+
+    if (this._isComplete && this.allRowsPromise !== undefined) {
+      this.resolveOrReject(this.allRowsPromise, this);
+    }
+  }
+
+  ensureAllRowsPromise(): Promise<QueryResult> {
+    if (this.allRowsPromise === undefined) {
+      this.waitAllRows();  // Will populate |this.allRowsPromise|.
+    }
+    return assertExists(this.allRowsPromise);
+  }
+
+  private resolveOrReject(promise: Deferred<QueryResult>, arg: QueryResult) {
+    if (this._error === undefined) {
+      promise.resolve(arg);
+    } else {
+      promise.reject(new Error(this._error));
+    }
+  }
+}
+
+// This class holds onto a received result batch (a Uint8Array) and does some
+// partial parsing to tokenize the various cell groups. This parsing mainly
+// consists of identifying and caching the offsets of each cell group and
+// initializing the varint decoders. This half parsing is done to keep the
+// iterator's next() fast, without decoding everything into memory.
+// This is an internal implementation detail and is not exposed outside. The
+// RowIteratorImpl uses this class to iterate through batches (this class takes
+// care of iterating within a batch, RowIteratorImpl takes care of switching
+// batches when needed).
+// Note: at any point in time there can be more than one ResultIterator
+// referencing the same batch. The batch must be immutable.
+class ResultBatch {
+  readonly isLastBatch: boolean = false;
+  readonly batchBytes: Uint8Array;
+  readonly cellTypesOff: number = 0;
+  readonly cellTypesLen: number = 0;
+  readonly varintOff: number = 0;
+  readonly varintLen: number = 0;
+  readonly float64Cells = new Float64Array();
+  readonly blobCells: Uint8Array[] = [];
+  readonly stringCells: string[] = [];
+
+  // batchBytes is a trace_processor.QueryResult.CellsBatch proto.
+  constructor(batchBytes: Uint8Array) {
+    this.batchBytes = batchBytes;
+    const reader = protobuf.Reader.create(batchBytes);
+    assertTrue(reader.pos === 0);
+    const end = reader.len;
+
+    // Here we deconstruct the proto by hand. The CellsBatch is carefully
+    // designed to allow a very fast parsing from the TS side. We pack all cells
+    // of the same types together, so we can do only one call (per batch) to
+    // TextDecoder.decode(), we can overlay a memory-aligned typedarray for
+    // float values and can quickly tell and type-check the cell types.
+    // One row = N cells (we know the number upfront from the outer message).
+    // Each bach contains always an integer multiple of N cells (i.e. rows are
+    // never fragmented across different batches).
+    while (reader.pos < end) {
+      const tag = reader.uint32();
+      switch (tag >>> 3) {
+        case 1:  // cell types, a packed array containing one CellType per cell.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          this.cellTypesLen = reader.uint32();
+          this.cellTypesOff = reader.pos;
+          reader.pos += this.cellTypesLen;
+          break;
+
+        case 2:  // varint_cells, a packed varint buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const packLen = reader.uint32();
+          this.varintOff = reader.pos;
+          this.varintLen = packLen;
+          assertTrue(reader.buf === batchBytes);
+          assertTrue(
+              this.varintOff + this.varintLen <=
+              batchBytes.byteOffset + batchBytes.byteLength);
+          reader.pos += packLen;
+          break;
+
+        case 3:  // float64_cells, a 64-bit aligned packed fixed64 buffer.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);  // Must be packed varint.
+          const f64Len = reader.uint32();
+          assertTrue(f64Len % 8 === 0);
+          // Float64Array's constructor is evil: the offset is in bytes but the
+          // length is in 8-byte words.
+          const f64Words = f64Len / 8;
+          const f64Off = batchBytes.byteOffset + reader.pos;
+          if (f64Off % 8 === 0) {
+            this.float64Cells =
+                new Float64Array(batchBytes.buffer, f64Off, f64Words);
+          } else {
+            // When using the production code in trace_processor's rpc.cc, the
+            // float64 should be 8-bytes aligned. The slow-path case is only for
+            // tests.
+            const slice = batchBytes.buffer.slice(f64Off, f64Off + f64Len);
+            this.float64Cells = new Float64Array(slice);
+          }
+          reader.pos += f64Len;
+          break;
+
+        case 4:  // blob_cells: one entry per blob.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          // protobufjs's bytes() under the hoods calls slice() and creates
+          // a copy. Fine here as blobs are rare and not a fastpath.
+          this.blobCells.push(new Uint8Array(reader.bytes()));
+          break;
+
+        case 5:  // string_cells: all the string cells concatenated with \0s.
+          assertTrue((tag & 7) === TAG_LEN_DELIM);
+          const strLen = reader.uint32();
+          assertTrue(reader.pos + strLen <= end);
+          const subArr = batchBytes.subarray(reader.pos, reader.pos + strLen);
+          assertTrue(subArr.length === strLen);
+          // The reason why we do this split rather than creating one string
+          // per entry is that utf8 decoding has some non-negligible cost. See
+          // go/postmessage-benchmark .
+          this.stringCells = utf8Decode(subArr).split('\0');
+          reader.pos += strLen;
+          break;
+
+        case 6:  // is_last_batch (boolean).
+          this.isLastBatch = !!reader.bool();
+          break;
+
+        case 7:  // padding for realignment, skip silently.
+          reader.skipType(tag & 7);
+          break;
+
+        default:
+          console.warn(`Unexpected QueryResult.CellsBatch field ${tag >>> 3}`);
+          reader.skipType(tag & 7);
+          break;
+      }  // switch(tag)
+    }    // while (pos < end)
+  }
+
+  get numCells() {
+    return this.cellTypesLen;
+  }
+}
+
+class RowIteratorImpl implements RowIteratorBase {
+  // The spec passed to the iter call containing the expected types, e.g.:
+  // {'colA': NUM, 'colB': NUM_NULL, 'colC': STRING}.
+  // This doesn't ever change.
+  readonly rowSpec: Row;
+
+  // The object that holds the current row. This points to the parent
+  // RowIteratorImplWithRowData instance that created this class.
+  rowData: Row;
+
+  // The QueryResult object we are reading data from. The engine will pump
+  // batches over time into this object.
+  private resultObj: QueryResultImpl;
+
+  // All the member variables in the group below point to the identically-named
+  // members in result.batch[batchIdx]. This is to avoid indirection layers in
+  // the next() hotpath, so we can do this.float64Cells vs
+  // this.resultObj.batch[this.batchIdx].float64Cells.
+  // These are re-set every time tryMoveToNextBatch() is called (and succeeds).
+  private batchIdx = -1;  // The batch index within |result.batches[]|.
+  private batchBytes = new Uint8Array();
+  private columnNames: string[] = [];
+  private numColumns = 0;
+  private cellTypesEnd = -1;  // -1 so the 1st next() hits tryMoveToNextBatch().
+  private float64Cells = new Float64Array();
+  private varIntReader = protobuf.Reader.create(this.batchBytes);
+  private blobCells: Uint8Array[] = [];
+  private stringCells: string[] = [];
+
+  // These members instead are incremented as we read cells from next(). They
+  // are the mutable state of the iterator.
+  private nextCellTypeOff = 0;
+  private nextFloat64Cell = 0;
+  private nextStringCell = 0;
+  private nextBlobCell = 0;
+  private isValid = false;
+
+  constructor(querySpec: Row, rowData: Row, res: QueryResultImpl) {
+    Object.assign(this, querySpec);
+    this.rowData = rowData;
+    this.rowSpec = {...querySpec};  // ... -> Copy all the key/value pairs.
+    this.resultObj = res;
+    this.next();
+  }
+
+  valid(): boolean {
+    return this.isValid;
+  }
+
+  // Moves the cursor next by one row and updates |isValid|.
+  // When this fails to move, two cases are possible:
+  // 1. We reached the end of the result set (this is the case if
+  //    QueryResult.isComplete() == true when this fails).
+  // 2. We reached the end of the current batch, but more rows might come later
+  //    (if QueryResult.isComplete() == false).
+  next() {
+    // At some point we might reach the end of the current batch, but the next
+    // batch might be available already. In this case we want next() to
+    // transparently move on to the next batch.
+    while (this.nextCellTypeOff + this.numColumns > this.cellTypesEnd) {
+      // If TraceProcessor is behaving well, we should never end up in a
+      // situation where we have leftover cells. TP is expected to serialize
+      // whole rows in each QueryResult batch and NOT truncate them midway.
+      // If this assert fires the TP RPC logic has a bug.
+      assertTrue(
+          this.nextCellTypeOff === this.cellTypesEnd ||
+          this.cellTypesEnd === -1);
+      if (!this.tryMoveToNextBatch()) {
+        this.isValid = false;
+        return;
+      }
+    }
+
+    const rowData = this.rowData;
+    const numColumns = this.numColumns;
+
+    // Read the current row.
+    for (let i = 0; i < numColumns; i++) {
+      const cellType = this.batchBytes[this.nextCellTypeOff++];
+      const colName = this.columnNames[i];
+
+      switch (cellType) {
+        case CellType.CELL_NULL:
+          rowData[colName] = null;
+          break;
+
+        case CellType.CELL_VARINT:
+          const val = this.varIntReader.int64();
+          // This is very subtle. The return type of int64 can be either a
+          // number or a Long.js {high:number, low:number} if Long.js support is
+          // enabled. The default state seems different in node and browser.
+          // We force-disable Long.js support in the top of this source file.
+          rowData[colName] = val as {} as number;
+          break;
+
+        case CellType.CELL_FLOAT64:
+          rowData[colName] = this.float64Cells[this.nextFloat64Cell++];
+          break;
+
+        case CellType.CELL_STRING:
+          rowData[colName] = this.stringCells[this.nextStringCell++];
+          break;
+
+        case CellType.CELL_BLOB:
+          const blob = this.blobCells[this.nextBlobCell++];
+          throw new Error(`TODO implement BLOB support (${blob})`);
+          // outRow[colName] = blob;
+          break;
+
+        default:
+          throw new Error(`Invalid cell type ${cellType}`);
+      }
+    }  // For (cells)
+    this.isValid = true;
+  }
+
+  private tryMoveToNextBatch(): boolean {
+    const nextBatchIdx = this.batchIdx + 1;
+    if (nextBatchIdx >= this.resultObj.batches.length) {
+      return false;
+    }
+
+    this.columnNames = this.resultObj.columnNames;
+    this.numColumns = this.columnNames.length;
+
+    this.batchIdx = nextBatchIdx;
+    const batch = assertExists(this.resultObj.batches[nextBatchIdx]);
+    this.batchBytes = batch.batchBytes;
+    this.nextCellTypeOff = batch.cellTypesOff;
+    this.cellTypesEnd = batch.cellTypesOff + batch.cellTypesLen;
+    this.float64Cells = batch.float64Cells;
+    this.blobCells = batch.blobCells;
+    this.stringCells = batch.stringCells;
+    this.varIntReader = protobuf.Reader.create(batch.batchBytes);
+    this.varIntReader.pos = batch.varintOff;
+    this.varIntReader.len = batch.varintOff + batch.varintLen;
+    this.nextFloat64Cell = 0;
+    this.nextStringCell = 0;
+    this.nextBlobCell = 0;
+
+    // Check that all the expected columns are present.
+    for (const expectedCol of Object.keys(this.rowSpec)) {
+      if (this.columnNames.indexOf(expectedCol) < 0) {
+        throw new Error(
+            `Column ${expectedCol} not found in the SQL result ` +
+            `set {${this.columnNames.join(' ')}}`);
+      }
+    }
+
+    // Check that the cells types are consistent.
+    const numColumns = this.numColumns;
+    if (numColumns === 0) {
+      assertTrue(batch.numCells === 0);
+    } else {
+      for (let i = this.nextCellTypeOff; i < this.cellTypesEnd; i++) {
+        const col = (i - this.nextCellTypeOff) % numColumns;
+        const colName = this.columnNames[col];
+        const actualType = this.batchBytes[i] as CellType;
+        const expType = this.rowSpec[colName];
+
+        // If undefined, the caller doesn't want to read this column at all, so
+        // it can be whatever.
+        if (expType === undefined) continue;
+
+        let err = '';
+        if (actualType === CellType.CELL_NULL &&
+            (expType !== STR_NULL && expType !== NUM_NULL)) {
+          err = 'SQL value is NULL but that was not expected' +
+              ` (expected type: ${columnTypeToString(expType)}).` +
+              'Did you intend to use NUM_NULL or STRING_NULL?';
+        } else if (
+            ((actualType === CellType.CELL_VARINT ||
+              actualType === CellType.CELL_FLOAT64) &&
+             (expType !== NUM && expType !== NUM_NULL)) ||
+            ((actualType === CellType.CELL_STRING) &&
+             (expType !== STR && expType !== STR_NULL))) {
+          err = `Incompatible cell type. Expected: ${
+              columnTypeToString(
+                  expType)} actual: ${CELL_TYPE_NAMES[actualType]}`;
+        }
+        if (err.length > 0) {
+          throw new Error(
+              `Error @ row: ${Math.floor(i / numColumns)} col: '` +
+              `${colName}': ${err}`);
+        }
+      }
+    }
+    return true;
+  }
+}
+
+// This is the object ultimately returned to the client when calling
+// QueryResult.iter(...).
+// The only reason why this is disjoint from RowIteratorImpl is to avoid
+// naming collisions between the members variables required by RowIteratorImpl
+// and the column names returned by the iterator.
+class RowIteratorImplWithRowData implements RowIteratorBase {
+  private _impl: RowIteratorImpl;
+
+  next: () => void;
+  valid: () => boolean;
+
+  constructor(querySpec: Row, res: QueryResultImpl) {
+    const thisAsRow = this as {} as Row;
+    Object.assign(thisAsRow, querySpec);
+    this._impl = new RowIteratorImpl(querySpec, thisAsRow, res);
+    this.next = this._impl.next.bind(this._impl);
+    this.valid = this._impl.valid.bind(this._impl);
+  }
+}
+
+// This is a proxy object that wraps QueryResultImpl, adding await-ability.
+// This is so that:
+// 1. Clients that just want to await for the full result set can just call
+//    await engine.query('...') and will get a QueryResult that is guaranteed
+//    to be complete.
+// 2. Clients that know how to handle the streaming can use it straight away.
+class WaitableQueryResultImpl implements QueryResult, WritableQueryResult,
+                                         PromiseLike<QueryResult> {
+  private impl = new QueryResultImpl();
+  private thenCalled = false;
+
+  // QueryResult implementation. Proxies all calls to the impl object.
+  iter<T extends Row>(spec: T) {
+     return this.impl.iter(spec);
+  }
+  firstRow<T extends Row>(spec: T) {
+     return this.impl.firstRow(spec);
+  }
+  waitAllRows() {
+     return this.impl.waitAllRows();
+  }
+  isComplete() {
+     return this.impl.isComplete();
+  }
+  numRows() {
+     return this.impl.numRows();
+  }
+  error() {
+     return this.impl.error();
+  }
+
+  // WritableQueryResult implementation.
+  appendResultBatch(resBytes: Uint8Array) {
+    return this.impl.appendResultBatch(resBytes);
+  }
+
+  // PromiseLike<QueryResult> implementaton.
+
+  // tslint:disable-next-line no-any
+  then(onfulfilled: any, onrejected: any): any {
+    assertFalse(this.thenCalled);
+    this.thenCalled = true;
+    return this.impl.ensureAllRowsPromise().then(onfulfilled, onrejected);
+  }
+
+  // tslint:disable-next-line no-any
+  catch(error: any): any {
+    return this.impl.ensureAllRowsPromise().catch(error);
+  }
+
+  // tslint:disable-next-line no-any
+  finally(callback: () => void): any {
+    return this.impl.ensureAllRowsPromise().finally(callback);
+  }
+
+  get[Symbol.toStringTag](): string {
+    return 'Promise<WaitableQueryResult>';
+  }
+}
+
+export function createQueryResult(): QueryResult&Promise<QueryResult>&
+    WritableQueryResult {
+  return new WaitableQueryResultImpl();
+}
diff --git a/ui/src/common/query_result_unittest.ts b/ui/src/common/query_result_unittest.ts
new file mode 100644
index 0000000..bacf480
--- /dev/null
+++ b/ui/src/common/query_result_unittest.ts
@@ -0,0 +1,260 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as protoNamespace from '../gen/protos';
+
+import {NUM, NUM_NULL, STR, STR_NULL} from './query_iterator';
+import {createQueryResult} from './query_result';
+
+const T = protoNamespace.perfetto.protos.QueryResult.CellsBatch.CellType;
+const QueryResultProto = protoNamespace.perfetto.protos.QueryResult;
+
+test('QueryResult.SimpleOneRow', () => {
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: [T.CELL_STRING, T.CELL_VARINT, T.CELL_STRING, T.CELL_FLOAT64],
+    varintCells: [42],
+    stringCells: ['the foo', 'the bar'].join('\0'),
+    float64Cells: [42.42],
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['a_str', 'b_int', 'c_str', 'd_float'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.isComplete()).toBe(true);
+  expect(qr.numRows()).toBe(1);
+
+  // First try iterating without selecting any column.
+  {
+    const iter = qr.iter({});
+    expect(iter.valid()).toBe(true);
+    iter.next();
+    expect(iter.valid()).toBe(false);
+  }
+
+  // Then select only two of them.
+  {
+    const iter = qr.iter({c_str: STR, d_float: NUM});
+    expect(iter.valid()).toBe(true);
+    expect(iter.c_str).toBe('the bar');
+    expect(iter.d_float).toBeCloseTo(42.42);
+    iter.next();
+    expect(iter.valid()).toBe(false);
+  }
+
+  // If a column is not present in the result set, iter() should throw.
+  expect(() => qr.iter({nx: NUM})).toThrowError(/\bnx\b.*not found/);
+});
+
+test('QueryResult.BigNumbers', () => {
+  const numAndExpectedStr = [
+    [0, '0'],
+    [-1, '-1'],
+    [-1000, '-1000'],
+    [1e12, '1000000000000'],
+    [1e12 * -1, '-1000000000000'],
+    [((1 << 31) - 1) | 0, '2147483647'],
+    [1 << 31, '-2147483648'],
+    [Number.MAX_SAFE_INTEGER, '9007199254740991'],
+    [Number.MIN_SAFE_INTEGER, '-9007199254740991'],
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(numAndExpectedStr.length).fill(T.CELL_VARINT),
+    varintCells: numAndExpectedStr.map(x => x[0]) as number[],
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: string[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    actual.push(BigInt(iter.n).toString());
+  }
+  expect(actual).toEqual(numAndExpectedStr.map(x => x[1]) as string[]);
+});
+
+test('QueryResult.Floats', () => {
+  const floats = [
+    0.0,
+    1.0,
+    -1.0,
+    3.14159265358,
+    Number.MIN_SAFE_INTEGER,
+    Number.MAX_SAFE_INTEGER,
+    Number.NEGATIVE_INFINITY,
+    Number.POSITIVE_INFINITY,
+    Number.NaN,
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(floats.length).fill(T.CELL_FLOAT64),
+    float64Cells: floats,
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: number[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    actual.push(iter.n);
+  }
+  expect(actual).toEqual(floats);
+});
+
+test('QueryResult.Strings', () => {
+  const strings = [
+    'a',
+    '',
+    '',
+    'hello world',
+    'In einem Bächlein helle da schoß in froher Eil',
+    '色は匂へど散りぬるを我が世誰ぞ常ならん有為の奥山今日越えて浅き夢見じ酔ひもせず'
+  ];
+  const batch = QueryResultProto.CellsBatch.create({
+    cells: new Array<number>(strings.length).fill(T.CELL_STRING),
+    stringCells: strings.join('\0'),
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['s'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actual: string[] = [];
+  for (const iter = qr.iter({s: STR}); iter.valid(); iter.next()) {
+    actual.push(iter.s);
+  }
+  expect(actual).toEqual(strings);
+});
+
+test('QueryResult.NullChecks', () => {
+  const cells: number[] = [];
+  cells.push(T.CELL_VARINT, T.CELL_NULL);
+  cells.push(T.CELL_NULL, T.CELL_STRING);
+  cells.push(T.CELL_VARINT, T.CELL_STRING);
+  const batch = QueryResultProto.CellsBatch.create({
+    cells,
+    varintCells: [1, 2],
+    stringCells: ['a', 'b'].join('\0'),
+    isLastBatch: true,
+  });
+  const resProto = QueryResultProto.create({
+    columnNames: ['n', 's'],
+    batch: [batch],
+  });
+
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  const actualNums = new Array<number|null>();
+  const actualStrings = new Array<string|null>();
+  for (const iter = qr.iter({n: NUM_NULL, s: STR_NULL}); iter.valid();
+       iter.next()) {
+    actualNums.push(iter.n);
+    actualStrings.push(iter.s);
+  }
+  expect(actualNums).toEqual([1, null, 2]);
+  expect(actualStrings).toEqual([null, 'a', 'b']);
+
+  // Check that using NUM / STR throws.
+  expect(() => qr.iter({n: NUM_NULL, s: STR}))
+      .toThrowError(/col: 's'.*is NULL.*not expected/);
+  expect(() => qr.iter({n: NUM, s: STR_NULL}))
+      .toThrowError(/col: 'n'.*is NULL.*not expected/);
+  expect(qr.iter({n: NUM_NULL})).toBeTruthy();
+  expect(qr.iter({s: STR_NULL})).toBeTruthy();
+});
+
+test('QueryResult.EarlyError', () => {
+  const resProto = QueryResultProto.create({
+    columnNames: ['n', 's'],
+    batch: [{isLastBatch: true}],
+    error: 'Oh dear, this SQL query is too complicated, I give up',
+  });
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.error()).toContain('Oh dear');
+  expect(qr.isComplete()).toBe(true);
+});
+
+test('QueryResult.LateError', () => {
+  const resProto = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [
+      {
+        cells: [T.CELL_VARINT],
+        varintCells: [1],
+      },
+      {
+        cells: [T.CELL_VARINT],
+        varintCells: [2],
+        isLastBatch: true,
+      },
+    ],
+    error: 'I tried, I was getting there, but then I failed',
+  });
+  const qr = createQueryResult();
+  qr.appendResultBatch(QueryResultProto.encode(resProto).finish());
+  expect(qr.error()).toContain('I failed');
+  const rows: number[] = [];
+  for (const iter = qr.iter({n: NUM}); iter.valid(); iter.next()) {
+    rows.push(iter.n);
+  }
+  expect(rows).toEqual([1, 2]);
+  expect(qr.isComplete()).toBe(true);
+});
+
+
+test('QueryResult.MultipleBatches', async () => {
+  const batch1 = QueryResultProto.create({
+    columnNames: ['n'],
+    batch: [{
+      cells: [T.CELL_VARINT],
+      varintCells: [1],
+      isLastBatch: false,
+    }],
+  });
+  const batch2 = QueryResultProto.create({
+    batch: [{
+      cells: [T.CELL_VARINT],
+      varintCells: [2],
+      isLastBatch: true,
+    }],
+  });
+
+  const qr = createQueryResult();
+  expect(qr.isComplete()).toBe(false);
+
+  qr.appendResultBatch(QueryResultProto.encode(batch1).finish());
+  qr.appendResultBatch(QueryResultProto.encode(batch2).finish());
+
+  const awaitRes = await qr;
+
+  expect(awaitRes.isComplete()).toBe(true);
+  expect(qr.isComplete()).toBe(true);
+
+  expect(awaitRes.numRows()).toBe(2);
+  expect(qr.numRows()).toBe(2);
+});
diff --git a/ui/src/controller/adb.ts b/ui/src/controller/adb.ts
index 5ed6152..47783a6 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/controller/adb.ts
@@ -114,8 +114,6 @@
     this.key = await AdbOverWebUsb.initKey();
 
     await this.dev.open();
-    await this.dev.reset();  // The reset is done so that we can claim the
-                             // device before adb server can.
 
     const {configValue, usbInterfaceNumber, endpoints} =
         this.findInterfaceAndEndpoint();
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 06f2d7a..8f71701 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -14,7 +14,7 @@
 
 import {TRACE_MARGIN_TIME_S} from '../common/constants';
 import {Engine} from '../common/engine';
-import {slowlyCountRows} from '../common/query_iterator';
+import {NUM, STR} from '../common/query_iterator';
 import {CurrentSearchResults, SearchSummary} from '../common/search_data';
 import {TimeSpan} from '../common/time';
 
@@ -59,11 +59,11 @@
   }
 
   private async setup() {
-    await this.query(`create virtual table search_summary_window
+    await this.queryV2(`create virtual table search_summary_window
       using window;`);
-    await this.query(`create virtual table search_summary_sched_span using
+    await this.queryV2(`create virtual table search_summary_sched_span using
       span_join(sched PARTITIONED cpu, search_summary_window);`);
-    await this.query(`create virtual table search_summary_slice_span using
+    await this.queryV2(`create virtual table search_summary_slice_span using
       span_join(slice PARTITIONED track_id, search_summary_window);`);
   }
 
@@ -151,22 +151,25 @@
 
     startNs = Math.floor(startNs / quantumNs) * quantumNs;
 
-    await this.query(`update search_summary_window set
+    await this.queryV2(`update search_summary_window set
       window_start=${startNs},
       window_dur=${endNs - startNs},
       quantum=${quantumNs}
       where rowid = 0;`);
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
       using(upid) where thread.name like ${searchLiteral}
       or process.name like ${searchLiteral}`);
 
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
     const cpus = await this.engine.getCpus();
     const maxCpu = Math.max(...cpus, -1);
 
-    const rawResult = await this.query(`
+    const res = await this.queryV2(`
         select
           (quantum_ts * ${quantumNs} + ${startNs})/1e9 as tsStart,
           ((quantum_ts+1) * ${quantumNs} + ${startNs})/1e9 as tsEnd,
@@ -185,18 +188,18 @@
           group by quantum_ts
           order by quantum_ts;`);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = res.numRows();
     const summary = {
       tsStarts: new Float64Array(numRows),
       tsEnds: new Float64Array(numRows),
       count: new Uint8Array(numRows)
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      summary.tsStarts[row] = +columns[0].doubleValues![row];
-      summary.tsEnds[row] = +columns[1].doubleValues![row];
-      summary.count[row] = +columns[2].longValues![row];
+    const it = res.iter({tsStart: NUM, tsEnd: NUM, count: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      summary.tsStarts[row] = it.tsStart;
+      summary.tsEnds[row] = it.tsEnd;
+      summary.count[row] = it.count;
     }
     return summary;
   }
@@ -226,80 +229,78 @@
       }
     }
 
-    const rawUtidResult = await this.query(`select utid from thread join process
+    const utidRes = await this.queryV2(`select utid from thread join process
     using(upid) where
       thread.name like ${searchLiteral} or
       process.name like ${searchLiteral}`);
-    const utids = [...rawUtidResult.columns[0].longValues!];
+    const utids = [];
+    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
+      utids.push(it.utid);
+    }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
     select
-      id as slice_id,
+      id as sliceId,
       ts,
       'cpu' as source,
-      cpu as source_id,
+      cpu as sourceId,
       utid
     from sched where utid in (${utids.join(',')})
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       where slice.name like ${searchLiteral}
     union
     select
-      slice_id,
+      slice_id as sliceId,
       ts,
       'track' as source,
-      track_id as source_id,
+      track_id as sourceId,
       0 as utid
       from slice
       join args using(arg_set_id)
       where string_value like ${searchLiteral}
     order by ts`);
 
-    const numRows = slowlyCountRows(rawResult);
-
     const searchResults: CurrentSearchResults = {
       sliceIds: [],
       tsStarts: [],
       utids: [],
       trackIds: [],
       sources: [],
-      totalResults: +numRows,
+      totalResults: queryRes.numRows(),
     };
 
-    const columns = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const source = columns[2].stringValues![row];
-      const sourceId = +columns[3].longValues![row];
+    const spec = {sliceId: NUM, ts: NUM, source: STR, sourceId: NUM, utid: NUM};
+    for (const it = queryRes.iter(spec); it.valid(); it.next()) {
       let trackId = undefined;
-      if (source === 'cpu') {
-        trackId = cpuToTrackId.get(sourceId);
-      } else if (source === 'track') {
-        trackId = engineTrackIdToTrackId.get(sourceId);
+      if (it.source === 'cpu') {
+        trackId = cpuToTrackId.get(it.sourceId);
+      } else if (it.source === 'track') {
+        trackId = engineTrackIdToTrackId.get(it.sourceId);
       }
 
+      // The .get() calls above could return undefined, this isn't just an else.
       if (trackId === undefined) {
         searchResults.totalResults--;
         continue;
       }
-
       searchResults.trackIds.push(trackId);
-      searchResults.sources.push(source);
-      searchResults.sliceIds.push(+columns[0].longValues![row]);
-      searchResults.tsStarts.push(+columns[1].longValues![row]);
-      searchResults.utids.push(+columns[4].longValues![row]);
+      searchResults.sources.push(it.source);
+      searchResults.sliceIds.push(it.sliceId);
+      searchResults.tsStarts.push(it.ts);
+      searchResults.utids.push(it.utid);
     }
     return searchResults;
   }
 
-
-  private async query(query: string) {
-    const result = await this.engine.query(query);
+  private async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
     return result;
   }
 }
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index b3742b3..b2a82fb 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -118,6 +118,11 @@
     return result;
   }
 
+  protected async queryV2(query: string) {
+    const result = await this.engine.queryV2(query);
+    return result;
+  }
+
   private shouldReload(): boolean {
     const {lastTrackReloadRequest} = globals.state;
     return !!lastTrackReloadRequest &&
diff --git a/ui/src/tracks/actual_frames/controller.ts b/ui/src/tracks/actual_frames/controller.ts
index 58140f7..95c9201 100644
--- a/ui/src/tracks/actual_frames/controller.ts
+++ b/ui/src/tracks/actual_frames/controller.ts
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists, assertTrue} from '../../base/logging';
-import {
-  iter,
-  NUM,
-  singleRow,
-  slowlyCountRows,
-  STR
-} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -51,18 +45,17 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select
           max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
             as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      const row = singleRow({maxDur: NUM}, maxDurResult);
-      this.maxDurNs = assertExists(row).maxDur;
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const rawResult = await this.queryV2(`
       SELECT
         (s.ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         s.ts as ts,
@@ -92,7 +85,7 @@
       order by tsq, s.layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = rawResult.numRows();
     const slices: Data = {
       start,
       end,
@@ -119,23 +112,21 @@
       return idx;
     }
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'ts': NUM,
-          'dur': NUM,
-          'layoutDepth': NUM,
-          'id': NUM,
-          'name': STR,
-          'isInstant': NUM,
-          'isIncomplete': NUM,
-          'color': STR,
-        },
-        rawResult);
+    const it = rawResult.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'layoutDepth': NUM,
+      'id': NUM,
+      'name': STR,
+      'isInstant': NUM,
+      'isIncomplete': NUM,
+      'color': STR,
+    });
     for (let i = 0; it.valid(); i++, it.next()) {
-      const startNsQ = it.row.tsq;
-      const startNs = it.row.ts;
-      const durNs = it.row.dur;
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
@@ -145,12 +136,12 @@
 
       slices.starts[i] = fromNs(startNsQ);
       slices.ends[i] = fromNs(endNsQ);
-      slices.depths[i] = it.row.layoutDepth;
-      slices.titles[i] = internString(it.row.name);
-      slices.colors![i] = internString(it.row.color);
-      slices.sliceIds[i] = it.row.id;
-      slices.isInstant[i] = it.row.isInstant;
-      slices.isIncomplete[i] = it.row.isIncomplete;
+      slices.depths[i] = it.layoutDepth;
+      slices.titles[i] = internString(it.name);
+      slices.colors![i] = internString(it.color);
+      slices.sliceIds[i] = it.id;
+      slices.isInstant[i] = it.isInstant;
+      slices.isIncomplete[i] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/android_log/controller.ts b/ui/src/tracks/android_log/controller.ts
index afd235c..3f1401a 100644
--- a/ui/src/tracks/android_log/controller.ts
+++ b/ui/src/tracks/android_log/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNsCeil, toNsFloor} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -33,17 +33,17 @@
     // |resolution| is in s/px the frontend wants.
     const quantNs = toNsCeil(resolution);
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
-        cast(ts / ${quantNs} as integer) * ${quantNs} as ts_quant,
+        cast(ts / ${quantNs} as integer) * ${quantNs} as tsQuant,
         prio,
-        count(prio)
+        count(prio) as numEvents
       from android_logs
       where ts >= ${startNs} and ts <= ${endNs}
-      group by ts_quant, prio
-      order by ts_quant, prio limit ${LIMIT};`);
+      group by tsQuant, prio
+      order by tsQuant, prio limit ${LIMIT};`);
 
-    const rowCount = slowlyCountRows(rawResult);
+    const rowCount = queryRes.numRows();
     const result = {
       start,
       end,
@@ -53,12 +53,14 @@
       timestamps: new Float64Array(rowCount),
       priorities: new Uint8Array(rowCount),
     };
-    const cols = rawResult.columns;
-    for (let i = 0; i < rowCount; i++) {
-      result.timestamps[i] = fromNs(+cols[0].longValues![i]);
-      const prio = Math.min(+cols[1].longValues![i], 7);
-      result.priorities[i] |= (1 << prio);
-      result.numEvents += +cols[2].longValues![i];
+
+
+    const it = queryRes.iter({tsQuant: NUM, prio: NUM, numEvents: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      result.timestamps[row] = fromNs(it.tsQuant);
+      const prio = Math.min(it.prio, 7);
+      result.priorities[row] |= (1 << prio);
+      result.numEvents += it.numEvents;
     }
     return result;
   }
diff --git a/ui/src/tracks/async_slices/controller.ts b/ui/src/tracks/async_slices/controller.ts
index 50a074a..0cfc609 100644
--- a/ui/src/tracks/async_slices/controller.ts
+++ b/ui/src/tracks/async_slices/controller.ts
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -37,36 +38,34 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-        from experimental_slice_layout
+        as maxDur from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
         max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
-        layout_depth,
+        layout_depth as depth,
         name,
         id,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       from experimental_slice_layout
       where
         filter_track_ids = '${this.config.trackIds.join(',')}' and
         ts >= ${startNs - this.maxDurNs} and
         ts <= ${endNs}
-      group by tsq, layout_depth
-      order by tsq, layout_depth
+      group by tsq, depth
+      order by tsq, depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -92,31 +91,37 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      name: STR,
+      id: NUM,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.titles[row] = internString(cols[4].stringValues![row]);
-      slices.sliceIds[row] = +cols[5].longValues![row];
-      slices.isInstant[row] = +cols[6].longValues![row];
-      slices.isIncomplete[row] = +cols[7].longValues![row];
+      slices.depths[row] = it.depth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
 }
 
-
 trackControllerRegistry.register(AsyncSliceTrackController);
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index 2f29388..e702fd6 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -41,31 +41,29 @@
     if (this.maxDurNs === 0) {
       const query = `
           SELECT max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-          FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
-      const rawResult = await this.query(query);
-      if (slowlyCountRows(rawResult) === 1) {
-        this.maxDurNs = rawResult.columns[0].longValues![0];
-      }
+          AS maxDur FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
+      const queryRes = await this.queryV2(query);
+      this.maxDurNs = queryRes.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
     const query = `
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
-        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)),
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
         depth,
-        id as slice_id,
+        id as sliceId,
         name,
-        dur = 0 as is_instant,
-        dur = -1 as is_incomplete
+        dur = 0 as isInstant,
+        dur = -1 as isIncomplete
       FROM ${tableName}
       WHERE track_id = ${this.config.trackId} AND
         ts >= (${startNs - this.maxDurNs}) AND
         ts <= ${endNs}
       GROUP BY depth, tsq`;
-    const rawResult = await this.query(query);
+    const queryRes = await this.queryV2(query);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -91,19 +89,26 @@
       return idx;
     }
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      depth: NUM,
+      sliceId: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
-      const isInstant = +cols[6].longValues![row];
-      const isIncomplete = +cols[7].longValues![row];
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (!isInstant && startNsQ === endNsQ) {
+      if (!it.isInstant && startNsQ === endNsQ) {
         throw new Error(
             'Expected startNsQ and endNsQ to differ (' +
             `startNsQ: ${startNsQ}, startNs: ${startNs},` +
@@ -113,11 +118,11 @@
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.depths[row] = +cols[3].longValues![row];
-      slices.sliceIds[row] = +cols[4].longValues![row];
-      slices.titles[row] = internString(cols[5].stringValues![row]);
-      slices.isInstant[row] = isInstant;
-      slices.isIncomplete[row] = isIncomplete;
+      slices.depths[row] = it.depth;
+      slices.sliceIds[row] = it.sliceId;
+      slices.titles[row] = internString(it.name);
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
     }
     return slices;
   }
diff --git a/ui/src/tracks/counter/controller.ts b/ui/src/tracks/counter/controller.ts
index 22dffe2..5b230f3 100644
--- a/ui/src/tracks/counter/controller.ts
+++ b/ui/src/tracks/counter/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -47,7 +47,7 @@
 
     if (!this.setup) {
       if (this.config.namespace === undefined) {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -59,7 +59,7 @@
           where track_id = ${this.config.trackId};
         `);
       } else {
-        await this.query(`
+        await this.queryV2(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -72,33 +72,33 @@
         `);
       }
 
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
           select
             max(
               iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
-            )
+            ) as maxDur
           from ${this.tableName('counter_view')}
       `);
-      if (slowlyCountRows(maxDurResult) === 1) {
-        this.maxDurNs = maxDurResult.columns[0].longValues![0];
-      }
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
 
-      const result = await this.query(`
+      const queryRes = await this.queryV2(`
         select
-          max(value) as maxValue,
-          min(value) as minValue,
-          max(delta) as maxDelta,
-          min(delta) as minDelta
+          ifnull(max(value), 0) as maxValue,
+          ifnull(min(value), 0) as minValue,
+          ifnull(max(delta), 0) as maxDelta,
+          ifnull(min(delta), 0) as minDelta
         from ${this.tableName('counter_view')}`);
-      this.maximumValueSeen = +result.columns[0].doubleValues![0];
-      this.minimumValueSeen = +result.columns[1].doubleValues![0];
-      this.maximumDeltaSeen = +result.columns[2].doubleValues![0];
-      this.minimumDeltaSeen = +result.columns[3].doubleValues![0];
+      const row = queryRes.firstRow(
+          {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
+      this.maximumValueSeen = row.maxValue;
+      this.minimumValueSeen = row.minValue;
+      this.maximumDeltaSeen = row.maxDelta;
+      this.minimumDeltaSeen = row.minDelta;
 
       this.setup = true;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         min(value) as minValue,
@@ -112,7 +112,7 @@
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -131,25 +131,22 @@
       totalDeltas: new Float64Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'lastId': NUM,
-          'minValue': NUM,
-          'maxValue': NUM,
-          'lastValue': NUM,
-          'totalDelta': NUM,
-        },
-        rawResult);
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.lastIds[i] = it.row.lastId;
-      data.minValues[i] = it.row.minValue;
-      data.maxValues[i] = it.row.maxValue;
-      data.lastValues[i] = it.row.lastValue;
-      data.totalDeltas[i] = it.row.totalDelta;
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'lastId': NUM,
+      'minValue': NUM,
+      'maxValue': NUM,
+      'lastValue': NUM,
+      'totalDelta': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.timestamps[row] = fromNs(it.tsq);
+      data.lastIds[row] = it.lastId;
+      data.minValues[row] = it.minValue;
+      data.maxValues[row] = it.maxValue;
+      data.lastValues[row] = it.lastValue;
+      data.totalDeltas[row] = it.totalDelta;
     }
-
     return data;
   }
 
diff --git a/ui/src/tracks/cpu_freq/controller.ts b/ui/src/tracks/cpu_freq/controller.ts
index 08ca0b5..c457da7 100644
--- a/ui/src/tracks/cpu_freq/controller.ts
+++ b/ui/src/tracks/cpu_freq/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -41,30 +41,37 @@
     this.maximumValueSeen = await this.queryMaxFrequency();
     this.maxDurNs = await this.queryMaxSourceDur();
 
-    const result = await this.query(`
-      select max(ts), dur, count(1)
+    const iter = (await this.queryV2(`
+      select max(ts) as maxTs, dur, count(1) as rowCount
       from ${this.tableName('freq_idle')}
-    `);
-    this.maxTsEndNs =
-        result.columns[0].longValues![0] + result.columns[1].longValues![0];
+    `)).firstRow({maxTs: NUM_NULL, dur: NUM_NULL, rowCount: NUM});
+    if (iter.maxTs === null || iter.dur === null) {
+      // We shoulnd't really hit this because trackDecider shouldn't create
+      // the track in the first place if there are no entries. But could happen
+      // if only one cpu has no cpufreq data.
+      return;
+    }
+    this.maxTsEndNs = iter.maxTs + iter.dur;
 
-    const rowCount = result.columns[2].longValues![0];
+    const rowCount = iter.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+
+    await this.queryV2(`
       create table ${this.tableName('freq_idle_cached')} as
       select
-        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
-        min(freq_value) as min_freq,
-        max(freq_value) as max_freq,
-        value_at_max_ts(ts, freq_value) as last_freq,
-        value_at_max_ts(ts, idle_value) as last_idle_value
+        (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cachedTsq,
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
-      group by cached_tsq
-      order by cached_tsq
+      group by cachedTsq
+      order by cachedTsq
     `);
+
     this.cachedBucketNs = bucketNs;
   }
 
@@ -82,10 +89,10 @@
     // be an even number, so we can snap in the middle.
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
-
     const freqResult = await this.queryData(startNs, endNs, bucketNs);
+    assertTrue(freqResult.isComplete());
 
-    const numRows = slowlyCountRows(freqResult);
+    const numRows = freqResult.numRows();
     const data: Data = {
       start,
       end,
@@ -100,68 +107,68 @@
       lastIdleValues: new Int8Array(numRows),
     };
 
-    const it = iter(
-        {
-          'tsq': NUM,
-          'minFreq': NUM,
-          'maxFreq': NUM,
-          'lastFreq': NUM,
-          'lastIdleValue': NUM,
-        },
-        freqResult);
+    const it = freqResult.iter({
+      'tsq': NUM,
+      'minFreq': NUM,
+      'maxFreq': NUM,
+      'lastFreq': NUM,
+      'lastIdleValue': NUM,
+    });
     for (let i = 0; it.valid(); ++i, it.next()) {
-      data.timestamps[i] = fromNs(it.row.tsq);
-      data.minFreqKHz[i] = it.row.minFreq;
-      data.maxFreqKHz[i] = it.row.maxFreq;
-      data.lastFreqKHz[i] = it.row.lastFreq;
-      data.lastIdleValues[i] = it.row.lastIdleValue;
+      data.timestamps[i] = fromNs(it.tsq);
+      data.minFreqKHz[i] = it.minFreq;
+      data.maxFreqKHz[i] = it.maxFreq;
+      data.lastFreqKHz[i] = it.lastFreq;
+      data.lastIdleValues[i] = it.lastIdleValue;
     }
+
     return data;
   }
 
   private async queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
 
     if (isCached) {
-      return this.query(`
+      return this.queryV2(`
         select
-          cached_tsq / ${bucketNs} * ${bucketNs} as tsq,
-          min(min_freq) as minFreq,
-          max(max_freq) as maxFreq,
-          value_at_max_ts(cached_tsq, last_freq) as lastFreq,
-          value_at_max_ts(cached_tsq, last_idle_value) as lastIdleValue
+          cachedTsq / ${bucketNs} * ${bucketNs} as tsq,
+          min(minFreq) as minFreq,
+          max(maxFreq) as maxFreq,
+          value_at_max_ts(cachedTsq, lastFreq) as lastFreq,
+          value_at_max_ts(cachedTsq, lastIdleValue) as lastIdleValue
         from ${this.tableName('freq_idle_cached')}
         where
-          cached_tsq >= ${startNs - this.maxDurNs} and
-          cached_tsq <= ${endNs}
+          cachedTsq >= ${startNs - this.maxDurNs} and
+          cachedTsq <= ${endNs}
         group by tsq
         order by tsq
       `);
     }
-
-    const minTsFreq = await this.query(`
-      select ifnull(max(ts), 0) from ${this.tableName('freq')}
+    const minTsFreq = await this.queryV2(`
+      select ifnull(max(ts), 0) as minTs from ${this.tableName('freq')}
       where ts < ${startNs}
     `);
-    let minTs = minTsFreq.columns[0].longValues![0];
+
+    let minTs = minTsFreq.iter({minTs: NUM}).minTs;
     if (this.config.idleTrackId !== undefined) {
-      const minTsIdle = await this.query(`
-        select ifnull(max(ts), 0) from ${this.tableName('idle')}
+      const minTsIdle = await this.queryV2(`
+        select ifnull(max(ts), 0) as minTs from ${this.tableName('idle')}
         where ts < ${startNs}
       `);
-      minTs = Math.min(minTsIdle.columns[0].longValues![0], minTs);
+      minTs = Math.min(minTsIdle.iter({minTs: NUM}).minTs, minTs);
     }
+
     const geqConstraint = this.config.idleTrackId === undefined ?
         `ts >= ${minTs}` :
         `source_geq(ts, ${minTs})`;
-    return this.query(`
+    return this.queryV2(`
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
-        min(freq_value) as minFreq,
-        max(freq_value) as maxFreq,
-        value_at_max_ts(ts, freq_value) as lastFreq,
-        value_at_max_ts(ts, idle_value) as lastIdleValue
+        min(freqValue) as minFreq,
+        max(freqValue) as maxFreq,
+        value_at_max_ts(ts, freqValue) as lastFreq,
+        value_at_max_ts(ts, idleValue) as lastIdleValue
       from ${this.tableName('freq_idle')}
       where
         ${geqConstraint} and
@@ -172,59 +179,59 @@
   }
 
   private async queryMaxFrequency(): Promise<number> {
-    const result = await this.query(`
-      select max(freq_value)
+    const result = await this.queryV2(`
+      select max(freqValue) as maxFreq
       from ${this.tableName('freq')}
     `);
-    return result.columns[0].doubleValues![0];
+    return result.firstRow({'maxFreq': NUM_NULL}).maxFreq || 0;
   }
 
   private async queryMaxSourceDur(): Promise<number> {
-    const maxDurFreqResult =
-        await this.query(`select max(dur) from ${this.tableName('freq')}`);
-    const maxFreqDurNs = maxDurFreqResult.columns[0].longValues![0];
+    const maxDurFreqResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('freq')}`);
+    const maxDurNs = maxDurFreqResult.firstRow({'maxDur': NUM}).maxDur;
     if (this.config.idleTrackId === undefined) {
-      return maxFreqDurNs;
+      return maxDurNs;
     }
 
-    const maxDurIdleResult =
-        await this.query(`select max(dur) from ${this.tableName('idle')}`);
-    return Math.max(maxFreqDurNs, maxDurIdleResult.columns[0].longValues![0]);
+    const maxDurIdleResult = await this.queryV2(
+        `select ifnull(max(dur), 0) as maxDur from ${this.tableName('idle')}`);
+    return Math.max(maxDurNs, maxDurIdleResult.firstRow({maxDur: NUM}).maxDur);
   }
 
   private async createFreqIdleViews() {
-    await this.query(`create view ${this.tableName('freq')} as
+    await this.queryV2(`create view ${this.tableName('freq')} as
       select
         ts,
         dur,
-        value as freq_value
+        value as freqValue
       from experimental_counter_dur c
       where track_id = ${this.config.freqTrackId};
     `);
 
     if (this.config.idleTrackId === undefined) {
-      await this.query(`create view ${this.tableName('freq_idle')} as
+      await this.queryV2(`create view ${this.tableName('freq_idle')} as
         select
           ts,
           dur,
-          -1 as idle_value,
-          freq_value
+          -1 as idleValue,
+          freqValue
         from ${this.tableName('freq')};
       `);
       return;
     }
 
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('idle')} as
       select
         ts,
         dur,
-        iif(value = 4294967295, -1, cast(value as int)) as idle_value
+        iif(value = 4294967295, -1, cast(value as int)) as idleValue
       from experimental_counter_dur c
       where track_id = ${this.config.idleTrackId};
     `);
 
-    await this.query(`
+    await this.queryV2(`
       create virtual table ${this.tableName('freq_idle')}
       using span_join(${this.tableName('freq')}, ${this.tableName('idle')});
     `);
diff --git a/ui/src/tracks/cpu_profile/controller.ts b/ui/src/tracks/cpu_profile/controller.ts
index 0042888..a6c5456 100644
--- a/ui/src/tracks/cpu_profile/controller.ts
+++ b/ui/src/tracks/cpu_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {iter, NUM, slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -36,9 +36,8 @@
       where utid = ${this.config.utid}
       order by ts`;
 
-    const result = await this.query(query);
-
-    const numRows = slowlyCountRows(result);
+    const result = await this.queryV2(query);
+    const numRows = result.numRows();
     const data: Data = {
       start,
       end,
@@ -49,11 +48,11 @@
       callsiteId: new Uint32Array(numRows),
     };
 
-    const it = iter({id: NUM, ts: NUM, callsiteId: NUM}, result);
-    for (let i = 0; it.valid(); it.next(), ++i) {
-      data.ids[i] = it.row.id;
-      data.tsStarts[i] = it.row.ts;
-      data.callsiteId[i] = it.row.callsiteId;
+    const it = result.iter({id: NUM, ts: NUM, callsiteId: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      data.ids[row] = it.id;
+      data.tsStarts[row] = it.ts;
+      data.callsiteId[row] = it.callsiteId;
     }
 
     return data;
diff --git a/ui/src/tracks/cpu_slices/controller.ts b/ui/src/tracks/cpu_slices/controller.ts
index d6ac723..2d891ef 100644
--- a/ui/src/tracks/cpu_slices/controller.ts
+++ b/ui/src/tracks/cpu_slices/controller.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -29,7 +29,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('sched')} as
       select
         ts,
@@ -40,18 +40,18 @@
       where cpu = ${this.config.cpu} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur), count(1)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as rowCount
       from ${this.tableName('sched')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
-
-    const rowCount = rawResult.columns[1].longValues![0];
+    const row = queryRes.firstRow({maxDur: NUM, rowCount: NUM});
+    this.maxDurNs = row.maxDur;
+    const rowCount = row.rowCount;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -90,7 +90,7 @@
         isCached ? this.tableName('sched_cached') : this.tableName('sched');
     const constraintColumn = isCached ? 'cached_tsq' : 'ts';
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       select
         ${queryTsq} as tsq,
         ts,
@@ -105,7 +105,7 @@
       order by tsq
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -117,31 +117,30 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({tsq: NUM, ts: NUM, dur: NUM, utid: NUM, id: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.utids[row] = +cols[3].longValues![row];
-      slices.ids[row] = +cols[4].longValues![row];
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
     }
 
     return slices;
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('sched_cached')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('sched_cached')}`);
   }
 }
 
diff --git a/ui/src/tracks/debug_slices/controller.ts b/ui/src/tracks/debug_slices/controller.ts
index 0afcb55..8400af8 100644
--- a/ui/src/tracks/debug_slices/controller.ts
+++ b/ui/src/tracks/debug_slices/controller.ts
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue} from '../../base/logging';
 import {Actions} from '../../common/actions';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {globals} from '../../controller/globals';
 import {
@@ -28,28 +27,26 @@
   static readonly kind = DEBUG_SLICE_TRACK_KIND;
 
   async onReload() {
-    const rawResult = await this.query(`select max(depth) from debug_slices`);
-    const maxDepth = (slowlyCountRows(rawResult) === 0) ?
-        1 :
-        rawResult.columns[0].longValues![0];
+    const rawResult = await this.queryV2(
+        `select ifnull(max(depth), 1) as maxDepth from debug_slices`);
+    const maxDepth = rawResult.firstRow({maxDepth: NUM}).maxDepth;
     globals.dispatch(
         Actions.updateTrackConfig({id: this.trackId, config: {maxDepth}}));
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
       Promise<Data> {
-    const rawResult = await this.query(`select id, name, ts,
-        iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur),
-        depth from debug_slices where
-        (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
+    const queryRes = await this.queryV2(`select
+      ifnull(id, -1) as id,
+      ifnull(name, '[null]') as name,
+      ts,
+      iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur) as dur,
+      ifnull(depth, 0) as depth
+      from debug_slices
+      where (ts + dur) >= ${toNs(start)} and ts <= ${toNs(end)}`);
 
-    assertTrue(rawResult.columns.length === 5);
-    const [idCol, nameCol, tsCol, durCol, depthCol] = rawResult.columns;
-    const idValues = idCol.longValues! || idCol.doubleValues!;
-    const tsValues = tsCol.longValues! || tsCol.doubleValues!;
-    const durValues = durCol.longValues! || durCol.doubleValues!;
+    const numRows = queryRes.numRows();
 
-    const numRows = slowlyCountRows(rawResult);
     const slices: Data = {
       start,
       end,
@@ -75,24 +72,24 @@
       return idx;
     }
 
-    for (let i = 0; i < slowlyCountRows(rawResult); i++) {
+    const it = queryRes.iter(
+        {id: NUM, name: STR, ts: NUM_NULL, dur: NUM_NULL, depth: NUM});
+    for (let row = 0; it.valid(); it.next(), row++) {
       let sliceStart: number, sliceEnd: number;
-      if (tsCol.isNulls![i] || durCol.isNulls![i]) {
+      if (it.ts === null || it.dur === null) {
         sliceStart = sliceEnd = -1;
       } else {
-        sliceStart = tsValues[i];
-        const sliceDur = durValues[i];
-        sliceEnd = sliceStart + sliceDur;
+        sliceStart = it.ts;
+        sliceEnd = sliceStart + it.dur;
       }
-      slices.sliceIds[i] = idCol.isNulls![i] ? -1 : idValues[i];
-      slices.starts[i] = fromNs(sliceStart);
-      slices.ends[i] = fromNs(sliceEnd);
-      slices.depths[i] = depthCol.isNulls![i] ? 0 : depthCol.longValues![i];
-      const sliceName =
-          nameCol.isNulls![i] ? '[null]' : nameCol.stringValues![i];
-      slices.titles[i] = internString(sliceName);
-      slices.isInstant[i] = 0;
-      slices.isIncomplete[i] = 0;
+      slices.sliceIds[row] = it.id;
+      slices.starts[row] = fromNs(sliceStart);
+      slices.ends[row] = fromNs(sliceEnd);
+      slices.depths[row] = it.depth;
+      const sliceName = it.name;
+      slices.titles[row] = internString(sliceName);
+      slices.isInstant[row] = 0;
+      slices.isIncomplete[row] = 0;
     }
 
     return slices;
diff --git a/ui/src/tracks/expected_frames/controller.ts b/ui/src/tracks/expected_frames/controller.ts
index 9707059..5227a81 100644
--- a/ui/src/tracks/expected_frames/controller.ts
+++ b/ui/src/tracks/expected_frames/controller.ts
@@ -12,14 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../../base/logging';
-import {
-  iter,
-  NUM,
-  singleRow,
-  slowlyCountRows,
-  STR
-} from '../../common/query_iterator';
+import {assertTrue} from '../../base/logging';
+import {NUM, NUM_NULL, STR} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -44,17 +38,16 @@
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
 
     if (this.maxDurNs === 0) {
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.queryV2(`
         select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
           as maxDur
         from experimental_slice_layout
         where filter_track_ids = '${this.config.trackIds.join(',')}'
       `);
-      const row = singleRow({maxDur: NUM}, maxDurResult);
-      this.maxDurNs = assertExists(row).maxDur;
+      this.maxDurNs = maxDurResult.firstRow({maxDur: NUM_NULL}).maxDur || 0;
     }
 
-    const rawResult = await this.query(`
+    const queryRes = await this.queryV2(`
       SELECT
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
@@ -73,7 +66,7 @@
       order by tsq, layout_depth
     `);
 
-    const numRows = slowlyCountRows(rawResult);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       start,
       end,
@@ -101,39 +94,35 @@
     }
     const greenIndex = internString('#4CAF50');
 
-    const it = iter(
-        {
-          tsq: NUM,
-          ts: NUM,
-          dur: NUM,
-          layoutDepth: NUM,
-          id: NUM,
-          name: STR,
-          isInstant: NUM,
-          isIncomplete: NUM,
-        },
-        rawResult);
-    for (let i = 0; it.valid(); it.next(), ++i) {
-      const startNsQ = it.row.tsq;
-      const startNs = it.row.ts;
-      const durNs = it.row.dur;
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      layoutDepth: NUM,
+      id: NUM,
+      name: STR,
+      isInstant: NUM,
+      isIncomplete: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
-      slices.starts[i] = fromNs(startNsQ);
-      slices.ends[i] = fromNs(endNsQ);
-      slices.depths[i] = it.row.layoutDepth;
-      slices.titles[i] = internString(it.row.name);
-      slices.sliceIds[i] = it.row.id;
-      slices.isInstant[i] = it.row.isInstant;
-      slices.isIncomplete[i] = it.row.isIncomplete;
-      slices.colors![i] = greenIndex;
+      slices.starts[row] = fromNs(startNsQ);
+      slices.ends[row] = fromNs(endNsQ);
+      slices.depths[row] = it.layoutDepth;
+      slices.titles[row] = internString(it.name);
+      slices.sliceIds[row] = it.id;
+      slices.isInstant[row] = it.isInstant;
+      slices.isIncomplete[row] = it.isIncomplete;
+      slices.colors![row] = greenIndex;
     }
     return slices;
   }
diff --git a/ui/src/tracks/heap_profile/controller.ts b/ui/src/tracks/heap_profile/controller.ts
index 2422af1..9873744 100644
--- a/ui/src/tracks/heap_profile/controller.ts
+++ b/ui/src/tracks/heap_profile/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM, STR} from '../../common/query_iterator';
 import {
   TrackController,
   trackControllerRegistry
@@ -38,7 +38,7 @@
         types: new Array<string>()
       };
     }
-    const result = await this.query(`
+    const queryRes = await this.queryV2(`
     select * from
     (select distinct(ts) as ts, 'native' as type from heap_profile_allocation
      where upid = ${this.config.upid}
@@ -46,7 +46,7 @@
         select distinct(graph_sample_ts) as ts, 'graph' as type from
         heap_graph_object
         where upid = ${this.config.upid}) order by ts`);
-    const numRows = slowlyCountRows(result);
+    const numRows = queryRes.numRows();
     const data: Data = {
       start,
       end,
@@ -56,11 +56,11 @@
       types: new Array<string>(numRows),
     };
 
-    for (let row = 0; row < numRows; row++) {
-      data.tsStarts[row] = +result.columns[0].longValues![row];
-      data.types[row] = result.columns[1].stringValues![row];
+    const it = queryRes.iter({ts: NUM, type: STR});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.tsStarts[row] = it.ts;
+      data.types[row] = it.type;
     }
-
     return data;
   }
 }
diff --git a/ui/src/tracks/process_scheduling/controller.ts b/ui/src/tracks/process_scheduling/controller.ts
index 687d16d..9441448 100644
--- a/ui/src/tracks/process_scheduling/controller.ts
+++ b/ui/src/tracks/process_scheduling/controller.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertTrue} from '../../base/logging';
-import {RawQueryResult} from '../../common/protos';
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
+import {QueryResult} from '../../common/query_result';
 import {fromNs, toNs} from '../../common/time';
 import {
   TrackController,
@@ -45,18 +45,19 @@
     assertTrue(cpus.length > 0);
     this.maxCpu = Math.max(...cpus) + 1;
 
-    const result = await this.query(`
-      select max(dur), count(1)
+    const result = (await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur, count(1) as count
       from ${this.tableName('process_sched')}
-    `);
-    this.maxDurNs = result.columns[0].longValues![0];
+    `)).iter({maxDur: NUM, count: NUM});
+    assertTrue(result.valid());
+    this.maxDurNs = result.maxDur;
 
-    const rowCount = result.columns[1].longValues![0];
+    const rowCount = result.count;
     const bucketNs = this.cachedBucketSizeNs(rowCount);
     if (bucketNs === undefined) {
       return;
     }
-    await this.query(`
+    await this.queryV2(`
       create table ${this.tableName('process_sched_cached')} as
       select
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as cached_tsq,
@@ -88,9 +89,8 @@
     const bucketNs =
         Math.max(Math.round(resolutionNs * this.pxSize() / 2) * 2, 1);
 
-    const rawResult = await this.queryData(startNs, endNs, bucketNs);
-
-    const numRows = slowlyCountRows(rawResult);
+    const queryRes = await this.queryData(startNs, endNs, bucketNs);
+    const numRows = queryRes.numRows();
     const slices: Data = {
       kind: 'slice',
       start,
@@ -104,38 +104,43 @@
       utids: new Uint32Array(numRows),
     };
 
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      tsq: NUM,
+      ts: NUM,
+      dur: NUM,
+      cpu: NUM,
+      utid: NUM,
+    });
+
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      if (startNsQ === endNsQ) {
-        throw new Error('Should never happen');
-      }
+      assertTrue(startNsQ !== endNsQ);
 
       slices.starts[row] = fromNs(startNsQ);
       slices.ends[row] = fromNs(endNsQ);
-      slices.cpus[row] = +cols[3].longValues![row];
-      slices.utids[row] = +cols[4].longValues![row];
+      slices.cpus[row] = it.cpu;
+      slices.utids[row] = it.utid;
       slices.end = Math.max(slices.ends[row], slices.end);
     }
     return slices;
   }
 
   private queryData(startNs: number, endNs: number, bucketNs: number):
-      Promise<RawQueryResult> {
+      Promise<QueryResult> {
     const isCached = this.cachedBucketNs <= bucketNs;
     const tsq = isCached ? `cached_tsq / ${bucketNs} * ${bucketNs}` :
                            `(ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs}`;
     const queryTable = isCached ? this.tableName('process_sched_cached') :
                                   this.tableName('process_sched');
     const constraintColumn = isCached ? 'cached_tsq' : 'ts';
-    return this.query(`
+    return this.queryV2(`
       select
         ${tsq} as tsq,
         ts,
@@ -152,7 +157,7 @@
   }
 
   private async createSchedView() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('process_sched')} as
       select ts, dur, cpu, utid
       from experimental_sched_upid
diff --git a/ui/src/tracks/process_summary/controller.ts b/ui/src/tracks/process_summary/controller.ts
index 75242fe..9e6ec5e 100644
--- a/ui/src/tracks/process_summary/controller.ts
+++ b/ui/src/tracks/process_summary/controller.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {slowlyCountRows} from '../../common/query_iterator';
+import {NUM} from '../../common/query_iterator';
 import {fromNs, toNs} from '../../common/time';
 import {LIMIT} from '../../common/track_data';
 import {
@@ -39,29 +39,35 @@
     const endNs = toNs(end);
 
     if (this.setup === false) {
-      await this.query(
+      await this.queryV2(
           `create virtual table ${this.tableName('window')} using window;`);
 
       let utids = [this.config.utid];
       if (this.config.upid) {
-        const threadQuery = await this.query(
+        const threadQuery = await this.queryV2(
             `select utid from thread where upid=${this.config.upid}`);
-        utids = threadQuery.columns[0].longValues!;
+        utids = [];
+        for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
+          utids.push(it.utid);
+        }
       }
 
-      const trackQuery = await this.query(
+      const trackQuery = await this.queryV2(
           `select id from thread_track where utid in (${utids.join(',')})`);
-      const tracks = trackQuery.columns[0].longValues!;
+      const tracks = [];
+      for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
+        tracks.push(it.id);
+      }
 
       const processSliceView = this.tableName('process_slice_view');
-      await this.query(
+      await this.queryV2(
           `create view ${processSliceView} as ` +
           // 0 as cpu is a dummy column to perform span join on.
           `select ts, dur/${utids.length} as dur ` +
           `from slice s ` +
           `where depth = 0 and track_id in ` +
           `(${tracks.join(',')})`);
-      await this.query(`create virtual table ${this.tableName('span')}
+      await this.queryV2(`create virtual table ${this.tableName('span')}
           using span_join(${processSliceView},
                           ${this.tableName('window')});`);
       this.setup = true;
@@ -73,7 +79,7 @@
     const windowStartNs = Math.floor(startNs / bucketSizeNs) * bucketSizeNs;
     const windowDurNs = Math.max(1, endNs - windowStartNs);
 
-    this.query(`update ${this.tableName('window')} set
+    await this.queryV2(`update ${this.tableName('window')} set
       window_start=${windowStartNs},
       window_dur=${windowDurNs},
       quantum=${bucketSizeNs}
@@ -98,9 +104,6 @@
       group by quantum_ts
       limit ${LIMIT}`;
 
-    const rawResult = await this.query(query);
-    const numRows = slowlyCountRows(rawResult);
-
     const summary: Data = {
       start,
       end,
@@ -109,21 +112,24 @@
       bucketSizeSeconds: fromNs(bucketSizeNs),
       utilizations: new Float64Array(numBuckets),
     };
-    const cols = rawResult.columns;
-    for (let row = 0; row < numRows; row++) {
-      const bucket = +cols[0].longValues![row];
+
+    const queryRes = await this.queryV2(query);
+    const it = queryRes.iter({bucket: NUM, utilization: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
       if (bucket > numBuckets) {
         continue;
       }
-      summary.utilizations[bucket] = +cols[1].doubleValues![row];
+      summary.utilizations[bucket] = it.utilization;
     }
+
     return summary;
   }
 
   onDestroy(): void {
     if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
+      this.queryV2(`drop table ${this.tableName('window')}`);
+      this.queryV2(`drop table ${this.tableName('span')}`);
       this.setup = false;
     }
   }
diff --git a/ui/src/tracks/thread_state/controller.ts b/ui/src/tracks/thread_state/controller.ts
index a4718c6..0de3a3e 100644
--- a/ui/src/tracks/thread_state/controller.ts
+++ b/ui/src/tracks/thread_state/controller.ts
@@ -14,10 +14,8 @@
 
 import {assertFalse} from '../../base/logging';
 import {
-  iter,
   NUM,
   NUM_NULL,
-  slowlyCountRows,
   STR_NULL
 } from '../../common/query_iterator';
 import {translateState} from '../../common/thread_state';
@@ -39,7 +37,7 @@
   private maxDurNs = 0;
 
   async onSetup() {
-    await this.query(`
+    await this.queryV2(`
       create view ${this.tableName('thread_state')} as
       select
         id,
@@ -52,11 +50,11 @@
       where utid = ${this.config.utid} and utid != 0
     `);
 
-    const rawResult = await this.query(`
-      select max(dur)
+    const queryRes = await this.queryV2(`
+      select ifnull(max(dur), 0) as maxDur
       from ${this.tableName('thread_state')}
     `);
-    this.maxDurNs = rawResult.columns[0].longValues![0];
+    this.maxDurNs = queryRes.firstRow({maxDur: NUM}).maxDur;
   }
 
   async onBoundsChange(start: number, end: number, resolution: number):
@@ -75,10 +73,10 @@
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         ts,
         max(dur) as dur,
-        cast(cpu as integer) as cpu,
+        ifnull(cast(cpu as integer), -1) as cpu,
         state,
         io_wait,
-        id
+        ifnull(id, -1) as id
       from ${this.tableName('thread_state')}
       where
         ts >= ${startNs - this.maxDurNs} and
@@ -87,8 +85,8 @@
       order by tsq, state, io_wait
     `;
 
-    const result = await this.query(query);
-    const numRows = slowlyCountRows(result);
+    const queryRes = await this.queryV2(query);
+    const numRows = queryRes.numRows();
 
     const data: Data = {
       start,
@@ -113,31 +111,28 @@
       stringIndexes.set({shortState, ioWait}, idx);
       return idx;
     }
-    iter(
-        {
-          'ts': NUM,
-          'dur': NUM,
-          'cpu': NUM_NULL,
-          'state': STR_NULL,
-          'io_wait': NUM_NULL,
-          'id': NUM_NULL,
-        },
-        result);
-    for (let row = 0; row < numRows; row++) {
-      const cols = result.columns;
-      const startNsQ = +cols[0].longValues![row];
-      const startNs = +cols[1].longValues![row];
-      const durNs = +cols[2].longValues![row];
+    const it = queryRes.iter({
+      'tsq': NUM,
+      'ts': NUM,
+      'dur': NUM,
+      'cpu': NUM,
+      'state': STR_NULL,
+      'io_wait': NUM_NULL,
+      'id': NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const startNsQ = it.tsq;
+      const startNs = it.ts;
+      const durNs = it.dur;
       const endNs = startNs + durNs;
 
       let endNsQ = Math.floor((endNs + bucketNs / 2 - 1) / bucketNs) * bucketNs;
       endNsQ = Math.max(endNsQ, startNsQ + bucketNs);
 
-      const cpu = cols[3].isNulls![row] ? -1 : cols[3].longValues![row];
-      const state = cols[4].stringValues![row];
-      const ioWait =
-          cols[5].isNulls![row] ? undefined : !!cols[5].longValues![row];
-      const id = cols[6].isNulls![row] ? -1 : cols[6].longValues![row];
+      const cpu = it.cpu;
+      const state = it.state || '[null]';
+      const ioWait = it.io_wait === null ? undefined : !!it.io_wait;
+      const id = it.id;
 
       // We should never have the end timestamp being the same as the bucket
       // start.
@@ -153,7 +148,8 @@
   }
 
   async onDestroy() {
-    await this.query(`drop table if exists ${this.tableName('thread_state')}`);
+    await this.queryV2(
+        `drop table if exists ${this.tableName('thread_state')}`);
   }
 }