micro optimizations in struct reader
This commit is contained in:
parent
3406247a3e
commit
3449687280
@ -130,47 +130,68 @@ export const readClientsStructRefs = (decoder, clientRefs, doc) => {
|
|||||||
/**
|
/**
|
||||||
* @type {Array<GC|Item>}
|
* @type {Array<GC|Item>}
|
||||||
*/
|
*/
|
||||||
const refs = []
|
const refs = new Array(numberOfStructs)
|
||||||
const client = decoder.readClient()
|
const client = decoder.readClient()
|
||||||
let clock = decoding.readVarUint(decoder.restDecoder)
|
let clock = decoding.readVarUint(decoder.restDecoder)
|
||||||
|
// const start = performance.now()
|
||||||
clientRefs.set(client, refs)
|
clientRefs.set(client, refs)
|
||||||
for (let i = 0; i < numberOfStructs; i++) {
|
for (let i = 0; i < numberOfStructs; i++) {
|
||||||
const info = decoder.readInfo()
|
const info = decoder.readInfo()
|
||||||
if ((binary.BITS5 & info) !== 0) {
|
if ((binary.BITS5 & info) !== 0) {
|
||||||
/**
|
/**
|
||||||
* The item that was originally to the left of this item.
|
* The optimized implementation doesn't use any variables because inlining variables is faster.
|
||||||
* @type {ID | null}
|
* Below a non-optimized version is shown that implements the basic algorithm with
|
||||||
|
* a few comments
|
||||||
*/
|
*/
|
||||||
|
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
|
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
|
// and we read the next string as parentYKey.
|
||||||
|
// It indicates how we store/retrieve parent from `y.share`
|
||||||
|
// @type {string|null}
|
||||||
|
const struct = new Item(
|
||||||
|
createID(client, clock),
|
||||||
|
null, // leftd
|
||||||
|
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
||||||
|
null, // right
|
||||||
|
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
||||||
|
cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent
|
||||||
|
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
||||||
|
readItemContent(decoder, info) // item content
|
||||||
|
)
|
||||||
|
/* A non-optimized implementation of the above algorithm:
|
||||||
|
|
||||||
|
// The item that was originally to the left of this item.
|
||||||
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
|
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
|
||||||
/**
|
// The item that was originally to the right of this item.
|
||||||
* The item that was originally to the right of this item.
|
|
||||||
* @type {ID | null}
|
|
||||||
*/
|
|
||||||
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
|
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
|
||||||
const canCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
|
||||||
const hasParentYKey = canCopyParentInfo ? decoder.readParentInfo() : false
|
const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false
|
||||||
/**
|
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
||||||
* If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
|
// and we read the next string as parentYKey.
|
||||||
* and we read the next string as parentYKey.
|
// It indicates how we store/retrieve parent from `y.share`
|
||||||
* It indicates how we store/retrieve parent from `y.share`
|
// @type {string|null}
|
||||||
* @type {string|null}
|
const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null
|
||||||
*/
|
|
||||||
const parentYKey = canCopyParentInfo && hasParentYKey ? decoder.readString() : null
|
|
||||||
|
|
||||||
const struct = new Item(
|
const struct = new Item(
|
||||||
createID(client, clock), null, origin, null, rightOrigin,
|
createID(client, clock),
|
||||||
canCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
|
null, // leftd
|
||||||
canCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
origin, // origin
|
||||||
/** @type {AbstractContent} */ (readItemContent(decoder, info)) // item content
|
null, // right
|
||||||
|
rightOrigin, // right origin
|
||||||
|
cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
|
||||||
|
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
|
||||||
|
readItemContent(decoder, info) // item content
|
||||||
)
|
)
|
||||||
refs.push(struct)
|
*/
|
||||||
|
refs[i] = struct
|
||||||
clock += struct.length
|
clock += struct.length
|
||||||
} else {
|
} else {
|
||||||
const len = decoder.readLen()
|
const len = decoder.readLen()
|
||||||
refs.push(new GC(createID(client, clock), len))
|
refs[i] = new GC(createID(client, clock), len)
|
||||||
clock += len
|
clock += len
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// console.log('time to read: ', performance.now() - start) // @todo remove
|
||||||
}
|
}
|
||||||
return clientRefs
|
return clientRefs
|
||||||
}
|
}
|
||||||
@ -221,18 +242,15 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const ref = stack[stack.length - 1]
|
const ref = stack[stack.length - 1]
|
||||||
const refID = ref.id
|
const localClock = getState(store, ref.id.client)
|
||||||
const client = refID.client
|
const offset = ref.id.clock < localClock ? localClock - ref.id.clock : 0
|
||||||
const refClock = refID.clock
|
if (ref.id.clock + offset !== localClock) {
|
||||||
const localClock = getState(store, client)
|
|
||||||
const offset = refClock < localClock ? localClock - refClock : 0
|
|
||||||
if (refClock + offset !== localClock) {
|
|
||||||
// A previous message from this client is missing
|
// A previous message from this client is missing
|
||||||
// check if there is a pending structRef with a smaller clock and switch them
|
// check if there is a pending structRef with a smaller clock and switch them
|
||||||
const structRefs = clientsStructRefs.get(client) || { refs: [], i: 0 }
|
const structRefs = clientsStructRefs.get(ref.id.client) || { refs: [], i: 0 }
|
||||||
if (structRefs.refs.length !== structRefs.i) {
|
if (structRefs.refs.length !== structRefs.i) {
|
||||||
const r = structRefs.refs[structRefs.i]
|
const r = structRefs.refs[structRefs.i]
|
||||||
if (r.id.clock < refClock) {
|
if (r.id.clock < ref.id.clock) {
|
||||||
// put ref with smaller clock on stack instead and continue
|
// put ref with smaller clock on stack instead and continue
|
||||||
structRefs.refs[structRefs.i] = ref
|
structRefs.refs[structRefs.i] = ref
|
||||||
stack[stack.length - 1] = r
|
stack[stack.length - 1] = r
|
||||||
@ -246,7 +264,12 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const missing = ref.getMissing(transaction, store)
|
const missing = ref.getMissing(transaction, store)
|
||||||
if (missing !== null) {
|
if (missing === null) {
|
||||||
|
if (offset < ref.length) {
|
||||||
|
ref.integrate(transaction, offset)
|
||||||
|
}
|
||||||
|
stack.pop()
|
||||||
|
} else {
|
||||||
// get the struct reader that has the missing struct
|
// get the struct reader that has the missing struct
|
||||||
const structRefs = clientsStructRefs.get(missing) || { refs: [], i: 0 }
|
const structRefs = clientsStructRefs.get(missing) || { refs: [], i: 0 }
|
||||||
if (structRefs.refs.length === structRefs.i) {
|
if (structRefs.refs.length === structRefs.i) {
|
||||||
@ -254,11 +277,6 @@ const resumeStructIntegration = (transaction, store) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
stack.push(structRefs.refs[structRefs.i++])
|
stack.push(structRefs.refs[structRefs.i++])
|
||||||
} else {
|
|
||||||
if (offset < ref.length) {
|
|
||||||
ref.integrate(transaction, offset)
|
|
||||||
}
|
|
||||||
stack.pop()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
store.pendingClientsStructRefs.clear()
|
store.pendingClientsStructRefs.clear()
|
||||||
@ -342,11 +360,22 @@ const cleanupPendingStructs = pendingClientsStructRefs => {
|
|||||||
*/
|
*/
|
||||||
export const readStructs = (decoder, transaction, store) => {
|
export const readStructs = (decoder, transaction, store) => {
|
||||||
const clientsStructRefs = new Map()
|
const clientsStructRefs = new Map()
|
||||||
|
let start = performance.now()
|
||||||
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
|
readClientsStructRefs(decoder, clientsStructRefs, transaction.doc)
|
||||||
|
console.log('time to read structs: ', performance.now() - start) // @todo remove
|
||||||
|
start = performance.now()
|
||||||
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
mergeReadStructsIntoPendingReads(store, clientsStructRefs)
|
||||||
|
console.log('time to merge: ', performance.now() - start) // @todo remove
|
||||||
|
start = performance.now()
|
||||||
resumeStructIntegration(transaction, store)
|
resumeStructIntegration(transaction, store)
|
||||||
|
console.log('time to integrate: ', performance.now() - start) // @todo remove
|
||||||
|
start = performance.now()
|
||||||
cleanupPendingStructs(store.pendingClientsStructRefs)
|
cleanupPendingStructs(store.pendingClientsStructRefs)
|
||||||
|
console.log('time to cleanup: ', performance.now() - start) // @todo remove
|
||||||
|
start = performance.now()
|
||||||
tryResumePendingDeleteReaders(transaction, store)
|
tryResumePendingDeleteReaders(transaction, store)
|
||||||
|
console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
|
||||||
|
start = performance.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,6 +205,40 @@ export const testFormattingRemovedInMidText = tc => {
|
|||||||
t.assert(Y.getTypeChildren(text0).length === 3)
|
t.assert(Y.getTypeChildren(text0).length === 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const id = Y.createID(0, 0)
|
||||||
|
const c = new Y.ContentString('a')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testBestCase = tc => {
|
||||||
|
const N = 2000000
|
||||||
|
const items = new Array(N)
|
||||||
|
t.measureTime('time to create two million items in the best case', () => {
|
||||||
|
const parent = /** @type {any} */ ({})
|
||||||
|
let prevItem = null
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
/**
|
||||||
|
* @type {Y.Item}
|
||||||
|
*/
|
||||||
|
const n = new Y.Item(Y.createID(0, 0), null, null, null, null, null, null, c)
|
||||||
|
// items.push(n)
|
||||||
|
items[i] = n
|
||||||
|
n.right = prevItem
|
||||||
|
n.rightOrigin = prevItem ? id : null
|
||||||
|
n.content = c
|
||||||
|
n.parent = parent
|
||||||
|
prevItem = n
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const newArray = new Array(N)
|
||||||
|
t.measureTime('time to copy two million items to new Array', () => {
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
newArray[i] = items[i]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const tryGc = () => {
|
const tryGc = () => {
|
||||||
if (typeof global !== 'undefined' && global.gc) {
|
if (typeof global !== 'undefined' && global.gc) {
|
||||||
global.gc()
|
global.gc()
|
||||||
@ -215,7 +249,7 @@ const tryGc = () => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testLargeFragmentedDocument = tc => {
|
export const testLargeFragmentedDocument = tc => {
|
||||||
const itemsToInsert = 1000000
|
const itemsToInsert = 2000000
|
||||||
let update = /** @type {any} */ (null)
|
let update = /** @type {any} */ (null)
|
||||||
;(() => {
|
;(() => {
|
||||||
const doc1 = new Y.Doc()
|
const doc1 = new Y.Doc()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user