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 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
- ## Data Flow & Zero-Copy Architecture
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() (scatter/gather sendv)
79
+ Raptiye.submitBatch() (In-Flight Sliding Window up to 128 MB)
80
80
  │
81
81
  ▼
82
- TCP (corked writev)
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
- Raptiye (frame decode + CRC32 verification)
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
- Teyyare chunk consumer
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
- FileHandle.write(payload, 0, length, offset) (Direct random-access write)
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 files
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
- ### Progress Display
122
- ```text
123
- Teyyare
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
- backup.tar.zst
159
+ ## Progress Display
126
160
 
127
- 6.42 / 10.00 GB
128
- 64.2%
161
+ ```text
162
+ Teyyare [Download]
163
+
164
+ app.zip
129
165
 
130
- throughput 312 MB/s
131
- ETA 00:11
166
+ 333.54 MB / 333.54 MB
167
+ 100.0%
132
168
 
133
- chunks 6574 / 10240
134
- generation 822
135
- inflight gen 4
136
- inflight bytes 31.8 MB
169
+ network speed 324.5 MB/s
170
+ disk speed 1240.2 MB/s
171
+ channels 8
172
+ ETA --:--
137
173
 
138
- muttafa queue 2
139
- raptiye RTT 18 ms
140
- TCP pressure 0
174
+ chunks 167 / 167
175
+ generation 21
176
+ verified CRC32 167
177
+ resume hits 0
141
178
  ```
142
179
 
143
180
  ---
144
181
 
145
- ## Benchmarks & Ablation Analysis
182
+ ## Test Suite & Softscope Profiling
146
183
 
147
- Run the benchmark suite with:
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 run bench
187
+ npm test
150
188
  ```
151
189
 
152
- ### 1. Layer Ablation (50 MB transfer)
153
- | Layer | Description | Throughput (MB/s) | Duration |
154
- |---|---|---|---|
155
- | **A** | raw fs -> TCP -> fs | **581.4 MB/s** | 86ms |
156
- | **B** | fs -> Muttafa -> TCP -> fs | **306.7 MB/s** | 163ms |
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
- ### 2. Chunk & Generation Size Matrix (16 MB transfer)
161
- | Chunk Size | Generation Size | Throughput (MB/s) | Duration (ms) | Peak RSS (MB) |
162
- |---|---|---|---|---|
163
- | **64 KiB** | 4 MiB | 39.7 MB/s | 403ms | 660.8 MB |
164
- | **256 KiB** | 4 MiB | 34.9 MB/s | 458ms | 679.6 MB |
165
- | **1 MiB** | 4 MiB | **40.1 MB/s** | 399ms | 757.7 MB |
166
- | **1 MiB** | 8 MiB | **40.0 MB/s** | 400ms | 718.2 MB |
167
- | **4 MiB** | 16 MiB | 38.6 MB/s | 414ms | 745.3 MB |
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
- ## Test Suite
211
+ ## Benchmark & Throughput Results
172
212
 
173
- Run the full test suite with:
174
- ```bash
175
- npm test
176
- ```
213
+ ### End-to-End Suite Summary (`test/e2e/e2e.js`)
177
214
 
178
- Includes:
179
- - **Unit Tests**: Packed binary chunk metadata protocol, resume state bitmap, generation lifecycle & ownership assertions.
180
- - **Integration Tests**: 10 MB single file transfer, directory transfer with small-file packing, resume after 50% interruption with bounded retransmission, slow receiver disk write backpressure with bounded RSS plateau, and deterministic chaos recovery.
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 || command === '--help' || command === '-h') {
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.7');
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.7 status: ready');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teyyare",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "High-performance resumable file transfer engine and CLI built on muttafa and raptiye",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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) => Number(a.meta.offset - b.meta.offset));
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);
@@ -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: () => new ArenaStore({
123
- chunkSize: Math.max(64, this.maxEntries),
124
- allocator: new BumpAllocator(arenaCapacity)
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 fresh chunk buffer
268
- const chunkBuf = new Uint8Array(chunkLen);
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
  }