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); expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
// ensure we can right click on the alpha-numeric widget and view historical data // 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' button: 'right'
}); });
await page.getByLabel('View Historical Data').click(); 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(); await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
// right click on the plot and select view large // 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 page.getByLabel('View Historical Data').click();
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible(); await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click(); await page.getByRole('button', { name: 'Close' }).click();

View File

@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
async function getToolTip(object) { async function getToolTip(object) {
await page.locator('.c-create-button').hover(); 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(); let tooltipText = await page.locator('.c-tooltip').textContent();
return tooltipText.replace('\n', '').trim(); return tooltipText.replace('\n', '').trim();
} }

View File

@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import installWorker from './WebSocketWorker.js'; import installWorker from './WebSocketWorker.js';
const DEFAULT_RATE_MS = 1000;
/** /**
* Describes the strategy to be used when batching WebSocket messages * Describes the strategy to be used when batching WebSocket messages
* *
@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
* *
* @memberof module:openmct.telemetry * @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 { class BatchingWebSocket extends EventTarget {
#worker; #worker;
#openmct; #openmct;
#showingRateLimitNotification; #showingRateLimitNotification;
#rate; #maxBatchSize;
#applicationIsInitializing;
#maxBatchWait;
#firstBatchReceived;
constructor(openmct) { constructor(openmct) {
super(); super();
@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
this.#worker = new Worker(workerUrl); this.#worker = new Worker(workerUrl);
this.#openmct = openmct; this.#openmct = openmct;
this.#showingRateLimitNotification = false; 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); const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
this.#worker.addEventListener('message', routeMessageToHandler); this.#worker.addEventListener('message', routeMessageToHandler);
@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
}, },
{ once: true } { 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, * @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
* the maximum number of telemetry values to batch before dropping them * 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. * 15 would probably be a better batch size.
*/ */
setMaxBatchSize(maxBatchSize) { 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({ this.#worker.postMessage({
type: 'setMaxBatchSize', type: 'setMaxBatchSize',
maxBatchSize maxBatchSize
}); });
} }
#sendBatchWaitToWorker(maxBatchWait) {
this.#worker.postMessage({
type: 'setMaxBatchWait',
maxBatchWait
});
}
/** /**
* Disconnect the associated WebSocket. Generally speaking there is no need to call * Disconnect the associated WebSocket. Generally speaking there is no need to call
* this manually. * this manually.
@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
#routeMessageToHandler(message) { #routeMessageToHandler(message) {
if (message.data.type === 'batch') { 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( const notification = this.#openmct.notifications.alert(
'Telemetry dropped due to client rate limiting.', 'Telemetry dropped due to client rate limiting.',
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' } { hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
this.#showingRateLimitNotification = false; this.#showingRateLimitNotification = false;
}); });
} }
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
setTimeout(() => { this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
this.#readyForNextBatch(); this.#waitUntilIdleAndRequestNextBatch(batch);
}, this.#rate);
} else if (message.data.type === 'message') { } else if (message.data.type === 'message') {
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message })); this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
} else if (message.data.type === 'reconnected') {
this.dispatchEvent(new CustomEvent('reconnected'));
} else { } else {
throw new Error(`Unknown message type: ${message.data.type}`); 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; export default BatchingWebSocket;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,7 +148,10 @@ export default {
} }
}, },
mounted() { 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.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem); this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
this.setTimeContext(); this.setTimeContext();

View File

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

View File

@ -105,7 +105,9 @@ export default {
return this.status ? `is-status--${this.status}` : ''; return this.status ? `is-status--${this.status}` : '';
}, },
ariaLabel() { 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() { mounted() {

View File

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