perf(db): Lazily deserialize models on the other side of the action bridge

Summary:
We send database `trigger()` events through the ActionBrige to all windows of the app. This means that during initial sync, we're serializing, IPCing and unserializing thousands of models a minute x "N" windows.

This diff converts the payload of the trigger method into an actual class that implements a custom toJSON. It converts the impacted `objects` into a string, and doesn't deserialize them until it's asked.

Bottom line: this means that in many scenarios, we can avoid creating Contact models, etc. in composer windows only to broadcast them and then gc them.

Test Plan: No new tests yet, but this should definitel be tested. #willfix.

Reviewers: juan, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2236
This commit is contained in:
Ben Gotow 2015-11-06 11:15:20 -08:00
parent d838610290
commit 118761d79e
6 changed files with 71 additions and 26 deletions

View file

@ -16,7 +16,7 @@ describe "DatabaseStore", ->
beforeEach -> beforeEach ->
TestModel.configureBasic() TestModel.configureBasic()
spyOn(ModelQuery.prototype, 'where').andCallThrough() spyOn(ModelQuery.prototype, 'where').andCallThrough()
spyOn(DatabaseStore, '_triggerSoon').andCallFake -> Promise.resolve() spyOn(DatabaseStore, '_accumulateAndTrigger').andCallFake -> Promise.resolve()
@performed = [] @performed = []
@ -122,9 +122,9 @@ describe "DatabaseStore", ->
it "should cause the DatabaseStore to trigger with a change that contains the model", -> it "should cause the DatabaseStore to trigger with a change that contains the model", ->
waitsForPromise -> waitsForPromise ->
DatabaseStore.persistModel(testModelInstance).then -> DatabaseStore.persistModel(testModelInstance).then ->
expect(DatabaseStore._triggerSoon).toHaveBeenCalled() expect(DatabaseStore._accumulateAndTrigger).toHaveBeenCalled()
change = DatabaseStore._triggerSoon.mostRecentCall.args[0] change = DatabaseStore._accumulateAndTrigger.mostRecentCall.args[0]
expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance], type:'persist'}) expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance], type:'persist'})
.catch (err) -> .catch (err) ->
console.log err console.log err
@ -141,9 +141,9 @@ describe "DatabaseStore", ->
it "should cause the DatabaseStore to trigger with a change that contains the models", -> it "should cause the DatabaseStore to trigger with a change that contains the models", ->
waitsForPromise -> waitsForPromise ->
DatabaseStore.persistModels([testModelInstanceA, testModelInstanceB]).then -> DatabaseStore.persistModels([testModelInstanceA, testModelInstanceB]).then ->
expect(DatabaseStore._triggerSoon).toHaveBeenCalled() expect(DatabaseStore._accumulateAndTrigger).toHaveBeenCalled()
change = DatabaseStore._triggerSoon.mostRecentCall.args[0] change = DatabaseStore._accumulateAndTrigger.mostRecentCall.args[0]
expect(change).toEqual expect(change).toEqual
objectClass: TestModel.name, objectClass: TestModel.name,
objects: [testModelInstanceA, testModelInstanceB] objects: [testModelInstanceA, testModelInstanceB]
@ -171,9 +171,9 @@ describe "DatabaseStore", ->
it "should cause the DatabaseStore to trigger() with a change that contains the model", -> it "should cause the DatabaseStore to trigger() with a change that contains the model", ->
waitsForPromise -> waitsForPromise ->
DatabaseStore.unpersistModel(testModelInstance).then -> DatabaseStore.unpersistModel(testModelInstance).then ->
expect(DatabaseStore._triggerSoon).toHaveBeenCalled() expect(DatabaseStore._accumulateAndTrigger).toHaveBeenCalled()
change = DatabaseStore._triggerSoon.mostRecentCall.args[0] change = DatabaseStore._accumulateAndTrigger.mostRecentCall.args[0]
expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance], type:'unpersist'}) expect(change).toEqual({objectClass: TestModel.name, objects: [testModelInstance], type:'unpersist'})
describe "when the model provides additional sqlite config", -> describe "when the model provides additional sqlite config", ->
@ -447,4 +447,4 @@ describe "DatabaseStore", ->
expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION" expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
expect(@performed[5].query).toBe "COMMIT" expect(@performed[5].query).toBe "COMMIT"
describe "DatabaseStore::_triggerSoon", -> describe "DatabaseStore::_accumulateAndTrigger", ->

View file

