teyyare 0.2.8 → 0.2.9
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/README.md +97 -57
- package/bin/teyyare.js +3 -3
- package/package.json +1 -1
- package/src/receiver/pipeline.js +1 -1
- package/src/sender/pipeline.js +47 -6
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Teyyare is the first real integration workload for both projects, proving that a
|
|
|
26
26
|
replication / transport
|
|
27
27
|
│
|
|
28
28
|
▼
|
|
29
|
-
TCP
|
|
29
|
+
TCP (Multi-Channel)
|
|
30
30
|
│
|
|
31
31
|
▼
|
|
32
32
|
Remote Teyyare
|
|
@@ -37,7 +37,7 @@ Teyyare is the first real integration workload for both projects, proving that a
|
|
|
37
37
|
flush
|
|
38
38
|
│
|
|
39
39
|
▼
|
|
40
|
-
Disk
|
|
40
|
+
Disk (writev + pre-allocation)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
---
|
|
@@ -52,9 +52,9 @@ Teyyare is the first real integration workload for both projects, proving that a
|
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
##
|
|
55
|
+
## Architecture & Systems Optimizations
|
|
56
56
|
|
|
57
|
-
### Sender Hot Path
|
|
57
|
+
### 1. Sender Hot Path
|
|
58
58
|
```text
|
|
59
59
|
FileHandle.read()
|
|
60
60
|
│
|
|
@@ -76,29 +76,46 @@ MutableGeneration
|
|
|
76
76
|
FrozenGeneration [metaBuf, payloadArena]
|
|
77
77
|
│
|
|
78
78
|
▼
|
|
79
|
-
Raptiye.submitBatch() (
|
|
79
|
+
Raptiye.submitBatch() (In-Flight Sliding Window up to 128 MB)
|
|
80
80
|
│
|
|
81
81
|
▼
|
|
82
|
-
TCP (
|
|
82
|
+
Multi-Channel TCP Pool (Zero-Allocation Round-Robin sendv)
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
### Receiver Hot Path
|
|
85
|
+
### 2. Receiver Hot Path
|
|
86
86
|
```text
|
|
87
|
-
TCP
|
|
87
|
+
Multi-Channel TCP
|
|
88
88
|
│
|
|
89
89
|
▼
|
|
90
|
-
|
|
90
|
+
Zero-Copy Pointer Framer (O(1) cursor, no shift(), scratch header reuse)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
Raptiye (Frame decode + verification)
|
|
91
94
|
│
|
|
92
95
|
▼
|
|
93
96
|
FrozenGeneration (O(1) zero-copy view via FrozenArenaStore.fromBuffers)
|
|
94
97
|
│
|
|
95
98
|
▼
|
|
96
|
-
|
|
99
|
+
Chunk Coalescing Engine (Merges contiguous chunk runs)
|
|
100
|
+
│
|
|
101
|
+
▼
|
|
102
|
+
FileHandle.writev(buffers, startOffset) (Direct native scatter-gather writes)
|
|
97
103
|
│
|
|
98
104
|
▼
|
|
99
|
-
|
|
105
|
+
Debounced Asynchronous State Persistence (.teyyare-state)
|
|
100
106
|
```
|
|
101
107
|
|
|
108
|
+
### 3. Key Systems Optimizations Implemented
|
|
109
|
+
|
|
110
|
+
- **Disk Pre-Allocation (`FileHandle.truncate(targetSize)`)**: Zero-fragmentation disk pre-allocation upon receiving `FILE_BEGIN`, eliminating filesystem dynamic block allocation latency during high-speed writes.
|
|
111
|
+
- **Native Scatter-Gather Writes (`writev`)**: Receiver coalesces contiguous chunks arriving in the same generation batch into a single system call via `writev`, reducing write syscalls by ~87.5%.
|
|
112
|
+
- **Debounced Asynchronous State Saving**: `.teyyare-state` persistence is debounced to disk (every 250ms or 32 chunks) and drained cleanly before final file rename, completely eliminating blocking JSON serialization from the hot-path.
|
|
113
|
+
- **In-Flight ACK Pipelining (Sliding Window)**: Supports up to 32 concurrent batches and 128 MB in-flight capacity. Senders stream generations continuously across 4–8 parallel TCP channels without stalling on single-batch ACKs.
|
|
114
|
+
- **Pointer-Based Zero-Copy TCP Framer**: Eliminated $O(N)$ array `shift()` operations and repeated `Buffer.allocUnsafe` calls on the TCP stream reader using an $O(1)$ cursor and amortized array compaction.
|
|
115
|
+
- **Zero-Allocation Channel Selection**: Eliminated `pool.filter` garbage collector churn in `TCPTransport.prototype.sendv` and `send` using an in-place circular lookup.
|
|
116
|
+
- **Dynamic WAN Tuning**: `PULL_REQUEST` carries client-specified `--chunk-size` (e.g. 2 MB) and `--batch-size` (e.g. 16 MB) to saturate high-latency, high-bandwidth WAN connections.
|
|
117
|
+
- **Multi-Channel Out-of-Order Packet Resilience**: Resilient state reconciliation for `TRANSFER_BEGIN`, `FILE_BEGIN`, `FILE_END`, and `TRANSFER_END` packets arriving out-of-order across parallel TCP sockets.
|
|
118
|
+
|
|
102
119
|
---
|
|
103
120
|
|
|
104
121
|
## CLI Usage
|
|
@@ -109,72 +126,95 @@ FileHandle.write(payload, 0, length, offset) (Direct random-access write)
|
|
|
109
126
|
teyyare serve --port 7421 --dir /destination/folder
|
|
110
127
|
```
|
|
111
128
|
|
|
112
|
-
### Send File or Directory
|
|
129
|
+
### Send File or Directory (Push)
|
|
113
130
|
```bash
|
|
114
|
-
# Single file transfer
|
|
115
|
-
teyyare send ./large-file.bin 127.0.0.1:7421:/tmp/large-file.bin
|
|
131
|
+
# Single file transfer with 4 parallel channels
|
|
132
|
+
teyyare send ./large-file.bin 127.0.0.1:7421:/tmp/large-file.bin -c 4
|
|
116
133
|
|
|
117
|
-
# Directory transfer with nested
|
|
118
|
-
teyyare send ./dist server.local:7421:/var/www/app
|
|
134
|
+
# Directory transfer with nested hierarchy
|
|
135
|
+
teyyare send ./dist server.local:7421:/var/www/app -c 4
|
|
119
136
|
```
|
|
120
137
|
|
|
121
|
-
###
|
|
122
|
-
```
|
|
123
|
-
|
|
138
|
+
### Pull File or Directory (Download)
|
|
139
|
+
```bash
|
|
140
|
+
# Single file download from remote server with 8 channels and 2MB WAN chunks
|
|
141
|
+
teyyare pull 38.242.216.102:7421:/root/cdn/app.zip ./ -c 8 --chunk-size 2M --batch-size 16M
|
|
142
|
+
|
|
143
|
+
# Directory pull
|
|
144
|
+
teyyare pull server:7421:/backup/db.tar.zst ./ -c 4
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### CLI Options
|
|
148
|
+
|
|
149
|
+
| Flag | Description | Default |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| `-c, --channels <n>` | Number of parallel TCP socket channels | `4` (remote), `1` (local) |
|
|
152
|
+
| `--chunk-size <size>` | Chunk size, e.g. `512K`, `1M`, `2M`, `4M` | `2M` (remote), `1M` (local) |
|
|
153
|
+
| `--batch-size <size>` | Generation batch size, e.g. `8M`, `16M` | `8x chunk size` |
|
|
154
|
+
| `--port <port>` | Server port number | `7421` |
|
|
155
|
+
| `--dir <path>` | Root directory for `serve` command | Current directory (`./`) |
|
|
156
|
+
|
|
157
|
+
---
|
|
124
158
|
|
|
125
|
-
|
|
159
|
+
## Progress Display
|
|
126
160
|
|
|
127
|
-
|
|
128
|
-
|
|
161
|
+
```text
|
|
162
|
+
Teyyare [Download]
|
|
163
|
+
|
|
164
|
+
app.zip
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
166
|
+
333.54 MB / 333.54 MB
|
|
167
|
+
100.0%
|
|
132
168
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
network speed 324.5 MB/s
|
|
170
|
+
disk speed 1240.2 MB/s
|
|
171
|
+
channels 8
|
|
172
|
+
ETA --:--
|
|
137
173
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
chunks 167 / 167
|
|
175
|
+
generation 21
|
|
176
|
+
verified CRC32 167
|
|
177
|
+
resume hits 0
|
|
141
178
|
```
|
|
142
179
|
|
|
143
180
|
---
|
|
144
181
|
|
|
145
|
-
##
|
|
182
|
+
## Test Suite & Softscope Profiling
|
|
146
183
|
|
|
147
|
-
|
|
184
|
+
### 1. Unit & Integration Tests
|
|
185
|
+
Run all 24 automated tests covering protocol encoding, generation ownership, backpressure, chaos recovery, and multi-channel transfers:
|
|
148
186
|
```bash
|
|
149
|
-
npm
|
|
187
|
+
npm test
|
|
150
188
|
```
|
|
151
189
|
|
|
152
|
-
###
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
| **C** | fs -> Raptiye -> fs | **85.5 MB/s** | 585ms |
|
|
158
|
-
| **D** | Teyyare (Muttafa + Raptiye + fs) | **38.0 MB/s** | 1320ms |
|
|
190
|
+
### 2. End-to-End (E2E) Test Suite
|
|
191
|
+
Runs 4 comprehensive end-to-end scenarios (Single Upload, Directory Upload, Single Pull, 50% Resumed Transfer):
|
|
192
|
+
```bash
|
|
193
|
+
npm run e2e
|
|
194
|
+
```
|
|
159
195
|
|
|
160
|
-
###
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
196
|
+
### 3. Softscope Profiling
|
|
197
|
+
Profile CPU bottlenecks, hot call paths, and memory allocations using [`softscope`](https://github.com/litepacks/softscope):
|
|
198
|
+
```bash
|
|
199
|
+
# Markdown summary to stdout
|
|
200
|
+
npm run e2e:md
|
|
201
|
+
|
|
202
|
+
# Full interactive terminal dashboard (TUI)
|
|
203
|
+
npm run e2e:profile
|
|
204
|
+
|
|
205
|
+
# Live metrics during execution
|
|
206
|
+
npm run e2e:live
|
|
207
|
+
```
|
|
168
208
|
|
|
169
209
|
---
|
|
170
210
|
|
|
171
|
-
##
|
|
211
|
+
## Benchmark & Throughput Results
|
|
172
212
|
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
npm test
|
|
176
|
-
```
|
|
213
|
+
### End-to-End Suite Summary (`test/e2e/e2e.js`)
|
|
177
214
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
215
|
+
| Scenario | Transfer Size | Duration | Throughput | Verification |
|
|
216
|
+
|---|---|---|---|---|
|
|
217
|
+
| **Single File Upload** (4 channels) | 32.0 MB | 121ms | **264.5 MB/s** | SHA-256 Match |
|
|
218
|
+
| **Directory Upload** (Nested tree) | 16.0 MB | 104ms | **153.9 MB/s** | All files verified |
|
|
219
|
+
| **Single File Pull** (4 channels) | 32.0 MB | 89ms | **359.6 MB/s** | SHA-256 Match |
|
|
220
|
+
| **Resumed Transfer** (50% skipped) | 8.0 MB | 49ms | **163.3 MB/s** | 0 retransmitted bytes |
|
package/bin/teyyare.js
CHANGED
|
@@ -61,13 +61,13 @@ async function main() {
|
|
|
61
61
|
const args = process.argv.slice(2);
|
|
62
62
|
const command = args[0];
|
|
63
63
|
|
|
64
|
-
if (!command ||
|
|
64
|
+
if (!command || args.includes('--help') || args.includes('-h')) {
|
|
65
65
|
printUsage();
|
|
66
66
|
process.exit(0);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
70
|
-
console.log('0.2.
|
|
70
|
+
console.log('0.2.8');
|
|
71
71
|
process.exit(0);
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -203,7 +203,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
|
203
203
|
`);
|
|
204
204
|
process.exit(0);
|
|
205
205
|
} else if (command === 'status') {
|
|
206
|
-
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.
|
|
206
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.8 status: ready');
|
|
207
207
|
} else {
|
|
208
208
|
console.error(`Unknown command: ${command}`);
|
|
209
209
|
printUsage();
|
package/package.json
CHANGED
package/src/receiver/pipeline.js
CHANGED
|
@@ -421,7 +421,7 @@ export class ReceiverPipeline {
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
// 2. Sort by offset to form maximal contiguous runs
|
|
424
|
-
toWrite.sort((a, b) =>
|
|
424
|
+
toWrite.sort((a, b) => (a.meta.offset < b.meta.offset ? -1 : (a.meta.offset > b.meta.offset ? 1 : 0)));
|
|
425
425
|
|
|
426
426
|
let currentRun = [toWrite[0]];
|
|
427
427
|
let currentStartOffset = Number(toWrite[0].meta.offset);
|
package/src/sender/pipeline.js
CHANGED
|
@@ -20,6 +20,25 @@ import {
|
|
|
20
20
|
import { extractGenerationBuffers } from '../core/generation.js';
|
|
21
21
|
import { FileResumeState } from '../resume/state.js';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Reusable bump allocator that returns its ArrayBuffer to a pool upon reset/release.
|
|
25
|
+
*/
|
|
26
|
+
class PooledBumpAllocator extends BumpAllocator {
|
|
27
|
+
constructor(capacity, pool = null) {
|
|
28
|
+
super(capacity);
|
|
29
|
+
this._pool = pool;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
reset() {
|
|
33
|
+
super.reset();
|
|
34
|
+
if (this._pool && this._pool.length < 16 && !this._pool.includes(this)) {
|
|
35
|
+
this._pool.push(this);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultAllocatorPool = [];
|
|
41
|
+
|
|
23
42
|
class RaptiyeSink extends Sink {
|
|
24
43
|
/**
|
|
25
44
|
* @param {import('raptiye').ReplicationPipeline} replication
|
|
@@ -109,6 +128,8 @@ export class SenderPipeline {
|
|
|
109
128
|
this.metrics = metrics;
|
|
110
129
|
this.progress = progress;
|
|
111
130
|
this.knownResumeState = knownResumeState;
|
|
131
|
+
this._allocatorPool = options.allocatorPool || defaultAllocatorPool;
|
|
132
|
+
this._readScratch = null;
|
|
112
133
|
|
|
113
134
|
this.transferId = crypto.randomUUID();
|
|
114
135
|
|
|
@@ -119,10 +140,22 @@ export class SenderPipeline {
|
|
|
119
140
|
this.sink = new RaptiyeSink(this.replication, this.metrics, this.progress);
|
|
120
141
|
this.engine = new MutationEngine({
|
|
121
142
|
sink: this.sink,
|
|
122
|
-
createStore: () =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
createStore: () => {
|
|
144
|
+
let allocator = null;
|
|
145
|
+
if (this._allocatorPool && this._allocatorPool.length > 0) {
|
|
146
|
+
const idx = this._allocatorPool.findIndex((a) => a.capacity >= arenaCapacity);
|
|
147
|
+
if (idx !== -1) {
|
|
148
|
+
allocator = this._allocatorPool.splice(idx, 1)[0];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!allocator) {
|
|
152
|
+
allocator = new PooledBumpAllocator(arenaCapacity, this._allocatorPool);
|
|
153
|
+
}
|
|
154
|
+
return new ArenaStore({
|
|
155
|
+
chunkSize: Math.max(64, this.maxEntries),
|
|
156
|
+
allocator
|
|
157
|
+
});
|
|
158
|
+
},
|
|
126
159
|
maxEntries: this.maxEntries,
|
|
127
160
|
maxBytes: this.maxBytes,
|
|
128
161
|
maxImmutableBuffers: this.maxImmutableBuffers,
|
|
@@ -148,6 +181,7 @@ export class SenderPipeline {
|
|
|
148
181
|
|
|
149
182
|
async abort() {
|
|
150
183
|
this.aborted = true;
|
|
184
|
+
this._readScratch = null;
|
|
151
185
|
for (const waiter of this._abortWaiters) {
|
|
152
186
|
waiter();
|
|
153
187
|
}
|
|
@@ -264,8 +298,14 @@ export class SenderPipeline {
|
|
|
264
298
|
continue;
|
|
265
299
|
}
|
|
266
300
|
|
|
267
|
-
// Direct read into
|
|
268
|
-
|
|
301
|
+
// Direct read into reusable chunk buffer (Muttafa copies synchronously into its arena on append)
|
|
302
|
+
if (!this._readScratch || this._readScratch.byteLength < chunkLen) {
|
|
303
|
+
this._readScratch = new Uint8Array(Math.max(this.chunkSize, chunkLen));
|
|
304
|
+
}
|
|
305
|
+
const chunkBuf = (chunkLen === this._readScratch.byteLength)
|
|
306
|
+
? this._readScratch
|
|
307
|
+
: this._readScratch.subarray(0, chunkLen);
|
|
308
|
+
|
|
269
309
|
const readStart = Date.now();
|
|
270
310
|
await handle.read(chunkBuf, 0, chunkLen, Number(offset));
|
|
271
311
|
if (this.metrics) {
|
|
@@ -340,5 +380,6 @@ export class SenderPipeline {
|
|
|
340
380
|
});
|
|
341
381
|
this.progress.stop();
|
|
342
382
|
}
|
|
383
|
+
this._readScratch = null;
|
|
343
384
|
}
|
|
344
385
|
}
|