blob: 3e84bb6b7b41c41f2305ae3bc05abec0732c9979 [file] [log] [blame]
// Copyright (C) 2023 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 m from 'mithril';
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 {drawTrackHoverTooltip} from '../common/canvas_utils';
import {raf} from '../core/raf_scheduler';
import {EngineProxy, LONG, NUM, Track} from '../public';
import {Button} from '../widgets/button';
import {MenuItem, MenuDivider, PopupMenu2} from '../widgets/menu';
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 {featureFlags} from '../core/feature_flags';
export const COUNTER_DEBUG_MENU_ITEMS = featureFlags.register({
id: 'counterDebugMenuItems',
name: 'Counter debug menu items',
description: 'Extra counter menu items for debugging purposes.',
defaultValue: false,
});
function roundAway(n: number): number {
const exp = Math.ceil(Math.log10(Math.max(Math.abs(n), 1)));
const pow10 = Math.pow(10, exp);
return Math.sign(n) * (Math.ceil(Math.abs(n) / (pow10 / 4)) * (pow10 / 4));
}
function toLabel(n: number): string {
if (n === 0) {
return '0';
}
const units: [number, string][] = [
[0.000000001, 'n'],
[0.000001, 'u'],
[0.001, 'm'],
[1, ''],
[1000, 'K'],
[1000*1000, 'M'],
[1000*1000*1000, 'G'],
[1000*1000*1000*1000, 'T'],
];
let largestMultiplier;
let largestUnit;
[largestMultiplier, largestUnit] = units[0];
for (const [multiplier, unit] of units) {
if (multiplier >= n) {
break;
}
[largestMultiplier, largestUnit] = [multiplier, unit];
}
return `${Math.round(n/largestMultiplier)}${largestUnit}`;
}
class RangeSharer {
static singleton?: RangeSharer;
static get(): RangeSharer {
if (RangeSharer.singleton === undefined) {
RangeSharer.singleton = new RangeSharer();
}
return RangeSharer.singleton;
}
private tagToRange: Map<string, [number, number]>;
private keyToEnabled: Map<string, boolean>;
constructor() {
this.tagToRange = new Map();
this.keyToEnabled = new Map();
}
isEnabled(key: string): boolean {
const value = this.keyToEnabled.get(key);
if (value === undefined) {
return true;
}
return value;
}
setEnabled(key: string, enabled: boolean): void {
this.keyToEnabled.set(key, enabled);
}
share(options: CounterOptions, [min, max]: [number, number]):
[number, number] {
const key = options.yRangeSharingKey;
if (key === undefined || !this.isEnabled(key)) {
return [min, max];
}
const tag = `${options.yRangeSharingKey}-${options.yMode}-${options.yDisplay}-${!!options.enlarge}`;
const cachedRange = this.tagToRange.get(tag);
if (cachedRange === undefined) {
this.tagToRange.set(tag, [min, max]);
return [min, max];
}
cachedRange[0] = Math.min(min, cachedRange[0]);
cachedRange[1] = Math.max(max, cachedRange[1]);
return [cachedRange[0], cachedRange[1]];
}
}
interface CounterData {
timestamps: BigInt64Array;
counts: Uint32Array;
avgValues: Float64Array;
minDisplayValues: Float64Array;
maxDisplayValues: Float64Array;
lastDisplayValues: Float64Array;
displayValueRange: [number, number];
}
// 0.5 Makes the horizontal lines sharp.
const MARGIN_TOP = 3.5;
interface CounterLimits {
maxDisplayValue: number;
minDisplayValue: number;
maxDurNs: duration;
}
interface CounterTooltipState {
lastDisplayValue: number;
avgValue: number;
count: number;
ts: time;
tsEnd?: time;
}
export interface CounterOptions {
// Mode for computing the y value. Options are:
// value = v[t] directly the value of the counter at time t
// delta = v[t] - v[t-1] delta between value and previous value
// rate = (v[t] - v[t-1]) / dt as delta but normalized for time
yMode: 'value'|'delta'|'rate';
// Whether Y scale should cover all of the possible values (and therefore, be
// static) or whether it should be dynamic and cover only the visible values.
yRange: 'all'|'viewport';
// Whether the Y scale should:
// zero = y-axis scale should cover the origin (zero)
// minmax = y-axis scale should cover just the range of yRange
// log = as minmax but also use a log scale
yDisplay: 'zero'|'minmax'|'log';
// Whether the range boundaries should be strict and use the precise min/max
// values or whether they should be rounded down/up to the nearest human
// readable value.
yRangeRounding: 'strict'|'human_readable';
// Allows *extending* the range of the y-axis counter increasing
// the maximum (via yOverrideMaximum) or decreasing the minimum
// (via yOverrideMinimum). This is useful for percentage counters
// where the range (0-100) is known statically upfront and even if
// the trace only includes smaller values.
yOverrideMaximum?: number;
yOverrideMinimum?: number;
// If set all counters with the same key share a range.
yRangeSharingKey?: string;
// Show the chart as 4x the height.
enlarge?: boolean;
// unit for the counter. This is displayed in the tooltip and
// legend.
unit?: string;
}
export type BaseCounterTrackArgs = NewTrackArgs & {
options?: Partial<CounterOptions>;
};
export abstract class BaseCounterTrack implements Track {
protected engine: EngineProxy;
protected trackKey: string;
// This is the over-skirted cached bounds:
private countersKey: CacheKey = CacheKey.zero();
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;
private limits?: CounterLimits;
private mousePos = {x: 0, y: 0};
private hover?: CounterTooltipState;
private defaultOptions: Partial<CounterOptions>;
private options?: CounterOptions;
private getCounterOptions(): CounterOptions {
if (this.options === undefined) {
const options = this.getDefaultCounterOptions();
for (const [key, value] of Object.entries(this.defaultOptions)) {
if (value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(options as any)[key] = value;
}
}
this.options = options;
}
return this.options;
}
// Extension points.
// onInit hook lets you do asynchronous set up e.g. creating a table
// etc. We guarantee that this will be resolved before doing any
// queries using the result of getSqlSource(). All persistent
// state in trace_processor should be cleaned up when dispose is
// called on the returned hook.
async onInit(): Promise<Disposable> {
return new NullDisposable();
}
// This should be an SQL expression returning the columns `ts` and `value`.
abstract getSqlSource(): string;
protected getDefaultCounterOptions(): CounterOptions {
return {
yRange: 'all',
yRangeRounding: 'human_readable',
yMode: 'value',
yDisplay: 'zero',
};
}
constructor(args: BaseCounterTrackArgs) {
this.engine = args.engine;
this.trackKey = args.trackKey;
this.defaultOptions = args.options ?? {};
}
getHeight() {
const height = 40;
return this.getCounterOptions().enlarge ? height * 4: height;
}
// A method to render menu items for switching the defualt
// rendering options. Useful if a subclass wants to incorporate it
// as a submenu.
protected getCounterContextMenuItems(): m.Children {
const options = this.getCounterOptions();
return [
m(MenuItem,
{
label: `Display (currently: ${options.yDisplay})`,
},
m(MenuItem, {
label: 'Zero-based',
icon: (options.yDisplay === 'zero') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yDisplay = 'zero';
this.invalidate();
},
}),
m(MenuItem, {
label: 'Min/Max',
icon: (options.yDisplay === 'minmax') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yDisplay = 'minmax';
this.invalidate();
},
}),
m(MenuItem, {
label: 'Log',
icon: (options.yDisplay === 'log') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yDisplay = 'log';
this.invalidate();
},
}),
),
m(MenuItem, {
label: 'Zoom on scroll',
icon: (options.yRange === 'viewport') ? 'check_box' : 'check_box_outline_blank',
onclick: () => {
options.yRange = options.yRange === 'viewport' ? 'all' : 'viewport';
this.invalidate();
},
}),
m(MenuItem, {
label: `Enlarge`,
icon: options.enlarge ? 'check_box' : 'check_box_outline_blank',
onclick: () => {
options.enlarge = !options.enlarge;
this.invalidate();
},
}),
options.yRangeSharingKey && m(MenuItem, {
label: `Share y-axis scale (group: ${options.yRangeSharingKey})`,
icon: RangeSharer.get().isEnabled(options.yRangeSharingKey) ? 'check_box' : 'check_box_outline_blank',
onclick: () => {
const key = options.yRangeSharingKey;
if (key === undefined) {
return;
}
const sharer = RangeSharer.get();
sharer.setEnabled(key, !sharer.isEnabled(key));
this.invalidate();
},
}),
COUNTER_DEBUG_MENU_ITEMS.get() && [
m(MenuDivider),
m(MenuItem,
{
label: `Mode (currently: ${options.yMode})`,
},
m(MenuItem, {
label: 'Value',
icon: (options.yMode === 'value') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yMode = 'value';
this.invalidate();
},
}),
m(MenuItem, {
label: 'Delta',
icon: (options.yMode === 'delta') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yMode = 'delta';
this.invalidate();
},
}),
m(MenuItem, {
label: 'Rate',
icon: (options.yMode === 'rate') ? 'radio_button_checked' : 'radio_button_unchecked',
onclick: () => {
options.yMode = 'rate';
this.invalidate();
},
}),
),
m(MenuItem, {
label: 'Round y-axis scale',
icon: (options.yRangeRounding === 'human_readable') ? 'check_box' : 'check_box_outline_blank',
onclick: () => {
options.yRangeRounding = options.yRangeRounding === 'human_readable' ? 'strict' : 'human_readable';
this.invalidate();
},
}),
],
];
}
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),
displayValueRange: [0, 0],
};
this.hover = undefined;
raf.scheduleFullRedraw();
}
// A method to render a context menu corresponding to switching the rendering
// modes. By default, getTrackShellButtons renders it, but a subclass can call
// it manually, if they want to customise rendering track buttons.
protected getCounterContextMenu(): m.Child {
return m(
PopupMenu2,
{
trigger: m(Button, {icon: 'show_chart', minimal: true}),
},
this.getCounterContextMenuItems(),
);
}
getTrackShellButtons(): m.Children {
return [
this.getCounterContextMenu(),
];
}
async onCreate(): Promise<void> {
this.initState = await this.onInit();
}
async onUpdate(): Promise<void> {
const {
visibleTimeScale: timeScale,
visibleWindowTime: vizTime,
} = globals.timeline;
const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
const rawStartNs = vizTime.start.toTime();
const rawEndNs = vizTime.end.toTime();
const rawCountersKey = CacheKey.create(rawStartNs, rawEndNs, windowSizePx);
// If the visible time range is outside the cached area, requests
// asynchronously new data from the SQL engine.
await this.maybeRequestData(rawCountersKey);
}
render(ctx: CanvasRenderingContext2D, size: PanelSize) {
const {
visibleTimeScale: timeScale,
// visibleWindowTime: vizTime,
} = globals.timeline;
// In any case, draw whatever we have (which might be stale/incomplete).
const limits = this.limits;
const data = this.counters;
if (data.timestamps.length === 0 || limits === undefined) {
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);
const options = this.getCounterOptions();
const timestamps = data.timestamps;
const minValues = data.minDisplayValues;
const maxValues = data.maxDisplayValues;
const lastValues = data.lastDisplayValues;
// Choose a range for the y-axis
const {yRange, yMin, yMax, yLabel} =
this.computeYRange(limits, data.displayValueRange);
const effectiveHeight = this.getHeight() - MARGIN_TOP;
const endPx = size.width;
const hasZero = yMin < 0 && yMax > 0;
let zeroY = effectiveHeight + MARGIN_TOP;
if (hasZero) {
zeroY = effectiveHeight * (yMax / (yMax - yMin)) + MARGIN_TOP;
}
// There are 360deg of hue. We want a scale that starts at green with
// exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet
// around exp >= 9 (1GB).
// The hue scale looks like this:
// 0 180 360
// Red orange green | blue purple magenta
// So we want to start @ 180deg with pow=0, go down to 0deg and then wrap
// back from 360deg back to 180deg.
const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
const expCapped = Math.min(Math.max(exp - 3), 9);
const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360;
ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
const calculateX = (ts: time) => {
return Math.floor(timeScale.timeToPx(ts));
};
const calculateY = (value: number) => {
return MARGIN_TOP + effectiveHeight -
Math.round(((value - yMin) / yRange) * effectiveHeight);
};
ctx.beginPath();
const timestamp = Time.fromRaw(timestamps[0]);
ctx.moveTo(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 minY = calculateY(minValues[i]);
const maxY = calculateY(maxValues[i]);
const lastY = calculateY(lastValues[i]);
ctx.lineTo(x, lastDrawnY);
if (minY === maxY) {
assertTrue(lastY === minY);
ctx.lineTo(x, lastY);
} else {
ctx.lineTo(x, minY);
ctx.lineTo(x, maxY);
ctx.lineTo(x, lastY);
}
lastDrawnY = lastY;
}
ctx.lineTo(endPx, lastDrawnY);
ctx.lineTo(endPx, zeroY);
ctx.closePath();
ctx.fill();
ctx.stroke();
if (hasZero) {
// Draw the Y=0 dashed line.
ctx.strokeStyle = `hsl(${hue}, 10%, 71%)`;
ctx.beginPath();
ctx.setLineDash([2, 4]);
ctx.moveTo(0, zeroY);
ctx.lineTo(endPx, zeroY);
ctx.closePath();
ctx.stroke();
ctx.setLineDash([]);
}
ctx.font = '10px Roboto Condensed';
const hover = this.hover;
if (hover !== undefined) {
let text = `${hover.avgValue.toLocaleString()}`;
const unit = this.unit;
switch (options.yMode) {
case 'value':
text = `${text} ${unit}`;
break;
case 'delta':
text = `${text} \u0394${unit}`;
break;
case 'rate':
text = `${text} \u0394${unit}/s`;
break;
default:
assertUnreachable(options.yMode);
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 xEnd = hover.tsEnd === undefined ?
endPx :
Math.floor(timeScale.timeToPx(hover.tsEnd));
const y = MARGIN_TOP + effectiveHeight -
Math.round(((hover.lastDisplayValue - yMin) / yRange) *
effectiveHeight);
// Highlight line.
ctx.beginPath();
ctx.moveTo(xStart, y);
ctx.lineTo(xEnd, y);
ctx.lineWidth = 3;
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 the tooltip.
drawTrackHoverTooltip(ctx, this.mousePos, this.getHeight(), text);
}
// Write the Y scale on the top left corner.
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
ctx.fillRect(0, 0, 42, 16);
ctx.fillStyle = '#666';
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
ctx.fillText(`${yLabel}`, 5, 14);
// TODO(hjd): Refactor this into checkerboardExcept
{
const counterEndPx = Infinity;
// Grey out RHS.
if (counterEndPx < endPx) {
ctx.fillStyle = '#0000001f';
ctx.fillRect(counterEndPx, 0, endPx - counterEndPx, this.getHeight());
}
}
// If the cached trace slices don't fully cover the visible time range,
// show a gray rectangle with a "Loading..." label.
checkerboardExcept(
ctx,
this.getHeight(),
0,
size.width,
timeScale.timeToPx(this.countersKey.start),
timeScale.timeToPx(this.countersKey.end));
}
onMouseMove(pos: {x: number, y: number}) {
const data = this.counters;
if (data === undefined) return;
this.mousePos = pos;
const {visibleTimeScale} = globals.timeline;
const time = visibleTimeScale.pxToHpTime(pos.x);
const [left, right] = searchSegment(data.timestamps, time.toTime());
if (left === -1) {
this.hover = undefined;
return;
}
const ts = Time.fromRaw(data.timestamps[left]);
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,
};
}
onMouseOut() {
this.hover = undefined;
}
onDestroy(): void {
if (this.initState) {
this.initState.dispose();
this.initState = undefined;
}
}
// Compute the range of values to display and range label.
private computeYRange(limits: CounterLimits, dataLimits: [number, number]): {
yMin: number,
yMax: number,
yRange: number,
yLabel: string,
} {
const options = this.getCounterOptions();
let yMin = limits.minDisplayValue;
let yMax = limits.maxDisplayValue;
if (options.yRange === 'viewport') {
[yMin, yMax] = dataLimits;
}
if (options.yDisplay === 'zero') {
yMin = Math.min(0, yMin);
}
if (options.yOverrideMaximum !== undefined) {
yMax = Math.max(options.yOverrideMaximum, yMax);
}
if (options.yOverrideMinimum !== undefined) {
yMin = Math.min(options.yOverrideMinimum, yMin);
}
if (options.yRangeRounding === 'human_readable') {
yMax = roundAway(yMax);
yMin = roundAway(yMin);
}
const sharer = RangeSharer.get();
[yMin, yMax] = sharer.share(options, [yMin, yMax]);
let yLabel: string;
if (options.yDisplay === 'minmax') {
yLabel = 'min - max';
} else {
let max = yMax;
let min = yMin;
if (options.yDisplay === 'log') {
max = Math.exp(max);
min = Math.exp(min);
}
const n = Math.abs(max - min);
yLabel = toLabel(n);
}
const unit = this.unit;
switch (options.yMode) {
case 'value':
yLabel += ` ${unit}`;
break;
case 'delta':
yLabel += `\u0394${unit}`;
break;
case 'rate':
yLabel += `\u0394${unit}/s`;
break;
default:
assertUnreachable(options.yMode);
break;
}
if (options.yDisplay === 'log') {
yLabel = `log(${yLabel})`;
}
return {
yMin,
yMax,
yLabel,
yRange: yMax-yMin,
};
}
// The underlying table has `ts` and `value` columns.
private getSqlPreamble(): string {
const options = this.getCounterOptions();
let valueExpr;
switch (options.yMode) {
case 'value':
valueExpr = 'value';
break;
case 'delta':
valueExpr = 'lead(value, 1, value) over (order by ts) - value';
break;
case 'rate':
valueExpr = '(lead(value, 1, value) over (order by ts) - value) / ((lead(ts, 1, 100) over (order by ts) - ts) / 1e9)';
break;
default:
assertUnreachable(options.yMode);
break;
}
let displayValueExpr = valueExpr;
if (options.yDisplay === 'log') {
displayValueExpr = `ifnull(ln(${displayValueExpr}), 0)`;
}
return `
WITH data AS (
SELECT
ts,
${valueExpr} as value,
${displayValueExpr} as displayValue
FROM (${this.getSqlSource()})
)
`;
}
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
`);
const displayValueRow = displayValueQuery.firstRow({
minDisplayValue: NUM,
maxDisplayValue: NUM,
});
const minDisplayValue = displayValueRow.minDisplayValue;
const maxDisplayValue = displayValueRow.maxDisplayValue;
limits = this.limits = {
minDisplayValue,
maxDisplayValue,
maxDurNs,
};
}
if (rawCountersKey.isCoveredBy(this.countersKey)) {
return; // We have the data already, no need to re-query.
}
const countersKey = rawCountersKey.normalize();
if (!rawCountersKey.isCoveredBy(countersKey)) {
throw new Error(`Normalization error ${countersKey.toString()} ${
rawCountersKey.toString()}`);
}
const maybeCachedCounters = this.cache.lookup(countersKey);
if (maybeCachedCounters) {
this.countersKey = countersKey;
this.counters = maybeCachedCounters;
}
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}
`);
const it = queryRes.iter({
tsq: LONG,
count: NUM,
avgValue: NUM,
minDisplayValue: NUM,
maxDisplayValue: NUM,
lastDisplayValue: NUM,
});
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),
displayValueRange: [0, 0],
};
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.minDisplayValues[row] = it.minDisplayValue;
data.maxDisplayValues[row] = it.maxDisplayValue;
data.lastDisplayValues[row] = it.lastDisplayValue;
min = Math.min(min, it.minDisplayValue);
max = Math.max(max, it.maxDisplayValue);
}
data.displayValueRange = [min, max];
this.cache.insert(countersKey, data);
this.counters = data;
raf.scheduleRedraw();
}
get unit(): string {
return this.getCounterOptions().unit ?? '?';
}
}