@ -80,7 +80,7 @@ class ActionBridge
if name == Message.DATABASE_STORE_TRIGGER if name == Message.DATABASE_STORE_TRIGGER
DatabaseStore.triggeringFromActionBridge = true DatabaseStore.triggeringFromActionBridge = true
DatabaseStore.trigger(args...) DatabaseStore.trigger(new DatabaseStore.ChangeRecord(args...))
DatabaseStore.triggeringFromActionBridge = false DatabaseStore.triggeringFromActionBridge = false
else if Actions[name] else if Actions[name]

View file

@ -54,13 +54,17 @@ class AttributeCollection extends Attribute
return [] unless json && json instanceof Array return [] unless json && json instanceof Array
objs = [] objs = []
for objJSON in json for objJSON in json
obj = new @itemClass(objJSON) if @itemClass.prototype.fromJSON?
# Important: if no ids are in the JSON, don't make them up obj = new @itemClass
# randomly. This causes an object to be "different" each time it's # Important: if no ids are in the JSON, don't make them up
# de-serialized even if it's actually the same, makes React # randomly. This causes an object to be "different" each time it's
# components re-render! # de-serialized even if it's actually the same, makes React
obj.clientId = undefined # components re-render!
obj.fromJSON(objJSON) if obj.fromJSON? obj.clientId = undefined
obj.fromJSON(objJSON)
else
obj = new @itemClass(objJSON)
obj.clientId = undefined
objs.push(obj) objs.push(obj)
objs objs

View file

@ -0,0 +1,39 @@
Utils = require '../models/utils'
###
DatabaseChangeRecord is the object emitted from the DatabaseStore when it triggers.
The DatabaseChangeRecord contains information about what type of model changed,
and references to the new model values. All mutations to the database produce these
change records.
###
class DatabaseChangeRecord
constructor: (options) ->
@options = options
# When DatabaseChangeRecords are sent over IPC to other windows, their object
# payload is sub-serialized into a JSON string. This means that we can wait
# to deserialize models until someone in the window asks for `change.objects`
@_objects = options.objects
@_objectsString = options.objectsString
Object.defineProperty(@, 'type', {
get: -> options.type
})
Object.defineProperty(@, 'objectClass', {
get: -> options.objectClass
})
Object.defineProperty(@, 'objects', {
get: ->
@_objects ?= Utils.deserializeRegisteredObjects(@_objectsString)
@_objects
})
toJSON: =>
@_objectsString ?= Utils.serializeRegisteredObjects(@_objects)
type: @type
objectClass: @objectClass
objectsString: @_objectsString
module.exports = DatabaseChangeRecord

View file

