mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-20 15:26:06 +08:00
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:
parent
d838610290
commit
118761d79e
|
@ -16,7 +16,7 @@ describe "DatabaseStore", ->
|
|||
beforeEach ->
|
||||
TestModel.configureBasic()
|
||||
spyOn(ModelQuery.prototype, 'where').andCallThrough()
|
||||
spyOn(DatabaseStore, '_triggerSoon').andCallFake -> Promise.resolve()
|
||||
spyOn(DatabaseStore, '_accumulateAndTrigger').andCallFake -> Promise.resolve()
|
||||
|
||||
@performed = []
|
||||
|
||||
|
@ -122,9 +122,9 @@ describe "DatabaseStore", ->
|
|||
it "should cause the DatabaseStore to trigger with a change that contains the model", ->
|
||||
waitsForPromise ->
|
||||
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'})
|
||||
.catch (err) ->
|
||||
console.log err
|
||||
|
@ -141,9 +141,9 @@ describe "DatabaseStore", ->
|
|||
it "should cause the DatabaseStore to trigger with a change that contains the models", ->
|
||||
waitsForPromise ->
|
||||
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
|
||||
objectClass: TestModel.name,
|
||||
objects: [testModelInstanceA, testModelInstanceB]
|
||||
|
@ -171,9 +171,9 @@ describe "DatabaseStore", ->
|
|||
it "should cause the DatabaseStore to trigger() with a change that contains the model", ->
|
||||
waitsForPromise ->
|
||||
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'})
|
||||
|
||||
describe "when the model provides additional sqlite config", ->
|
||||
|
@ -447,4 +447,4 @@ describe "DatabaseStore", ->
|
|||
expect(@performed[4].query).toBe "BEGIN EXCLUSIVE TRANSACTION"
|
||||
expect(@performed[5].query).toBe "COMMIT"
|
||||
|
||||
describe "DatabaseStore::_triggerSoon", ->
|
||||
describe "DatabaseStore::_accumulateAndTrigger", ->
|
||||
|
|
|
@ -80,7 +80,7 @@ class ActionBridge
|
|||
|
||||
if name == Message.DATABASE_STORE_TRIGGER
|
||||
DatabaseStore.triggeringFromActionBridge = true
|
||||
DatabaseStore.trigger(args...)
|
||||
DatabaseStore.trigger(new DatabaseStore.ChangeRecord(args...))
|
||||
DatabaseStore.triggeringFromActionBridge = false
|
||||
|
||||
else if Actions[name]
|
||||
|
|
|
@ -54,13 +54,17 @@ class AttributeCollection extends Attribute
|
|||
return [] unless json && json instanceof Array
|
||||
objs = []
|
||||
for objJSON in json
|
||||
obj = new @itemClass(objJSON)
|
||||
# Important: if no ids are in the JSON, don't make them up
|
||||
# randomly. This causes an object to be "different" each time it's
|
||||
# de-serialized even if it's actually the same, makes React
|
||||
# components re-render!
|
||||
obj.clientId = undefined
|
||||
obj.fromJSON(objJSON) if obj.fromJSON?
|
||||
if @itemClass.prototype.fromJSON?
|
||||
obj = new @itemClass
|
||||
# Important: if no ids are in the JSON, don't make them up
|
||||
# randomly. This causes an object to be "different" each time it's
|
||||
# de-serialized even if it's actually the same, makes React
|
||||
# components re-render!
|
||||
obj.clientId = undefined
|
||||
obj.fromJSON(objJSON)
|
||||
else
|
||||
obj = new @itemClass(objJSON)
|
||||
obj.clientId = undefined
|
||||
objs.push(obj)
|
||||
objs
|
||||
|
||||
|
|
39
src/flux/stores/database-change-record.coffee
Normal file
39
src/flux/stores/database-change-record.coffee
Normal 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
|
|
@ -10,6 +10,7 @@ ModelQuery = require '../models/query'
|
|||
NylasStore = require '../../global/nylas-store'
|
||||
PromiseQueue = require 'promise-queue'
|
||||
DatabaseSetupQueryBuilder = require './database-setup-query-builder'
|
||||
DatabaseChangeRecord = require './database-change-record'
|
||||
PriorityUICoordinator = require '../../priority-ui-coordinator'
|
||||
|
||||
{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.")
|
||||
|
||||
@_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
|
||||
# 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
|
||||
|
||||
@_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.
|
||||
#
|
||||
|
@ -455,12 +456,12 @@ class DatabaseStore extends NylasStore
|
|||
# callbacks failed
|
||||
unpersistModel: (model) =>
|
||||
@_deleteModel(model).then =>
|
||||
@_triggerSoon({objectClass: model.constructor.name, objects: [model], type: 'unpersist'})
|
||||
@_accumulateAndTrigger({objectClass: model.constructor.name, objects: [model], type: 'unpersist'})
|
||||
|
||||
persistJSONObject: (key, json) ->
|
||||
jsonString = serializeRegisteredObjects(json)
|
||||
@_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) ->
|
||||
@_query("SELECT `data` FROM `JSONObject` WHERE key = ? LIMIT 1", [key]).then (results) =>
|
||||
|
@ -486,19 +487,19 @@ class DatabaseStore extends NylasStore
|
|||
########################### 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
|
||||
# from 100 task objects queued at the same time, it will only create one
|
||||
# `trigger` event. This is important since the database triggering impacts
|
||||
# the entire application.
|
||||
_triggerSoon: (change) =>
|
||||
_accumulateAndTrigger: (change) =>
|
||||
@_triggerPromise ?= new Promise (resolve, reject) =>
|
||||
@_resolve = resolve
|
||||
|
||||
flush = =>
|
||||
return unless @_changeAccumulated
|
||||
clearTimeout(@_changeFireTimer) if @_changeFireTimer
|
||||
@trigger(@_changeAccumulated)
|
||||
@trigger(new DatabaseChangeRecord(@_changeAccumulated))
|
||||
@_changeAccumulated = null
|
||||
@_changeFireTimer = null
|
||||
@_resolve?()
|
||||
|
@ -662,3 +663,4 @@ class DatabaseStore extends NylasStore
|
|||
|
||||
|
||||
module.exports = new DatabaseStore()
|
||||
module.exports.ChangeRecord = DatabaseChangeRecord
|
||||
|
|
|
@ -20,7 +20,7 @@ class SectionConfig
|
|||
class PreferencesSectionStore extends NylasStore
|
||||
constructor: ->
|
||||
@_sectionConfigs = []
|
||||
@_triggerSoon ?= _.debounce(( => @trigger()), 20)
|
||||
@_accumulateAndTrigger ?= _.debounce(( => @trigger()), 20)
|
||||
@Section = {}
|
||||
@SectionConfig = SectionConfig
|
||||
|
||||
|
@ -64,12 +64,12 @@ class PreferencesSectionStore extends NylasStore
|
|||
@Section[sectionConfig.sectionId] = sectionConfig.sectionId
|
||||
@_sectionConfigs.push(sectionConfig)
|
||||
@_sectionConfigs = _.sortBy(@_sectionConfigs, "order")
|
||||
@_triggerSoon()
|
||||
@_accumulateAndTrigger()
|
||||
|
||||
unregisterPreferenceSection: (sectionId) ->
|
||||
delete @Section[sectionId]
|
||||
@_sectionConfigs = _.reject @_sectionConfigs, (sectionConfig) ->
|
||||
sectionConfig.sectionId is sectionId
|
||||
@_triggerSoon()
|
||||
@_accumulateAndTrigger()
|
||||
|
||||
module.exports = new PreferencesSectionStore()
|
||||
|
|
Loading…
Reference in a new issue