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 ->
|
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", ->
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
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'
|
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue