diff options
author | Shefali Joshi <simplyrender@gmail.com> | 2021-07-31 01:23:02 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-31 01:23:02 +0300 |
commit | b329ed6ed510eb3563bacd20433f21a97c7ec795 (patch) | |
tree | b716998e951cded2b0e1b42b78c42e3e3bb78191 | |
parent | 9b7a0d7e4c883efbea0d8b9cee719462e418389c (diff) |
Couch object provider performance improvement using SharedWorker (#3993)vista-r4.8.0-rc2vista-r4.8.0-rc1
* Use the window SharedWorker instead of the WorkerService
* Use relative asset path for Shared Workers
* Remove beforeunload listener on destroy
-rw-r--r-- | src/MCT.js | 4 | ||||
-rw-r--r-- | src/plugins/persistence/couch/CouchChangesFeed.js | 106 | ||||
-rw-r--r-- | src/plugins/persistence/couch/CouchObjectProvider.js | 118 | ||||
-rw-r--r-- | webpack.config.js | 16 |
4 files changed, 225 insertions, 19 deletions
diff --git a/src/MCT.js b/src/MCT.js index cea589f79..2eaef6e1f 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -122,6 +122,7 @@ define([ } }; + this.destroy = this.destroy.bind(this); /** * Tracks current selection state of the application. * @private @@ -435,6 +436,8 @@ define([ Browse(this); } + window.addEventListener('beforeunload', this.destroy); + this.router.start(); this.emit('start'); }.bind(this)); @@ -458,6 +461,7 @@ define([ }; MCT.prototype.destroy = function () { + window.removeEventListener('beforeunload', this.destroy); this.emit('destroy'); this.router.destroy(); }; diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js new file mode 100644 index 000000000..e773270a8 --- /dev/null +++ b/src/plugins/persistence/couch/CouchChangesFeed.js @@ -0,0 +1,106 @@ +(function () { + const connections = []; + let connected = false; + const controller = new AbortController(); + const signal = controller.signal; + + self.onconnect = function (e) { + let port = e.ports[0]; + connections.push(port); + + port.postMessage({ + type: 'connection', + connectionId: connections.length + }); + + port.onmessage = async function (event) { + if (event.data.request === 'close') { + connections.splice(event.data.connectionId - 1, 1); + if (connections.length <= 0) { + // abort any outstanding requests if there's nobody listening to it. + controller.abort(); + } + + return; + } + + if (event.data.request === 'changes') { + if (connected === true) { + return; + } + + connected = true; + + let url = event.data.url; + let body = event.data.body; + let error = false; + // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection + // style=main_only returns only the current winning revision of the document + + const response = await fetch(url, { + method: 'POST', + headers: { + "Content-Type": 'application/json' + }, + signal, + body + }); + + let reader; + + if (response.body === undefined) { + error = true; + } else { + reader = response.body.getReader(); + } + + while (!error) { + const {done, value} = await reader.read(); + //done is true when we lose connection with the provider + if (done) { + error = true; + } + + if (value) { + let chunk = new Uint8Array(value.length); + chunk.set(value, 0); + const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n'); + if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') { + decodedChunk.forEach((doc, index) => { + try { + if (doc) { + const objectChanges = JSON.parse(doc); + connections.forEach(function (connection) { + connection.postMessage({ + objectChanges + }); + }); + } + } catch (decodeError) { + //do nothing; + console.log(decodeError); + } + }); + } + } + + } + + if (error) { + port.postMessage({ + error + }); + } + } + }; + + port.start(); + + }; + + self.onerror = function () { + //do nothing + console.log('Error on feed'); + }; + +}()); diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 1bf6ede08..93da948f1 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -40,6 +40,64 @@ export default class CouchObjectProvider { this.batchIds = []; } + /** + * @private + */ + startSharedWorker() { + let provider = this; + let sharedWorker; + + const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}couchDBChangesFeed.js`; + + sharedWorker = new SharedWorker(sharedWorkerURL); + sharedWorker.port.onmessage = provider.onSharedWorkerMessage.bind(this); + sharedWorker.port.onmessageerror = provider.onSharedWorkerMessageError.bind(this); + sharedWorker.port.start(); + + this.openmct.on('destroy', () => { + this.changesFeedSharedWorker.port.postMessage({ + request: 'close', + connectionId: this.changesFeedSharedWorkerConnectionId + }); + this.changesFeedSharedWorker.port.close(); + }); + + return sharedWorker; + } + + onSharedWorkerMessageError(event) { + console.log('Error', event); + } + + onSharedWorkerMessage(event) { + if (event.data.type === 'connection') { + this.changesFeedSharedWorkerConnectionId = event.data.connectionId; + } else { + const error = event.data.error; + if (error && Object.keys(this.observers).length > 0) { + this.observeObjectChanges(); + + return; + } + + let objectChanges = event.data.objectChanges; + objectChanges.identifier = { + namespace: this.namespace, + key: objectChanges.id + }; + let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier); + //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. + let observersForObject = this.observers[keyString]; + + if (observersForObject) { + observersForObject.forEach(async (observer) => { + const updatedObject = await this.get(objectChanges.identifier); + observer(updatedObject); + }); + } + } + } + //backwards compatibility, options used to be a url. Now it's an object _normalize(options) { if (typeof options === 'string') { @@ -334,9 +392,8 @@ export default class CouchObjectProvider { /** * @private */ - async observeObjectChanges() { - const controller = new AbortController(); - const signal = controller.signal; + observeObjectChanges() { + let filter = {selector: {}}; if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) { @@ -354,6 +411,51 @@ export default class CouchObjectProvider { }; } + // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection + // style=main_only returns only the current winning revision of the document + let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`; + + let body = {}; + if (filter) { + url = `${url}&filter=_selector`; + body = JSON.stringify(filter); + } + + if (typeof SharedWorker === 'undefined') { + this.fetchChanges(url, body); + } else { + this.initiateSharedWorkerFetchChanges(url, body); + } + + } + + /** + * @private + */ + initiateSharedWorkerFetchChanges(url, body) { + if (!this.changesFeedSharedWorker) { + this.changesFeedSharedWorker = this.startSharedWorker(); + + if (typeof this.stopObservingObjectChanges === 'function') { + this.stopObservingObjectChanges(); + } + + this.stopObservingObjectChanges = () => { + delete this.stopObservingObjectChanges; + }; + + this.changesFeedSharedWorker.port.postMessage({ + request: 'changes', + body, + url + }); + } + } + + async fetchChanges(url, body) { + const controller = new AbortController(); + const signal = controller.signal; + let error = false; if (typeof this.stopObservingObjectChanges === 'function') { @@ -365,16 +467,6 @@ export default class CouchObjectProvider { delete this.stopObservingObjectChanges; }; - // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection - // style=main_only returns only the current winning revision of the document - let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`; - - let body = {}; - if (filter) { - url = `${url}&filter=_selector`; - body = JSON.stringify(filter); - } - const response = await fetch(url, { method: 'POST', signal, diff --git a/webpack.config.js b/webpack.config.js index c652c7fa5..104927594 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,19 +14,21 @@ const gitRevision = require('child_process') const gitBranch = require('child_process') .execSync('git rev-parse --abbrev-ref HEAD') .toString().trim(); -const vueFile = devMode ? - path.join(__dirname, "node_modules/vue/dist/vue.js") : - path.join(__dirname, "node_modules/vue/dist/vue.min.js"); +const vueFile = devMode + ? path.join(__dirname, "node_modules/vue/dist/vue.js") + : path.join(__dirname, "node_modules/vue/dist/vue.min.js"); const webpackConfig = { mode: devMode ? 'development' : 'production', entry: { openmct: './openmct.js', + couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js', espressoTheme: './src/plugins/themes/espresso-theme.scss', snowTheme: './src/plugins/themes/snow-theme.scss', maelstromTheme: './src/plugins/themes/maelstrom-theme.scss' }, output: { + globalObject: "this", filename: '[name].js', library: '[name]', libraryTarget: 'umd', @@ -105,13 +107,15 @@ const webpackConfig = { name: '[name].[ext]', outputPath(url, resourcePath, context) { if (/\.(jpg|jpeg|png|svg)$/.test(url)) { - return `images/${url}` + return `images/${url}`; } + if (/\.ico$/.test(url)) { - return `icons/${url}` + return `icons/${url}`; } + if (/\.(woff2?|eot|ttf)$/.test(url)) { - return `fonts/${url}` + return `fonts/${url}`; } else { return `${url}`; } |