Files
yjs/tests/IdMap.tests.js

238 lines
8.1 KiB
JavaScript
Raw Normal View History

2025-04-12 14:44:37 +02:00
import * as t from 'lib0/testing'
2025-04-19 15:21:14 +02:00
import * as idmap from '../src/utils/IdMap.js'
2025-04-30 22:12:09 +02:00
import * as prng from 'lib0/prng'
import * as math from 'lib0/math'
import { compareIdmaps as compareIdMaps, createIdMap, ID, createRandomIdSet, createRandomIdMap, createAttributionItem } from './testHelper.js'
import * as YY from '../src/internals.js'
2025-11-17 14:55:35 +01:00
import * as time from 'lib0/time'
2025-04-12 14:44:37 +02:00
/**
* @template T
* @param {Array<[number, number, number, Array<T>]>} ops
*/
const simpleConstructAttrs = ops => {
const attrs = createIdMap()
2025-04-12 14:44:37 +02:00
ops.forEach(op => {
attrs.add(op[0], op[1], op[2], op[3].map(v => createAttributionItem('', v)))
2025-04-12 14:44:37 +02:00
})
return attrs
}
/**
* @param {t.TestCase} _tc
*/
export const testAmMerge = _tc => {
const attrs = [42]
t.group('filter out empty items (1))', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 0, attrs]]),
simpleConstructAttrs([])
)
})
t.group('filter out empty items (2))', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 0, attrs], [0, 2, 0, attrs]]),
simpleConstructAttrs([])
)
})
t.group('filter out empty items (3 - end))', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 1, attrs], [0, 2, 0, attrs]]),
simpleConstructAttrs([[0, 1, 1, attrs]])
)
})
t.group('filter out empty items (4 - middle))', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 1, attrs], [0, 2, 0, attrs], [0, 3, 1, attrs]]),
simpleConstructAttrs([[0, 1, 1, attrs], [0, 3, 1, attrs]])
)
})
t.group('filter out empty items (5 - beginning))', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 0, attrs], [0, 2, 1, attrs], [0, 3, 1, attrs]]),
simpleConstructAttrs([[0, 2, 1, attrs], [0, 3, 1, attrs]])
)
})
t.group('merge of overlapping id ranges', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 2, attrs], [0, 0, 2, attrs]]),
simpleConstructAttrs([[0, 0, 3, attrs]])
)
})
t.group('construct without hole', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 2, attrs], [0, 3, 1, attrs]]),
simpleConstructAttrs([[0, 1, 3, attrs]])
)
})
t.group('no merge of overlapping id ranges with different attributes', () => {
2025-04-30 22:12:09 +02:00
compareIdMaps(
2025-04-12 14:44:37 +02:00
simpleConstructAttrs([[0, 1, 2, [1]], [0, 0, 2, [2]]]),
2025-04-12 16:12:00 +02:00
simpleConstructAttrs([[0, 0, 1, [2]], [0, 1, 1, [1, 2]], [0, 2, 1, [1]]])
2025-04-12 14:44:37 +02:00
)
})
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatMergingMultipleIdMaps = tc => {
2025-04-12 14:44:37 +02:00
const clients = 4
2025-04-12 16:12:00 +02:00
const clockRange = 5
2025-04-12 14:44:37 +02:00
/**
2025-04-19 15:21:14 +02:00
* @type {Array<idmap.IdMap<number>>}
2025-04-12 14:44:37 +02:00
*/
const sets = []
for (let i = 0; i < 3; i++) {
sets.push(createRandomIdMap(tc.prng, clients, clockRange, [1, 2, 3]))
2025-04-12 14:44:37 +02:00
}
2025-04-19 15:21:14 +02:00
const merged = idmap.mergeIdMaps(sets)
const mergedReverse = idmap.mergeIdMaps(sets.reverse())
2025-04-30 22:12:09 +02:00
compareIdMaps(merged, mergedReverse)
2025-04-19 15:21:14 +02:00
const composed = idmap.createIdMap()
2025-04-12 14:44:37 +02:00
for (let iclient = 0; iclient < clients; iclient++) {
for (let iclock = 0; iclock < clockRange + 42; iclock++) {
2025-04-30 22:12:09 +02:00
const mergedHas = merged.hasId(new ID(iclient, iclock))
const oneHas = sets.some(ids => ids.hasId(new ID(iclient, iclock)))
2025-04-12 14:44:37 +02:00
t.assert(mergedHas === oneHas)
2025-05-09 20:34:18 +02:00
const mergedAttrs = merged.sliceId(new ID(iclient, iclock), 1)
2025-04-21 01:13:41 +02:00
mergedAttrs.forEach(a => {
if (a.attrs != null) {
composed.add(iclient, a.clock, a.len, a.attrs)
}
})
2025-04-12 14:44:37 +02:00
}
}
2025-04-30 22:12:09 +02:00
compareIdMaps(merged, composed)
2025-04-12 14:44:37 +02:00
}
2025-04-12 17:20:21 +02:00
/**
* @param {t.TestCase} tc
*/
export const testRepeatRandomDiffing = tc => {
const clients = 4
const clockRange = 100
const attrs = [1, 2, 3]
2025-04-19 15:33:09 +02:00
const idset1 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
const idset2 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
const merged = idmap.mergeIdMaps([idset1, idset2])
const e1 = idmap.diffIdMap(idset1, idset2)
const e2 = idmap.diffIdMap(merged, idset2)
2025-04-30 22:12:09 +02:00
compareIdMaps(e1, e2)
const copy = YY.decodeIdMap(YY.encodeIdMap(e1))
2025-04-30 22:12:09 +02:00
compareIdMaps(e1, copy)
2025-04-12 17:20:21 +02:00
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatRandomDiffing2 = tc => {
const clients = 4
const clockRange = 100
const attrs = [1, 2, 3]
2025-04-19 15:21:14 +02:00
const idmap1 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
const idmap2 = createRandomIdMap(tc.prng, clients, clockRange, attrs)
2025-04-12 17:20:21 +02:00
const idsExclude = createRandomIdSet(tc.prng, clients, clockRange)
2025-04-19 15:21:14 +02:00
const merged = idmap.mergeIdMaps([idmap1, idmap2])
const mergedExcluded = idmap.diffIdMap(merged, idsExclude)
const e1 = idmap.diffIdMap(idmap1, idsExclude)
const e2 = idmap.diffIdMap(idmap2, idsExclude)
const excludedMerged = idmap.mergeIdMaps([e1, e2])
2025-04-30 22:12:09 +02:00
compareIdMaps(mergedExcluded, excludedMerged)
const copy = YY.decodeIdMap(YY.encodeIdMap(mergedExcluded))
2025-04-30 22:12:09 +02:00
compareIdMaps(mergedExcluded, copy)
}
/**
* @param {t.TestCase} tc
*/
export const testRepeatRandomDeletes = tc => {
const clients = 1
const clockRange = 100
const idset = createRandomIdMap(tc.prng, clients, clockRange, [])
const client = Array.from(idset.clients.keys())[0]
const clock = prng.int31(tc.prng, 0, clockRange)
const len = prng.int31(tc.prng, 0, math.round((clockRange - clock) * 1.2)) // allow exceeding range to cover more edge cases
const idsetOfDeletes = idmap.createIdMap()
idsetOfDeletes.add(client, clock, len, [])
const diffed = idmap.diffIdMap(idset, idsetOfDeletes)
idset.delete(client, clock, len)
for (let i = 0; i < len; i++) {
t.assert(!idset.has(client, clock + i))
}
compareIdMaps(idset, diffed)
2025-04-12 17:20:21 +02:00
}
2025-05-09 20:34:18 +02:00
/**
* @param {t.TestCase} tc
*/
2025-11-17 14:55:35 +01:00
export const testRepeatRandomIntersects = tc => {
2025-05-09 20:34:18 +02:00
const clients = 4
const clockRange = 100
const ids1 = createRandomIdMap(tc.prng, clients, clockRange, [1])
const ids2 = createRandomIdMap(tc.prng, clients, clockRange, ['two'])
const intersected = idmap.intersectMaps(ids1, ids2)
for (let client = 0; client < clients; client++) {
for (let clock = 0; clock < clockRange; clock++) {
t.assert((ids1.has(client, clock) && ids2.has(client, clock)) === intersected.has(client, clock))
/**
* @type {Array<any>?}
*/
const slice1 = ids1.slice(client, clock, 1)[0].attrs
/**
* @type {Array<any>?}
*/
const slice2 = ids2.slice(client, clock, 1)[0].attrs
/**
* @type {Array<any>?}
*/
const expectedAttrs = (slice1 != null && slice2 != null) ? slice1.concat(slice2) : null
const attrs = intersected.slice(client, clock, 1)[0].attrs
t.assert(attrs?.length === expectedAttrs?.length)
}
}
const diffed1 = idmap.diffIdMap(ids1, ids2)
const altDiffed1 = idmap.diffIdMap(ids1, intersected)
compareIdMaps(diffed1, altDiffed1)
}
2025-11-17 14:55:35 +01:00
/**
* @param {t.TestCase} tc
*/
export const testUserAttributionEncodingBenchmark = tc => {
/**
* @todo debug why this approach needs 30 bytes per item
* @todo it should be possible to only use a single idmap and, in each attr entry, encode the diff
* to the previous entries (e.g. remove a,b, insert c,d)
*/
2025-11-17 23:59:01 +01:00
const attributions = createIdMap()
2025-11-17 14:55:35 +01:00
let currentTime = time.getUnixTime()
const ydoc = new YY.Doc()
ydoc.on('afterTransaction', tr => {
idmap.insertIntoIdMap(attributions, idmap.createIdMapFromIdSet(tr.insertSet, [createAttributionItem('insert', 'userX'), createAttributionItem('insertAt', currentTime)]))
idmap.insertIntoIdMap(attributions, idmap.createIdMapFromIdSet(tr.deleteSet, [createAttributionItem('delete', 'userX'), createAttributionItem('deleteAt', currentTime)]))
currentTime += 1
})
const ytext = ydoc.getText()
const N = 10000
2025-11-17 23:59:01 +01:00
t.measureTime(`time to attribute ${N / 1000}k changes`, () => {
2025-11-17 14:55:35 +01:00
for (let i = 0; i < N; i++) {
if (i % 2 > 0 && ytext.length > 0) {
const pos = prng.int31(tc.prng, 0, ytext.length)
const delLen = prng.int31(tc.prng, 0, ytext.length - pos)
ytext.delete(pos, delLen)
} else {
ytext.insert(prng.int31(tc.prng, 0, ytext.length), prng.word(tc.prng))
}
}
})
2025-11-17 23:59:01 +01:00
t.measureTime('time to encode attributions map', () => {
2025-11-17 14:55:35 +01:00
/**
* @todo I can optimize size by encoding only the differences to the prev item.
*/
const encAttributions = idmap.encodeIdMap(attributions)
t.info('encoded size: ' + encAttributions.byteLength)
2025-11-17 23:59:01 +01:00
t.info('size per change: ' + math.floor((encAttributions.byteLength / N) * 100) / 100 + ' bytes')
2025-11-17 14:55:35 +01:00
})
}