ui: utilise segment forest to improve counter tracks on large traces
This CL introduces the use of the segment forest to significantly speed
up the queries of counter tracks on large traces. Specifically the data
structure is exposed to Typescript via the use of a new "CounterMipmap"
trace processor operator.
Also while I'm here, fix a few subtle issues I found in the rendering
of counter tracks:
1) Don't draw things deep in negative x: this can cause disappearing
tracks when very very zoomed in
2) Improve perf of counter panels query.
3) Fix some subtle overlap between counter units and the hover line:
this is pixel peeping but still nice to fix. Specifically, on my
monitor, I saw the bottom 3/4 pixels overlapping with the top of the
counter line when the counter was at its maximum.
Change-Id: I603faaccd080f5e5ff07c37462a039e6850803ea
diff --git a/Android.bp b/Android.bp
index 8a4ba32..c66ad4e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -12286,6 +12286,7 @@
filegroup {
name: "perfetto_src_trace_processor_perfetto_sql_intrinsics_operators_operators",
srcs: [
+ "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
"src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
"src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc",
],
diff --git a/BUILD b/BUILD
index c9f3381..57b3bf6 100644
--- a/BUILD
+++ b/BUILD
@@ -2313,6 +2313,8 @@
perfetto_filegroup(
name = "src_trace_processor_perfetto_sql_intrinsics_operators_operators",
srcs = [
+ "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc",
+ "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h",
"src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.cc",
"src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h",
"src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.cc",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
index bbcb771..2dca8b6 100644
--- a/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/BUILD.gn
@@ -18,6 +18,8 @@
source_set("operators") {
sources = [
+ "counter_mipmap_operator.cc",
+ "counter_mipmap_operator.h",
"span_join_operator.cc",
"span_join_operator.h",
"window_operator.cc",
@@ -30,6 +32,7 @@
"../../../../../include/perfetto/trace_processor",
"../../../../../protos/perfetto/trace_processor:zero",
"../../../../base",
+ "../../../containers",
"../../../sqlite",
"../../../util",
"../../engine",
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
new file mode 100644
index 0000000..83321ce
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.cc
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h"
+
+#include <sqlite3.h>
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_result.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+#include "src/trace_processor/sqlite/sql_source.h"
+#include "src/trace_processor/sqlite/sqlite_utils.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+constexpr char kSchema[] = R"(
+ CREATE TABLE x(
+ in_window_start BIGINT HIDDEN,
+ in_window_end BIGINT HIDDEN,
+ in_window_step BIGINT HIDDEN,
+ min_value DOUBLE,
+ max_value DOUBLE,
+ last_ts BIGINT,
+ last_value DOUBLE,
+ PRIMARY KEY(last_ts)
+ ) WITHOUT ROWID
+)";
+
+enum ColumnIndex : size_t {
+ kInWindowStart = 0,
+ kInWindowEnd,
+ kInWindowStep,
+
+ kMinValue,
+ kMaxValue,
+ kLastTs,
+ kLastValue,
+};
+
+constexpr size_t kArgCount = kInWindowStep + 1;
+
+bool IsArgColumn(size_t index) {
+ return index < kArgCount;
+}
+
+using Counter = CounterMipmapOperator::Counter;
+using Agg = CounterMipmapOperator::Agg;
+using Forest = ImplicitSegmentForest<Counter, Agg>;
+
+} // namespace
+
+int CounterMipmapOperator::Create(sqlite3* db,
+ void* raw_ctx,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** vtab,
+ char** zErr) {
+ if (argc != 4) {
+ *zErr = sqlite3_mprintf("counter_mipmap: wrong number of arguments");
+ return SQLITE_ERROR;
+ }
+
+ if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+ return ret;
+ }
+
+ auto* ctx = GetContext(raw_ctx);
+ auto state = std::make_unique<State>();
+
+ std::string sql = "SELECT ts, value FROM ";
+ sql.append(argv[3]);
+ auto res = ctx->engine->ExecuteUntilLastStatement(
+ SqlSource::FromTraceProcessorImplementation(std::move(sql)));
+ if (!res.ok()) {
+ *zErr = sqlite3_mprintf("%s", res.status().c_message());
+ return SQLITE_ERROR;
+ }
+ do {
+ int64_t ts = sqlite3_column_int64(res->stmt.sqlite_stmt(), 0);
+ auto value =
+ static_cast<float>(sqlite3_column_double(res->stmt.sqlite_stmt(), 1));
+ state->timestamps.push_back(ts);
+ state->forest.Push(Counter{value, value});
+ } while (res->stmt.Step());
+ if (!res->stmt.status().ok()) {
+ *zErr = sqlite3_mprintf("%s", res->stmt.status().c_message());
+ return SQLITE_ERROR;
+ }
+
+ std::unique_ptr<Vtab> vtab_res = std::make_unique<Vtab>();
+ vtab_res->state = ctx->manager.OnCreate(argv, std::move(state));
+ *vtab = vtab_res.release();
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Destroy(sqlite3_vtab* vtab) {
+ std::unique_ptr<Vtab> tab(GetVtab(vtab));
+ sqlite::ModuleStateManager<CounterMipmapOperator>::OnDestroy(tab->state);
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Connect(sqlite3* db,
+ void* raw_ctx,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** vtab,
+ char**) {
+ PERFETTO_CHECK(argc == 4);
+ if (int ret = sqlite3_declare_vtab(db, kSchema); ret != SQLITE_OK) {
+ return ret;
+ }
+ auto* ctx = GetContext(raw_ctx);
+ std::unique_ptr<Vtab> res = std::make_unique<Vtab>();
+ res->state = ctx->manager.OnConnect(argv);
+ *vtab = res.release();
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Disconnect(sqlite3_vtab* vtab) {
+ std::unique_ptr<Vtab> tab(GetVtab(vtab));
+ sqlite::ModuleStateManager<CounterMipmapOperator>::OnDisconnect(tab->state);
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::BestIndex(sqlite3_vtab*, sqlite3_index_info* info) {
+ base::Status status =
+ sqlite::utils::ValidateFunctionArguments(info, kArgCount, IsArgColumn);
+ if (!status.ok()) {
+ return SQLITE_CONSTRAINT;
+ }
+ if (info->nConstraint != kArgCount) {
+ return SQLITE_CONSTRAINT;
+ }
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Open(sqlite3_vtab*, sqlite3_vtab_cursor** cursor) {
+ std::unique_ptr<Cursor> c = std::make_unique<Cursor>();
+ *cursor = c.release();
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Close(sqlite3_vtab_cursor* cursor) {
+ std::unique_ptr<Cursor> c(GetCursor(cursor));
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Filter(sqlite3_vtab_cursor* cursor,
+ int,
+ const char*,
+ int argc,
+ sqlite3_value** argv) {
+ auto* c = GetCursor(cursor);
+ auto* t = GetVtab(c->pVtab);
+ auto* state =
+ sqlite::ModuleStateManager<CounterMipmapOperator>::GetState(t->state);
+ PERFETTO_CHECK(argc == kArgCount);
+
+ int64_t start_ts = sqlite3_value_int64(argv[0]);
+ int64_t end_ts = sqlite3_value_int64(argv[1]);
+ int64_t step_ts = sqlite3_value_int64(argv[2]);
+ if (start_ts == end_ts) {
+ return sqlite::utils::SetError(t, "counter_mipmap: empty range provided");
+ }
+
+ c->index = 0;
+ c->counters.clear();
+
+ // If there is a counter value before the start of this window, include it in
+ // the aggregation as well becaue it contributes to what should be rendered
+ // here.
+ auto ts_lb = std::lower_bound(state->timestamps.begin(),
+ state->timestamps.end(), start_ts);
+ if (ts_lb != state->timestamps.begin() &&
+ (ts_lb == state->timestamps.end() || *ts_lb != start_ts)) {
+ --ts_lb;
+ }
+ int64_t start_idx = std::distance(state->timestamps.begin(), ts_lb);
+ for (int64_t s = start_ts; s < end_ts; s += step_ts) {
+ int64_t end_idx =
+ std::distance(state->timestamps.begin(),
+ std::lower_bound(state->timestamps.begin() +
+ static_cast<int64_t>(start_idx),
+ state->timestamps.end(), s + step_ts));
+ if (start_idx == end_idx) {
+ continue;
+ }
+ c->counters.emplace_back(Cursor::Result{
+ state->forest.Query(static_cast<uint32_t>(start_idx),
+ static_cast<uint32_t>(end_idx)),
+ state->forest[static_cast<uint32_t>(end_idx) - 1],
+ state->timestamps[static_cast<uint32_t>(end_idx) - 1],
+ });
+ start_idx = end_idx;
+ }
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Next(sqlite3_vtab_cursor* cursor) {
+ GetCursor(cursor)->index++;
+ return SQLITE_OK;
+}
+
+int CounterMipmapOperator::Eof(sqlite3_vtab_cursor* cursor) {
+ auto* c = GetCursor(cursor);
+ return c->index >= c->counters.size();
+}
+
+int CounterMipmapOperator::Column(sqlite3_vtab_cursor* cursor,
+ sqlite3_context* ctx,
+ int N) {
+ auto* t = GetVtab(cursor->pVtab);
+ auto* c = GetCursor(cursor);
+ const auto& res = c->counters[c->index];
+ switch (N) {
+ case ColumnIndex::kMinValue:
+ sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.min));
+ return SQLITE_OK;
+ case ColumnIndex::kMaxValue:
+ sqlite::result::Double(ctx, static_cast<double>(res.min_max_counter.max));
+ return SQLITE_OK;
+ case ColumnIndex::kLastTs:
+ sqlite::result::Long(ctx, res.last_ts);
+ return SQLITE_OK;
+ case ColumnIndex::kLastValue:
+ PERFETTO_DCHECK(
+ std::equal_to<>()(res.last_counter.min, res.last_counter.max));
+ sqlite::result::Double(ctx, static_cast<double>(res.last_counter.min));
+ return SQLITE_OK;
+ default:
+ return sqlite::utils::SetError(t, "Bad column");
+ }
+ PERFETTO_FATAL("For GCC");
+}
+
+int CounterMipmapOperator::Rowid(sqlite3_vtab_cursor*, sqlite_int64*) {
+ return SQLITE_ERROR;
+}
+
+} // namespace perfetto::trace_processor
diff --git a/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
new file mode 100644
index 0000000..1b0897c
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
+#define SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
+
+#include <sqlite3.h>
+#include <cstdint>
+#include <vector>
+
+#include "src/trace_processor/containers/implicit_segment_forest.h"
+#include "src/trace_processor/perfetto_sql/engine/perfetto_sql_engine.h"
+#include "src/trace_processor/sqlite/bindings/sqlite_module.h"
+#include "src/trace_processor/sqlite/module_lifecycle_manager.h"
+
+namespace perfetto::trace_processor {
+
+// Operator for building "mipmaps" [1] over the counter-like tracks.
+//
+// In the context of trace data, mipmap really means aggregating the counter
+// values in a given time period into the {min, max, last} value for that
+// period, allowing UIs to efficiently display the contents of a counter track
+// when very zoomed out.
+//
+// Specifically, we are computing the query:
+// ```
+// select
+// last_value(ts),
+// min(value),
+// max(value),
+// last_value(value)
+// from $input in
+// where in.ts_end >= $window_start and in.ts <= $window_end
+// group by ts / $window_resolution
+// order by ts
+// ```
+// but in O(logn) time by using a segment-tree like data structure (see
+// ImplicitSegmentForest).
+//
+// [1] https://en.wikipedia.org/wiki/Mipmap
+struct CounterMipmapOperator : sqlite::Module<CounterMipmapOperator> {
+ struct Counter {
+ float min;
+ float max;
+ };
+ struct Agg {
+ Counter operator()(const Counter& a, const Counter& b) {
+ Counter res;
+ res.min = b.min < a.min ? b.min : a.min;
+ res.max = b.max > a.max ? b.max : a.max;
+ return res;
+ }
+ };
+ struct State {
+ ImplicitSegmentForest<Counter, Agg> forest;
+ std::vector<int64_t> timestamps;
+ };
+ struct Context {
+ explicit Context(PerfettoSqlEngine* _engine) : engine(_engine) {}
+ PerfettoSqlEngine* engine;
+ sqlite::ModuleStateManager<CounterMipmapOperator> manager;
+ };
+ struct Vtab : sqlite::Module<CounterMipmapOperator>::Vtab {
+ sqlite::ModuleStateManager<CounterMipmapOperator>::PerVtabState* state;
+ };
+ struct Cursor : sqlite::Module<CounterMipmapOperator>::Cursor {
+ struct Result {
+ Counter min_max_counter;
+ Counter last_counter;
+ int64_t last_ts;
+ };
+ std::vector<Result> counters;
+ uint32_t index;
+ };
+
+ static constexpr auto kType = kCreateOnly;
+ static constexpr bool kSupportsWrites = false;
+ static constexpr bool kDoesOverloadFunctions = false;
+
+ static int Create(sqlite3*,
+ void*,
+ int,
+ const char* const*,
+ sqlite3_vtab**,
+ char**);
+ static int Destroy(sqlite3_vtab*);
+
+ static int Connect(sqlite3*,
+ void*,
+ int,
+ const char* const*,
+ sqlite3_vtab**,
+ char**);
+ static int Disconnect(sqlite3_vtab*);
+
+ static int BestIndex(sqlite3_vtab*, sqlite3_index_info*);
+
+ static int Open(sqlite3_vtab*, sqlite3_vtab_cursor**);
+ static int Close(sqlite3_vtab_cursor*);
+
+ static int Filter(sqlite3_vtab_cursor*,
+ int,
+ const char*,
+ int,
+ sqlite3_value**);
+ static int Next(sqlite3_vtab_cursor*);
+ static int Eof(sqlite3_vtab_cursor*);
+ static int Column(sqlite3_vtab_cursor*, sqlite3_context*, int);
+ static int Rowid(sqlite3_vtab_cursor*, sqlite_int64*);
+};
+
+} // namespace perfetto::trace_processor
+
+#endif // SRC_TRACE_PROCESSOR_PERFETTO_SQL_INTRINSICS_OPERATORS_COUNTER_MIPMAP_OPERATOR_H_
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index b36a63a..2deba7a 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -81,6 +81,7 @@
#include "src/trace_processor/perfetto_sql/intrinsics/functions/to_ftrace.h"
#include "src/trace_processor/perfetto_sql/intrinsics/functions/utils.h"
#include "src/trace_processor/perfetto_sql/intrinsics/functions/window_functions.h"
+#include "src/trace_processor/perfetto_sql/intrinsics/operators/counter_mipmap_operator.h"
#include "src/trace_processor/perfetto_sql/intrinsics/operators/span_join_operator.h"
#include "src/trace_processor/perfetto_sql/intrinsics/operators/window_operator.h"
#include "src/trace_processor/perfetto_sql/intrinsics/table_functions/ancestor.h"
@@ -747,6 +748,9 @@
std::make_unique<SpanJoinOperatorModule::Context>(engine_.get()));
engine_->sqlite_engine()->RegisterVirtualTableModule<WindowOperatorModule>(
"window", std::make_unique<WindowOperatorModule::Context>());
+ engine_->sqlite_engine()->RegisterVirtualTableModule<CounterMipmapOperator>(
+ "__intrinsic_counter_mipmap",
+ std::make_unique<CounterMipmapOperator::Context>(engine_.get()));
// Initalize the tables and views in the prelude.
InitializePreludeTablesViews(db);
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 656ec56..6285b27 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -17,7 +17,7 @@
import {searchSegment} from '../base/binary_search';
import {Disposable, NullDisposable} from '../base/disposable';
import {assertTrue, assertUnreachable} from '../base/logging';
-import {duration, Time, time} from '../base/time';
+import {Time, time} from '../base/time';
import {drawTrackHoverTooltip} from '../common/canvas_utils';
import {raf} from '../core/raf_scheduler';
import {EngineProxy, LONG, NUM, Track} from '../public';
@@ -27,9 +27,8 @@
import {checkerboardExcept} from './checkerboard';
import {globals} from './globals';
import {PanelSize} from './panel';
-import {constraintsToQuerySuffix} from './sql_utils';
import {NewTrackArgs} from './track';
-import {CacheKey, TimelineCache} from '../core/timeline_cache';
+import {CacheKey} from '../core/timeline_cache';
import {featureFlags} from '../core/feature_flags';
export const COUNTER_DEBUG_MENU_ITEMS = featureFlags.register({
@@ -128,8 +127,6 @@
interface CounterData {
timestamps: BigInt64Array;
- counts: Uint32Array;
- avgValues: Float64Array;
minDisplayValues: Float64Array;
maxDisplayValues: Float64Array;
lastDisplayValues: Float64Array;
@@ -142,13 +139,10 @@
interface CounterLimits {
maxDisplayValue: number;
minDisplayValue: number;
- maxDurNs: duration;
}
interface CounterTooltipState {
lastDisplayValue: number;
- avgValue: number;
- count: number;
ts: time;
tsEnd?: time;
}
@@ -207,16 +201,12 @@
private counters: CounterData = {
timestamps: new BigInt64Array(0),
- counts: new Uint32Array(0),
- avgValues: new Float64Array(0),
minDisplayValues: new Float64Array(0),
maxDisplayValues: new Float64Array(0),
lastDisplayValues: new Float64Array(0),
displayValueRange: [0, 0],
};
- private cache: TimelineCache<CounterData> = new TimelineCache(5);
-
// Cleanup hook for onInit.
private initState?: Disposable;
@@ -266,7 +256,7 @@
constructor(args: BaseCounterTrackArgs) {
this.engine = args.engine;
- this.trackKey = args.trackKey;
+ this.trackKey = args.trackKey.replaceAll('-', '_');
this.defaultOptions = args.options ?? {};
}
@@ -427,12 +417,9 @@
protected invalidate() {
this.limits = undefined;
- this.cache.invalidate();
this.countersKey = CacheKey.zero();
this.counters = {
timestamps: new BigInt64Array(0),
- counts: new Uint32Array(0),
- avgValues: new Float64Array(0),
minDisplayValues: new Float64Array(0),
maxDisplayValues: new Float64Array(0),
lastDisplayValues: new Float64Array(0),
@@ -479,10 +466,7 @@
}
render(ctx: CanvasRenderingContext2D, size: PanelSize) {
- const {
- visibleTimeScale: timeScale,
- // visibleWindowTime: vizTime,
- } = globals.timeline;
+ const {visibleTimeScale: timeScale} = globals.timeline;
// In any case, draw whatever we have (which might be stale/incomplete).
@@ -490,11 +474,17 @@
const data = this.counters;
if (data.timestamps.length === 0 || limits === undefined) {
+ checkerboardExcept(
+ ctx,
+ this.getHeight(),
+ 0,
+ size.width,
+ timeScale.timeToPx(this.countersKey.start),
+ timeScale.timeToPx(this.countersKey.end),
+ );
return;
}
- assertTrue(data.timestamps.length === data.counts.length);
- assertTrue(data.timestamps.length === data.avgValues.length);
assertTrue(data.timestamps.length === data.minDisplayValues.length);
assertTrue(data.timestamps.length === data.maxDisplayValues.length);
assertTrue(data.timestamps.length === data.lastDisplayValues.length);
@@ -541,11 +531,11 @@
ctx.beginPath();
const timestamp = Time.fromRaw(timestamps[0]);
- ctx.moveTo(calculateX(timestamp), zeroY);
+ ctx.moveTo(Math.max(0, calculateX(timestamp)), zeroY);
let lastDrawnY = zeroY;
for (let i = 0; i < timestamps.length; i++) {
const timestamp = Time.fromRaw(timestamps[i]);
- const x = calculateX(timestamp);
+ const x = Math.max(0, calculateX(timestamp));
const minY = calculateY(minValues[i]);
const maxY = calculateY(maxValues[i]);
const lastY = calculateY(lastValues[i]);
@@ -582,7 +572,7 @@
const hover = this.hover;
if (hover !== undefined) {
- let text = `${hover.avgValue.toLocaleString()}`;
+ let text = `${hover.lastDisplayValue.toLocaleString()}`;
const unit = this.unit;
switch (options.yMode) {
@@ -600,14 +590,11 @@
break;
}
- if (hover.count > 1) {
- text += ` (avg of ${hover.count})`;
- }
-
ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
- const xStart = Math.floor(timeScale.timeToPx(hover.ts));
+ const rawXStart = calculateX(hover.ts);
+ const xStart = Math.max(0, rawXStart);
const xEnd =
hover.tsEnd === undefined
? endPx
@@ -627,17 +614,19 @@
ctx.stroke();
ctx.lineWidth = 1;
- // Draw change marker.
- ctx.beginPath();
- ctx.arc(
- xStart,
- y,
- 3 /* r*/,
- 0 /* start angle*/,
- 2 * Math.PI /* end angle*/,
- );
- ctx.fill();
- ctx.stroke();
+ // Draw change marker if it would be visible.
+ if (rawXStart >= -6) {
+ ctx.beginPath();
+ ctx.arc(
+ xStart,
+ y,
+ 3 /* r*/,
+ 0 /* start angle*/,
+ 2 * Math.PI /* end angle*/,
+ );
+ ctx.fill();
+ ctx.stroke();
+ }
// Draw the tooltip.
drawTrackHoverTooltip(ctx, this.mousePos, this.getHeight(), text);
@@ -645,11 +634,11 @@
// Write the Y scale on the top left corner.
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
- ctx.fillRect(0, 0, 42, 16);
+ ctx.fillRect(0, 0, 42, 13);
ctx.fillStyle = '#666';
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
- ctx.fillText(`${yLabel}`, 5, 14);
+ ctx.fillText(`${yLabel}`, 5, 11);
// TODO(hjd): Refactor this into checkerboardExcept
{
@@ -691,14 +680,10 @@
const tsEnd =
right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
const lastDisplayValue = data.lastDisplayValues[left];
- const count = data.counts[left];
- const avgValue = data.avgValues[left];
this.hover = {
ts,
tsEnd,
lastDisplayValue,
- count,
- avgValue,
};
}
@@ -785,7 +770,6 @@
break;
default:
assertUnreachable(options.yMode);
- break;
}
if (options.yDisplay === 'log') {
@@ -805,7 +789,6 @@
const options = this.getCounterOptions();
let valueExpr;
-
switch (options.yMode) {
case 'value':
valueExpr = 'value';
@@ -819,7 +802,6 @@
break;
default:
assertUnreachable(options.yMode);
- break;
}
let displayValueExpr = valueExpr;
@@ -831,7 +813,6 @@
WITH data AS (
SELECT
ts,
- ${valueExpr} as value,
${displayValueExpr} as displayValue
FROM (${this.getSqlSource()})
)
@@ -841,39 +822,32 @@
private async maybeRequestData(rawCountersKey: CacheKey) {
let limits = this.limits;
if (limits === undefined) {
- const maxDurQuery = await this.engine.query(`
- ${this.getSqlPreamble()}
- SELECT
- max(dur) as maxDur
- FROM (
- SELECT
- lead(ts, 1, ts) over (order by ts) - ts as dur
- FROM data
- )
- `);
- const maxDurRow = maxDurQuery.firstRow({
- maxDur: LONG,
- });
- const maxDurNs = maxDurRow.maxDur;
-
const displayValueQuery = await this.engine.query(`
- ${this.getSqlPreamble()}
- SELECT
- max(displayValue) as maxDisplayValue,
- min(displayValue) as minDisplayValue
- FROM data
+ drop table if exists counter_${this.trackKey};
+
+ create virtual table counter_${this.trackKey}
+ using __intrinsic_counter_mipmap((
+ ${this.getSqlPreamble()}
+ SELECT
+ ts,
+ displayValue as value
+ FROM data
+ ));
+
+ select
+ min_value as minDisplayValue,
+ max_value as maxDisplayValue
+ from counter_${this.trackKey}(
+ trace_start(), trace_end(), trace_dur()
+ );
`);
- const displayValueRow = displayValueQuery.firstRow({
+ const {minDisplayValue, maxDisplayValue} = displayValueQuery.firstRow({
minDisplayValue: NUM,
maxDisplayValue: NUM,
});
-
- const minDisplayValue = displayValueRow.minDisplayValue;
- const maxDisplayValue = displayValueRow.maxDisplayValue;
limits = this.limits = {
minDisplayValue,
maxDisplayValue,
- maxDurNs,
};
}
@@ -888,42 +862,21 @@
);
}
- const maybeCachedCounters = this.cache.lookup(countersKey);
- if (maybeCachedCounters) {
- this.countersKey = countersKey;
- this.counters = maybeCachedCounters;
- return;
- }
-
- const bucketNs = countersKey.bucketSize;
-
- const constraint = constraintsToQuerySuffix({
- filters: [
- `ts >= ${countersKey.start} - ${limits.maxDurNs}`,
- `ts <= ${countersKey.end}`,
- `value is not null`,
- ],
- groupBy: ['tsq'],
- orderBy: ['tsq'],
- });
-
const queryRes = await this.engine.query(`
- ${this.getSqlPreamble()}
SELECT
- (ts + ${bucketNs / 2n}) / ${bucketNs} * ${bucketNs} as tsq,
- count(value) as count,
- avg(value) as avgValue,
- min(displayValue) as minDisplayValue,
- max(displayValue) as maxDisplayValue,
- value_at_max_ts(ts, displayValue) as lastDisplayValue
- FROM data
- ${constraint}
+ min_value as minDisplayValue,
+ max_value as maxDisplayValue,
+ last_ts as ts,
+ last_value as lastDisplayValue
+ FROM counter_${this.trackKey}(
+ ${countersKey.start},
+ ${countersKey.end},
+ ${countersKey.bucketSize}
+ );
`);
const it = queryRes.iter({
- tsq: LONG,
- count: NUM,
- avgValue: NUM,
+ ts: LONG,
minDisplayValue: NUM,
maxDisplayValue: NUM,
lastDisplayValue: NUM,
@@ -932,8 +885,6 @@
const numRows = queryRes.numRows();
const data: CounterData = {
timestamps: new BigInt64Array(numRows),
- counts: new Uint32Array(numRows),
- avgValues: new Float64Array(numRows),
minDisplayValues: new Float64Array(numRows),
maxDisplayValues: new Float64Array(numRows),
lastDisplayValues: new Float64Array(numRows),
@@ -943,10 +894,7 @@
let min = 0;
let max = 0;
for (let row = 0; it.valid(); it.next(), row++) {
- const ts = Time.fromRaw(it.tsq);
- data.timestamps[row] = ts;
- data.counts[row] = it.count;
- data.avgValues[row] = it.avgValue;
+ data.timestamps[row] = Time.fromRaw(it.ts);
data.minDisplayValues[row] = it.minDisplayValue;
data.maxDisplayValues[row] = it.maxDisplayValue;
data.lastDisplayValues[row] = it.lastDisplayValue;
@@ -956,7 +904,6 @@
data.displayValueRange = [min, max];
- this.cache.insert(countersKey, data);
this.countersKey = countersKey;
this.counters = data;
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 5fdf938..55773cd 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -136,22 +136,24 @@
const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
const query = `
- WITH X AS (
- SELECT
- id,
- ts AS leftTs,
- LEAD(ts) OVER (ORDER BY ts) AS rightTs
- FROM counter
- WHERE track_id = ${this.trackId}
- ORDER BY ts
- )
- SELECT
+ select
id,
- leftTs,
- rightTs
- FROM X
- WHERE rightTs > ${time}
- LIMIT 1
+ ts as leftTs,
+ (
+ select ts
+ from ${this.rootTable}
+ where
+ track_id = ${this.trackId}
+ and ts >= ${time}
+ order by ts
+ limit 1
+ ) as rightTs
+ from ${this.rootTable}
+ where
+ track_id = ${this.trackId}
+ and ts < ${time}
+ order by ts DESC
+ limit 1
`;
this.engine.query(query).then((result) => {