Merge "ui: Add delta mode to counters"
diff --git a/src/trace_processor/dynamic/experimental_counter_dur_generator.cc b/src/trace_processor/dynamic/experimental_counter_dur_generator.cc
index 13417bf..88d167f 100644
--- a/src/trace_processor/dynamic/experimental_counter_dur_generator.cc
+++ b/src/trace_processor/dynamic/experimental_counter_dur_generator.cc
@@ -29,6 +29,9 @@
   schema.columns.emplace_back(
       Table::Schema::Column{"dur", SqlValue::Type::kLong, false /* is_id */,
                             false /* is_sorted */, false /* is_hidden */});
+  schema.columns.emplace_back(
+      Table::Schema::Column{"delta", SqlValue::Type::kLong, false /* is_id */,
+                            false /* is_sorted */, false /* is_hidden */});
   return schema;
 }
 
@@ -51,9 +54,17 @@
   if (!dur_column_) {
     dur_column_.reset(
         new NullableVector<int64_t>(ComputeDurColumn(*counter_table_)));
+    delta_column_.reset(
+        new NullableVector<double>(ComputeDeltaColumn(*counter_table_)));
   }
-  return std::unique_ptr<Table>(new Table(counter_table_->ExtendWithColumn(
-      "dur", dur_column_.get(), TypedColumn<int64_t>::default_flags())));
+
+  Table t = counter_table_
+                ->ExtendWithColumn("dur", std::move(dur_column_.get()),
+                                   TypedColumn<int64_t>::default_flags())
+                .ExtendWithColumn("delta", std::move(delta_column_.get()),
+                                  TypedColumn<int64_t>::default_flags());
+
+  return std::unique_ptr<Table>(new Table(t.Copy()));
 }
 
 // static
@@ -91,5 +102,38 @@
   return dur;
 }
 
+// static
+NullableVector<double> ExperimentalCounterDurGenerator::ComputeDeltaColumn(
+    const Table& table) {
+  // Keep track of the last seen row for each track id.
+  std::unordered_map<TrackId, uint32_t> last_row_for_track_id;
+  NullableVector<double> delta;
+
+  const auto* value_col =
+      TypedColumn<double>::FromColumn(table.GetColumnByName("value"));
+  const auto* track_id_col =
+      TypedColumn<tables::CounterTrackTable::Id>::FromColumn(
+          table.GetColumnByName("track_id"));
+
+  for (uint32_t i = 0; i < table.row_count(); ++i) {
+    // Check if we already have a previous row for the current track id.
+    TrackId track_id = (*track_id_col)[i];
+    auto it = last_row_for_track_id.find(track_id);
+    if (it == last_row_for_track_id.end()) {
+      // This means we don't have any row - start tracking this row for the
+      // future.
+      last_row_for_track_id.emplace(track_id, i);
+    } else {
+      // This means we have an previous row for the current track id. Update
+      // the duration of the previous row to be up to the current ts.
+      uint32_t old_row = it->second;
+      it->second = i;
+      delta.Set(old_row, (*value_col)[i] - (*value_col)[old_row]);
+    }
+    delta.Append(0);
+  }
+  return delta;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/dynamic/experimental_counter_dur_generator.h b/src/trace_processor/dynamic/experimental_counter_dur_generator.h
index 9265e47..38f9773 100644
--- a/src/trace_processor/dynamic/experimental_counter_dur_generator.h
+++ b/src/trace_processor/dynamic/experimental_counter_dur_generator.h
@@ -39,10 +39,12 @@
 
   // public + static for testing
   static NullableVector<int64_t> ComputeDurColumn(const Table& table);
+  static NullableVector<double> ComputeDeltaColumn(const Table& table);
 
  private:
   const tables::CounterTable* counter_table_ = nullptr;
   std::unique_ptr<NullableVector<int64_t>> dur_column_;
+  std::unique_ptr<NullableVector<double>> delta_column_;
 };
 
 }  // namespace trace_processor
