teyyare 0.2.7 → 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
@@ -19,6 +19,8 @@ function printUsage() {
19
19
 
20
20
  \x1b[1mOPTIONS:\x1b[0m
21
21
  -c, --channels <n> Parallel TCP connections (default: 4 for remote, 1 for local)
22
+ --chunk-size <size> Chunk size, e.g. 512K, 1M, 2M, 4M (default: 2M remote, 1M local)
23
+ --batch-size <size> Generation batch size, e.g. 8M, 16M (default: 8x chunk size)
22
24
  --port <port> Port number (default: 7421)
23
25
  --dir <path> Serve root directory (default: current dir)
24
26
 
@@ -30,6 +32,15 @@ function printUsage() {
30
32
  `);
31
33
  }
32
34
 
35
+ function parseByteSize(str) {
36
+ if (!str) return undefined;
37
+ const s = String(str).trim().toLowerCase();
38
+ if (s.endsWith('k') || s.endsWith('kb')) return Math.round(parseFloat(s) * 1024);
39
+ if (s.endsWith('m') || s.endsWith('mb')) return Math.round(parseFloat(s) * 1024 * 1024);
40
+ if (s.endsWith('g') || s.endsWith('gb')) return Math.round(parseFloat(s) * 1024 * 1024 * 1024);
41
+ return Number(str);
42
+ }
43
+
33
44
  function parseDestination(target) {
34
45
  // Matches host:port:path or host:path or local path
35
46
  if (!target.includes(':')) {
@@ -50,13 +61,13 @@ async function main() {
50
61
  const args = process.argv.slice(2);
51
62
  const command = args[0];
52
63
 
53
- if (!command || command === '--help' || command === '-h') {
64
+ if (!command || args.includes('--help') || args.includes('-h')) {
54
65
  printUsage();
55
66
  process.exit(0);
56
67
  }
57
68
 
58
69
  if (command === '--version' || command === '-v' || command === 'version') {
59
- console.log('0.2.7');
70
+ console.log('0.2.8');
60
71
  process.exit(0);
61
72
  }
62
73
 
@@ -88,10 +99,16 @@ async function main() {
88
99
  await new Promise(() => {});
89
100
  } else if (command === 'send') {
90
101
  let channels = undefined;
102
+ let chunkSize = undefined;
103
+ let batchSize = undefined;
91
104
  const positional = [];
92
105
  for (let i = 1; i < args.length; i++) {
93
106
  if ((args[i] === '-c' || args[i] === '--channels') && args[i + 1]) {
94
107
  channels = Number(args[++i]);
108
+ } else if ((args[i] === '--chunk-size' || args[i] === '--chunk') && args[i + 1]) {
109
+ chunkSize = parseByteSize(args[++i]);
110
+ } else if ((args[i] === '--batch-size' || args[i] === '--gen-size') && args[i + 1]) {
111
+ batchSize = parseByteSize(args[++i]);
95
112
  } else {
96
113
  positional.push(args[i]);
97
114
  }
@@ -106,12 +123,17 @@ async function main() {
106
123
  process.exit(1);
107
124
  }
108
125
 
126
+ const pipelineOptions = {};
127
+ if (chunkSize) pipelineOptions.chunkSize = chunkSize;
128
+ if (batchSize) pipelineOptions.maxBytes = batchSize;
129
+
109
130
  const { host, port } = parseDestination(target);
110
131
  const metrics = await send({
111
132
  sourcePath: source,
112
133
  host,
113
134
  port,
114
135
  channels,
136
+ pipelineOptions,
115
137
  showProgress: true
116
138
  });
117
139
 
@@ -129,10 +151,16 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
129
151
  process.exit(0);
130
152
  } else if (command === 'pull') {
131
153
  let channels = undefined;
154
+ let chunkSize = undefined;
155
+ let batchSize = undefined;
132
156
  const positional = [];
133
157
  for (let i = 1; i < args.length; i++) {
134
158
  if ((args[i] === '-c' || args[i] === '--channels') && args[i + 1]) {
135
159
  channels = Number(args[++i]);
160
+ } else if ((args[i] === '--chunk-size' || args[i] === '--chunk') && args[i + 1]) {
161
+ chunkSize = parseByteSize(args[++i]);
162
+ } else if ((args[i] === '--batch-size' || args[i] === '--gen-size') && args[i + 1]) {
163
+ batchSize = parseByteSize(args[++i]);
136
164
  } else {
137
165
  positional.push(args[i]);
138
166
  }
@@ -147,6 +175,10 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
147
175
  process.exit(1);
148
176
  }
149
177
 
178
+ const pipelineOptions = {};
179
+ if (chunkSize) pipelineOptions.chunkSize = chunkSize;
180
+ if (batchSize) pipelineOptions.maxBytes = batchSize;
181
+
150
182
  const { host, port, destPath: remotePath } = parseDestination(source);
151
183
  const metrics = await pull({
152
184
  sourceRemotePath: remotePath,
@@ -154,6 +186,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
154
186
  port,
155
187
  channels,
156
188
  destinationDir: target,
189
+ pipelineOptions,
157
190
  showProgress: true
158
191
  });
159
192
 
@@ -170,7 +203,7 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
170
203
  `);
171
204
  process.exit(0);
172
205
  } else if (command === 'status') {
173
- 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');
174
207
  } else {
175
208
  console.error(`Unknown command: ${command}`);
176
209
  printUsage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teyyare",
3
- "version": "0.2.7",
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",
@@ -9,7 +9,11 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test test/**/*.test.js",
12
- "bench": "node bench/run-all.js"
12
+ "bench": "node bench/run-all.js",
13
+ "e2e": "node test/e2e/e2e.js",
14
+ "e2e:profile": "softscope test/e2e/e2e.js",
15
+ "e2e:live": "softscope --live test/e2e/e2e.js",
16
+ "e2e:md": "softscope --md test/e2e/e2e.js"
13
17
  },
14
18
  "keywords": [
15
19
  "file-transfer",
@@ -76,10 +76,10 @@ export function unpackChunkMeta(buf, offset = 0) {
76
76
  * 14..45: sha256 hex (32 bytes raw or 64 hex chars)
77
77
  * 46..: path utf8
78
78
  */
79
- export function packFileBegin({ fileId, fileSize, path, sha256 = '' }) {
79
+ export function packFileBegin({ fileId, fileSize, path, sha256 = '', chunkSize = 1048576 }) {
80
80
  const pathBytes = encoder.encode(path);
81
81
  const shaBytes = encoder.encode(sha256.padEnd(64, '0').slice(0, 64));
82
- const total = 14 + 64 + pathBytes.byteLength;
82
+ const total = 14 + 64 + pathBytes.byteLength + 4;
83
83
  const buf = new Uint8Array(total);
84
84
  const view = new DataView(buf.buffer, buf.byteOffset, total);
85
85
 
@@ -88,6 +88,7 @@ export function packFileBegin({ fileId, fileSize, path, sha256 = '' }) {
88
88
  view.setUint16(12, pathBytes.byteLength, false);
89
89
  buf.set(shaBytes, 14);
90
90
  buf.set(pathBytes, 14 + 64);
91
+ view.setUint32(14 + 64 + pathBytes.byteLength, chunkSize >>> 0, false);
91
92
  return buf;
92
93
  }
93
94
 
@@ -98,7 +99,12 @@ export function unpackFileBegin(buf) {
98
99
  const pathLen = view.getUint16(12, false);
99
100
  const sha256 = decoder.decode(buf.subarray(14, 14 + 64)).replace(/0+$/, '');
100
101
  const path = decoder.decode(buf.subarray(14 + 64, 14 + 64 + pathLen));
101
- return { fileId, fileSize, path, sha256 };
102
+ let chunkSize = 1048576;
103
+ if (buf.byteLength >= 14 + 64 + pathLen + 4) {
104
+ const cs = view.getUint32(14 + 64 + pathLen, false);
105
+ if (cs > 0) chunkSize = cs;
106
+ }
107
+ return { fileId, fileSize, path, sha256, chunkSize };
102
108
  }
103
109
 
104
110
  /**
@@ -167,10 +173,18 @@ export function unpackTransferEnd(buf) {
167
173
  /**
168
174
  * Pack PULL_REQUEST metadata with optional verifiedChunks bitmap/list for resume.
169
175
  */
170
- export function packPullRequest({ remotePath, verifiedChunks = [] }) {
176
+ export function packPullRequest({
177
+ remotePath,
178
+ verifiedChunks = [],
179
+ chunkSize = 0,
180
+ maxEntries = 0,
181
+ maxBytes = 0
182
+ }) {
171
183
  const pathBytes = encoder.encode(remotePath);
172
184
  const count = verifiedChunks.length;
173
- const buf = new Uint8Array(2 + pathBytes.byteLength + 4 + count * 4);
185
+ const hasTuning = (chunkSize > 0 || maxEntries > 0 || maxBytes > 0);
186
+ const tuningLen = hasTuning ? 10 : 0;
187
+ const buf = new Uint8Array(2 + pathBytes.byteLength + 4 + count * 4 + tuningLen);
174
188
  const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
175
189
  view.setUint16(0, pathBytes.byteLength, false);
176
190
  buf.set(pathBytes, 2);
@@ -182,6 +196,13 @@ export function packPullRequest({ remotePath, verifiedChunks = [] }) {
182
196
  view.setUint32(off, verifiedChunks[i], false);
183
197
  off += 4;
184
198
  }
199
+
200
+ if (hasTuning) {
201
+ view.setUint32(off, chunkSize >>> 0, false);
202
+ view.setUint16(off + 4, maxEntries & 0xffff, false);
203
+ view.setUint32(off + 6, maxBytes >>> 0, false);
204
+ }
205
+
185
206
  return buf;
186
207
  }
187
208
 
@@ -201,7 +222,19 @@ export function unpackPullRequest(buf) {
201
222
  }
202
223
  }
203
224
 
204
- return { remotePath, verifiedChunks };
225
+ let chunkSize = undefined;
226
+ let maxEntries = undefined;
227
+ let maxBytes = undefined;
228
+ if (buf.byteLength >= off + 10) {
229
+ const cs = view.getUint32(off, false);
230
+ const me = view.getUint16(off + 4, false);
231
+ const mb = view.getUint32(off + 6, false);
232
+ if (cs > 0) chunkSize = cs;
233
+ if (me > 0) maxEntries = me;
234
+ if (mb > 0) maxBytes = mb;
235
+ }
236
+
237
+ return { remotePath, verifiedChunks, chunkSize, maxEntries, maxBytes };
205
238
  }
206
239
 
207
240
  /**