From 845c796b96ad92903a960d9f9157f2d5ac859206 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Sep 2017 11:55:03 +0100 Subject: [PATCH] Make re-emitting events much more memory efficient The previous impl bluntly created a closure for every event type and source emitter we set up a re-emit for. We can do much better than this fairly easily by having one bound handler for each event name and moving it into a class so we have one emitter per target, since 99% of the time the target is the client object. --- src/ReEmitter.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/client.js | 8 +++++--- src/models/room.js | 8 +++++--- src/reemit.js | 44 -------------------------------------------- src/sync.js | 16 +++++++--------- 5 files changed, 63 insertions(+), 59 deletions(-) create mode 100644 src/ReEmitter.js delete mode 100644 src/reemit.js diff --git a/src/ReEmitter.js b/src/ReEmitter.js new file mode 100644 index 000000000..51a4ab3b2 --- /dev/null +++ b/src/ReEmitter.js @@ -0,0 +1,46 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @module + */ + +export default class Reemitter { + constructor(target) { + this.target = target; + + // We keep one bound event handler for each event name so we know + // what event is arriving + this.boundHandlers = {}; + } + + _handleEvent(eventName, ...args) { + this.target.emit(eventName, ...args); + } + + reEmit(source, eventNames) { + for (const eventName of eventNames) { + if (this.boundHandlers[eventName] === undefined) { + this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName); + } + const boundHandler = this.boundHandlers[eventName]; + + source.on(eventName, boundHandler); + } + } +} diff --git a/src/client.js b/src/client.js index e201197e2..b2ee6eb32 100644 --- a/src/client.js +++ b/src/client.js @@ -40,7 +40,7 @@ const SyncApi = require("./sync"); const MatrixBaseApis = require("./base-apis"); const MatrixError = httpApi.MatrixError; -import reEmit from './reemit'; +import ReEmitter from './ReEmitter'; const SCROLLBACK_DELAY_MS = 3000; let CRYPTO_ENABLED = false; @@ -115,6 +115,8 @@ try { function MatrixClient(opts) { MatrixBaseApis.call(this, opts); + this.reEmitter = new ReEmitter(this); + this.store = opts.store || new StubStore(); this.deviceId = opts.deviceId || null; @@ -364,7 +366,7 @@ MatrixClient.prototype.initCrypto = async function() { this._cryptoStore, ); - reEmit(this, crypto, [ + this.reEmitter.reEmit(crypto, [ "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", ]); @@ -3275,7 +3277,7 @@ function _PojoToMatrixEventMapper(client) { function mapper(plainOldJsObject) { const event = new MatrixEvent(plainOldJsObject); if (event.isEncrypted()) { - reEmit(client, event, [ + client.reEmitter.reEmit(event, [ "Event.decrypted", ]); event.attemptDecryption(client._crypto); diff --git a/src/models/room.js b/src/models/room.js index 54823840b..17ebdc31c 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -27,7 +27,7 @@ const ContentRepo = require("../content-repo"); const EventTimeline = require("./event-timeline"); const EventTimelineSet = require("./event-timeline-set"); -import reEmit from '../reemit'; +import ReEmitter from '../ReEmitter'; function synthesizeReceipt(userId, event, receiptType) { // console.log("synthesizing receipt for "+event.getId()); @@ -106,6 +106,8 @@ function Room(roomId, opts) { opts = opts || {}; opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; + this.reEmitter = new ReEmitter(this); + if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) { throw new Error( "opts.pendingEventOrdering MUST be either 'chronological' or " + @@ -153,7 +155,7 @@ function Room(roomId, opts) { // all our per-room timeline sets. the first one is the unfiltered ones; // the subsequent ones are the filtered ones in no particular order. this._timelineSets = [new EventTimelineSet(this, opts)]; - reEmit(this, this.getUnfilteredTimelineSet(), + this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), ["Room.timeline", "Room.timelineReset"]); this._fixUpLegacyTimelineFields(); @@ -490,7 +492,7 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) { } const opts = Object.assign({ filter: filter }, this._opts); const timelineSet = new EventTimelineSet(this, opts); - reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]); + this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]); this._filteredTimelineSets[filter.filterId] = timelineSet; this._timelineSets.push(timelineSet); diff --git a/src/reemit.js b/src/reemit.js deleted file mode 100644 index db27f7d6e..000000000 --- a/src/reemit.js +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/** - * @module - */ - -/** - * re-emit events raised by one EventEmitter from another - * - * @param {external:EventEmitter} reEmitEntity - * entity from which we want events to be emitted - * @param {external:EventEmitter} emittableEntity - * entity from which events are currently emitted - * @param {Array} eventNames - * list of events to be reemitted - */ -export default function reEmit(reEmitEntity, emittableEntity, eventNames) { - for (const eventName of eventNames) { - // setup a listener on the entity (the Room, User, etc) for this event - emittableEntity.on(eventName, function(...args) { - // take the args from the listener and reuse them, adding the - // event name to the arg list so it works with .emit() - // Transformation Example: - // listener on "foo" => function(a,b) { ... } - // Re-emit on "thing" => thing.emit("foo", a, b) - reEmitEntity.emit(eventName, ...args); - }); - } -} diff --git a/src/sync.js b/src/sync.js index 1adfa298e..f5eb884c3 100644 --- a/src/sync.js +++ b/src/sync.js @@ -32,8 +32,6 @@ const utils = require("./utils"); const Filter = require("./filter"); const EventTimeline = require("./models/event-timeline"); -import reEmit from './reemit'; - const DEBUG = true; // /sync requests allow you to set a timeout= but the request may continue @@ -100,7 +98,7 @@ function SyncApi(client, opts) { this._failedSyncCount = 0; // Number of consecutive failed /sync requests if (client.getNotifTimelineSet()) { - reEmit(client, client.getNotifTimelineSet(), + client.reEmitter.reEmit(client.getNotifTimelineSet(), ["Room.timeline", "Room.timelineReset"]); } } @@ -115,7 +113,7 @@ SyncApi.prototype.createRoom = function(roomId) { pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, }); - reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction", + client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction", "Room.receipt", "Room.tags", "Room.timelineReset", "Room.localEchoUpdated", @@ -132,7 +130,7 @@ SyncApi.prototype.createRoom = function(roomId) { SyncApi.prototype.createGroup = function(groupId) { const client = this.client; const group = new Group(groupId); - reEmit(client, group, ["Group.profile", "Group.myMembership"]); + client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]); return group; }; @@ -145,13 +143,13 @@ SyncApi.prototype._registerStateListeners = function(room) { // we need to also re-emit room state and room member events, so hook it up // to the client now. We need to add a listener for RoomState.members in // order to hook them correctly. (TODO: find a better way?) - reEmit(client, room.currentState, [ + client.reEmitter.reEmit(room.currentState, [ "RoomState.events", "RoomState.members", "RoomState.newMember", ]); room.currentState.on("RoomState.newMember", function(event, state, member) { member.user = client.getUser(member.userId); - reEmit( - client, member, + client.reEmitter.reEmit( + member, [ "RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel", "RoomMember.membership", @@ -1337,7 +1335,7 @@ SyncApi.prototype._onOnline = function() { function createNewUser(client, userId) { const user = new User(userId); - reEmit(client, user, [ + client.reEmitter.reEmit(user, [ "User.avatarUrl", "User.displayName", "User.presence", "User.currentlyActive", "User.lastPresenceTs", ]);