teyyare 0.1.0
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 +180 -0
- package/bench/ablation.bench.js +229 -0
- package/bench/matrix.bench.js +86 -0
- package/bench/run-all.js +19 -0
- package/bin/teyyare.js +124 -0
- package/package.json +27 -0
- package/src/cli/progress.js +125 -0
- package/src/core/protocol.js +163 -0
- package/src/index.js +139 -0
- package/src/manifest/manifest.js +84 -0
- package/src/metrics/collector.js +119 -0
- package/src/receiver/pipeline.js +234 -0
- package/src/resume/state.js +125 -0
- package/src/sender/pipeline.js +288 -0
- package/test/integration/backpressure.test.js +63 -0
- package/test/integration/chaos.test.js +104 -0
- package/test/integration/directory.test.js +75 -0
- package/test/integration/resume.test.js +97 -0
- package/test/integration/single-file.test.js +64 -0
- package/test/unit/ownership.test.js +74 -0
- package/test/unit/protocol.test.js +73 -0
- package/test/unit/state.test.js +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Teyyare
|
|
2
|
+
|
|
3
|
+
**Teyyare** is a high-performance, resumable file transfer engine and CLI built directly on top of:
|
|
4
|
+
|
|
5
|
+
- [`muttafa`](https://github.com/ahmet/muttafa): low-level mutation buffering, generation freeze & lifecycle engine
|
|
6
|
+
- [`raptiye`](https://github.com/ahmet/raptiye): high-performance byte-first replicated log & TCP binary transport
|
|
7
|
+
|
|
8
|
+
Teyyare is the first real integration workload for both projects, proving that a mutation lifecycle engine and a binary replication/transport engine compose into a fast, bounded-memory, resumable file transfer system without introducing redundant batching layers or memory copies.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
TEYYARE
|
|
12
|
+
│
|
|
13
|
+
File / Directory API
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
MUTTAFA
|
|
17
|
+
transfer write pipeline
|
|
18
|
+
│
|
|
19
|
+
freeze
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
Frozen Generation
|
|
23
|
+
│
|
|
24
|
+
▼
|
|
25
|
+
RAPTIYE
|
|
26
|
+
replication / transport
|
|
27
|
+
│
|
|
28
|
+
▼
|
|
29
|
+
TCP
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
Remote Teyyare
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
MUTTAFA
|
|
36
|
+
│
|
|
37
|
+
flush
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
Disk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Core Responsibility Split
|
|
46
|
+
|
|
47
|
+
| Layer | Responsibility |
|
|
48
|
+
|---|---|
|
|
49
|
+
| **Muttafa** | Chunk ingestion, mutation buffering, generation lifecycle, freeze, immutable batches, backpressure, memory accounting. |
|
|
50
|
+
| **Raptiye** | Peer connection, TCP transport, binary framing, replication stream, ACKs/progress, delivery ordering, flow control. |
|
|
51
|
+
| **Teyyare** | File/directory semantics, manifest, chunks, checksums, resume state, file reconstruction, verification, atomic finalization (`.teyyare-part` -> final), CLI. |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Data Flow & Zero-Copy Architecture
|
|
56
|
+
|
|
57
|
+
### Sender Hot Path
|
|
58
|
+
```text
|
|
59
|
+
FileHandle.read()
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
chunk buffer
|
|
63
|
+
│
|
|
64
|
+
▼
|
|
65
|
+
crc32 checksum
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
Muttafa mutation (OP_CHUNK)
|
|
69
|
+
│
|
|
70
|
+
▼
|
|
71
|
+
MutableGeneration
|
|
72
|
+
│
|
|
73
|
+
freeze
|
|
74
|
+
│
|
|
75
|
+
▼
|
|
76
|
+
FrozenGeneration [metaBuf, payloadArena]
|
|
77
|
+
│
|
|
78
|
+
▼
|
|
79
|
+
Raptiye.submitBatch() (scatter/gather sendv)
|
|
80
|
+
│
|
|
81
|
+
▼
|
|
82
|
+
TCP (corked writev)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Receiver Hot Path
|
|
86
|
+
```text
|
|
87
|
+
TCP
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
Raptiye (frame decode + CRC32 verification)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
FrozenGeneration (O(1) zero-copy view via FrozenArenaStore.fromBuffers)
|
|
94
|
+
│
|
|
95
|
+
▼
|
|
96
|
+
Teyyare chunk consumer
|
|
97
|
+
│
|
|
98
|
+
▼
|
|
99
|
+
FileHandle.write(payload, 0, length, offset) (Direct random-access write)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## CLI Usage
|
|
105
|
+
|
|
106
|
+
### Start Receiver Server
|
|
107
|
+
```bash
|
|
108
|
+
# Listen on default port 7421
|
|
109
|
+
teyyare serve --port 7421 --dir /destination/folder
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Send File or Directory
|
|
113
|
+
```bash
|
|
114
|
+
# Single file transfer
|
|
115
|
+
teyyare send ./large-file.bin 127.0.0.1:7421:/tmp/large-file.bin
|
|
116
|
+
|
|
117
|
+
# Directory transfer with nested files
|
|
118
|
+
teyyare send ./dist server.local:7421:/var/www/app
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Progress Display
|
|
122
|
+
```text
|
|
123
|
+
Teyyare
|
|
124
|
+
|
|
125
|
+
backup.tar.zst
|
|
126
|
+
|
|
127
|
+
6.42 / 10.00 GB
|
|
128
|
+
64.2%
|
|
129
|
+
|
|
130
|
+
throughput 312 MB/s
|
|
131
|
+
ETA 00:11
|
|
132
|
+
|
|
133
|
+
chunks 6574 / 10240
|
|
134
|
+
generation 822
|
|
135
|
+
inflight gen 4
|
|
136
|
+
inflight bytes 31.8 MB
|
|
137
|
+
|
|
138
|
+
muttafa queue 2
|
|
139
|
+
raptiye RTT 18 ms
|
|
140
|
+
TCP pressure 0
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Benchmarks & Ablation Analysis
|
|
146
|
+
|
|
147
|
+
Run the benchmark suite with:
|
|
148
|
+
```bash
|
|
149
|
+
npm run bench
|
|
150
|
+
```
|
|
151
|
+
|
|
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 |
|
|
159
|
+
|
|
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 |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Test Suite
|
|
172
|
+
|
|
173
|
+
Run the full test suite with:
|
|
174
|
+
```bash
|
|
175
|
+
npm test
|
|
176
|
+
```
|
|
177
|
+
|
|
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.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ablation Benchmark:
|
|
3
|
+
* Compares:
|
|
4
|
+
* A: raw fs -> TCP -> fs
|
|
5
|
+
* B: fs -> Muttafa -> TCP -> fs
|
|
6
|
+
* C: fs -> Raptiye -> fs
|
|
7
|
+
* D: fs -> Muttafa -> Raptiye -> fs (Teyyare)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
|
+
import net from 'node:net';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
import { MutationEngine, ArenaStore, Sink } from 'muttafa';
|
|
17
|
+
import { TCPTransport, ReplicationPipeline } from 'raptiye';
|
|
18
|
+
import { serve, send } from '../src/index.js';
|
|
19
|
+
|
|
20
|
+
const BENCH_SIZE = 50 * 1024 * 1024; // 50 MB for rapid accurate benchmark
|
|
21
|
+
|
|
22
|
+
async function setupTestData(dir) {
|
|
23
|
+
const src = path.join(dir, 'source.bin');
|
|
24
|
+
const data = crypto.randomBytes(BENCH_SIZE);
|
|
25
|
+
await fs.writeFile(src, data);
|
|
26
|
+
return src;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Baseline A: Raw fs -> TCP -> fs
|
|
30
|
+
async function runBaselineA(srcFile, tmpDir) {
|
|
31
|
+
const dest = path.join(tmpDir, 'dest-a.bin');
|
|
32
|
+
const port = 8601;
|
|
33
|
+
|
|
34
|
+
let server;
|
|
35
|
+
await new Promise((resolve) => {
|
|
36
|
+
server = net.createServer((sock) => {
|
|
37
|
+
const out = createWriteStream(dest);
|
|
38
|
+
sock.pipe(out);
|
|
39
|
+
});
|
|
40
|
+
server.listen(port, '127.0.0.1', resolve);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const t0 = Date.now();
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
const sock = net.createConnection({ port, host: '127.0.0.1' }, () => {
|
|
46
|
+
const inp = createReadStream(srcFile);
|
|
47
|
+
inp.pipe(sock);
|
|
48
|
+
sock.on('close', resolve);
|
|
49
|
+
});
|
|
50
|
+
sock.on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const durationMs = Date.now() - t0;
|
|
54
|
+
await new Promise((resolve) => server.close(resolve));
|
|
55
|
+
|
|
56
|
+
const throughput = (BENCH_SIZE / (1024 * 1024)) / (durationMs / 1000);
|
|
57
|
+
return { name: 'A: raw fs -> TCP -> fs', durationMs, throughputMBs: throughput };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Baseline B: fs -> Muttafa -> TCP -> fs
|
|
61
|
+
async function runBaselineB(srcFile, tmpDir) {
|
|
62
|
+
const dest = path.join(tmpDir, 'dest-b.bin');
|
|
63
|
+
const port = 8602;
|
|
64
|
+
|
|
65
|
+
let server;
|
|
66
|
+
await new Promise((resolve) => {
|
|
67
|
+
server = net.createServer((sock) => {
|
|
68
|
+
const out = createWriteStream(dest);
|
|
69
|
+
sock.pipe(out);
|
|
70
|
+
});
|
|
71
|
+
server.listen(port, '127.0.0.1', resolve);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const t0 = Date.now();
|
|
75
|
+
|
|
76
|
+
const sock = net.createConnection({ port, host: '127.0.0.1' });
|
|
77
|
+
await new Promise((resolve) => sock.on('connect', resolve));
|
|
78
|
+
|
|
79
|
+
class NetSink extends Sink {
|
|
80
|
+
async flush(batch) {
|
|
81
|
+
const bufs = batch.buffers();
|
|
82
|
+
for (const b of bufs) {
|
|
83
|
+
if (b.byteLength > 0) sock.write(b);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const engine = new MutationEngine({
|
|
89
|
+
sink: new NetSink(),
|
|
90
|
+
createStore: () => new ArenaStore(),
|
|
91
|
+
maxEntries: 8,
|
|
92
|
+
maxBytes: 8 * 1024 * 1024
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const handle = await fs.open(srcFile, 'r');
|
|
96
|
+
const chunkSize = 1024 * 1024;
|
|
97
|
+
for (let off = 0; off < BENCH_SIZE; off += chunkSize) {
|
|
98
|
+
const len = Math.min(chunkSize, BENCH_SIZE - off);
|
|
99
|
+
const buf = new Uint8Array(len);
|
|
100
|
+
await handle.read(buf, 0, len, off);
|
|
101
|
+
await engine.putAsync(new Uint8Array(4), buf, 4, len);
|
|
102
|
+
}
|
|
103
|
+
await handle.close();
|
|
104
|
+
await engine.flush();
|
|
105
|
+
|
|
106
|
+
sock.end();
|
|
107
|
+
await new Promise((resolve) => sock.on('close', resolve));
|
|
108
|
+
const durationMs = Date.now() - t0;
|
|
109
|
+
await new Promise((resolve) => server.close(resolve));
|
|
110
|
+
|
|
111
|
+
const throughput = (BENCH_SIZE / (1024 * 1024)) / (durationMs / 1000);
|
|
112
|
+
return { name: 'B: fs -> Muttafa -> TCP -> fs', durationMs, throughputMBs: throughput };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Baseline C: fs -> Raptiye -> fs
|
|
116
|
+
async function runBaselineC(srcFile, tmpDir) {
|
|
117
|
+
const dest = path.join(tmpDir, 'dest-c.bin');
|
|
118
|
+
const port = 8603;
|
|
119
|
+
|
|
120
|
+
const tReceiver = new TCPTransport({ id: 2, port, host: '127.0.0.1', peerAddresses: new Map() });
|
|
121
|
+
await tReceiver.start();
|
|
122
|
+
const rReceiver = new ReplicationPipeline({ transport: tReceiver, localNode: 2, remoteNode: 1 });
|
|
123
|
+
|
|
124
|
+
const destHandle = await fs.open(dest, 'w+');
|
|
125
|
+
rReceiver.onBatch(async (msg) => {
|
|
126
|
+
for (const b of msg.buffers) {
|
|
127
|
+
if (b.byteLength > 0) {
|
|
128
|
+
await destHandle.write(b);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const tSender = new TCPTransport({ id: 1, port: 8604, host: '127.0.0.1', peerAddresses: new Map([[2, { host: '127.0.0.1', port }]]) });
|
|
134
|
+
await tSender.start();
|
|
135
|
+
const rSender = new ReplicationPipeline({ transport: tSender, localNode: 1, remoteNode: 2 });
|
|
136
|
+
|
|
137
|
+
const t0 = Date.now();
|
|
138
|
+
const handle = await fs.open(srcFile, 'r');
|
|
139
|
+
const chunkSize = 1024 * 1024;
|
|
140
|
+
let genId = 1;
|
|
141
|
+
|
|
142
|
+
for (let off = 0; off < BENCH_SIZE; off += chunkSize) {
|
|
143
|
+
const len = Math.min(chunkSize, BENCH_SIZE - off);
|
|
144
|
+
const buf = new Uint8Array(len);
|
|
145
|
+
await handle.read(buf, 0, len, off);
|
|
146
|
+
const receipt = await rSender.submitBatch({
|
|
147
|
+
generationId: genId++,
|
|
148
|
+
firstSequence: BigInt(off),
|
|
149
|
+
lastSequence: BigInt(off + len),
|
|
150
|
+
entryCount: 1,
|
|
151
|
+
buffers: [buf]
|
|
152
|
+
});
|
|
153
|
+
await receipt.delivered();
|
|
154
|
+
}
|
|
155
|
+
await handle.close();
|
|
156
|
+
await destHandle.sync();
|
|
157
|
+
await destHandle.close();
|
|
158
|
+
|
|
159
|
+
const durationMs = Date.now() - t0;
|
|
160
|
+
await tSender.close();
|
|
161
|
+
await tReceiver.close();
|
|
162
|
+
|
|
163
|
+
const throughput = (BENCH_SIZE / (1024 * 1024)) / (durationMs / 1000);
|
|
164
|
+
return { name: 'C: fs -> Raptiye -> fs', durationMs, throughputMBs: throughput };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Pipeline D: fs -> Muttafa -> Raptiye -> fs (Teyyare)
|
|
168
|
+
async function runPipelineD(srcFile, tmpDir) {
|
|
169
|
+
const destDir = path.join(tmpDir, 'dest-d');
|
|
170
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
171
|
+
const port = 8605;
|
|
172
|
+
|
|
173
|
+
const server = await serve({ port, destinationDir: destDir });
|
|
174
|
+
|
|
175
|
+
const t0 = Date.now();
|
|
176
|
+
const metrics = await send({
|
|
177
|
+
sourcePath: srcFile,
|
|
178
|
+
host: '127.0.0.1',
|
|
179
|
+
port,
|
|
180
|
+
pipelineOptions: {
|
|
181
|
+
chunkSize: 1024 * 1024,
|
|
182
|
+
maxEntries: 8,
|
|
183
|
+
maxBytes: 8 * 1024 * 1024
|
|
184
|
+
},
|
|
185
|
+
showProgress: false
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await server.receiver.waitForCompletion();
|
|
189
|
+
const durationMs = Date.now() - t0;
|
|
190
|
+
await server.close();
|
|
191
|
+
|
|
192
|
+
const throughput = (BENCH_SIZE / (1024 * 1024)) / (durationMs / 1000);
|
|
193
|
+
return { name: 'D: Teyyare (Muttafa + Raptiye)', durationMs, throughputMBs: throughput, metrics: metrics.snapshot() };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function runAblation() {
|
|
197
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-ablation-'));
|
|
198
|
+
console.log(`\n\x1b[1m=== ABLATION BENCHMARK (${BENCH_SIZE / (1024 * 1024)} MB transfer) ===\x1b[0m\n`);
|
|
199
|
+
|
|
200
|
+
const srcFile = await setupTestData(tmpDir);
|
|
201
|
+
|
|
202
|
+
const resA = await runBaselineA(srcFile, tmpDir);
|
|
203
|
+
console.log(`${resA.name.padEnd(35)} : ${resA.throughputMBs.toFixed(1)} MB/s (${resA.durationMs}ms)`);
|
|
204
|
+
|
|
205
|
+
const resB = await runBaselineB(srcFile, tmpDir);
|
|
206
|
+
console.log(`${resB.name.padEnd(35)} : ${resB.throughputMBs.toFixed(1)} MB/s (${resB.durationMs}ms)`);
|
|
207
|
+
|
|
208
|
+
const resC = await runBaselineC(srcFile, tmpDir);
|
|
209
|
+
console.log(`${resC.name.padEnd(35)} : ${resC.throughputMBs.toFixed(1)} MB/s (${resC.durationMs}ms)`);
|
|
210
|
+
|
|
211
|
+
const resD = await runPipelineD(srcFile, tmpDir);
|
|
212
|
+
console.log(`${resD.name.padEnd(35)} : ${resD.throughputMBs.toFixed(1)} MB/s (${resD.durationMs}ms)`);
|
|
213
|
+
|
|
214
|
+
console.log(`\n\x1b[1mLayer Overhead Analysis:\x1b[0m`);
|
|
215
|
+
const costB = ((resA.throughputMBs - resB.throughputMBs) / resA.throughputMBs) * 100;
|
|
216
|
+
const costC = ((resA.throughputMBs - resC.throughputMBs) / resA.throughputMBs) * 100;
|
|
217
|
+
const costD = ((resA.throughputMBs - resD.throughputMBs) / resA.throughputMBs) * 100;
|
|
218
|
+
console.log(`Muttafa write pipeline overhead : ${costB.toFixed(1)}%`);
|
|
219
|
+
console.log(`Raptiye binary delivery overhead: ${costC.toFixed(1)}%`);
|
|
220
|
+
console.log(`Total Teyyare integrated cost : ${costD.toFixed(1)}%`);
|
|
221
|
+
console.log(`Protocol wire overhead : ${resD.metrics.protocolOverheadPercent.toFixed(2)}%`);
|
|
222
|
+
console.log(`Peak RSS : ${(resD.metrics.peakRSS / (1024 * 1024)).toFixed(1)} MB`);
|
|
223
|
+
|
|
224
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (process.argv[1]?.endsWith('ablation.bench.js')) {
|
|
228
|
+
runAblation().catch(console.error);
|
|
229
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matrix Benchmark:
|
|
3
|
+
* Sweeps chunk sizes: 64 KiB, 256 KiB, 1 MiB, 4 MiB
|
|
4
|
+
* Sweeps generation sizes: 1 MiB, 4 MiB, 8 MiB, 16 MiB, 32 MiB
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import { serve, send } from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
const MATRIX_FILE_SIZE = 16 * 1024 * 1024; // 16 MB
|
|
14
|
+
|
|
15
|
+
export async function runMatrix() {
|
|
16
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'teyyare-matrix-'));
|
|
17
|
+
const src = path.join(tmpDir, 'matrix-src.bin');
|
|
18
|
+
const data = crypto.randomBytes(MATRIX_FILE_SIZE);
|
|
19
|
+
await fs.writeFile(src, data);
|
|
20
|
+
|
|
21
|
+
console.log(`\n\x1b[1m=== GENERATION & CHUNK SIZE MATRIX BENCHMARK (${MATRIX_FILE_SIZE / (1024 * 1024)} MB) ===\x1b[0m\n`);
|
|
22
|
+
|
|
23
|
+
const chunkSizes = [
|
|
24
|
+
{ label: '64 KiB', bytes: 64 * 1024 },
|
|
25
|
+
{ label: '256 KiB', bytes: 256 * 1024 },
|
|
26
|
+
{ label: '1 MiB', bytes: 1024 * 1024 },
|
|
27
|
+
{ label: '4 MiB', bytes: 4 * 1024 * 1024 }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const genSizes = [
|
|
31
|
+
{ label: '1 MiB', bytes: 1 * 1024 * 1024 },
|
|
32
|
+
{ label: '4 MiB', bytes: 4 * 1024 * 1024 },
|
|
33
|
+
{ label: '8 MiB', bytes: 8 * 1024 * 1024 },
|
|
34
|
+
{ label: '16 MiB', bytes: 16 * 1024 * 1024 },
|
|
35
|
+
{ label: '32 MiB', bytes: 32 * 1024 * 1024 }
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
console.log('| Chunk Size | Generation Size | Throughput (MB/s) | Duration (ms) | Peak RSS (MB) |');
|
|
39
|
+
console.log('|------------|-----------------|-------------------|---------------|---------------|');
|
|
40
|
+
|
|
41
|
+
let basePort = 8700;
|
|
42
|
+
|
|
43
|
+
for (const c of chunkSizes) {
|
|
44
|
+
for (const g of genSizes) {
|
|
45
|
+
if (c.bytes > g.bytes) continue; // chunk size cannot exceed generation size
|
|
46
|
+
|
|
47
|
+
const port = basePort++;
|
|
48
|
+
const destDir = path.join(tmpDir, `dest-${c.label}-${g.label}`);
|
|
49
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
50
|
+
|
|
51
|
+
const server = await serve({ port, destinationDir: destDir });
|
|
52
|
+
const maxEntries = Math.max(1, Math.floor(g.bytes / c.bytes));
|
|
53
|
+
|
|
54
|
+
const t0 = Date.now();
|
|
55
|
+
const metrics = await send({
|
|
56
|
+
sourcePath: src,
|
|
57
|
+
host: '127.0.0.1',
|
|
58
|
+
port,
|
|
59
|
+
pipelineOptions: {
|
|
60
|
+
chunkSize: c.bytes,
|
|
61
|
+
maxEntries,
|
|
62
|
+
maxBytes: g.bytes
|
|
63
|
+
},
|
|
64
|
+
showProgress: false
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await server.receiver.waitForCompletion();
|
|
68
|
+
const durationMs = Date.now() - t0;
|
|
69
|
+
await server.close();
|
|
70
|
+
|
|
71
|
+
const throughput = (MATRIX_FILE_SIZE / (1024 * 1024)) / (durationMs / 1000);
|
|
72
|
+
const snap = metrics.snapshot();
|
|
73
|
+
const rssMB = (snap.peakRSS / (1024 * 1024)).toFixed(1);
|
|
74
|
+
|
|
75
|
+
console.log(
|
|
76
|
+
`| ${c.label.padEnd(10)} | ${g.label.padEnd(15)} | ${throughput.toFixed(1).padStart(17)} | ${String(durationMs).padStart(13)} | ${rssMB.padStart(13)} |`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await fs.rm(tmpDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (process.argv[1]?.endsWith('matrix.bench.js')) {
|
|
85
|
+
runMatrix().catch(console.error);
|
|
86
|
+
}
|
package/bench/run-all.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Master benchmark runner for Teyyare.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { runAblation } from './ablation.bench.js';
|
|
6
|
+
import { runMatrix } from './matrix.bench.js';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
console.log('\x1b[1m\x1b[36m========================================\x1b[0m');
|
|
10
|
+
console.log('\x1b[1m\x1b[36m TEYYARE INTEGRATION BENCHMARK SUITE \x1b[0m');
|
|
11
|
+
console.log('\x1b[1m\x1b[36m========================================\x1b[0m');
|
|
12
|
+
|
|
13
|
+
await runAblation();
|
|
14
|
+
await runMatrix();
|
|
15
|
+
|
|
16
|
+
console.log('\n\x1b[1m\x1b[32mAll benchmarks completed successfully!\x1b[0m\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main().catch(console.error);
|
package/bin/teyyare.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Teyyare CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { serve, send } from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
function printUsage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
\x1b[1m\x1b[36mTeyyare\x1b[0m - High-performance resumable file transfer
|
|
13
|
+
|
|
14
|
+
\x1b[1mUSAGE:\x1b[0m
|
|
15
|
+
teyyare serve [--port 7421] [--dir <path>]
|
|
16
|
+
teyyare send <source> <destination>
|
|
17
|
+
teyyare pull <source> <destination>
|
|
18
|
+
teyyare status
|
|
19
|
+
|
|
20
|
+
\x1b[1mEXAMPLES:\x1b[0m
|
|
21
|
+
teyyare serve --port 7421
|
|
22
|
+
teyyare send ./10gb.bin localhost:/tmp/10gb.bin
|
|
23
|
+
teyyare send ./dist 127.0.0.1:7421:/var/www/app
|
|
24
|
+
teyyare pull server:7421:/backup/db.tar.zst ./
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseDestination(target) {
|
|
29
|
+
// Matches host:port:path or host:path or local path
|
|
30
|
+
if (!target.includes(':')) {
|
|
31
|
+
return { host: '127.0.0.1', port: 7421, destPath: target };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parts = target.split(':');
|
|
35
|
+
if (parts.length === 2) {
|
|
36
|
+
return { host: parts[0] || '127.0.0.1', port: 7421, destPath: parts[1] };
|
|
37
|
+
}
|
|
38
|
+
if (parts.length === 3) {
|
|
39
|
+
return { host: parts[0] || '127.0.0.1', port: Number(parts[1]) || 7421, destPath: parts[2] };
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Invalid destination format: ${target}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const command = args[0];
|
|
47
|
+
|
|
48
|
+
if (!command || command === '--help' || command === '-h') {
|
|
49
|
+
printUsage();
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === 'serve') {
|
|
54
|
+
let port = 7421;
|
|
55
|
+
let dir = process.cwd();
|
|
56
|
+
|
|
57
|
+
for (let i = 1; i < args.length; i++) {
|
|
58
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
59
|
+
port = Number(args[++i]);
|
|
60
|
+
} else if (args[i] === '--dir' && args[i + 1]) {
|
|
61
|
+
dir = args[++i];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\x1b[1m\x1b[36mTeyyare receiver\x1b[0m listening on port \x1b[33m${port}\x1b[0m (dir: ${dir})`);
|
|
66
|
+
const server = await serve({ port, destinationDir: dir });
|
|
67
|
+
|
|
68
|
+
const shutdown = async () => {
|
|
69
|
+
console.log('\nShutting down receiver...');
|
|
70
|
+
await server.close();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
process.on('SIGINT', shutdown);
|
|
75
|
+
process.on('SIGTERM', shutdown);
|
|
76
|
+
|
|
77
|
+
// Keep alive
|
|
78
|
+
await new Promise(() => {});
|
|
79
|
+
} else if (command === 'send') {
|
|
80
|
+
const source = args[1];
|
|
81
|
+
const target = args[2];
|
|
82
|
+
|
|
83
|
+
if (!source || !target) {
|
|
84
|
+
console.error('Error: teyyare send requires <source> and <destination>');
|
|
85
|
+
printUsage();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { host, port } = parseDestination(target);
|
|
90
|
+
const metrics = await send({
|
|
91
|
+
sourcePath: source,
|
|
92
|
+
host,
|
|
93
|
+
port,
|
|
94
|
+
showProgress: true
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const snap = metrics.snapshot();
|
|
98
|
+
console.log(`
|
|
99
|
+
\x1b[1m\x1b[32mTransfer completed successfully!\x1b[0m
|
|
100
|
+
Throughput: \x1b[1m${snap.throughputMBs.toFixed(2)} MB/s\x1b[0m
|
|
101
|
+
Duration: ${(snap.elapsedMs / 1000).toFixed(2)}s
|
|
102
|
+
Bytes: ${(snap.fileBytesRead / (1024 * 1024)).toFixed(2)} MB
|
|
103
|
+
Generations: ${snap.generations}
|
|
104
|
+
Backpressure: ${snap.backpressureCount}
|
|
105
|
+
Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
|
|
106
|
+
Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
107
|
+
`);
|
|
108
|
+
} else if (command === 'pull') {
|
|
109
|
+
console.log('Pull command: inverting peer roles to request remote transfer.');
|
|
110
|
+
// In point-to-point architecture, pull is receiver-initiated transfer
|
|
111
|
+
console.error('Pulling is handled via remote agent or server command.');
|
|
112
|
+
} else if (command === 'status') {
|
|
113
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.1.0 status: ready');
|
|
114
|
+
} else {
|
|
115
|
+
console.error(`Unknown command: ${command}`);
|
|
116
|
+
printUsage();
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch((err) => {
|
|
122
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teyyare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance resumable file transfer engine and CLI built on muttafa and raptiye",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"teyyare": "./bin/teyyare.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/**/*.test.js",
|
|
12
|
+
"bench": "node bench/run-all.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"file-transfer",
|
|
16
|
+
"resumable",
|
|
17
|
+
"systems-programming",
|
|
18
|
+
"zero-copy",
|
|
19
|
+
"high-throughput"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"muttafa": "^0.1.0",
|
|
25
|
+
"raptiye": "^0.1.0"
|
|
26
|
+
}
|
|
27
|
+
}
|