improve mem usage by conditional execution of the integration part (step throught the integration if there are conflicting items)

This commit is contained in:
Kevin Jahns 2020-06-09 16:34:07 +02:00
parent 7fb63de8fc
commit 6b0154f046
5 changed files with 129 additions and 96 deletions

View File

@ -14,7 +14,13 @@ export class AbstractStruct {
constructor (id, length) { constructor (id, length) {
this.id = id this.id = id
this.length = length this.length = length
this.deleted = false }
/**
* @type {boolean}
*/
get deleted () {
throw error.methodUnimplemented()
} }
/** /**

View File

@ -68,7 +68,7 @@ export class ContentDeleted {
*/ */
integrate (transaction, item) { integrate (transaction, item) {
addToDeleteSet(transaction.deleteSet, item.id, this.len) addToDeleteSet(transaction.deleteSet, item.id, this.len)
item.deleted = true item.markDeleted()
} }
/** /**

View File

@ -13,13 +13,8 @@ export const structGCRefNumber = 0
* @private * @private
*/ */
export class GC extends AbstractStruct { export class GC extends AbstractStruct {
/** get deleted () {
* @param {ID} id return true
* @param {number} length
*/
constructor (id, length) {
super(id, length)
this.deleted = true
} }
delete () {} delete () {}

View File

@ -100,7 +100,7 @@ export const splitItem = (transaction, leftItem, diff) => {
leftItem.content.splice(diff) leftItem.content.splice(diff)
) )
if (leftItem.deleted) { if (leftItem.deleted) {
rightItem.deleted = true rightItem.markDeleted()
} }
if (leftItem.keep) { if (leftItem.keep) {
rightItem.keep = true rightItem.keep = true
@ -279,11 +279,6 @@ export class Item extends AbstractStruct {
* @type {String | null} * @type {String | null}
*/ */
this.parentSub = parentSub this.parentSub = parentSub
/**
* Whether this item was deleted or not.
* @type {Boolean}
*/
this.deleted = false
/** /**
* If this type's effect is reundone this type refers to the type that undid * If this type's effect is reundone this type refers to the type that undid
* this operation. * this operation.
@ -294,14 +289,44 @@ export class Item extends AbstractStruct {
* @type {AbstractContent} * @type {AbstractContent}
*/ */
this.content = content this.content = content
/** this.info = this.content.isCountable() ? binary.BIT2 : 0
* If true, do not garbage collect this Item.
*/ // this.keep = false
this.keep = false }
/**
* If true, do not garbage collect this Item.
*/
get keep () {
return (this.info & binary.BIT1) > 0
}
set keep (doKeep) {
if (this.keep !== doKeep) {
this.info ^= binary.BIT1
}
} }
get countable () { get countable () {
return this.content.isCountable() return (this.info & binary.BIT2) > 0
}
/**
* Whether this item was deleted or not.
* @type {Boolean}
*/
get deleted () {
return (this.info & binary.BIT3) > 0
}
set deleted (doDelete) {
if (this.deleted !== doDelete) {
this.info ^= binary.BIT3
}
}
markDeleted () {
this.info |= binary.BIT3
} }
/** /**
@ -367,110 +392,108 @@ export class Item extends AbstractStruct {
* @param {number} offset * @param {number} offset
*/ */
integrate (transaction, offset) { integrate (transaction, offset) {
const store = transaction.doc.store
if (offset > 0) { if (offset > 0) {
this.id.clock += offset this.id.clock += offset
this.left = getItemCleanEnd(transaction, store, createID(this.id.client, this.id.clock - 1)) this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1))
this.origin = this.left.lastId this.origin = this.left.lastId
this.content = this.content.splice(offset) this.content = this.content.splice(offset)
this.length -= offset this.length -= offset
} }
const parentSub = this.parentSub
const length = this.length
const parent = /** @type {AbstractType<any>|null} */ (this.parent)
if (parent) { if (this.parent) {
/** if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) {
* @type {Item|null} /**
*/ * @type {Item|null}
let left = this.left */
let left = this.left
/** /**
* @type {Item|null} * @type {Item|null}
*/ */
let o let o
// set o to the first conflicting item // set o to the first conflicting item
if (left !== null) { if (left !== null) {
o = left.right o = left.right
} else if (parentSub !== null) { } else if (this.parentSub !== null) {
o = parent._map.get(parentSub) || null o = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
while (o !== null && o.left !== null) { while (o !== null && o.left !== null) {
o = o.left o = o.left
}
} else {
o = parent._start
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set()
/**
* @type {Set<Item>}
*/
const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o)
conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < this.id.client) {
left = o
conflictingItems.clear()
}
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(store, o.origin))) {
// case 2
if (o.origin === null || !conflictingItems.has(getItem(store, o.origin))) {
left = o
conflictingItems.clear()
} }
} else { } else {
break o = /** @type {AbstractType<any>} */ (this.parent)._start
} }
o = o.right // TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set()
/**
* @type {Set<Item>}
*/
const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin
// ***{origin}bbbb{this}{c,b}{c,b}{o}***
// Note that conflictingItems is a subset of itemsBeforeOrigin
while (o !== null && o !== this.right) {
itemsBeforeOrigin.add(o)
conflictingItems.add(o)
if (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < this.id.client) {
left = o
conflictingItems.clear()
}
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) {
// case 2
if (o.origin === null || !conflictingItems.has(getItem(transaction.doc.store, o.origin))) {
left = o
conflictingItems.clear()
}
} else {
break
}
o = o.right
}
this.left = left
} }
this.left = left
// reconnect left/right + update parent map/start if necessary // reconnect left/right + update parent map/start if necessary
if (left !== null) { if (this.left !== null) {
const right = left.right const right = this.left.right
this.right = right this.right = right
left.right = this this.left.right = this
} else { } else {
let r let r
if (parentSub !== null) { if (this.parentSub !== null) {
r = parent._map.get(parentSub) || null r = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
while (r !== null && r.left !== null) { while (r !== null && r.left !== null) {
r = r.left r = r.left
} }
} else { } else {
r = parent._start r = /** @type {AbstractType<any>} */ (this.parent)._start
parent._start = this ;/** @type {AbstractType<any>} */ (this.parent)._start = this
} }
this.right = r this.right = r
} }
if (this.right !== null) { if (this.right !== null) {
this.right.left = this this.right.left = this
} else if (parentSub !== null) { } else if (this.parentSub !== null) {
// set as current parent value if right === null and this is parentSub // set as current parent value if right === null and this is parentSub
parent._map.set(parentSub, this) /** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
if (left !== null) { if (this.left !== null) {
// this is the current attribute value of parent. delete right // this is the current attribute value of parent. delete right
left.delete(transaction) this.left.delete(transaction)
} }
} }
// adjust length of parent // adjust length of parent
if (parentSub === null && this.countable && !this.deleted) { if (this.parentSub === null && this.countable && !this.deleted) {
parent._length += length /** @type {AbstractType<any>} */ (this.parent)._length += this.length
} }
addStruct(store, this) addStruct(transaction.doc.store, this)
this.content.integrate(transaction, this) this.content.integrate(transaction, this)
// add parent to transaction.changed // add parent to transaction.changed
addChangedTypeToTransaction(transaction, parent, parentSub) addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub)
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) { if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.right !== null && this.parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent // delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction) this.delete(transaction)
} }
@ -554,7 +577,7 @@ export class Item extends AbstractStruct {
if (this.countable && this.parentSub === null) { if (this.countable && this.parentSub === null) {
parent._length -= this.length parent._length -= this.length
} }
this.deleted = true this.markDeleted()
addToDeleteSet(transaction.deleteSet, this.id, this.length) addToDeleteSet(transaction.deleteSet, this.id, this.length)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
this.content.delete(transaction) this.content.delete(transaction)

View File

@ -205,29 +205,38 @@ export const testFormattingRemovedInMidText = tc => {
t.assert(Y.getTypeChildren(text0).length === 3) t.assert(Y.getTypeChildren(text0).length === 3)
} }
const tryGc = () => {
if (typeof global !== 'undefined' && global.gc) {
global.gc()
}
}
/** /**
* @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()
const text0 = doc1.getText('txt') const text0 = doc1.getText('txt')
t.measureTime(`time to insert ${itemsToInsert}`, () => { tryGc()
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
doc1.transact(() => { doc1.transact(() => {
for (let i = 0; i < itemsToInsert; i++) { for (let i = 0; i < itemsToInsert; i++) {
text0.insert(0, '0') text0.insert(0, '0')
} }
}) })
}) })
t.measureTime('time to encode', () => { tryGc()
t.measureTime('time to encode document', () => {
update = Y.encodeStateAsUpdate(doc1) update = Y.encodeStateAsUpdate(doc1)
}) })
})() })()
;(() => { ;(() => {
const doc2 = new Y.Doc() const doc2 = new Y.Doc()
t.measureTime('time to apply', () => { tryGc()
t.measureTime(`time to apply ${itemsToInsert} updates`, () => {
Y.applyUpdate(doc2, update) Y.applyUpdate(doc2, update)
}) })
})() })()