grafana/public/app/features/dashboard/dashgrid/liveTimer.ts
Artur Wierzbicki f45eb309ef
Live: move centrifuge service to a web worker (#41090)
* Fix: make webpack pickup workers written in TS

* Add comlink to dependencies

* Temporary fix: copy paste `toDataQueryError` from @grafana/runtime to avoid web dependencies

* Implemented comlink-based centrifuge worker & worker proxy

* Temporary fix: implement comlink transferHandlers for subscriptions and streamingdataframes

* Move liveTimer filtering from CentrifugeService into GrafanaLiveService

* Switch from CentrifugeService to CentrifugeServiceWorkerProxy in GrafanaLive

* Naming fix

* Refactor: move liveTimer-based data filtering from GrafanaLiveService to CentrifugeServiceWorker

* observe dataStream on an async scheduler

* Fix: - Unsubscribe is now propagated from the main thread to the worker, - improve worker&workerProxy types

* Fix: Prettify types

* Fix: Add error & complete observers

* Docs: Add comment explaining the `subscriberTransferHandler`

* Fix: Replace `StreamingDataFrameHandler` with explicitly converting StreamingDataFrame to a DataFrameDTO

* Refactor: move liveTimer filtering to service.ts to make it easy to implement a `live-service-web-worker` feature flag

* Feat: add `live-service-web-worker` feature flag

* Fix: extract toDataQueryError.ts to a separate file within `@grafana-runtime` to avoid having a dependency from webworker to the whole package (@grafana-runtime/index.ts)

* Update public/app/features/dashboard/dashgrid/liveTimer.ts

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>

* Fix: fixed default import class in worker file

* Fix: cast worker as Endpoint

* Migrate from worker-loader to webpack native worker support v1 - broken prod build

* Fix: Use custom path in HtmlWebpackPlugin

* Fix: Loading workers from CDNs

* Fix: Avoid issues with jest ESM support by mocking `createWorker` files

* Fix: move the custom mockWorker rendering layout to `test/mocks`

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
2021-11-09 21:05:01 +04:00

120 lines
3.1 KiB
TypeScript

import { dateMath, dateTime, TimeRange } from '@grafana/data';
import { BehaviorSubject } from 'rxjs';
import { PanelChrome } from './PanelChrome';
// target is 20hz (50ms), but we poll at 100ms to smooth out jitter
const interval = 100;
interface LiveListener {
last: number;
intervalMs: number;
panel: PanelChrome;
}
class LiveTimer {
listeners: LiveListener[] = [];
budget = 1;
threshold = 1.5; // trial and error appears about right
ok = new BehaviorSubject(true);
lastUpdate = Date.now();
isLive = false; // the dashboard time range ends in "now"
timeRange?: TimeRange;
liveTimeOffset = 0;
/** Called when the dashboard time range changes */
setLiveTimeRange(v?: TimeRange) {
this.timeRange = v;
this.isLive = v?.raw?.to === 'now';
if (this.isLive) {
const from = dateMath.parse(v!.raw.from, false)?.valueOf()!;
const to = dateMath.parse(v!.raw.to, true)?.valueOf()!;
this.liveTimeOffset = to - from;
for (const listener of this.listeners) {
listener.intervalMs = getLiveTimerInterval(this.liveTimeOffset, listener.panel.props.width);
}
}
}
listen(panel: PanelChrome) {
this.listeners.push({
last: this.lastUpdate,
panel: panel,
intervalMs: getLiveTimerInterval(
60000, // 1min
panel.props.width
),
});
}
remove(panel: PanelChrome) {
this.listeners = this.listeners.filter((v) => v.panel !== panel);
}
updateInterval(panel: PanelChrome) {
if (!this.timeRange || !this.isLive) {
return;
}
for (const listener of this.listeners) {
if (listener.panel === panel) {
listener.intervalMs = getLiveTimerInterval(this.liveTimeOffset, listener.panel.props.width);
return;
}
}
}
// Called at the consistent dashboard interval
measure = () => {
const now = Date.now();
this.budget = (now - this.lastUpdate) / interval;
const oldOk = this.ok.getValue();
const newOk = this.budget <= this.threshold;
if (oldOk !== newOk) {
this.ok.next(newOk);
}
this.lastUpdate = now;
// For live dashboards, listen to changes
if (this.isLive && this.ok.getValue() && this.timeRange) {
// when the time-range is relative fire events
let tr: TimeRange | undefined = undefined;
for (const listener of this.listeners) {
if (!listener.panel.props.isInView) {
continue;
}
const elapsed = now - listener.last;
if (elapsed >= listener.intervalMs) {
if (!tr) {
const { raw } = this.timeRange;
tr = {
raw,
from: dateTime(now - this.liveTimeOffset),
to: dateTime(now),
};
}
listener.panel.liveTimeChanged(tr);
listener.last = now;
}
}
}
};
}
const FIVE_MINS = 5 * 60 * 1000;
export function getLiveTimerInterval(delta: number, width: number): number {
const millisPerPixel = Math.ceil(delta / width / 100) * 100;
if (millisPerPixel > FIVE_MINS) {
return FIVE_MINS;
}
return millisPerPixel;
}
export const liveTimer = new LiveTimer();
setInterval(liveTimer.measure, interval);