@ -10,6 +10,7 @@ ModelQuery = require '../models/query'
NylasStore = require '../../global/nylas-store' NylasStore = require '../../global/nylas-store'
PromiseQueue = require 'promise-queue' PromiseQueue = require 'promise-queue'
DatabaseSetupQueryBuilder = require './database-setup-query-builder' DatabaseSetupQueryBuilder = require './database-setup-query-builder'
DatabaseChangeRecord = require './database-change-record'
PriorityUICoordinator = require '../../priority-ui-coordinator' PriorityUICoordinator = require '../../priority-ui-coordinator'
{AttributeCollection, AttributeJoinedData} = require '../attributes' {AttributeCollection, AttributeJoinedData} = require '../attributes'
@ -414,7 +415,7 @@ class DatabaseStore extends NylasStore
throw new Error("DatabaseStore::persistModel - You must pass an instance of the Model class.") throw new Error("DatabaseStore::persistModel - You must pass an instance of the Model class.")
@_writeModels([model]).then => @_writeModels([model]).then =>
@_triggerSoon({objectClass: model.constructor.name, objects: [model], type: 'persist'}) @_accumulateAndTrigger({objectClass: model.constructor.name, objects: [model], type: 'persist'})
# Public: Asynchronously writes `models` to the cache and triggers a single change # Public: Asynchronously writes `models` to the cache and triggers a single change
# event. Note: Models must be of the same class to be persisted in a batch operation. # event. Note: Models must be of the same class to be persisted in a batch operation.
@ -442,7 +443,7 @@ class DatabaseStore extends NylasStore
ids[model.id] = true ids[model.id] = true
@_writeModels(models).then => @_writeModels(models).then =>
@_triggerSoon({objectClass: models[0].constructor.name, objects: models, type: 'persist'}) @_accumulateAndTrigger({objectClass: models[0].constructor.name, objects: models, type: 'persist'})
# Public: Asynchronously removes `model` from the cache and triggers a change event. # Public: Asynchronously removes `model` from the cache and triggers a change event.
# #
@ -455,12 +456,12 @@ class DatabaseStore extends NylasStore
# callbacks failed # callbacks failed
unpersistModel: (model) => unpersistModel: (model) =>
@_deleteModel(model).then => @_deleteModel(model).then =>
@_triggerSoon({objectClass: model.constructor.name, objects: [model], type: 'unpersist'}) @_accumulateAndTrigger({objectClass: model.constructor.name, objects: [model], type: 'unpersist'})
persistJSONObject: (key, json) -> persistJSONObject: (key, json) ->
jsonString = serializeRegisteredObjects(json) jsonString = serializeRegisteredObjects(json)
@_query("REPLACE INTO `JSONObject` (`key`,`data`) VALUES (?,?)", [key, jsonString]).then => @_query("REPLACE INTO `JSONObject` (`key`,`data`) VALUES (?,?)", [key, jsonString]).then =>
@trigger({objectClass: 'JSONObject', objects: [{key: key, json: json}], type: 'persist'}) @trigger(new DatabaseChangeRecord({objectClass: 'JSONObject', objects: [{key: key, json: json}], type: 'persist'}))
findJSONObject: (key) -> findJSONObject: (key) ->
@_query("SELECT `data` FROM `JSONObject` WHERE key = ? LIMIT 1", [key]).then (results) => @_query("SELECT `data` FROM `JSONObject` WHERE key = ? LIMIT 1", [key]).then (results) =>
@ -486,19 +487,19 @@ class DatabaseStore extends NylasStore
########################### PRIVATE METHODS ############################ ########################### PRIVATE METHODS ############################
######################################################################## ########################################################################
# _TriggerSoon is a guarded version of trigger that can accumulate changes. # _accumulateAndTrigger is a guarded version of trigger that can accumulate changes.
# This means that even if you're a bad person and call `persistModel` 100 times # This means that even if you're a bad person and call `persistModel` 100 times
# from 100 task objects queued at the same time, it will only create one # from 100 task objects queued at the same time, it will only create one
# `trigger` event. This is important since the database triggering impacts # `trigger` event. This is important since the database triggering impacts
# the entire application. # the entire application.
_triggerSoon: (change) => _accumulateAndTrigger: (change) =>
@_triggerPromise ?= new Promise (resolve, reject) => @_triggerPromise ?= new Promise (resolve, reject) =>
@_resolve = resolve @_resolve = resolve
flush = => flush = =>
return unless @_changeAccumulated return unless @_changeAccumulated
clearTimeout(@_changeFireTimer) if @_changeFireTimer clearTimeout(@_changeFireTimer) if @_changeFireTimer
@trigger(@_changeAccumulated) @trigger(new DatabaseChangeRecord(@_changeAccumulated))
@_changeAccumulated = null @_changeAccumulated = null
@_changeFireTimer = null @_changeFireTimer = null
@_resolve?() @_resolve?()
@ -662,3 +663,4 @@ class DatabaseStore extends NylasStore
module.exports = new DatabaseStore() module.exports = new DatabaseStore()
module.exports.ChangeRecord = DatabaseChangeRecord

View file

@ -20,7 +20,7 @@ class SectionConfig
class PreferencesSectionStore extends NylasStore class PreferencesSectionStore extends NylasStore
constructor: -> constructor: ->
@_sectionConfigs = [] @_sectionConfigs = []
@_triggerSoon ?= _.debounce(( => @trigger()), 20) @_accumulateAndTrigger ?= _.debounce(( => @trigger()), 20)
@Section = {} @Section = {}
@SectionConfig = SectionConfig @SectionConfig = SectionConfig
@ -64,12 +64,12 @@ class PreferencesSectionStore extends NylasStore
@Section[sectionConfig.sectionId] = sectionConfig.sectionId @Section[sectionConfig.sectionId] = sectionConfig.sectionId
@_sectionConfigs.push(sectionConfig) @_sectionConfigs.push(sectionConfig)
@_sectionConfigs = _.sortBy(@_sectionConfigs, "order") @_sectionConfigs = _.sortBy(@_sectionConfigs, "order")
@_triggerSoon() @_accumulateAndTrigger()
unregisterPreferenceSection: (sectionId) -> unregisterPreferenceSection: (sectionId) ->
delete @Section[sectionId] delete @Section[sectionId]
@_sectionConfigs = _.reject @_sectionConfigs, (sectionConfig) -> @_sectionConfigs = _.reject @_sectionConfigs, (sectionConfig) ->
sectionConfig.sectionId is sectionId sectionConfig.sectionId is sectionId
@_triggerSoon() @_accumulateAndTrigger()
module.exports = new PreferencesSectionStore() module.exports = new PreferencesSectionStore()