@wovin/core 0.0.0-ciao-mobx-955482e8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/applog/applog-helpers.d.ts +47 -0
- package/dist/applog/applog-helpers.d.ts.map +1 -0
- package/dist/applog/applog-utils.d.ts +57 -0
- package/dist/applog/applog-utils.d.ts.map +1 -0
- package/dist/applog/datom-types.d.ts +128 -0
- package/dist/applog/datom-types.d.ts.map +1 -0
- package/dist/applog.d.ts +4 -0
- package/dist/applog.d.ts.map +1 -0
- package/dist/applog.js +101 -0
- package/dist/applog.js.map +1 -0
- package/dist/blockstore/index.d.ts +21 -0
- package/dist/blockstore/index.d.ts.map +1 -0
- package/dist/blockstore.d.ts +2 -0
- package/dist/blockstore.d.ts.map +1 -0
- package/dist/blockstore.js +24 -0
- package/dist/blockstore.js.map +1 -0
- package/dist/chunk-6MQKRL6W.js +86 -0
- package/dist/chunk-6MQKRL6W.js.map +1 -0
- package/dist/chunk-7MW34UEO.js +40 -0
- package/dist/chunk-7MW34UEO.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-7Z5YDQKK.js.map +1 -0
- package/dist/chunk-CY4NLISM.js +144 -0
- package/dist/chunk-CY4NLISM.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/chunk-E46VTKTZ.js.map +1 -0
- package/dist/chunk-O43W7UW6.js +434 -0
- package/dist/chunk-O43W7UW6.js.map +1 -0
- package/dist/chunk-XIQSYEV3.js +1604 -0
- package/dist/chunk-XIQSYEV3.js.map +1 -0
- package/dist/chunk-XVGW4QC3.js +55 -0
- package/dist/chunk-XVGW4QC3.js.map +1 -0
- package/dist/chunk-YDAKBU6Q.js +9 -0
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/chunk-ZXCJRYD7.js +883 -0
- package/dist/chunk-ZXCJRYD7.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/ipfs/car.d.ts +59 -0
- package/dist/ipfs/car.d.ts.map +1 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
- package/dist/ipfs/ipfs-utils.d.ts +35 -0
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
- package/dist/ipfs.d.ts +4 -0
- package/dist/ipfs.d.ts.map +1 -0
- package/dist/ipfs.js +60 -0
- package/dist/ipfs.js.map +1 -0
- package/dist/ipns/ipns-record.d.ts +34 -0
- package/dist/ipns/ipns-record.d.ts.map +1 -0
- package/dist/ipns.d.ts +2 -0
- package/dist/ipns.d.ts.map +1 -0
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/connector.d.ts +9 -0
- package/dist/pubsub/connector.d.ts.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +14 -0
- package/dist/pubsub/pub-pull.d.ts.map +1 -0
- package/dist/pubsub/pubsub-types.d.ts +72 -0
- package/dist/pubsub/pubsub-types.d.ts.map +1 -0
- package/dist/pubsub/snap-push.d.ts +41 -0
- package/dist/pubsub/snap-push.d.ts.map +1 -0
- package/dist/pubsub/ucan-example.d.ts +3 -0
- package/dist/pubsub/ucan-example.d.ts.map +1 -0
- package/dist/pubsub/ucan.d.ts +16 -0
- package/dist/pubsub/ucan.d.ts.map +1 -0
- package/dist/pubsub.d.ts +5 -0
- package/dist/pubsub.d.ts.map +1 -0
- package/dist/pubsub.js +31 -0
- package/dist/pubsub.js.map +1 -0
- package/dist/query/basic.d.ts +105 -0
- package/dist/query/basic.d.ts.map +1 -0
- package/dist/query/divergences.d.ts +12 -0
- package/dist/query/divergences.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +4 -0
- package/dist/query/matchers.d.ts.map +1 -0
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/query-steps.d.ts +4 -0
- package/dist/query/query-steps.d.ts.map +1 -0
- package/dist/query/situations.d.ts +80 -0
- package/dist/query/situations.d.ts.map +1 -0
- package/dist/query/subscribable.d.ts +102 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +70 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +108 -0
- package/dist/query.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -0
- package/dist/retrieve/update-thread.d.ts +64 -0
- package/dist/retrieve/update-thread.d.ts.map +1 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +14 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/thread/basic.d.ts +60 -0
- package/dist/thread/basic.d.ts.map +1 -0
- package/dist/thread/filters.d.ts +47 -0
- package/dist/thread/filters.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +31 -0
- package/dist/thread/mapped.d.ts.map +1 -0
- package/dist/thread/utils.d.ts +23 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/writeable.d.ts +41 -0
- package/dist/thread/writeable.d.ts.map +1 -0
- package/dist/thread.d.ts +6 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +54 -0
- package/dist/thread.js.map +1 -0
- package/dist/types/typescript-utils.d.ts +34 -0
- package/dist/types/typescript-utils.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/package.json +110 -0
- package/src/applog/applog-helpers.ts +150 -0
- package/src/applog/applog-utils.ts +398 -0
- package/src/applog/datom-types.ts +148 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +277 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/basic.ts +1061 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/matchers.ts +8 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +536 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +225 -0
- package/src/query/types.ts +155 -0
- package/src/query.ts +7 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +175 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +234 -0
- package/src/thread/mapped.ts +166 -0
- package/src/thread/utils.ts +146 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +5 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1M applog stress test — static query + live query incremental update.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import type { Applog, ApplogForInsert } from '../../applog/datom-types.ts'
|
|
6
|
+
import { ThreadInMemory } from '../../thread/writeable.ts'
|
|
7
|
+
import { liveQuery, query } from '../../query/basic.ts'
|
|
8
|
+
|
|
9
|
+
const AGENT = 'stress-agent'
|
|
10
|
+
|
|
11
|
+
function generateLarge(entityCount: number): Applog[] {
|
|
12
|
+
const inputs: ApplogForInsert[] = []
|
|
13
|
+
const types = ['block', 'page', 'image', 'link', 'heading']
|
|
14
|
+
let ts = Date.now() - entityCount * 6
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < entityCount; i++) {
|
|
17
|
+
const en = `e${i}`
|
|
18
|
+
const nextTs = () => new Date(ts++).toISOString()
|
|
19
|
+
inputs.push({ en, at: 'entity/name', vl: `Entity ${i}`, ag: AGENT, ts: nextTs() } as any)
|
|
20
|
+
inputs.push({ en, at: 'entity/type', vl: types[i % types.length], ag: AGENT, ts: nextTs() } as any)
|
|
21
|
+
inputs.push({ en, at: 'entity/status', vl: i % 3 === 0 ? 'active' : 'draft', ag: AGENT, ts: nextTs() } as any)
|
|
22
|
+
if (i % 2 === 0) {
|
|
23
|
+
inputs.push({ en, at: 'entity/content', vl: `Content ${i}`, ag: AGENT, ts: nextTs() } as any)
|
|
24
|
+
}
|
|
25
|
+
if (i % 4 === 0) {
|
|
26
|
+
inputs.push({ en, at: 'relation/parent', vl: `e${Math.floor(i / 4)}`, ag: AGENT, ts: nextTs() } as any)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Already sorted by construction (ts is monotonically increasing)
|
|
31
|
+
return inputs.map(i => ({ pv: null, ...i })) as Applog[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('1M applog stress test', () => {
|
|
35
|
+
let db: ReturnType<typeof ThreadInMemory.fromArray>
|
|
36
|
+
|
|
37
|
+
it('generate ~1M applogs + load', () => {
|
|
38
|
+
const start = performance.now()
|
|
39
|
+
const dataset = generateLarge(200_000)
|
|
40
|
+
const genTime = performance.now() - start
|
|
41
|
+
|
|
42
|
+
const start2 = performance.now()
|
|
43
|
+
db = ThreadInMemory.fromArray(dataset, 'stress-1m')
|
|
44
|
+
const loadTime = performance.now() - start2
|
|
45
|
+
|
|
46
|
+
console.log(`\n [1M] Generated ${dataset.length.toLocaleString()} applogs in ${genTime.toFixed(0)}ms`)
|
|
47
|
+
console.log(` [1M] ThreadInMemory.fromArray: ${loadTime.toFixed(0)}ms`)
|
|
48
|
+
console.log(` [1M] Total thread size: ${db.size.toLocaleString()}`)
|
|
49
|
+
expect(db.size).toBeGreaterThan(700_000)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('static query() — single step on ~750K applogs', () => {
|
|
53
|
+
const start = performance.now()
|
|
54
|
+
const result = query(db, [{ at: 'entity/type', vl: 'block' }])
|
|
55
|
+
const elapsed = performance.now() - start
|
|
56
|
+
|
|
57
|
+
console.log(` [1M] query() single-step: ${elapsed.toFixed(1)}ms → ${result.nodes.length.toLocaleString()} results`)
|
|
58
|
+
expect(result.nodes.length).toBe(40_000) // 200K / 5 types
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('liveQuery() — setup + single insert propagation on ~750K applogs', () => {
|
|
62
|
+
// Setup
|
|
63
|
+
const setupStart = performance.now()
|
|
64
|
+
const live = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
|
|
65
|
+
const setupTime = performance.now() - setupStart
|
|
66
|
+
const initialCount = live.nodes.length
|
|
67
|
+
|
|
68
|
+
console.log(` [1M] liveQuery() setup: ${setupTime.toFixed(1)}ms → ${initialCount.toLocaleString()} initial results`)
|
|
69
|
+
|
|
70
|
+
// Insert one matching applog and measure propagation
|
|
71
|
+
let eventTime: number | null = null
|
|
72
|
+
live.subscribe(() => {
|
|
73
|
+
eventTime = performance.now()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const insertStart = performance.now()
|
|
77
|
+
db.insert([{
|
|
78
|
+
en: 'new-entity-1m',
|
|
79
|
+
at: 'entity/type',
|
|
80
|
+
vl: 'block',
|
|
81
|
+
ag: AGENT,
|
|
82
|
+
}])
|
|
83
|
+
const totalTime = performance.now() - insertStart
|
|
84
|
+
const subscribeDelta = eventTime ? eventTime - insertStart : null
|
|
85
|
+
|
|
86
|
+
const newCount = live.nodes.length
|
|
87
|
+
console.log(` [1M] Insert→result updated: ${totalTime.toFixed(3)}ms`)
|
|
88
|
+
console.log(` [1M] Insert→subscribe fired: ${subscribeDelta?.toFixed(3) ?? 'N/A'}ms`)
|
|
89
|
+
console.log(` [1M] Nodes: ${initialCount.toLocaleString()} → ${newCount.toLocaleString()}`)
|
|
90
|
+
|
|
91
|
+
expect(newCount).toBe(initialCount + 1)
|
|
92
|
+
live.dispose()
|
|
93
|
+
})
|
|
94
|
+
}, { timeout: 120_000 })
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance benchmarks for the query engine.
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx vitest run src/query/query.perf.test.ts
|
|
5
|
+
*
|
|
6
|
+
* Purpose: compare performance between MobX-based (main) and push-based (ciao-mobx)
|
|
7
|
+
* implementations. Each benchmark uses performance.now() and reports median/p95
|
|
8
|
+
* across multiple iterations.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest'
|
|
11
|
+
import { sortApplogsByTs } from '../../applog/applog-utils.ts'
|
|
12
|
+
import type { Applog, ApplogForInsert } from '../../applog/datom-types.ts'
|
|
13
|
+
import { ThreadInMemory } from '../../thread/writeable.ts'
|
|
14
|
+
import { rollingFilter } from '../../thread/filters.ts'
|
|
15
|
+
import { liveQuery, query, lastWriteWins, withoutDeleted } from '../../query/basic.ts'
|
|
16
|
+
|
|
17
|
+
// ─── Benchmark Helpers ──────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function benchmarkSync(name: string, fn: () => void, iterations = 100): { median: number; p95: number; min: number } {
|
|
20
|
+
// Warmup
|
|
21
|
+
for (let i = 0; i < 3; i++) fn()
|
|
22
|
+
|
|
23
|
+
const times: number[] = []
|
|
24
|
+
for (let i = 0; i < iterations; i++) {
|
|
25
|
+
const start = performance.now()
|
|
26
|
+
fn()
|
|
27
|
+
times.push(performance.now() - start)
|
|
28
|
+
}
|
|
29
|
+
times.sort((a, b) => a - b)
|
|
30
|
+
const median = times[Math.floor(times.length / 2)]
|
|
31
|
+
const p95 = times[Math.floor(times.length * 0.95)]
|
|
32
|
+
const min = times[0]
|
|
33
|
+
console.log(` [PERF] ${name}: median=${median.toFixed(3)}ms p95=${p95.toFixed(3)}ms min=${min.toFixed(3)}ms (${iterations} runs)`)
|
|
34
|
+
return { median, p95, min }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Data Generation ────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
const ENTITY_COUNT = 500
|
|
40
|
+
const TYPES = ['block', 'page', 'image', 'link', 'heading', 'list', 'table']
|
|
41
|
+
const STATUSES = ['draft', 'published', 'archived', 'deleted']
|
|
42
|
+
const AGENT = 'perf-agent'
|
|
43
|
+
|
|
44
|
+
function generateDataset(entityCount: number): Applog[] {
|
|
45
|
+
const inputs: ApplogForInsert[] = []
|
|
46
|
+
let tsCounter = Date.now() - entityCount * 10 // stagger timestamps
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < entityCount; i++) {
|
|
49
|
+
const en = `e${i}`
|
|
50
|
+
const ts = new Date(tsCounter).toISOString()
|
|
51
|
+
tsCounter += 5
|
|
52
|
+
|
|
53
|
+
// entity/name
|
|
54
|
+
inputs.push({ en, at: 'entity/name', vl: `Entity ${i}`, ag: AGENT })
|
|
55
|
+
// entity/type
|
|
56
|
+
inputs.push({ en, at: 'entity/type', vl: TYPES[i % TYPES.length], ag: AGENT })
|
|
57
|
+
// entity/status
|
|
58
|
+
inputs.push({ en, at: 'entity/status', vl: STATUSES[i % STATUSES.length], ag: AGENT })
|
|
59
|
+
|
|
60
|
+
// relation/parent — ~80% of entities have a parent
|
|
61
|
+
if (i > 0 && i % 5 !== 0) {
|
|
62
|
+
const parentIdx = Math.floor(i / 5) * 5 // parent is the "section head"
|
|
63
|
+
inputs.push({ en, at: 'relation/parent', vl: `e${parentIdx}`, ag: AGENT })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Some entities get extra attributes to increase log count
|
|
67
|
+
if (i % 3 === 0) {
|
|
68
|
+
inputs.push({ en, at: 'entity/content', vl: `Content for entity ${i} with some text to simulate real data`, ag: AGENT })
|
|
69
|
+
}
|
|
70
|
+
if (i % 7 === 0) {
|
|
71
|
+
inputs.push({ en, at: 'entity/created', vl: new Date(tsCounter - 1000).toISOString(), ag: AGENT })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add some deletions (~5%)
|
|
76
|
+
for (let i = 0; i < entityCount; i++) {
|
|
77
|
+
if (i % 20 === 0) {
|
|
78
|
+
inputs.push({ en: `e${i}`, at: 'isDeleted', vl: true, ag: AGENT })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add duplicate-attribute writes (for lastWriteWins testing) — ~10% of entities get a second status
|
|
83
|
+
for (let i = 0; i < entityCount; i++) {
|
|
84
|
+
if (i % 10 === 0) {
|
|
85
|
+
inputs.push({ en: `e${i}`, at: 'entity/status', vl: 'updated', ag: AGENT })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const logs = inputs.map(input => ({
|
|
90
|
+
ts: new Date(tsCounter++).toISOString(),
|
|
91
|
+
pv: null,
|
|
92
|
+
ag: input.ag || AGENT,
|
|
93
|
+
...input,
|
|
94
|
+
})) as Applog[]
|
|
95
|
+
sortApplogsByTs(logs)
|
|
96
|
+
return logs
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Datasets ───────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const dataset500 = generateDataset(500)
|
|
102
|
+
const dataset2000 = generateDataset(2000)
|
|
103
|
+
|
|
104
|
+
console.log(`\n[PERF] Dataset sizes: 500-entity=${dataset500.length} applogs, 2000-entity=${dataset2000.length} applogs\n`)
|
|
105
|
+
|
|
106
|
+
// ═════════════════════════════════════════════════════════════════
|
|
107
|
+
// BENCHMARKS
|
|
108
|
+
// ═════════════════════════════════════════════════════════════════
|
|
109
|
+
|
|
110
|
+
describe('query() performance', () => {
|
|
111
|
+
it('single-step: find all entities by type', () => {
|
|
112
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-single-step')
|
|
113
|
+
const result = benchmarkSync('query single-step (2K entities)', () => {
|
|
114
|
+
query(db, [{ at: 'entity/type', vl: 'block' }])
|
|
115
|
+
}, 200)
|
|
116
|
+
// Sanity: should find ~1/7 of entities
|
|
117
|
+
const check = query(db, [{ at: 'entity/type', vl: 'block' }])
|
|
118
|
+
expect(check.nodes.length).toBeGreaterThan(100)
|
|
119
|
+
expect(result.median).toBeLessThan(500) // generous upper bound
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('multi-step: two-step variable binding', () => {
|
|
123
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-multi-step')
|
|
124
|
+
const result = benchmarkSync('query multi-step (2K entities)', () => {
|
|
125
|
+
query(db, [
|
|
126
|
+
{ en: '?id', at: 'entity/type', vl: 'block' },
|
|
127
|
+
{ en: '?id', at: 'entity/status', vl: '?status' },
|
|
128
|
+
])
|
|
129
|
+
}, 100)
|
|
130
|
+
const check = query(db, [
|
|
131
|
+
{ en: '?id', at: 'entity/type', vl: 'block' },
|
|
132
|
+
{ en: '?id', at: 'entity/status', vl: '?status' },
|
|
133
|
+
])
|
|
134
|
+
expect(check.records.length).toBeGreaterThan(100)
|
|
135
|
+
expect(result.median).toBeLessThan(1000)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('three-step: entity -> parent -> parent name', () => {
|
|
139
|
+
const db = ThreadInMemory.fromArray([...dataset500], 'perf-three-step')
|
|
140
|
+
const result = benchmarkSync('query three-step (500 entities)', () => {
|
|
141
|
+
query(db, [
|
|
142
|
+
{ en: '?childId', at: 'entity/type', vl: 'block' },
|
|
143
|
+
{ en: '?childId', at: 'relation/parent', vl: '?parentId' },
|
|
144
|
+
{ en: '?parentId', at: 'entity/name', vl: '?parentName' },
|
|
145
|
+
])
|
|
146
|
+
}, 50)
|
|
147
|
+
expect(result.median).toBeLessThan(2000)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('memoization: same query 1000x', () => {
|
|
151
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-memo')
|
|
152
|
+
// First call populates cache
|
|
153
|
+
query(db, [{ at: 'entity/type', vl: 'block' }])
|
|
154
|
+
|
|
155
|
+
const result = benchmarkSync('query memoized cache hit (2K entities)', () => {
|
|
156
|
+
query(db, [{ at: 'entity/type', vl: 'block' }])
|
|
157
|
+
}, 1000)
|
|
158
|
+
// Cache hits should be sub-millisecond
|
|
159
|
+
expect(result.median).toBeLessThan(1)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('liveQuery() performance', () => {
|
|
164
|
+
it('setup cost: create live query and get initial results', () => {
|
|
165
|
+
const result = benchmarkSync('liveQuery setup (2K entities)', () => {
|
|
166
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-setup')
|
|
167
|
+
const lq = liveQuery(db, [{ en: '?id', at: 'entity/type', vl: '?type' }])
|
|
168
|
+
expect(lq.nodes.length).toBeGreaterThan(0)
|
|
169
|
+
lq.dispose()
|
|
170
|
+
}, 50)
|
|
171
|
+
expect(result.median).toBeLessThan(2000)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('incremental update: insert 1 applog into live query', () => {
|
|
175
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-incr')
|
|
176
|
+
const lq = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
|
|
177
|
+
const initialCount = lq.nodes.length
|
|
178
|
+
|
|
179
|
+
let insertCounter = 0
|
|
180
|
+
const result = benchmarkSync('liveQuery incremental insert (2K entities)', () => {
|
|
181
|
+
db.insert([{
|
|
182
|
+
en: `new-entity-${insertCounter++}`,
|
|
183
|
+
at: 'entity/type',
|
|
184
|
+
vl: 'block',
|
|
185
|
+
ag: AGENT,
|
|
186
|
+
}])
|
|
187
|
+
}, 200)
|
|
188
|
+
|
|
189
|
+
expect(lq.nodes.length).toBeGreaterThan(initialCount)
|
|
190
|
+
lq.dispose()
|
|
191
|
+
// Incremental should be fast
|
|
192
|
+
expect(result.median).toBeLessThan(50)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('incremental update: insert into multi-step live query', () => {
|
|
196
|
+
const db = ThreadInMemory.fromArray([...dataset500], 'perf-live-multi-incr')
|
|
197
|
+
const lq = liveQuery(db, [
|
|
198
|
+
{ en: '?id', at: 'entity/type', vl: 'block' },
|
|
199
|
+
{ en: '?id', at: 'entity/status', vl: '?status' },
|
|
200
|
+
])
|
|
201
|
+
const initialCount = lq.nodes.length
|
|
202
|
+
|
|
203
|
+
let insertCounter = 0
|
|
204
|
+
const result = benchmarkSync('liveQuery multi-step incremental (500 entities)', () => {
|
|
205
|
+
const en = `new-multi-${insertCounter++}`
|
|
206
|
+
db.insert([
|
|
207
|
+
{ en, at: 'entity/type', vl: 'block', ag: AGENT },
|
|
208
|
+
{ en, at: 'entity/status', vl: 'draft', ag: AGENT },
|
|
209
|
+
])
|
|
210
|
+
}, 100)
|
|
211
|
+
|
|
212
|
+
expect(lq.nodes.length).toBeGreaterThan(initialCount)
|
|
213
|
+
lq.dispose()
|
|
214
|
+
expect(result.median).toBeLessThan(100)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('subscribe event delivery latency', () => {
|
|
218
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-live-subscribe')
|
|
219
|
+
const lq = liveQuery(db, [{ at: 'entity/type', vl: 'block' }])
|
|
220
|
+
const times: number[] = []
|
|
221
|
+
|
|
222
|
+
lq.subscribe(() => {
|
|
223
|
+
times.push(performance.now())
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
let insertCounter = 0
|
|
227
|
+
for (let i = 0; i < 100; i++) {
|
|
228
|
+
const start = performance.now()
|
|
229
|
+
db.insert([{
|
|
230
|
+
en: `sub-entity-${insertCounter++}`,
|
|
231
|
+
at: 'entity/type',
|
|
232
|
+
vl: 'block',
|
|
233
|
+
ag: AGENT,
|
|
234
|
+
}])
|
|
235
|
+
// times array is populated synchronously by the subscribe callback
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
expect(times).toHaveLength(100)
|
|
239
|
+
lq.dispose()
|
|
240
|
+
console.log(` [PERF] liveQuery subscribe: 100 events delivered synchronously`)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
describe('withoutDeleted() performance', () => {
|
|
245
|
+
it('initial filtering on large thread', () => {
|
|
246
|
+
const result = benchmarkSync('withoutDeleted initial (2K entities)', () => {
|
|
247
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-wod')
|
|
248
|
+
const filtered = withoutDeleted(db)
|
|
249
|
+
expect(filtered.applogs.length).toBeLessThan(dataset2000.length)
|
|
250
|
+
}, 50)
|
|
251
|
+
expect(result.median).toBeLessThan(1000)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('incremental: insert deletion into large thread', () => {
|
|
255
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-wod-incr')
|
|
256
|
+
const filtered = withoutDeleted(db)
|
|
257
|
+
const initialCount = filtered.applogs.length
|
|
258
|
+
|
|
259
|
+
let deleteCounter = 0
|
|
260
|
+
const result = benchmarkSync('withoutDeleted incremental delete (2K entities)', () => {
|
|
261
|
+
db.insert([{
|
|
262
|
+
en: `e${100 + deleteCounter++}`, // delete existing entities
|
|
263
|
+
at: 'isDeleted',
|
|
264
|
+
vl: true,
|
|
265
|
+
ag: AGENT,
|
|
266
|
+
}])
|
|
267
|
+
}, 100)
|
|
268
|
+
|
|
269
|
+
expect(filtered.applogs.length).toBeLessThanOrEqual(initialCount)
|
|
270
|
+
expect(result.median).toBeLessThan(50)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('lastWriteWins() performance', () => {
|
|
275
|
+
it('initial deduplication on large thread', () => {
|
|
276
|
+
const result = benchmarkSync('lastWriteWins initial (2K entities)', () => {
|
|
277
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-lww')
|
|
278
|
+
const lww = lastWriteWins(db)
|
|
279
|
+
expect(lww.applogs.length).toBeLessThan(dataset2000.length)
|
|
280
|
+
}, 50)
|
|
281
|
+
expect(result.median).toBeLessThan(1000)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('incremental: insert overwriting attribute', () => {
|
|
285
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-lww-incr')
|
|
286
|
+
const lww = lastWriteWins(db)
|
|
287
|
+
const initialCount = lww.applogs.length
|
|
288
|
+
|
|
289
|
+
let updateCounter = 0
|
|
290
|
+
const result = benchmarkSync('lastWriteWins incremental update (2K entities)', () => {
|
|
291
|
+
db.insert([{
|
|
292
|
+
en: `e${updateCounter++ % 500}`,
|
|
293
|
+
at: 'entity/status',
|
|
294
|
+
vl: `updated-${updateCounter}`,
|
|
295
|
+
ag: AGENT,
|
|
296
|
+
}])
|
|
297
|
+
}, 200)
|
|
298
|
+
|
|
299
|
+
// Count should stay roughly the same (overwrites, not additions)
|
|
300
|
+
expect(lww.applogs.length).toBeLessThanOrEqual(initialCount + 200) // some might be new en+at combos
|
|
301
|
+
expect(result.median).toBeLessThan(50)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe('rollingFilter() performance', () => {
|
|
306
|
+
it('initial filter on large thread', () => {
|
|
307
|
+
const result = benchmarkSync('rollingFilter initial (2K entities)', () => {
|
|
308
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf')
|
|
309
|
+
const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
|
|
310
|
+
expect(filtered.applogs.length).toBeGreaterThan(0)
|
|
311
|
+
}, 50)
|
|
312
|
+
expect(result.median).toBeLessThan(500)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('incremental: insert matching applog', () => {
|
|
316
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf-incr')
|
|
317
|
+
const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
|
|
318
|
+
const initialCount = filtered.applogs.length
|
|
319
|
+
|
|
320
|
+
let insertCounter = 0
|
|
321
|
+
const result = benchmarkSync('rollingFilter incremental insert (2K entities)', () => {
|
|
322
|
+
db.insert([{
|
|
323
|
+
en: `rf-entity-${insertCounter++}`,
|
|
324
|
+
at: 'entity/type',
|
|
325
|
+
vl: 'block',
|
|
326
|
+
ag: AGENT,
|
|
327
|
+
}])
|
|
328
|
+
}, 200)
|
|
329
|
+
|
|
330
|
+
expect(filtered.applogs.length).toBeGreaterThan(initialCount)
|
|
331
|
+
expect(result.median).toBeLessThan(50)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('incremental: insert non-matching applog (should be fast no-op)', () => {
|
|
335
|
+
const db = ThreadInMemory.fromArray([...dataset2000], 'perf-rf-nomatch')
|
|
336
|
+
const filtered = rollingFilter(db, { at: 'entity/type', vl: 'block' })
|
|
337
|
+
const initialCount = filtered.applogs.length
|
|
338
|
+
|
|
339
|
+
let insertCounter = 0
|
|
340
|
+
const result = benchmarkSync('rollingFilter non-matching insert (2K entities)', () => {
|
|
341
|
+
db.insert([{
|
|
342
|
+
en: `rf-nomatch-${insertCounter++}`,
|
|
343
|
+
at: 'entity/type',
|
|
344
|
+
vl: 'image',
|
|
345
|
+
ag: AGENT,
|
|
346
|
+
}])
|
|
347
|
+
}, 200)
|
|
348
|
+
|
|
349
|
+
expect(filtered.applogs.length).toBe(initialCount)
|
|
350
|
+
expect(result.median).toBeLessThan(50)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('combined pipeline performance', () => {
|
|
355
|
+
it('full pipeline: withoutDeleted -> lastWriteWins -> query', () => {
|
|
356
|
+
const result = benchmarkSync('full pipeline (500 entities)', () => {
|
|
357
|
+
const db = ThreadInMemory.fromArray([...dataset500], 'perf-pipeline')
|
|
358
|
+
const noDeleted = withoutDeleted(db)
|
|
359
|
+
const lww = lastWriteWins(noDeleted)
|
|
360
|
+
const qr = query(lww, [
|
|
361
|
+
{ en: '?id', at: 'entity/type', vl: 'block' },
|
|
362
|
+
{ en: '?id', at: 'entity/name', vl: '?name' },
|
|
363
|
+
])
|
|
364
|
+
expect(qr.records.length).toBeGreaterThan(0)
|
|
365
|
+
}, 30)
|
|
366
|
+
expect(result.median).toBeLessThan(3000)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('full pipeline incremental: insert into active pipeline', () => {
|
|
370
|
+
const db = ThreadInMemory.fromArray([...dataset500], 'perf-pipeline-incr')
|
|
371
|
+
const noDeleted = withoutDeleted(db)
|
|
372
|
+
const lww = lastWriteWins(noDeleted)
|
|
373
|
+
const lq = liveQuery(lww, [{ en: '?id', at: 'entity/type', vl: 'block' }])
|
|
374
|
+
const initialCount = lq.nodes.length
|
|
375
|
+
|
|
376
|
+
let insertCounter = 0
|
|
377
|
+
const result = benchmarkSync('full pipeline incremental (500 entities)', () => {
|
|
378
|
+
db.insert([
|
|
379
|
+
{ en: `pipe-${insertCounter}`, at: 'entity/type', vl: 'block', ag: AGENT },
|
|
380
|
+
{ en: `pipe-${insertCounter}`, at: 'entity/name', vl: `New ${insertCounter}`, ag: AGENT },
|
|
381
|
+
])
|
|
382
|
+
insertCounter++
|
|
383
|
+
}, 100)
|
|
384
|
+
|
|
385
|
+
expect(lq.nodes.length).toBeGreaterThan(initialCount)
|
|
386
|
+
lq.dispose()
|
|
387
|
+
expect(result.median).toBeLessThan(100)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-data performance test — uses exported applogs from a real note3 thread (47K applogs).
|
|
3
|
+
* Tests real query patterns from note3's reactive.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { readFileSync } from 'fs'
|
|
7
|
+
import type { Applog } from '../../applog/datom-types.ts'
|
|
8
|
+
import { sortApplogsByTs } from '../../applog/applog-utils.ts'
|
|
9
|
+
import { ThreadInMemory } from '../../thread/writeable.ts'
|
|
10
|
+
import { liveQuery, liveQueryNot, query, queryNot, queryAndMap, lastWriteWins, withoutDeleted } from '../../query/basic.ts'
|
|
11
|
+
|
|
12
|
+
// ─── Load real data ─────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const lines = readFileSync('/repo/tmp/real-applogs.jsonl', 'utf-8').trim().split('\n')
|
|
15
|
+
const raw = lines.map(l => JSON.parse(l)) as Applog[]
|
|
16
|
+
sortApplogsByTs(raw)
|
|
17
|
+
console.log(`\n [REAL] Loaded ${raw.length.toLocaleString()} real applogs from note3 thread (sorted)`)
|
|
18
|
+
|
|
19
|
+
// Check what attributes exist
|
|
20
|
+
const attrCounts = new Map<string, number>()
|
|
21
|
+
for (const log of raw) {
|
|
22
|
+
attrCounts.set(log.at, (attrCounts.get(log.at) || 0) + 1)
|
|
23
|
+
}
|
|
24
|
+
const top = [...attrCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
|
|
25
|
+
console.log(` [REAL] Top attributes:`, top.map(([at, n]) => `${at}(${n})`).join(', '))
|
|
26
|
+
|
|
27
|
+
describe('real note3 data — query performance', () => {
|
|
28
|
+
let db: ThreadInMemory
|
|
29
|
+
let lww: ReturnType<typeof lastWriteWins>
|
|
30
|
+
let clean: ReturnType<typeof withoutDeleted>
|
|
31
|
+
|
|
32
|
+
it('setup: load + lastWriteWins + withoutDeleted', () => {
|
|
33
|
+
const t0 = performance.now()
|
|
34
|
+
db = ThreadInMemory.fromArray([...raw], 'real-note3')
|
|
35
|
+
const loadTime = performance.now() - t0
|
|
36
|
+
|
|
37
|
+
const t1 = performance.now()
|
|
38
|
+
lww = lastWriteWins(db)
|
|
39
|
+
const lwwTime = performance.now() - t1
|
|
40
|
+
|
|
41
|
+
const t2 = performance.now()
|
|
42
|
+
clean = withoutDeleted(lww)
|
|
43
|
+
const wodTime = performance.now() - t2
|
|
44
|
+
|
|
45
|
+
console.log(` [REAL] Load: ${loadTime.toFixed(1)}ms (${db.size.toLocaleString()} applogs)`)
|
|
46
|
+
console.log(` [REAL] lastWriteWins: ${lwwTime.toFixed(1)}ms (${db.size.toLocaleString()} → ${lww.size.toLocaleString()})`)
|
|
47
|
+
console.log(` [REAL] withoutDeleted: ${wodTime.toFixed(1)}ms (${lww.size.toLocaleString()} → ${clean.size.toLocaleString()})`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// ── Real query 1: all blocks ───────────────────────────────
|
|
51
|
+
it('all blocks with content (1-step)', () => {
|
|
52
|
+
const t0 = performance.now()
|
|
53
|
+
const blocks = query(clean, [
|
|
54
|
+
{ en: '?blockID', at: 'block/content' },
|
|
55
|
+
])
|
|
56
|
+
const elapsed = performance.now() - t0
|
|
57
|
+
console.log(` [REAL] all blocks: ${elapsed.toFixed(3)}ms → ${blocks.size} blocks`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ── Real query 2: useParents (2-step) ──────────────────────
|
|
61
|
+
it('useParents (2-step): find parent of a block', () => {
|
|
62
|
+
// Find a block that actually has a parent
|
|
63
|
+
const relations = query(clean, [
|
|
64
|
+
{ en: '?relID', at: 'relation/block', vl: '?blockID' },
|
|
65
|
+
{ en: '?relID', at: 'relation/childOf', vl: '?parentID' },
|
|
66
|
+
])
|
|
67
|
+
console.log(` [REAL] total relations: ${relations.size}`)
|
|
68
|
+
|
|
69
|
+
if (relations.size > 0) {
|
|
70
|
+
const blockID = relations.records[0].blockID
|
|
71
|
+
const t0 = performance.now()
|
|
72
|
+
const parents = queryAndMap(clean, [
|
|
73
|
+
{ en: '?relID', at: 'relation/block', vl: blockID },
|
|
74
|
+
{ en: '?relID', at: 'relation/childOf', vl: '?parentID' },
|
|
75
|
+
], 'parentID')
|
|
76
|
+
const elapsed = performance.now() - t0
|
|
77
|
+
console.log(` [REAL] useParents (block=${blockID}): ${elapsed.toFixed(3)}ms → ${(parents as any[]).length} parents`)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// ── Real query 3: useRoots (1-step + queryNot) ─────────────
|
|
82
|
+
it('useRoots: all blocks, then queryNot(has parent)', () => {
|
|
83
|
+
const t0 = performance.now()
|
|
84
|
+
const blocks = query(clean, [
|
|
85
|
+
{ en: '?blockID', at: 'block/content' },
|
|
86
|
+
])
|
|
87
|
+
const queryTime = performance.now() - t0
|
|
88
|
+
|
|
89
|
+
const t1 = performance.now()
|
|
90
|
+
// Single-step suffices: relation entities are deleted when unparenting,
|
|
91
|
+
// so any relation/block pointing to blockID means it has a parent.
|
|
92
|
+
const roots = queryNot(clean, blocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
|
|
93
|
+
const notTime = performance.now() - t1
|
|
94
|
+
|
|
95
|
+
console.log(` [REAL] useRoots — query blocks: ${queryTime.toFixed(3)}ms → ${blocks.size} blocks`)
|
|
96
|
+
console.log(` [REAL] useRoots — queryNot(parent): ${notTime.toFixed(3)}ms → ${roots.size} roots (of ${blocks.size} blocks)`)
|
|
97
|
+
|
|
98
|
+
// Debug: if roots == 0 or roots == blocks, investigate
|
|
99
|
+
if (roots.size === 0 || roots.size === blocks.size) {
|
|
100
|
+
// Check how many blocks actually have relations
|
|
101
|
+
const blockIDs = blocks.records.map(r => r.blockID) as string[]
|
|
102
|
+
const relBlocks = query(clean, [
|
|
103
|
+
{ en: '?relID', at: 'relation/block', vl: blockIDs.slice(0, 5) },
|
|
104
|
+
])
|
|
105
|
+
console.log(` [REAL] DEBUG: first 5 blocks have ${relBlocks.size} relations`)
|
|
106
|
+
|
|
107
|
+
// Check what relation/childOf values exist
|
|
108
|
+
const childOfs = query(clean, [{ at: 'relation/childOf' }])
|
|
109
|
+
console.log(` [REAL] DEBUG: total relation/childOf applogs: ${childOfs.size}`)
|
|
110
|
+
if (childOfs.size > 0) {
|
|
111
|
+
const sample = childOfs.nodes.slice(0, 3).map(n => `en=${n.logsOfThisNode.applogs[0]?.en} vl=${n.logsOfThisNode.applogs[0]?.vl}`)
|
|
112
|
+
console.log(` [REAL] DEBUG: sample childOf:`, sample)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ── Real query 4: 3-step query ────────────────────────────
|
|
118
|
+
it('3-step: block → relation → parent name', () => {
|
|
119
|
+
const t0 = performance.now()
|
|
120
|
+
const result = query(clean, [
|
|
121
|
+
{ en: '?blockID', at: 'block/content', vl: '?content' },
|
|
122
|
+
{ en: '?relID', at: 'relation/block', vl: '?blockID' },
|
|
123
|
+
{ en: '?relID', at: 'relation/childOf', vl: '?parentID' },
|
|
124
|
+
])
|
|
125
|
+
const elapsed = performance.now() - t0
|
|
126
|
+
console.log(` [REAL] 3-step (block→relation→parent): ${elapsed.toFixed(3)}ms → ${result.size} results`)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ── Live: useRoots reactive + insert ──────────────────────
|
|
130
|
+
it('liveQuery useRoots + insert', () => {
|
|
131
|
+
const t0 = performance.now()
|
|
132
|
+
const liveBlocks = liveQuery(clean, [
|
|
133
|
+
{ en: '?blockID', at: 'block/content' },
|
|
134
|
+
])
|
|
135
|
+
const setupTime = performance.now() - t0
|
|
136
|
+
const initialBlocks = liveBlocks.size
|
|
137
|
+
|
|
138
|
+
const t1 = performance.now()
|
|
139
|
+
// Single-step: relation entities are deleted on unparent (see useRoots comment above)
|
|
140
|
+
const liveRoots = liveQueryNot(clean, liveBlocks, { en: '?relID', at: 'relation/block', vl: '?blockID' })
|
|
141
|
+
const notSetupTime = performance.now() - t1
|
|
142
|
+
const initialRoots = liveRoots.size
|
|
143
|
+
|
|
144
|
+
console.log(` [REAL] liveQuery blocks setup: ${setupTime.toFixed(1)}ms → ${initialBlocks} blocks`)
|
|
145
|
+
console.log(` [REAL] liveQueryNot roots setup: ${notSetupTime.toFixed(1)}ms → ${initialRoots} roots`)
|
|
146
|
+
|
|
147
|
+
// Insert a new block
|
|
148
|
+
const t2 = performance.now()
|
|
149
|
+
db.insert([{ en: 'new-block-perf', at: 'block/content', vl: 'Perf test block', ag: 'perf-test' }])
|
|
150
|
+
const insertBlockTime = performance.now() - t2
|
|
151
|
+
|
|
152
|
+
console.log(` [REAL] Insert block: ${insertBlockTime.toFixed(3)}ms`)
|
|
153
|
+
console.log(` [REAL] Blocks: ${initialBlocks} → ${liveBlocks.size}`)
|
|
154
|
+
console.log(` [REAL] Roots: ${initialRoots} → ${liveRoots.size}`)
|
|
155
|
+
|
|
156
|
+
// Insert a relation for the new block (should remove from roots)
|
|
157
|
+
// Note: vl must be a real parent ID (string), not null.
|
|
158
|
+
// queryNot treats each pattern independently — step 1 ({relation/block, vl: ?blockID})
|
|
159
|
+
// does the actual exclusion. vl:null in step 2 is a no-op (matches only literal null).
|
|
160
|
+
const rootsBefore = liveRoots.size
|
|
161
|
+
const t3 = performance.now()
|
|
162
|
+
db.insert([
|
|
163
|
+
{ en: 'rel-perf-1', at: 'relation/block', vl: 'new-block-perf', ag: 'perf-test' },
|
|
164
|
+
{ en: 'rel-perf-1', at: 'relation/childOf', vl: 'some-parent-id', ag: 'perf-test' },
|
|
165
|
+
])
|
|
166
|
+
const insertRelTime = performance.now() - t3
|
|
167
|
+
|
|
168
|
+
console.log(` [REAL] Insert parent relation: ${insertRelTime.toFixed(3)}ms`)
|
|
169
|
+
console.log(` [REAL] Roots: ${rootsBefore} → ${liveRoots.size}`)
|
|
170
|
+
expect(liveRoots.size).toBe(rootsBefore - 1)
|
|
171
|
+
|
|
172
|
+
liveRoots.dispose()
|
|
173
|
+
liveBlocks.dispose()
|
|
174
|
+
})
|
|
175
|
+
}, { timeout: 60_000 })
|