Request batch when idle (#7526)

See https://github.com/nasa/openmct/pull/7526 for details
This commit is contained in:
Andrew Henry 2024-03-12 13:46:06 -07:00 committed by GitHub
parent 8c2558bfe0
commit 5f0bd10c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 242 additions and 84 deletions

View File

@ -272,7 +272,7 @@ test.describe('Display Layout', () => {
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data
await page.getByLabel('Sine', { exact: true }).click({
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();

View File

@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
button: 'right'
});
await page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();

View File

@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) {
await page.locator('.c-create-button').hover();
await page.getByRole('cell', { name: object.name }).hover();
await page.getByLabel('lad name').getByText(object.name).hover();
let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim();
}

View File

@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import installWorker from './WebSocketWorker.js';
const DEFAULT_RATE_MS = 1000;
/**
* Describes the strategy to be used when batching WebSocket messages
*
@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
*
* @memberof module:openmct.telemetry
*/
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
const requestIdleCallback =
// eslint-disable-next-line compat/compat
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
const ONE_SECOND = 1000;
const FIVE_SECONDS = 5 * ONE_SECOND;
class BatchingWebSocket extends EventTarget {
#worker;
#openmct;
#showingRateLimitNotification;
#rate;
#maxBatchSize;
#applicationIsInitializing;
#maxBatchWait;
#firstBatchReceived;
constructor(openmct) {
super();
@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
this.#worker = new Worker(workerUrl);
this.#openmct = openmct;
this.#showingRateLimitNotification = false;
this.#rate = DEFAULT_RATE_MS;
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#maxBatchWait = ONE_SECOND;
this.#applicationIsInitializing = true;
this.#firstBatchReceived = false;
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
this.#worker.addEventListener('message', routeMessageToHandler);
@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
},
{ once: true }
);
openmct.once('start', () => {
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
// Force it after 5 seconds if it hasn't happened yet.
requestIdleCallback(
() => {
this.#applicationIsInitializing = false;
this.setMaxBatchSize(this.#maxBatchSize);
},
{
timeout: FIVE_SECONDS
}
);
});
}
/**
@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
});
}
/**
* When using batching, sets the rate at which batches of messages are released.
* @param {Number} rate the amount of time to wait, in ms, between batches.
*/
setRate(rate) {
this.#rate = rate;
}
/**
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them
@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
* 15 would probably be a better batch size.
*/
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
if (!this.#applicationIsInitializing) {
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
}
}
setMaxBatchWait(wait) {
this.#maxBatchWait = wait;
this.#sendBatchWaitToWorker(this.#maxBatchWait);
}
#sendMaxBatchSizeToWorker(maxBatchSize) {
this.#worker.postMessage({
type: 'setMaxBatchSize',
maxBatchSize
});
}
#sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({
type: 'setMaxBatchWait',
maxBatchWait
});
}
/**
* Disconnect the associated WebSocket. Generally speaking there is no need to call
* this manually.
@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) {
if (message.data.type === 'batch') {
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
this.start = Date.now();
const batch = message.data.batch;
if (batch.dropped === true && !this.#showingRateLimitNotification) {
const notification = this.#openmct.notifications.alert(
'Telemetry dropped due to client rate limiting.',
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
this.#showingRateLimitNotification = false;
});
}
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
setTimeout(() => {
this.#readyForNextBatch();
}, this.#rate);
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
this.#waitUntilIdleAndRequestNextBatch(batch);
} else if (message.data.type === 'message') {
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
} else if (message.data.type === 'reconnected') {
this.dispatchEvent(new CustomEvent('reconnected'));
} else {
throw new Error(`Unknown message type: ${message.data.type}`);
}
}
#waitUntilIdleAndRequestNextBatch(batch) {
requestIdleCallback(
(state) => {
if (this.#firstBatchReceived === false) {
this.#firstBatchReceived = true;
}
const now = Date.now();
const waitedFor = now - this.start;
if (state.didTimeout === true) {
if (document.visibilityState === 'visible') {
console.warn(`Event loop is too busy to process batch.`);
this.#waitUntilIdleAndRequestNextBatch(batch);
} else {
// After ingesting a telemetry batch, wait until the event loop is idle again before
// informing the worker we are ready for another batch.
this.#readyForNextBatch();
}
} else {
if (waitedFor > ONE_SECOND) {
console.warn(`Warning, batch processing took ${waitedFor}ms`);
}
this.#readyForNextBatch();
}
},
{ timeout: ONE_SECOND }
);
}
}
export default BatchingWebSocket;

View File

@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
export default class TelemetryAPI {
#isGreedyLAD;
#subscribeCache;
#hasReturnedFirstData;
get SUBSCRIBE_STRATEGY() {
return SUBSCRIBE_STRATEGY;
@ -108,6 +109,7 @@ export default class TelemetryAPI {
this.#isGreedyLAD = true;
this.BatchingWebSocket = BatchingWebSocket;
this.#subscribeCache = {};
this.#hasReturnedFirstData = false;
}
abortAllRequests() {
@ -383,7 +385,10 @@ export default class TelemetryAPI {
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
try {
const telemetry = await provider.request(...arguments);
if (!this.#hasReturnedFirstData) {
this.#hasReturnedFirstData = true;
performance.mark('firstHistoricalDataReturned');
}
return telemetry;
} catch (error) {
if (error.name !== 'AbortError') {

View File

@ -21,6 +21,7 @@
*****************************************************************************/
/* eslint-disable max-classes-per-file */
export default function installWorker() {
const ONE_SECOND = 1000;
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
/**
@ -44,6 +45,13 @@ export default function installWorker() {
#currentWaitIndex = 0;
#messageCallbacks = [];
#wsUrl;
#reconnecting = false;
#worker;
constructor(worker) {
super();
this.#worker = worker;
}
/**
* Establish a new WebSocket connection to the given URL
@ -62,6 +70,9 @@ export default function installWorker() {
this.#isConnecting = true;
this.#webSocket = new WebSocket(url);
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
// Playwright does not support forcing websocket state changes.
this.#worker.currentWebSocket = this.#webSocket;
const boundConnected = this.#connected.bind(this);
this.#webSocket.addEventListener('open', boundConnected);
@ -100,12 +111,17 @@ export default function installWorker() {
}
#connected() {
console.debug('Websocket connected.');
console.info('Websocket connected.');
this.#isConnected = true;
this.#isConnecting = false;
this.#currentWaitIndex = 0;
this.dispatchEvent(new Event('connected'));
if (this.#reconnecting) {
this.#worker.postMessage({
type: 'reconnected'
});
this.#reconnecting = false;
}
this.#flushQueue();
}
@ -138,6 +154,7 @@ export default function installWorker() {
if (this.#reconnectTimeoutHandle) {
return;
}
this.#reconnecting = true;
this.#reconnectTimeoutHandle = setTimeout(() => {
this.connect(this.#wsUrl);
@ -207,6 +224,9 @@ export default function installWorker() {
case 'setMaxBatchSize':
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
break;
case 'setMaxBatchWait':
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
break;
default:
throw new Error(`Unknown message type: ${type}`);
}
@ -245,7 +265,6 @@ export default function installWorker() {
}
routeMessageToHandler(data) {
//Implement batching here
if (this.#messageBatcher.shouldBatchMessage(data)) {
this.#messageBatcher.addMessageToBatch(data);
} else {
@ -267,12 +286,15 @@ export default function installWorker() {
#maxBatchSize;
#readyForNextBatch;
#worker;
#throttledSendNextBatch;
constructor(worker) {
this.#maxBatchSize = 10;
// No dropping telemetry unless we're explicitly told to.
this.#maxBatchSize = Number.POSITIVE_INFINITY;
this.#readyForNextBatch = false;
this.#worker = worker;
this.#resetBatch();
this.setMaxBatchWait(ONE_SECOND);
}
#resetBatch() {
this.#batch = {};
@ -310,23 +332,29 @@ export default function installWorker() {
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
let batch = this.#batch[batchId];
if (batch === undefined) {
this.#hasBatch = true;
batch = this.#batch[batchId] = [message];
} else {
batch.push(message);
}
if (batch.length > this.#maxBatchSize) {
console.warn(
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
);
batch.shift();
this.#batch.dropped = this.#batch.dropped || true;
this.#batch.dropped = true;
}
if (this.#readyForNextBatch) {
this.#sendNextBatch();
} else {
this.#hasBatch = true;
this.#throttledSendNextBatch();
}
}
setMaxBatchSize(maxBatchSize) {
this.#maxBatchSize = maxBatchSize;
}
setMaxBatchWait(maxBatchWait) {
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
}
/**
* Indicates that client code is ready to receive the next batch of
* messages. If a batch is available, it will be immediately sent.
@ -335,7 +363,7 @@ export default function installWorker() {
*/
readyForNextBatch() {
if (this.#hasBatch) {
this.#sendNextBatch();
this.#throttledSendNextBatch();
} else {
this.#readyForNextBatch = true;
}
@ -352,7 +380,34 @@ export default function installWorker() {
}
}
const websocket = new ResilientWebSocket();
function throttle(callback, wait) {
let last = 0;
let throttling = false;
return function (...args) {
if (throttling) {
return;
}
const now = performance.now();
const timeSinceLast = now - last;
if (timeSinceLast >= wait) {
last = now;
callback(...args);
} else if (!throttling) {
throttling = true;
setTimeout(() => {
last = performance.now();
throttling = false;
callback(...args);
}, wait - timeSinceLast);
}
};
}
const websocket = new ResilientWebSocket(self);
const messageBatcher = new MessageBatcher(self);
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
@ -363,4 +418,6 @@ export default function installWorker() {
websocket.registerMessageCallback((data) => {
websocketBroker.routeMessageToHandler(data);
});
self.websocketInstance = websocket;
}

View File

@ -24,11 +24,13 @@
<tr
ref="tableRow"
class="js-lad-table__body__row c-table__selectable-row"
aria-label="lad row"
@click="clickedRow"
@contextmenu.prevent="showContextMenu"
>
<td
ref="tableCell"
aria-label="lad name"
class="js-first-data"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"

View File

@ -22,7 +22,7 @@
<template>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
<table class="c-table c-lad-table" :class="applyLayoutClass">
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
<thead>
<tr>
<th>Name</th>

View File

@ -22,6 +22,7 @@
<template>
<div
aria-label="sub object frame"
class="l-layout__frame c-frame"
:class="{
'no-frame': !item.hasFrame,

View File

@ -37,24 +37,24 @@
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
aria-label="Alpha-numeric telemetry"
@contextmenu.prevent="showContextMenu"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>
<div
class="is-status__indicator"
:aria-label="`This item is ${status}`"
:title="`This item is ${status}`"
></div>
<div class="is-status__indicator"></div>
<div v-if="showLabel" class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">
<div
class="c-telemetry-view__label-text"
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
>
{{ domainObject.name }}
</div>
</div>
<div
v-if="showValue"
:aria-label="fieldName"
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
:title="fieldName"
class="c-telemetry-view__value"
:class="[telemetryClass]"

View File

@ -220,6 +220,7 @@
lengthAdjust="spacing"
text-anchor="middle"
dominant-baseline="middle"
:aria-label="`gauge value of ${curVal}`"
x="50%"
y="50%"
>

View File

@ -51,6 +51,6 @@ export default class OpenInNewTab {
*/
invoke(objectPath, _view, customUrlParams) {
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
window.open(url);
window.open(url, undefined, 'noopener');
}
}

View File

@ -287,7 +287,7 @@
<script>
import _ from 'lodash';
import { toRaw } from 'vue';
import { onMounted, ref, toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import throttle from '../../../utils/throttle';
import { useResizeObserver } from '../../../ui/composables/resize.js';
import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue';
@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
export default {
@ -354,6 +353,15 @@ export default {
}
},
emits: ['marked-rows-updated', 'filter'],
setup() {
const root = ref(null);
const { size: containerSize, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(root.value);
});
return { containerSize, root };
},
data() {
let configuration = this.table.configuration.getConfiguration();
@ -441,6 +449,13 @@ export default {
}
},
watch: {
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
containerSize: {
handler() {
this.debouncedRescaleToContainer();
},
deep: true
},
loading: {
handler(isLoading) {
if (isLoading) {
@ -500,9 +515,10 @@ export default {
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
},
mounted() {
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
this.csvExporter = new CSVExporter();
this.rowsAdded = _.throttle(this.rowsAdded, 200);
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
@ -515,8 +531,6 @@ export default {
});
}
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('refresh', this.clearRowsAndRerender);
@ -526,8 +540,8 @@ export default {
this.table.tableRows.on('add', this.rowsAdded);
this.table.tableRows.on('remove', this.rowsRemoved);
this.table.tableRows.on('sort', this.updateVisibleRows);
this.table.tableRows.on('filter', this.updateVisibleRows);
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
this.openmct.time.on('bounds', this.boundsChanged);
@ -540,10 +554,10 @@ export default {
this.table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize();
this.pollForResize();
this.calculateScrollbarWidth();
this.table.initialize();
this.rescaleToContainer();
},
beforeUnmount() {
this.table.off('object-added', this.addObject);
@ -555,15 +569,13 @@ export default {
this.table.tableRows.off('add', this.rowsAdded);
this.table.tableRows.off('remove', this.rowsRemoved);
this.table.tableRows.off('sort', this.updateVisibleRows);
this.table.tableRows.off('filter', this.updateVisibleRows);
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
this.table.configuration.off('change', this.updateConfiguration);
this.openmct.time.off('bounds', this.boundsChanged);
clearInterval(this.resizePollHandle);
this.table.configuration.destroy();
this.table.destroy();
@ -684,7 +696,7 @@ export default {
this.table.sortBy(this.sortOptions);
},
scroll() {
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
this.synchronizeScrollX();
if (this.shouldAutoScroll()) {
@ -757,11 +769,11 @@ export default {
this.initiateAutoScroll();
}
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
rowsRemoved(rows) {
this.setHeight();
this.updateVisibleRows();
this.throttledUpdateVisibleRows();
},
/**
* Calculates height based on total number of rows, and sets table height.
@ -880,35 +892,27 @@ export default {
dropTargetActive(isActive) {
this.isDropTargetActive = isActive;
},
pollForResize() {
let el = this.$refs.root;
let width = el.clientWidth;
let height = el.clientHeight;
rescaleToContainer() {
let scrollTop = this.scrollable.scrollTop;
this.resizePollHandle = setInterval(() => {
this.renderWhenVisible(() => {
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
width = el.clientWidth;
height = el.clientHeight;
this.renderWhenVisible(() => {
if (this.isAutosizeEnabled) {
this.calculateTableSize();
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
}
scrollTop = this.scrollable.scrollTop;
});
}, RESIZE_POLL_INTERVAL);
scrollTop = this.scrollable.scrollTop;
});
},
clearRowsAndRerender() {
this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows);
this.$nextTick().then(this.throttledUpdateVisibleRows);
},
pause(byButton) {
if (byButton) {

View File

@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
const PADDING = 1;
const DEFAULT_DURATION_FORMATTER = 'duration';
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
@ -92,7 +91,6 @@ export default {
//Respond to changes in conductor
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
beforeUnmount() {
clearInterval(this.resizeTimer);

View File

@ -148,7 +148,10 @@ export default {
}
},
mounted() {
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
leading: true,
trailing: false
});
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext();

View File

@ -46,6 +46,8 @@
<div
ref="objectName"
class="c-object-label__name"
aria-label="object name"
:title="domainObject && domainObject.name"
@mouseover.ctrl="showToolTip"
@mouseleave="hideToolTip"
>

View File

@ -105,7 +105,9 @@ export default {
return this.status ? `is-status--${this.status}` : '';
},
ariaLabel() {
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${
this.domainObject.type
} Object`;
}
},
mounted() {

View File

@ -29,12 +29,14 @@
import { axisTop } from 'd3-axis';
import { scaleLinear, scaleUtc } from 'd3-scale';
import { select } from 'd3-selection';
import { onMounted, ref } from 'vue';
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
import { useResizeObserver } from '../composables/resize';
//TODO: UI direction needed for the following property values
const PADDING = 1;
const RESIZE_POLL_INTERVAL = 200;
const PIXELS_PER_TICK = 100;
const PIXELS_PER_TICK_WIDE = 200;
//This offset needs to be re-considered
@ -73,6 +75,16 @@ export default {
}
}
},
setup() {
const axisHolder = ref(null);
const { size, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(axisHolder.value);
});
return {
containerSize: size
};
},
watch: {
bounds(newBounds) {
this.drawAxis(newBounds, this.timeSystem);
@ -82,6 +94,9 @@ export default {
},
contentHeight() {
this.updateNowMarker();
},
containerSize() {
this.resize();
}
},
mounted() {
@ -100,7 +115,7 @@ export default {
this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.resize();
},
unmounted() {
clearInterval(this.resizeTimer);