liveTimer.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { BehaviorSubject } from 'rxjs';
  2. import { dateMath, dateTime, TimeRange } from '@grafana/data';
  3. import { PanelChrome } from './PanelChrome';
  4. // target is 20hz (50ms), but we poll at 100ms to smooth out jitter
  5. const interval = 100;
  6. interface LiveListener {
  7. last: number;
  8. intervalMs: number;
  9. panel: PanelChrome;
  10. }
  11. class LiveTimer {
  12. listeners: LiveListener[] = [];
  13. budget = 1;
  14. threshold = 1.5; // trial and error appears about right
  15. ok = new BehaviorSubject(true);
  16. lastUpdate = Date.now();
  17. isLive = false; // the dashboard time range ends in "now"
  18. timeRange?: TimeRange;
  19. liveTimeOffset = 0;
  20. /** Called when the dashboard time range changes */
  21. setLiveTimeRange(v?: TimeRange) {
  22. this.timeRange = v;
  23. this.isLive = v?.raw?.to === 'now';
  24. if (this.isLive) {
  25. const from = dateMath.parse(v!.raw.from, false)?.valueOf()!;
  26. const to = dateMath.parse(v!.raw.to, true)?.valueOf()!;
  27. this.liveTimeOffset = to - from;
  28. for (const listener of this.listeners) {
  29. listener.intervalMs = getLiveTimerInterval(this.liveTimeOffset, listener.panel.props.width);
  30. }
  31. }
  32. }
  33. listen(panel: PanelChrome) {
  34. this.listeners.push({
  35. last: this.lastUpdate,
  36. panel: panel,
  37. intervalMs: getLiveTimerInterval(
  38. 60000, // 1min
  39. panel.props.width
  40. ),
  41. });
  42. }
  43. remove(panel: PanelChrome) {
  44. this.listeners = this.listeners.filter((v) => v.panel !== panel);
  45. }
  46. updateInterval(panel: PanelChrome) {
  47. if (!this.timeRange || !this.isLive) {
  48. return;
  49. }
  50. for (const listener of this.listeners) {
  51. if (listener.panel === panel) {
  52. listener.intervalMs = getLiveTimerInterval(this.liveTimeOffset, listener.panel.props.width);
  53. return;
  54. }
  55. }
  56. }
  57. // Called at the consistent dashboard interval
  58. measure = () => {
  59. const now = Date.now();
  60. this.budget = (now - this.lastUpdate) / interval;
  61. const oldOk = this.ok.getValue();
  62. const newOk = this.budget <= this.threshold;
  63. if (oldOk !== newOk) {
  64. this.ok.next(newOk);
  65. }
  66. this.lastUpdate = now;
  67. // For live dashboards, listen to changes
  68. if (this.isLive && this.ok.getValue() && this.timeRange) {
  69. // when the time-range is relative fire events
  70. let tr: TimeRange | undefined = undefined;
  71. for (const listener of this.listeners) {
  72. if (!listener.panel.props.isInView) {
  73. continue;
  74. }
  75. const elapsed = now - listener.last;
  76. if (elapsed >= listener.intervalMs) {
  77. if (!tr) {
  78. const { raw } = this.timeRange;
  79. tr = {
  80. raw,
  81. from: dateTime(now - this.liveTimeOffset),
  82. to: dateTime(now),
  83. };
  84. }
  85. listener.panel.liveTimeChanged(tr);
  86. listener.last = now;
  87. }
  88. }
  89. }
  90. };
  91. }
  92. const FIVE_MINS = 5 * 60 * 1000;
  93. export function getLiveTimerInterval(delta: number, width: number): number {
  94. const millisPerPixel = Math.ceil(delta / width / 100) * 100;
  95. if (millisPerPixel > FIVE_MINS) {
  96. return FIVE_MINS;
  97. }
  98. return millisPerPixel;
  99. }
  100. export const liveTimer = new LiveTimer();
  101. setInterval(liveTimer.measure, interval);