diff --git a/ui/src/tracks/counter/common.ts b/ui/src/tracks/counter/common.ts
index 8e1380a..3751161 100644
--- a/ui/src/tracks/counter/common.ts
+++ b/ui/src/tracks/counter/common.ts
@@ -16,14 +16,19 @@
 
 export const COUNTER_TRACK_KIND = 'CounterTrack';
 
+export type CounterScaleOptions = 'DEFAULT'|'RELATIVE'|'DELTA';
+
 export interface Data extends TrackData {
   maximumValue: number;
   minimumValue: number;
+  maximumDelta: number;
+  minimumDelta: number;
   timestamps: Float64Array;
   lastIds: Float64Array;
   minValues: Float64Array;
   maxValues: Float64Array;
   lastValues: Float64Array;
+  totalDeltas: Float64Array;
 }
 
 export interface Config {
@@ -34,5 +39,5 @@
   endTs?: number;
   namespace: string;
   trackId: number;
-  scale?: 'DEFAULT'|'RELATIVE';
+  scale?: CounterScaleOptions;
 }
diff --git a/ui/src/tracks/counter/controller.ts b/ui/src/tracks/counter/controller.ts
index fa678cc..22dffe2 100644
--- a/ui/src/tracks/counter/controller.ts
+++ b/ui/src/tracks/counter/controller.ts
@@ -30,6 +30,8 @@
   private setup = false;
   private maximumValueSeen = 0;
   private minimumValueSeen = 0;
+  private maximumDeltaSeen = 0;
+  private minimumDeltaSeen = 0;
   private maxDurNs = 0;
 
   async onBoundsChange(start: number, end: number, resolution: number):
@@ -51,7 +53,8 @@
             id,
             ts,
             dur,
-            value
+            value,
+            delta
           from experimental_counter_dur
           where track_id = ${this.config.trackId};
         `);
@@ -62,6 +65,7 @@
             id,
             ts,
             lead(ts, 1, ts) over (order by ts) - ts as dur,
+            lead(value, 1, value) over (order by ts) - value as delta,
             value
           from ${this.namespaceTable('counter')}
           where track_id = ${this.config.trackId};
@@ -80,10 +84,16 @@
       }
 
       const result = await this.query(`
-        select max(value), min(value)
+        select
+          max(value) as maxValue,
+          min(value) as minValue,
+          max(delta) as maxDelta,
+          min(delta) 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];
 
       this.setup = true;
     }
@@ -93,6 +103,7 @@
         (ts + ${bucketNs / 2}) / ${bucketNs} * ${bucketNs} as tsq,
         min(value) as minValue,
         max(value) as maxValue,
+        sum(delta) as totalDelta,
         value_at_max_ts(ts, id) as lastId,
         value_at_max_ts(ts, value) as lastValue
       from ${this.tableName('counter_view')}
@@ -109,12 +120,15 @@
       length: numRows,
       maximumValue: this.maximumValue(),
       minimumValue: this.minimumValue(),
+      maximumDelta: this.maximumDeltaSeen,
+      minimumDelta: this.minimumDeltaSeen,
       resolution,
       timestamps: new Float64Array(numRows),
       lastIds: new Float64Array(numRows),
       minValues: new Float64Array(numRows),
       maxValues: new Float64Array(numRows),
       lastValues: new Float64Array(numRows),
+      totalDeltas: new Float64Array(numRows),
     };
 
     const it = iter(
@@ -123,7 +137,8 @@
           'lastId': NUM,
           'minValue': NUM,
           'maxValue': NUM,
-          'lastValue': NUM
+          'lastValue': NUM,
+          'totalDelta': NUM,
         },
         rawResult);
     for (let i = 0; it.valid(); ++i, it.next()) {
@@ -132,6 +147,7 @@
       data.minValues[i] = it.row.minValue;
       data.maxValues[i] = it.row.maxValue;
       data.lastValues[i] = it.row.lastValue;
+      data.totalDeltas[i] = it.row.totalDelta;
     }
 
     return data;
