Live: use StreamingDataSource for live measurments (#31713)

This commit is contained in:
Ryan McKinley
2021-03-17 21:46:56 -07:00
committed by GitHub
parent 5286ddbbc0
commit 5bb1b1602d
5 changed files with 101 additions and 431 deletions
@@ -50,8 +50,20 @@ export class StreamingDataFrame implements DataFrame {
update(msg: DataFrameJSON) { update(msg: DataFrameJSON) {
const { schema, data } = msg; const { schema, data } = msg;
if (schema) { if (schema) {
if (this.fields.length > 0) { // Keep old values if they are the same shape
// ?? keep existing data? let oldValues: ArrayVector[] | undefined;
if (schema.fields.length === this.fields.length) {
let same = true;
oldValues = this.fields.map((f, idx) => {
const oldField = this.fields[idx];
if (f.name !== oldField.name || f.type !== oldField.type) {
same = false;
}
return f.values;
});
if (!same) {
oldValues = undefined;
}
} }
this.name = schema.name; this.name = schema.name;
@@ -59,13 +71,13 @@ export class StreamingDataFrame implements DataFrame {
this.meta = schema.meta; this.meta = schema.meta;
// Create new fields from the schema // Create new fields from the schema
this.fields = schema.fields.map((f) => { this.fields = schema.fields.map((f, idx) => {
return { return {
config: f.config ?? {}, config: f.config ?? {},
name: f.name, name: f.name,
labels: f.labels, labels: f.labels,
type: f.type ?? FieldType.other, type: f.type ?? FieldType.other,
values: new ArrayVector(), values: oldValues ? oldValues[idx] : new ArrayVector(),
}; };
}); });
@@ -1,52 +1,42 @@
import { FieldType } from '@grafana/data';
import { MeasurementCollector } from './collector'; import { MeasurementCollector } from './collector';
import { MeasurementAction } from './types';
describe('MeasurementCollector', () => { describe('MeasurementCollector', () => {
it('should collect values', () => { it('should collect values', () => {
const collector = new MeasurementCollector(); const collector = new MeasurementCollector();
collector.addBatch({ collector.addBatch({
measurements: [ batch: [
{ {
name: 'test', key: 'aaa',
labels: { host: 'a' }, schema: {
time: 100, fields: [
values: { { name: 'time', type: FieldType.time },
f0: 0, { name: 'value', type: FieldType.number },
f1: 1, ],
f2: 'hello', },
data: {
values: [
[100, 200],
[1, 2],
],
}, },
}, },
{ {
name: 'test', key: 'aaa',
labels: { host: 'b' }, data: { values: [[300], [3]] },
time: 101,
values: {
f0: 0,
f1: 1,
f2: 'hello',
},
config: {
f2: {
unit: 'mph',
},
},
}, },
{ {
name: 'test', key: 'aaa',
time: 102, data: { values: [[400], [4]] },
labels: { host: 'a' }, // should append to first value
values: {
// note the missing values for f0/1
f2: 'world',
},
}, },
], ],
}); });
const frames = collector.getData(); const frames = collector.getData();
expect(frames.length).toEqual(2); expect(frames.length).toEqual(1);
(frames[0] as any).lastUpdateTime = 0;
expect(frames[0]).toMatchInlineSnapshot(` expect(frames[0]).toMatchInlineSnapshot(`
Object { StreamingDataFrame {
"fields": Array [ "fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
@@ -55,196 +45,33 @@ describe('MeasurementCollector', () => {
"type": "time", "type": "time",
"values": Array [ "values": Array [
100, 100,
102, 200,
300,
400,
], ],
}, },
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f0",
"type": "number",
"values": Array [
0,
undefined,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f1",
"type": "number",
"values": Array [
1,
undefined,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f2",
"type": "string",
"values": Array [
"hello",
"world",
],
},
],
"meta": Object {
"custom": Object {
"labels": Object {
"host": "a",
},
},
},
"name": "test",
"refId": undefined,
}
`);
expect(frames[1]).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object { Object {
"config": Object {}, "config": Object {},
"labels": undefined, "labels": undefined,
"name": "time", "name": "value",
"type": "time",
"values": Array [
101,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "b",
},
"name": "f0",
"type": "number",
"values": Array [
0,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "b",
},
"name": "f1",
"type": "number", "type": "number",
"values": Array [ "values": Array [
1, 1,
], 2,
}, 3,
Object { 4,
"config": Object {
"unit": "mph",
},
"labels": Object {
"host": "b",
},
"name": "f2",
"type": "string",
"values": Array [
"hello",
], ],
}, },
], ],
"meta": Object { "lastUpdateTime": 0,
"custom": Object { "meta": undefined,
"labels": Object { "name": undefined,
"host": "b", "options": Object {
}, "maxLength": 600,
},
}, },
"name": "test",
"refId": undefined, "refId": undefined,
"timeFieldIndex": 0,
} }
`); `);
collector.addBatch({
action: MeasurementAction.Replace,
measurements: [
{
name: 'test',
time: 105,
labels: { host: 'a' },
values: {
f1: 10,
},
},
],
});
const frames2 = collector.getData();
expect(frames2.length).toEqual(2);
expect(frames2[0].length).toEqual(1); // not three!
expect(frames2[0]).toMatchInlineSnapshot(`
Object {
"fields": Array [
Object {
"config": Object {},
"labels": undefined,
"name": "time",
"type": "time",
"values": Array [
105,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f0",
"type": "number",
"values": Array [
undefined,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f1",
"type": "number",
"values": Array [
10,
],
},
Object {
"config": Object {},
"labels": Object {
"host": "a",
},
"name": "f2",
"type": "string",
"values": Array [
undefined,
],
},
],
"meta": Object {
"custom": Object {
"labels": Object {
"host": "a",
},
},
},
"name": "test",
"refId": undefined,
}
`);
collector.addBatch({
action: MeasurementAction.Clear,
measurements: [],
});
expect(collector.getData().length).toEqual(0);
}); });
}); });
@@ -1,118 +1,15 @@
import { import { DataFrame, DataFrameJSON, StreamingDataFrame, StreamingFrameOptions } from '@grafana/data';
CircularDataFrame, import { MeasurementBatch, LiveMeasurements, MeasurementsQuery } from './types';
Labels,
formatLabels,
FieldType,
DataFrame,
matchAllLabels,
parseLabels,
CircularVector,
ArrayVector,
} from '@grafana/data';
import { Measurement, MeasurementBatch, LiveMeasurements, MeasurementsQuery, MeasurementAction } from './types';
interface MeasurementCacheConfig { /**
append?: 'head' | 'tail'; * This will collect
capacity?: number;
}
/** This is a cache scoped to a the measurement name
* *
* @alpha -- experimental * @alpha -- experimental
*/ */
export class MeasurementCache {
readonly frames: Record<string, CircularDataFrame> = {}; // key is the labels
constructor(public name: string, private config: MeasurementCacheConfig) {
if (!this.config) {
this.config = {
append: 'tail',
capacity: 600, // Default capacity 10min @ 1hz
};
}
}
getFrames(match?: Labels): DataFrame[] {
const frames = Object.values(this.frames);
if (!match) {
return frames;
}
return frames.filter((f) => {
return matchAllLabels(match, f.meta?.custom?.labels);
});
}
addMeasurement(m: Measurement, action: MeasurementAction): DataFrame {
const key = m.labels ? formatLabels(m.labels) : '';
let frame = this.frames[key];
if (!frame) {
frame = new CircularDataFrame(this.config);
frame.name = this.name;
frame.addField({
name: 'time',
type: FieldType.time,
});
for (const [key, value] of Object.entries(m.values)) {
frame.addFieldFor(value, key).labels = m.labels;
}
frame.meta = {
custom: {
labels: m.labels,
},
};
this.frames[key] = frame;
}
// Clear existing values
if (action === MeasurementAction.Replace) {
for (const field of frame.fields) {
(field.values as ArrayVector).buffer.length = 0; // same buffer, but reset to empty length
}
}
// Add the timestamp
frame.fields[0].values.add(m.time || Date.now());
// Attach field config to the current fields
if (m.config) {
for (const [key, value] of Object.entries(m.config)) {
const f = frame.fields.find((f) => f.name === key);
if (f) {
f.config = value;
}
}
}
// Append all values (a row)
for (const [key, value] of Object.entries(m.values)) {
const existingField = frame.fields.find((v) => v.name === key);
if (!existingField) {
const f = frame.addFieldFor(value, key);
f.labels = m.labels;
f.values.add(value);
} else {
existingField.values.add(value);
}
}
// Make sure all fields have the same length
frame.validate();
return frame;
}
}
/**
* @alpha -- experimental
*/
export class MeasurementCollector implements LiveMeasurements { export class MeasurementCollector implements LiveMeasurements {
measurements = new Map<string, MeasurementCache>(); measurements = new Map<string, StreamingDataFrame>();
config: MeasurementCacheConfig = { config: StreamingFrameOptions = {
append: 'tail', maxLength: 600, // Default capacity 10min @ 1hz
capacity: 600, // Default capacity 10min @ 1hz
}; };
//------------------------------------------------------ //------------------------------------------------------
@@ -120,93 +17,68 @@ export class MeasurementCollector implements LiveMeasurements {
//------------------------------------------------------ //------------------------------------------------------
getData(query?: MeasurementsQuery): DataFrame[] { getData(query?: MeasurementsQuery): DataFrame[] {
const { name, labels, fields } = query || {}; const { key, fields } = query || {};
let data: DataFrame[] = []; // Find the data
if (name) { let data: StreamingDataFrame[] = [];
// for now we only match exact names if (key) {
const m = this.measurements.get(name); const f = this.measurements.get(key);
if (m) { if (!f) {
data = m.getFrames(labels); return [];
} }
data.push(f);
} else { } else {
// Add all frames
for (const f of this.measurements.values()) { for (const f of this.measurements.values()) {
data.push.apply(data, f.getFrames(labels)); data.push(f);
} }
} }
// Filter the fields we want
if (fields && fields.length) { if (fields && fields.length) {
let filtered: DataFrame[] = []; let filtered: DataFrame[] = [];
for (const frame of data) { for (const frame of data) {
const match = frame.fields.filter((f) => fields.includes(f.name)); const match = frame.fields.filter((f) => fields.includes(f.name));
if (match.length > 0) { if (match.length > 0) {
filtered.push({ ...frame, fields: match }); // Copy the frame with fewer fields filtered.push({ ...frame, fields: match, length: frame.length }); // Copy the frame with fewer fields
} }
} }
if (filtered.length) {
return filtered;
}
} }
return data; return data;
} }
getDistinctNames(): string[] { getKeys(): string[] {
return Object.keys(this.measurements); return Object.keys(this.measurements);
} }
getDistinctLabels(name: string): Labels[] { ensureCapacity(size: number) {
const m = this.measurements.get(name); // TODO...
if (m) {
return Object.keys(m.frames).map((k) => parseLabels(k));
}
return [];
}
setCapacity(size: number) {
this.config.capacity = size;
// Now update all the circular buffers
for (const wrap of this.measurements.values()) {
for (const frame of Object.values(wrap.frames)) {
for (const field of frame.fields) {
(field.values as CircularVector).setCapacity(size);
}
}
}
}
getCapacity() {
return this.config.capacity!;
}
clear() {
this.measurements.clear();
} }
//------------------------------------------------------ //------------------------------------------------------
// Collector // Collector
//------------------------------------------------------ //------------------------------------------------------
addBatch = (batch: MeasurementBatch) => { addBatch = (msg: MeasurementBatch) => {
let action = batch.action ?? MeasurementAction.Append; // HACK! sending one message from the backend, not a batch
if (action === MeasurementAction.Clear) { if (!msg.batch) {
this.measurements.clear(); const df: DataFrameJSON = msg as any;
action = MeasurementAction.Append; msg = { batch: [df] };
console.log('NOTE converting message to batch');
} }
// Change the local buffer size for (const measure of msg.batch) {
if (batch.capacity && batch.capacity !== this.config.capacity) { const key = measure.key ?? measure.schema?.name ?? '';
this.setCapacity(batch.capacity);
}
for (const measure of batch.measurements) { let s = this.measurements.get(key);
const name = measure.name || ''; if (s) {
let m = this.measurements.get(name); s.update(measure);
if (!m) {
m = new MeasurementCache(name, this.config);
this.measurements.set(name, m);
}
if (measure.values) {
m.addMeasurement(measure, action);
} else { } else {
console.log('invalid measurement', measure); s = new StreamingDataFrame(measure, this.config); //
this.measurements.set(key, s);
} }
} }
return this; return this;
@@ -1,31 +1,4 @@
import { DataFrame, Labels, FieldConfig } from '@grafana/data'; import { DataFrame, DataFrameJSON } from '@grafana/data';
/**
* the raw channel events are batches of Measurements
*
* @alpha -- experimental
*/
export interface Measurement {
name: string;
time?: number; // Missing will use the browser time
values: Record<string, any>;
config?: Record<string, FieldConfig>;
labels?: Labels;
}
/**
* @alpha -- experimental
*/
export enum MeasurementAction {
/** The measurements will be added to the client buffer */
Append = 'append',
/** The measurements will replace the client buffer */
Replace = 'replace',
/** All measurements will be removed from the client buffer before processing */
Clear = 'clear',
}
/** /**
* List of Measurements sent in a batch * List of Measurements sent in a batch
@@ -33,29 +6,17 @@ export enum MeasurementAction {
* @alpha -- experimental * @alpha -- experimental
*/ */
export interface MeasurementBatch { export interface MeasurementBatch {
/**
* The default action is to append values to the client buffer
*/
action?: MeasurementAction;
/** /**
* List of measurements to process * List of measurements to process
*/ */
measurements: Measurement[]; batch: DataFrameJSON[];
/**
* This will set the capacity on the client buffer for everything
* in the measurement channel
*/
capacity?: number;
} }
/** /**
* @alpha -- experimental * @alpha -- experimental
*/ */
export interface MeasurementsQuery { export interface MeasurementsQuery {
name?: string; key?: string;
labels?: Labels;
fields?: string[]; // only include the fields with these names fields?: string[]; // only include the fields with these names
} }
@@ -66,8 +27,6 @@ export interface MeasurementsQuery {
*/ */
export interface LiveMeasurements { export interface LiveMeasurements {
getData(query?: MeasurementsQuery): DataFrame[]; getData(query?: MeasurementsQuery): DataFrame[];
getDistinctNames(): string[]; getKeys(): string[];
getDistinctLabels(name: string): Labels[]; ensureCapacity(size: number): void;
setCapacity(size: number): void;
getCapacity(): number;
} }
@@ -43,7 +43,7 @@ export class QueryEditor extends PureComponent<Props> {
...query, ...query,
measurements: { measurements: {
...query.measurements, ...query.measurements,
name: sel?.value, key: sel?.value,
}, },
}); });
onRunQuery(); onRunQuery();
@@ -79,12 +79,12 @@ export class QueryEditor extends PureComponent<Props> {
let foundName = false; let foundName = false;
if (info) { if (info) {
for (const name of info.getDistinctNames()) { for (const name of info.getKeys()) {
names.push({ names.push({
value: name, value: name,
label: name, label: name,
}); });
if (name === measurements.name) { if (name === measurements.key) {
foundName = true; foundName = true;
} }
} }
@@ -92,11 +92,11 @@ export class QueryEditor extends PureComponent<Props> {
console.log('NO INFO for', channel); console.log('NO INFO for', channel);
} }
if (measurements.name && !foundName) { if (measurements.key && !foundName) {
names.push({ names.push({
label: measurements.name, label: measurements.key,
value: measurements.name, value: measurements.key,
description: `Frames with name ${measurements.name}`, description: `Frames with key ${measurements.key}`,
}); });
} }
} }
@@ -123,7 +123,7 @@ export class QueryEditor extends PureComponent<Props> {
<InlineField label="Measurement" grow={true} labelWidth={labelWidth}> <InlineField label="Measurement" grow={true} labelWidth={labelWidth}>
<Select <Select
options={names} options={names}
value={names.find((v) => v.value === measurements?.name) || names[0]} value={names.find((v) => v.value === measurements?.key) || names[0]}
onChange={this.onMeasurementNameChanged} onChange={this.onMeasurementNameChanged}
allowCustomValue={true} allowCustomValue={true}
backspaceRemovesValue={true} backspaceRemovesValue={true}