@vuer-ai/vuer-rtc-server 0.2.0 → 0.2.2
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/.env +1 -0
- package/S3_COMPRESSION_GUIDE.md +233 -0
- package/dist/archive/ArchivalService.d.ts +117 -0
- package/dist/archive/ArchivalService.d.ts.map +1 -0
- package/dist/archive/ArchivalService.js +181 -0
- package/dist/archive/ArchivalService.js.map +1 -0
- package/dist/broker/InMemoryBroker.d.ts +2 -0
- package/dist/broker/InMemoryBroker.d.ts.map +1 -1
- package/dist/broker/InMemoryBroker.js +4 -0
- package/dist/broker/InMemoryBroker.js.map +1 -1
- package/dist/compression/CompressionUtils.d.ts +57 -0
- package/dist/compression/CompressionUtils.d.ts.map +1 -0
- package/dist/compression/CompressionUtils.js +90 -0
- package/dist/compression/CompressionUtils.js.map +1 -0
- package/dist/compression/index.d.ts +7 -0
- package/dist/compression/index.d.ts.map +1 -0
- package/dist/compression/index.js +7 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/journal/CoalescingService.d.ts +63 -0
- package/dist/journal/CoalescingService.d.ts.map +1 -0
- package/dist/journal/CoalescingService.js +507 -0
- package/dist/journal/CoalescingService.js.map +1 -0
- package/dist/journal/JournalRLE.d.ts +81 -0
- package/dist/journal/JournalRLE.d.ts.map +1 -0
- package/dist/journal/JournalRLE.js +199 -0
- package/dist/journal/JournalRLE.js.map +1 -0
- package/dist/journal/JournalService.d.ts +7 -3
- package/dist/journal/JournalService.d.ts.map +1 -1
- package/dist/journal/JournalService.js +152 -12
- package/dist/journal/JournalService.js.map +1 -1
- package/dist/journal/RLECompression.d.ts +73 -0
- package/dist/journal/RLECompression.d.ts.map +1 -0
- package/dist/journal/RLECompression.js +152 -0
- package/dist/journal/RLECompression.js.map +1 -0
- package/dist/journal/rle-demo.d.ts +8 -0
- package/dist/journal/rle-demo.d.ts.map +1 -0
- package/dist/journal/rle-demo.js +159 -0
- package/dist/journal/rle-demo.js.map +1 -0
- package/dist/persistence/S3ColdStorage.d.ts +62 -0
- package/dist/persistence/S3ColdStorage.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorage.js +88 -0
- package/dist/persistence/S3ColdStorage.js.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts +78 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.js +93 -0
- package/dist/persistence/S3ColdStorageIntegration.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +623 -15
- package/dist/serve.js.map +1 -1
- package/docs/RLE_COMPRESSION.md +397 -0
- package/examples/compression-example.ts +259 -0
- package/package.json +14 -14
- package/src/archive/ArchivalService.ts +250 -0
- package/src/broker/InMemoryBroker.ts +5 -0
- package/src/compression/CompressionUtils.ts +113 -0
- package/src/compression/index.ts +14 -0
- package/src/journal/COALESCING.md +267 -0
- package/src/journal/CoalescingService.ts +626 -0
- package/src/journal/JournalRLE.ts +265 -0
- package/src/journal/JournalService.ts +163 -11
- package/src/journal/RLECompression.ts +210 -0
- package/src/journal/rle-demo.ts +193 -0
- package/src/serve.ts +702 -15
- package/tests/benchmark/journal-optimization-benchmark.test.ts +482 -0
- package/tests/compression/compression.test.ts +343 -0
- package/tests/integration/repositories.test.ts +89 -0
- package/tests/journal/compaction-load-bug.test.ts +409 -0
- package/tests/journal/compaction.test.ts +42 -2
- package/tests/journal/journal-rle.test.ts +511 -0
- package/tests/journal/lww-ordering-bug.test.ts +248 -0
- package/tests/journal/multi-session-coalescing.test.ts +871 -0
- package/tests/journal/rle-compression.test.ts +526 -0
- package/tests/journal/text-coalescing.test.ts +210 -0
- package/tests/unit/s3-compression.test.ts +257 -0
- package/PHASE1_SUMMARY.md +0 -94
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive Benchmark: Journal Storage Optimization
|
|
3
|
+
*
|
|
4
|
+
* This benchmark measures the before/after impact of:
|
|
5
|
+
* 1. Client-side coalescing (EditBuffer)
|
|
6
|
+
* 2. Server-side coalescing (during compaction)
|
|
7
|
+
* 3. RLE encoding (Run-Length Encoding)
|
|
8
|
+
* 4. Gzip compression (Cold storage)
|
|
9
|
+
*
|
|
10
|
+
* Metrics captured:
|
|
11
|
+
* - Journal size (bytes) for 100/500/1000/5000 ops
|
|
12
|
+
* - Time to sync new client (ms)
|
|
13
|
+
* - Compaction duration (ms)
|
|
14
|
+
* - Compression ratio at each stage
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from '@jest/globals';
|
|
18
|
+
import type { CRDTMessage } from '@vuer-ai/vuer-rtc';
|
|
19
|
+
import {
|
|
20
|
+
encodeJournalRLE,
|
|
21
|
+
decodeJournalRLE,
|
|
22
|
+
getCompressionStats as getRLEStats,
|
|
23
|
+
} from '../../src/journal/JournalRLE.js';
|
|
24
|
+
import {
|
|
25
|
+
compressSnapshot,
|
|
26
|
+
getCompressionStats as getGzipStats,
|
|
27
|
+
} from '../../src/compression/CompressionUtils.js';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helper: Create test messages
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function createMessage(
|
|
34
|
+
id: string,
|
|
35
|
+
sessionId: string,
|
|
36
|
+
lamportTime: number,
|
|
37
|
+
ops: any[] = []
|
|
38
|
+
): CRDTMessage {
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
sessionId,
|
|
42
|
+
clock: { [sessionId]: lamportTime },
|
|
43
|
+
lamportTime,
|
|
44
|
+
timestamp: Date.now() / 1000,
|
|
45
|
+
ops: ops.length > 0 ? ops : [{
|
|
46
|
+
key: 'cube-1',
|
|
47
|
+
otype: 'vector3.set',
|
|
48
|
+
path: 'position',
|
|
49
|
+
value: [lamportTime, 0, 0],
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Scenario Generator: Realistic editing patterns
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
interface ScenarioConfig {
|
|
59
|
+
name: string;
|
|
60
|
+
messageCount: number;
|
|
61
|
+
agentCount: number;
|
|
62
|
+
pattern: 'single-agent' | 'burst' | 'alternating' | 'collaborative';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generateScenario(config: ScenarioConfig): CRDTMessage[] {
|
|
66
|
+
const messages: CRDTMessage[] = [];
|
|
67
|
+
const { messageCount, agentCount, pattern } = config;
|
|
68
|
+
|
|
69
|
+
switch (pattern) {
|
|
70
|
+
case 'single-agent': {
|
|
71
|
+
// One agent doing all edits (best case for RLE)
|
|
72
|
+
for (let i = 0; i < messageCount; i++) {
|
|
73
|
+
messages.push(
|
|
74
|
+
createMessage(`msg-${i}`, 'session-1', i, [
|
|
75
|
+
{ key: 'doc', otype: 'text.insert', path: 'content', value: 'a' },
|
|
76
|
+
])
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'burst': {
|
|
83
|
+
// Multiple agents, each doing bursts of edits (realistic collaborative)
|
|
84
|
+
let lamport = 0;
|
|
85
|
+
const burstSize = Math.floor(messageCount / agentCount);
|
|
86
|
+
for (let agent = 0; agent < agentCount; agent++) {
|
|
87
|
+
for (let i = 0; i < burstSize; i++) {
|
|
88
|
+
messages.push(
|
|
89
|
+
createMessage(
|
|
90
|
+
`msg-${lamport}`,
|
|
91
|
+
`session-${agent}`,
|
|
92
|
+
lamport,
|
|
93
|
+
[{ key: 'doc', otype: 'text.insert', path: 'content', value: 'x' }]
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
lamport++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'alternating': {
|
|
103
|
+
// Worst case: different agent each message
|
|
104
|
+
for (let i = 0; i < messageCount; i++) {
|
|
105
|
+
const sessionId = `session-${i % agentCount}`;
|
|
106
|
+
messages.push(
|
|
107
|
+
createMessage(`msg-${i}`, sessionId, i, [
|
|
108
|
+
{ key: 'doc', otype: 'text.insert', path: 'content', value: 'y' },
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'collaborative': {
|
|
116
|
+
// Realistic: multiple agents with overlapping bursts
|
|
117
|
+
let lamport = 0;
|
|
118
|
+
const rounds = Math.floor(messageCount / (agentCount * 5));
|
|
119
|
+
for (let round = 0; round < rounds; round++) {
|
|
120
|
+
for (let agent = 0; agent < agentCount; agent++) {
|
|
121
|
+
for (let burst = 0; burst < 5; burst++) {
|
|
122
|
+
messages.push(
|
|
123
|
+
createMessage(
|
|
124
|
+
`msg-${lamport}`,
|
|
125
|
+
`session-${agent}`,
|
|
126
|
+
lamport,
|
|
127
|
+
[{ key: 'doc', otype: 'text.insert', path: 'content', value: 'z' }]
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
lamport++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return messages;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Benchmark Utilities
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
function getJsonSize(data: any): number {
|
|
146
|
+
return Buffer.byteLength(JSON.stringify(data), 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatBytes(bytes: number): string {
|
|
150
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
151
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
152
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatRatio(before: number, after: number): string {
|
|
156
|
+
const ratio = (after / before) * 100;
|
|
157
|
+
const reduction = ((before - after) / before) * 100;
|
|
158
|
+
return `${ratio.toFixed(1)}% of original (${reduction.toFixed(1)}% reduction)`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface BenchmarkResult {
|
|
162
|
+
scenario: string;
|
|
163
|
+
messageCount: number;
|
|
164
|
+
rawSize: number;
|
|
165
|
+
rleSize: number;
|
|
166
|
+
gzipSize: number;
|
|
167
|
+
rleRatio: number;
|
|
168
|
+
gzipRatio: number;
|
|
169
|
+
totalRatio: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function benchmarkScenario(config: ScenarioConfig): BenchmarkResult {
|
|
173
|
+
const messages = generateScenario(config);
|
|
174
|
+
|
|
175
|
+
// Measure raw size
|
|
176
|
+
const rawSize = getJsonSize(messages);
|
|
177
|
+
|
|
178
|
+
// Measure RLE size
|
|
179
|
+
const encoded = encodeJournalRLE(messages);
|
|
180
|
+
const rleSize = getJsonSize(encoded);
|
|
181
|
+
|
|
182
|
+
// Measure gzip size (on top of RLE)
|
|
183
|
+
const compressed = compressSnapshot(encoded);
|
|
184
|
+
const gzipSize = compressed.compressedSize;
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
scenario: config.name,
|
|
188
|
+
messageCount: messages.length,
|
|
189
|
+
rawSize,
|
|
190
|
+
rleSize,
|
|
191
|
+
gzipSize,
|
|
192
|
+
rleRatio: (rleSize / rawSize) * 100,
|
|
193
|
+
gzipRatio: (gzipSize / rleSize) * 100,
|
|
194
|
+
totalRatio: (gzipSize / rawSize) * 100,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Benchmark Tests
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
describe('Journal Storage Optimization Benchmark', () => {
|
|
203
|
+
describe('Storage Size Comparison', () => {
|
|
204
|
+
it('should measure compression for single-agent editing (100 ops)', () => {
|
|
205
|
+
const result = benchmarkScenario({
|
|
206
|
+
name: 'Single Agent - 100 ops',
|
|
207
|
+
messageCount: 100,
|
|
208
|
+
agentCount: 1,
|
|
209
|
+
pattern: 'single-agent',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log(`\n📊 ${result.scenario}`);
|
|
213
|
+
console.log(` Raw size: ${formatBytes(result.rawSize)}`);
|
|
214
|
+
console.log(` After RLE: ${formatBytes(result.rleSize)} (${formatRatio(result.rawSize, result.rleSize)})`);
|
|
215
|
+
console.log(` After RLE+Gzip: ${formatBytes(result.gzipSize)} (${formatRatio(result.rawSize, result.gzipSize)})`);
|
|
216
|
+
console.log(` Total reduction: ${((result.rawSize - result.gzipSize) / result.rawSize * 100).toFixed(1)}%`);
|
|
217
|
+
|
|
218
|
+
// Single agent should show good RLE compression
|
|
219
|
+
expect(result.totalRatio).toBeLessThan(30); // At least 70% total compression
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should measure compression for burst collaboration (500 ops)', () => {
|
|
223
|
+
const result = benchmarkScenario({
|
|
224
|
+
name: 'Burst Collaboration - 500 ops',
|
|
225
|
+
messageCount: 500,
|
|
226
|
+
agentCount: 5,
|
|
227
|
+
pattern: 'burst',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log(`\n📊 ${result.scenario}`);
|
|
231
|
+
console.log(` Raw size: ${formatBytes(result.rawSize)}`);
|
|
232
|
+
console.log(` After RLE: ${formatBytes(result.rleSize)} (${formatRatio(result.rawSize, result.rleSize)})`);
|
|
233
|
+
console.log(` After RLE+Gzip: ${formatBytes(result.gzipSize)} (${formatRatio(result.rawSize, result.gzipSize)})`);
|
|
234
|
+
console.log(` Total reduction: ${((result.rawSize - result.gzipSize) / result.rawSize * 100).toFixed(1)}%`);
|
|
235
|
+
|
|
236
|
+
// Burst pattern should still compress well
|
|
237
|
+
expect(result.totalRatio).toBeLessThan(35); // At least 65% total compression
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should measure compression for collaborative editing (1000 ops)', () => {
|
|
241
|
+
const result = benchmarkScenario({
|
|
242
|
+
name: 'Collaborative - 1000 ops',
|
|
243
|
+
messageCount: 1000,
|
|
244
|
+
agentCount: 10,
|
|
245
|
+
pattern: 'collaborative',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(`\n📊 ${result.scenario}`);
|
|
249
|
+
console.log(` Raw size: ${formatBytes(result.rawSize)}`);
|
|
250
|
+
console.log(` After RLE: ${formatBytes(result.rleSize)} (${formatRatio(result.rawSize, result.rleSize)})`);
|
|
251
|
+
console.log(` After RLE+Gzip: ${formatBytes(result.gzipSize)} (${formatRatio(result.rawSize, result.gzipSize)})`);
|
|
252
|
+
console.log(` Total reduction: ${((result.rawSize - result.gzipSize) / result.rawSize * 100).toFixed(1)}%`);
|
|
253
|
+
|
|
254
|
+
// Real-world collaborative should still show significant savings
|
|
255
|
+
expect(result.totalRatio).toBeLessThan(40); // At least 60% total compression
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should measure compression for large journal (5000 ops)', () => {
|
|
259
|
+
const result = benchmarkScenario({
|
|
260
|
+
name: 'Large Journal - 5000 ops',
|
|
261
|
+
messageCount: 5000,
|
|
262
|
+
agentCount: 20,
|
|
263
|
+
pattern: 'collaborative',
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
console.log(`\n📊 ${result.scenario}`);
|
|
267
|
+
console.log(` Raw size: ${formatBytes(result.rawSize)}`);
|
|
268
|
+
console.log(` After RLE: ${formatBytes(result.rleSize)} (${formatRatio(result.rawSize, result.rleSize)})`);
|
|
269
|
+
console.log(` After RLE+Gzip: ${formatBytes(result.gzipSize)} (${formatRatio(result.rawSize, result.gzipSize)})`);
|
|
270
|
+
console.log(` Total reduction: ${((result.rawSize - result.gzipSize) / result.rawSize * 100).toFixed(1)}%`);
|
|
271
|
+
|
|
272
|
+
// Large journals benefit from better gzip compression
|
|
273
|
+
expect(result.totalRatio).toBeLessThan(30); // At least 70% total compression
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should compare all patterns side-by-side', () => {
|
|
277
|
+
const scenarios: ScenarioConfig[] = [
|
|
278
|
+
{ name: 'Single Agent', messageCount: 1000, agentCount: 1, pattern: 'single-agent' },
|
|
279
|
+
{ name: 'Burst (5 agents)', messageCount: 1000, agentCount: 5, pattern: 'burst' },
|
|
280
|
+
{ name: 'Collaborative (10 agents)', messageCount: 1000, agentCount: 10, pattern: 'collaborative' },
|
|
281
|
+
{ name: 'Alternating (worst case)', messageCount: 1000, agentCount: 10, pattern: 'alternating' },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
console.log('\n📊 Compression Comparison Across Patterns (1000 ops each)\n');
|
|
285
|
+
console.log('Pattern | Raw | RLE | Gzip | Total Reduction');
|
|
286
|
+
console.log('--------------------------- | -------- | -------- | -------- | ---------------');
|
|
287
|
+
|
|
288
|
+
const results = scenarios.map(benchmarkScenario);
|
|
289
|
+
|
|
290
|
+
results.forEach((r) => {
|
|
291
|
+
const name = r.scenario.padEnd(27);
|
|
292
|
+
const raw = formatBytes(r.rawSize).padStart(8);
|
|
293
|
+
const rle = formatBytes(r.rleSize).padStart(8);
|
|
294
|
+
const gzip = formatBytes(r.gzipSize).padStart(8);
|
|
295
|
+
const reduction = `${((r.rawSize - r.gzipSize) / r.rawSize * 100).toFixed(1)}%`.padStart(15);
|
|
296
|
+
console.log(`${name} | ${raw} | ${rle} | ${gzip} | ${reduction}`);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// All patterns should show significant total compression
|
|
300
|
+
results.forEach((r) => {
|
|
301
|
+
expect(r.totalRatio).toBeLessThan(50); // At least 50% total compression
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Sync Time Comparison', () => {
|
|
307
|
+
it('should measure time to decode and replay journal (100 ops)', () => {
|
|
308
|
+
const messages = generateScenario({
|
|
309
|
+
name: 'Sync Test - 100 ops',
|
|
310
|
+
messageCount: 100,
|
|
311
|
+
agentCount: 5,
|
|
312
|
+
pattern: 'burst',
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Encode
|
|
316
|
+
const encoded = encodeJournalRLE(messages);
|
|
317
|
+
|
|
318
|
+
// Measure decode time
|
|
319
|
+
const startDecode = performance.now();
|
|
320
|
+
const decoded = decodeJournalRLE(encoded);
|
|
321
|
+
const decodeTime = performance.now() - startDecode;
|
|
322
|
+
|
|
323
|
+
console.log(`\n⏱️ Sync Time (100 ops)`);
|
|
324
|
+
console.log(` Decode time: ${decodeTime.toFixed(2)} ms`);
|
|
325
|
+
console.log(` Ops/sec: ${(100 / (decodeTime / 1000)).toFixed(0)}`);
|
|
326
|
+
|
|
327
|
+
expect(decoded.length).toBe(100);
|
|
328
|
+
expect(decodeTime).toBeLessThan(10); // Should be very fast
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should measure time to decode and replay journal (1000 ops)', () => {
|
|
332
|
+
const messages = generateScenario({
|
|
333
|
+
name: 'Sync Test - 1000 ops',
|
|
334
|
+
messageCount: 1000,
|
|
335
|
+
agentCount: 10,
|
|
336
|
+
pattern: 'collaborative',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const encoded = encodeJournalRLE(messages);
|
|
340
|
+
|
|
341
|
+
const startDecode = performance.now();
|
|
342
|
+
const decoded = decodeJournalRLE(encoded);
|
|
343
|
+
const decodeTime = performance.now() - startDecode;
|
|
344
|
+
|
|
345
|
+
console.log(`\n⏱️ Sync Time (1000 ops)`);
|
|
346
|
+
console.log(` Decode time: ${decodeTime.toFixed(2)} ms`);
|
|
347
|
+
console.log(` Ops/sec: ${(1000 / (decodeTime / 1000)).toFixed(0)}`);
|
|
348
|
+
|
|
349
|
+
expect(decoded.length).toBe(1000);
|
|
350
|
+
expect(decodeTime).toBeLessThan(20); // Still very fast
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should measure time to decode large journal (5000 ops)', () => {
|
|
354
|
+
const messages = generateScenario({
|
|
355
|
+
name: 'Sync Test - 5000 ops',
|
|
356
|
+
messageCount: 5000,
|
|
357
|
+
agentCount: 20,
|
|
358
|
+
pattern: 'collaborative',
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const encoded = encodeJournalRLE(messages);
|
|
362
|
+
|
|
363
|
+
const startDecode = performance.now();
|
|
364
|
+
const decoded = decodeJournalRLE(encoded);
|
|
365
|
+
const decodeTime = performance.now() - startDecode;
|
|
366
|
+
|
|
367
|
+
console.log(`\n⏱️ Sync Time (5000 ops)`);
|
|
368
|
+
console.log(` Decode time: ${decodeTime.toFixed(2)} ms`);
|
|
369
|
+
console.log(` Ops/sec: ${(5000 / (decodeTime / 1000)).toFixed(0)}`);
|
|
370
|
+
|
|
371
|
+
expect(decoded.length).toBe(5000);
|
|
372
|
+
expect(decodeTime).toBeLessThan(50); // Linear scaling
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('Compaction Speed Comparison', () => {
|
|
377
|
+
it('should measure RLE encoding time (100 ops)', () => {
|
|
378
|
+
const messages = generateScenario({
|
|
379
|
+
name: 'Compaction - 100 ops',
|
|
380
|
+
messageCount: 100,
|
|
381
|
+
agentCount: 5,
|
|
382
|
+
pattern: 'burst',
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const startEncode = performance.now();
|
|
386
|
+
const encoded = encodeJournalRLE(messages);
|
|
387
|
+
const encodeTime = performance.now() - startEncode;
|
|
388
|
+
|
|
389
|
+
console.log(`\n⚡ Compaction Speed (100 ops)`);
|
|
390
|
+
console.log(` RLE encode: ${encodeTime.toFixed(2)} ms`);
|
|
391
|
+
console.log(` Segments: ${encoded.segments.length}`);
|
|
392
|
+
|
|
393
|
+
expect(encodeTime).toBeLessThan(5);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should measure RLE encoding time (1000 ops)', () => {
|
|
397
|
+
const messages = generateScenario({
|
|
398
|
+
name: 'Compaction - 1000 ops',
|
|
399
|
+
messageCount: 1000,
|
|
400
|
+
agentCount: 10,
|
|
401
|
+
pattern: 'collaborative',
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const startEncode = performance.now();
|
|
405
|
+
const encoded = encodeJournalRLE(messages);
|
|
406
|
+
const encodeTime = performance.now() - startEncode;
|
|
407
|
+
|
|
408
|
+
console.log(`\n⚡ Compaction Speed (1000 ops)`);
|
|
409
|
+
console.log(` RLE encode: ${encodeTime.toFixed(2)} ms`);
|
|
410
|
+
console.log(` Segments: ${encoded.segments.length}`);
|
|
411
|
+
|
|
412
|
+
expect(encodeTime).toBeLessThan(15);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should measure full compression pipeline (1000 ops)', () => {
|
|
416
|
+
const messages = generateScenario({
|
|
417
|
+
name: 'Full Pipeline - 1000 ops',
|
|
418
|
+
messageCount: 1000,
|
|
419
|
+
agentCount: 10,
|
|
420
|
+
pattern: 'collaborative',
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// RLE encoding
|
|
424
|
+
const startRLE = performance.now();
|
|
425
|
+
const encoded = encodeJournalRLE(messages);
|
|
426
|
+
const rleTime = performance.now() - startRLE;
|
|
427
|
+
|
|
428
|
+
// Gzip compression
|
|
429
|
+
const startGzip = performance.now();
|
|
430
|
+
const compressed = compressSnapshot(encoded);
|
|
431
|
+
const gzipTime = performance.now() - startGzip;
|
|
432
|
+
|
|
433
|
+
const totalTime = rleTime + gzipTime;
|
|
434
|
+
|
|
435
|
+
console.log(`\n⚡ Full Compression Pipeline (1000 ops)`);
|
|
436
|
+
console.log(` RLE encode: ${rleTime.toFixed(2)} ms`);
|
|
437
|
+
console.log(` Gzip: ${gzipTime.toFixed(2)} ms`);
|
|
438
|
+
console.log(` Total: ${totalTime.toFixed(2)} ms`);
|
|
439
|
+
console.log(` Throughput: ${(1000 / (totalTime / 1000)).toFixed(0)} ops/sec`);
|
|
440
|
+
|
|
441
|
+
expect(totalTime).toBeLessThan(30);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('Summary Report', () => {
|
|
446
|
+
it('should generate comprehensive optimization summary', () => {
|
|
447
|
+
const testSizes = [100, 500, 1000, 5000];
|
|
448
|
+
|
|
449
|
+
console.log('\n' + '='.repeat(80));
|
|
450
|
+
console.log('📋 JOURNAL STORAGE OPTIMIZATION SUMMARY');
|
|
451
|
+
console.log('='.repeat(80));
|
|
452
|
+
console.log('\nOptimizations Applied:');
|
|
453
|
+
console.log(' 1. ✓ Client-side EditBuffer coalescing (verified separately)');
|
|
454
|
+
console.log(' 2. ✓ RLE encoding (run-length encoding by agent)');
|
|
455
|
+
console.log(' 3. ✓ Gzip compression (cold storage)');
|
|
456
|
+
console.log('\n' + '-'.repeat(80));
|
|
457
|
+
console.log('Size | Raw | RLE | Gzip | Total Savings');
|
|
458
|
+
console.log('-'.repeat(80));
|
|
459
|
+
|
|
460
|
+
testSizes.forEach((size) => {
|
|
461
|
+
const result = benchmarkScenario({
|
|
462
|
+
name: `${size} ops`,
|
|
463
|
+
messageCount: size,
|
|
464
|
+
agentCount: Math.min(10, Math.floor(size / 50)),
|
|
465
|
+
pattern: 'collaborative',
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const sizeStr = `${size}`.padStart(4);
|
|
469
|
+
const raw = formatBytes(result.rawSize).padStart(9);
|
|
470
|
+
const rle = formatBytes(result.rleSize).padStart(9);
|
|
471
|
+
const gzip = formatBytes(result.gzipSize).padStart(9);
|
|
472
|
+
const savings = `${((result.rawSize - result.gzipSize) / result.rawSize * 100).toFixed(1)}%`.padStart(13);
|
|
473
|
+
|
|
474
|
+
console.log(`${sizeStr} | ${raw} | ${rle} | ${gzip} | ${savings}`);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
console.log('-'.repeat(80));
|
|
478
|
+
console.log('\n✅ All optimizations are working as expected!');
|
|
479
|
+
console.log('='.repeat(80) + '\n');
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|