diff --git a/ui/src/tracks/counter/frontend.ts b/ui/src/tracks/counter/frontend.ts
index 7fd9203..693d449 100644
--- a/ui/src/tracks/counter/frontend.ts
+++ b/ui/src/tracks/counter/frontend.ts
@@ -28,6 +28,7 @@
 import {
   Config,
   COUNTER_TRACK_KIND,
+  CounterScaleOptions,
   Data,
 } from './common';
 
@@ -35,6 +36,41 @@
 const MARGIN_TOP = 3.5;
 const RECT_HEIGHT = 24.5;
 
+function scaleTooltip(scale?: CounterScaleOptions): string {
+  switch (scale) {
+    case 'RELATIVE':
+      return 'Use zero-based scale';
+    case 'DELTA':
+      return 'Use deta scale';
+    case 'DEFAULT':
+    default:
+      return 'Use zero-based scale';
+  }
+}
+
+function scaleIcon(scale?: CounterScaleOptions): string {
+  switch (scale) {
+    case 'DELTA':
+      return 'bar_chart';
+    case 'RELATIVE':
+    case 'DEFAULT':
+    default:
+      return 'show_chart';
+  }
+}
+
+function nextScale(scale?: CounterScaleOptions): CounterScaleOptions {
+  switch (scale) {
+    case 'RELATIVE':
+      return 'DELTA';
+    case 'DELTA':
+      return 'DEFAULT';
+    case 'DEFAULT':
+    default:
+      return 'RELATIVE';
+  }
+}
+
 class CounterTrack extends Track<Config, Data> {
   static readonly kind = COUNTER_TRACK_KIND;
   static create(trackState: TrackState): CounterTrack {
@@ -58,19 +94,14 @@
     const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
     buttons.push(m(TrackButton, {
       action: () => {
-        if (this.config.scale === 'RELATIVE') {
-          this.config.scale = 'DEFAULT';
-        } else {
-          this.config.scale = 'RELATIVE';
-        }
+        this.config.scale = nextScale(this.config.scale);
         Actions.updateTrackConfig(
             {id: this.trackState.id, config: this.config});
         globals.rafScheduler.scheduleFullRedraw();
       },
-      i: 'show_chart',
-      tooltip: (this.config.scale === 'RELATIVE') ? 'Use zero-based scale' :
-                                                    'Use relative scale',
-      showButton: this.config.scale === 'RELATIVE',
+      i: scaleIcon(this.config.scale),
+      tooltip: scaleTooltip(this.config.scale),
+      showButton: !!this.config.scale && this.config.scale !== 'DEFAULT',
     }));
     return buttons;
   }
@@ -88,14 +119,30 @@
     assertTrue(data.timestamps.length === data.minValues.length);
     assertTrue(data.timestamps.length === data.maxValues.length);
     assertTrue(data.timestamps.length === data.lastValues.length);
+    assertTrue(data.timestamps.length === data.totalDeltas.length);
+
+    const scale: CounterScaleOptions = this.config.scale || 'DEFAULT';
+
+    let minValues = data.minValues;
+    let maxValues = data.maxValues;
+    let lastValues = data.lastValues;
+    let maximumValue = data.maximumValue;
+    let minimumValue = data.minimumValue;
+    if (scale === 'DELTA') {
+      lastValues = data.totalDeltas;
+      minValues = data.totalDeltas;
+      maxValues = data.totalDeltas;
+      maximumValue = data.maximumDelta;
+      minimumValue = data.minimumDelta;
+    }
 
     const endPx = Math.floor(timeScale.timeToPx(visibleWindowTime.end));
-    const zeroY = MARGIN_TOP + RECT_HEIGHT / (data.minimumValue < 0 ? 2 : 1);
+    const zeroY = MARGIN_TOP + RECT_HEIGHT / (minimumValue < 0 ? 2 : 1);
 
     // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
-    const maxValue = Math.max(data.maximumValue, 0);
+    const maxValue = Math.max(maximumValue, 0);
 
-    let yMax = Math.max(Math.abs(data.minimumValue), maxValue);
+    let yMax = Math.max(Math.abs(minimumValue), maxValue);
     const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
     const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
     const pow10 = Math.pow(10, exp);
@@ -104,14 +151,17 @@
     const unitGroup = Math.floor(exp / 3);
     let yMin = 0;
     let yLabel = '';
-    if (this.config.scale === 'RELATIVE') {
-      yRange = data.maximumValue - data.minimumValue;
-      yMin = data.minimumValue;
+    if (scale === 'RELATIVE') {
+      yRange = maximumValue - minimumValue;
+      yMin = minimumValue;
       yLabel = 'min - max';
     } else {
-      yRange = data.minimumValue < 0 ? yMax * 2 : yMax;
-      yMin = data.minimumValue < 0 ? -yMax : 0;
+      yRange = minimumValue < 0 ? yMax * 2 : yMax;
+      yMin = minimumValue < 0 ? -yMax : 0;
       yLabel = `${yMax / Math.pow(10, unitGroup * 3)} ${kUnits[unitGroup]}`;
+      if (scale === 'DELTA') {
+        yLabel += '\u0394';
+      }
     }
 
     // There are 360deg of hue. We want a scale that starts at green with
@@ -132,7 +182,8 @@
       return Math.floor(timeScale.timeToPx(ts));
     };
     const calculateY = (value: number) => {
-      return zeroY - Math.round(((value - yMin) / yRange) * RECT_HEIGHT);
+      return MARGIN_TOP + RECT_HEIGHT -
+          Math.round(((value - yMin) / yRange) * RECT_HEIGHT);
     };
 
     ctx.beginPath();
@@ -140,9 +191,9 @@
     let lastDrawnY = zeroY;
     for (let i = 0; i < data.timestamps.length; i++) {
       const x = calculateX(data.timestamps[i]);
-      const minY = calculateY(data.minValues[i]);
-      const maxY = calculateY(data.maxValues[i]);
-      const lastY = calculateY(data.lastValues[i]);
+      const minY = calculateY(minValues[i]);
+      const maxY = calculateY(maxValues[i]);
+      const lastY = calculateY(lastValues[i]);
 
       ctx.lineTo(x, lastDrawnY);
       if (minY === maxY) {
@@ -175,7 +226,7 @@
 
     if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
       // TODO(hjd): Add units.
-      let text = 'value: ';
+      let text = scale === 'DELTA' ? 'delta: ' : 'value: ';
       text += `${this.hoveredValue.toLocaleString()}`;
 
       ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
@@ -185,7 +236,7 @@
       const xEnd = this.hoveredTsEnd === undefined ?
           endPx :
           Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
-      const y = zeroY -
+      const y = MARGIN_TOP + RECT_HEIGHT -
           Math.round(((this.hoveredValue - yMin) / yRange) * RECT_HEIGHT);
 
       // Highlight line.
@@ -245,10 +296,12 @@
     const {timeScale} = globals.frontendLocalState;
     const time = timeScale.pxToTime(x);
 
+    const values =
+        this.config.scale === 'DELTA' ? data.totalDeltas : data.lastValues;
     const [left, right] = searchSegment(data.timestamps, time);
     this.hoveredTs = left === -1 ? undefined : data.timestamps[left];
     this.hoveredTsEnd = right === -1 ? undefined : data.timestamps[right];
-    this.hoveredValue = left === -1 ? undefined : data.lastValues[left];
+    this.hoveredValue = left === -1 ? undefined : values[left];
   }
 
   onMouseOut() {