teyyare 0.2.8 → 0.3.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 +97 -57
- package/bin/teyyare.js +26 -3
- package/package.json +7 -7
- package/src/cli/progress.js +47 -8
- package/src/index.js +152 -23
- package/src/metrics/collector.js +1 -0
- package/src/receiver/pipeline.js +32 -6
- 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
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
* Teyyare CLI
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
// Option C: Ensure libuv threadpool has sufficient C++ workers for multi-channel I/O.
|
|
11
|
+
// Node.js defaults to only 4 threads. Multi-channel file transfers (writev, read, sync)
|
|
12
|
+
// require at least 16 threads to avoid C++ threadpool contention.
|
|
13
|
+
const RECOMMENDED_POOL_SIZE = Math.min(128, Math.max(16, (os.availableParallelism ? os.availableParallelism() : 8) * 2));
|
|
14
|
+
if (!process.env.TEYYARE_BOOTSTRAPPED && (!process.env.UV_THREADPOOL_SIZE || Number(process.env.UV_THREADPOOL_SIZE) < 16)) {
|
|
15
|
+
process.env.UV_THREADPOOL_SIZE = String(RECOMMENDED_POOL_SIZE);
|
|
16
|
+
process.env.TEYYARE_BOOTSTRAPPED = '1';
|
|
17
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
env: process.env
|
|
20
|
+
});
|
|
21
|
+
child.on('exit', (code, signal) => {
|
|
22
|
+
if (signal) process.kill(process.pid, signal);
|
|
23
|
+
else process.exit(code ?? 0);
|
|
24
|
+
});
|
|
25
|
+
// Hold parent until child process finishes
|
|
26
|
+
await new Promise(() => {});
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
import path from 'node:path';
|
|
8
30
|
import { serve, send, pull } from '../src/index.js';
|
|
9
31
|
|
|
@@ -61,13 +83,13 @@ async function main() {
|
|
|
61
83
|
const args = process.argv.slice(2);
|
|
62
84
|
const command = args[0];
|
|
63
85
|
|
|
64
|
-
if (!command ||
|
|
86
|
+
if (!command || args.includes('--help') || args.includes('-h')) {
|
|
65
87
|
printUsage();
|
|
66
88
|
process.exit(0);
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
if (command === '--version' || command === '-v' || command === 'version') {
|
|
70
|
-
console.log('0.2.
|
|
92
|
+
console.log('0.2.8');
|
|
71
93
|
process.exit(0);
|
|
72
94
|
}
|
|
73
95
|
|
|
@@ -203,7 +225,8 @@ Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
|
203
225
|
`);
|
|
204
226
|
process.exit(0);
|
|
205
227
|
} else if (command === 'status') {
|
|
206
|
-
|
|
228
|
+
const poolSize = Number(process.env.UV_THREADPOOL_SIZE) || 4;
|
|
229
|
+
console.log(`\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.9 status: ready (libuv threadpool: ${poolSize})`);
|
|
207
230
|
} else {
|
|
208
231
|
console.error(`Unknown command: ${command}`);
|
|
209
232
|
printUsage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teyyare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"teyyare": "bin/teyyare.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --test test/**/*.test.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"
|
|
11
|
+
"test": "UV_THREADPOOL_SIZE=16 node --test test/**/*.test.js",
|
|
12
|
+
"bench": "UV_THREADPOOL_SIZE=16 node bench/run-all.js",
|
|
13
|
+
"e2e": "UV_THREADPOOL_SIZE=16 node test/e2e/e2e.js",
|
|
14
|
+
"e2e:profile": "UV_THREADPOOL_SIZE=16 softscope test/e2e/e2e.js",
|
|
15
|
+
"e2e:live": "UV_THREADPOOL_SIZE=16 softscope --live test/e2e/e2e.js",
|
|
16
|
+
"e2e:md": "UV_THREADPOOL_SIZE=16 softscope --md test/e2e/e2e.js"
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"file-transfer",
|
package/src/cli/progress.js
CHANGED
|
@@ -17,6 +17,7 @@ export class ProgressReporter {
|
|
|
17
17
|
|
|
18
18
|
this.fileName = '';
|
|
19
19
|
this.bytesDone = 0;
|
|
20
|
+
this.networkBytesDone = 0;
|
|
20
21
|
this.totalBytes = 0;
|
|
21
22
|
this.chunksDone = 0;
|
|
22
23
|
this.totalChunks = 0;
|
|
@@ -35,12 +36,18 @@ export class ProgressReporter {
|
|
|
35
36
|
this._timer = null;
|
|
36
37
|
this._linesRendered = 0;
|
|
37
38
|
this._speedHistory = [];
|
|
39
|
+
this._netSpeedHistory = [];
|
|
40
|
+
this._emaNetSpeed = 0;
|
|
41
|
+
this._emaSpeed = 0;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
start() {
|
|
41
45
|
if (!this.enabled) return;
|
|
42
46
|
this.startTime = Date.now();
|
|
43
47
|
this._speedHistory = [{ time: this.startTime, bytes: this.bytesDone }];
|
|
48
|
+
this._netSpeedHistory = [{ time: this.startTime, bytes: this.networkBytesDone || this.bytesDone }];
|
|
49
|
+
this._emaNetSpeed = 0;
|
|
50
|
+
this._emaSpeed = 0;
|
|
44
51
|
this._timer = setInterval(() => this.render(), this.intervalMs);
|
|
45
52
|
}
|
|
46
53
|
|
|
@@ -58,6 +65,7 @@ export class ProgressReporter {
|
|
|
58
65
|
update(state = {}) {
|
|
59
66
|
if (state.fileName !== undefined) this.fileName = state.fileName;
|
|
60
67
|
if (state.bytesDone !== undefined) this.bytesDone = state.bytesDone;
|
|
68
|
+
if (state.networkBytesDone !== undefined) this.networkBytesDone = state.networkBytesDone;
|
|
61
69
|
if (state.totalBytes !== undefined) this.totalBytes = state.totalBytes;
|
|
62
70
|
if (state.chunksDone !== undefined) this.chunksDone = state.chunksDone;
|
|
63
71
|
if (state.totalChunks !== undefined && state.totalChunks > 0) this.totalChunks = state.totalChunks;
|
|
@@ -91,7 +99,7 @@ export class ProgressReporter {
|
|
|
91
99
|
const now = Date.now();
|
|
92
100
|
const elapsedSec = Math.max(0.001, (now - this.startTime) / 1000);
|
|
93
101
|
|
|
94
|
-
//
|
|
102
|
+
// 1. Committed bytes speed history (disk commit rate & upload throughput)
|
|
95
103
|
this._speedHistory.push({ time: now, bytes: this.bytesDone });
|
|
96
104
|
while (this._speedHistory.length > 2 && (now - this._speedHistory[0].time) > 2000) {
|
|
97
105
|
this._speedHistory.shift();
|
|
@@ -101,15 +109,45 @@ export class ProgressReporter {
|
|
|
101
109
|
const windowBytes = Math.max(0, this.bytesDone - oldest.bytes);
|
|
102
110
|
|
|
103
111
|
const avgSpeedMBs = (this.bytesDone / (1024 * 1024)) / elapsedSec;
|
|
104
|
-
const currentSpeedMBs = windowSec >= 0.
|
|
105
|
-
const
|
|
112
|
+
const currentSpeedMBs = windowSec >= 0.3 ? (windowBytes / (1024 * 1024)) / windowSec : avgSpeedMBs;
|
|
113
|
+
const rawSpeed = currentSpeedMBs > 0.05 ? currentSpeedMBs : avgSpeedMBs;
|
|
114
|
+
|
|
115
|
+
// Apply Exponential Moving Average (EMA, alpha = 0.20) to filter micro-pauses and jitter
|
|
116
|
+
if (!this._emaSpeed || this._emaSpeed === 0) {
|
|
117
|
+
this._emaSpeed = rawSpeed;
|
|
118
|
+
} else {
|
|
119
|
+
this._emaSpeed = (0.20 * rawSpeed) + (0.80 * this._emaSpeed);
|
|
120
|
+
}
|
|
121
|
+
const effectiveSpeed = this._emaSpeed;
|
|
122
|
+
|
|
123
|
+
// 2. Real wire network bytes speed history (streaming sockets)
|
|
124
|
+
const netBytes = this.networkBytesDone > 0 ? this.networkBytesDone : this.bytesDone;
|
|
125
|
+
this._netSpeedHistory.push({ time: now, bytes: netBytes });
|
|
126
|
+
while (this._netSpeedHistory.length > 2 && (now - this._netSpeedHistory[0].time) > 2000) {
|
|
127
|
+
this._netSpeedHistory.shift();
|
|
128
|
+
}
|
|
129
|
+
const netOldest = this._netSpeedHistory[0];
|
|
130
|
+
const netWindowSec = Math.max(0.001, (now - netOldest.time) / 1000);
|
|
131
|
+
const netWindowBytes = Math.max(0, netBytes - netOldest.bytes);
|
|
132
|
+
const netAvgSpeedMBs = (netBytes / (1024 * 1024)) / elapsedSec;
|
|
133
|
+
const netInstantSpeedMBs = netWindowSec >= 0.3 ? (netWindowBytes / (1024 * 1024)) / netWindowSec : netAvgSpeedMBs;
|
|
134
|
+
const netRawSpeed = netInstantSpeedMBs > 0.05 ? netInstantSpeedMBs : netAvgSpeedMBs;
|
|
135
|
+
|
|
136
|
+
// Smooth network wire rate with Exponential Moving Average (EMA, alpha = 0.20)
|
|
137
|
+
if (!this._emaNetSpeed || this._emaNetSpeed === 0) {
|
|
138
|
+
this._emaNetSpeed = netRawSpeed;
|
|
139
|
+
} else {
|
|
140
|
+
this._emaNetSpeed = (0.20 * netRawSpeed) + (0.80 * this._emaNetSpeed);
|
|
141
|
+
}
|
|
142
|
+
const networkSpeedMBs = this._emaNetSpeed;
|
|
106
143
|
|
|
107
144
|
const percent = this.totalBytes > 0 ? ((this.bytesDone / this.totalBytes) * 100).toFixed(1) : '0.0';
|
|
108
145
|
|
|
146
|
+
const activeSpeed = (this.role === 'receiver' && networkSpeedMBs > 0) ? networkSpeedMBs : effectiveSpeed;
|
|
109
147
|
let etaStr = '--:--';
|
|
110
|
-
if (
|
|
148
|
+
if (activeSpeed > 0 && this.totalBytes > this.bytesDone) {
|
|
111
149
|
const remainingBytes = this.totalBytes - this.bytesDone;
|
|
112
|
-
const remainingSec = Math.round(remainingBytes / (
|
|
150
|
+
const remainingSec = Math.round(remainingBytes / (activeSpeed * 1024 * 1024));
|
|
113
151
|
const mm = String(Math.floor(remainingSec / 60)).padStart(2, '0');
|
|
114
152
|
const ss = String(remainingSec % 60).padStart(2, '0');
|
|
115
153
|
etaStr = `${mm}:${ss}`;
|
|
@@ -117,7 +155,7 @@ export class ProgressReporter {
|
|
|
117
155
|
|
|
118
156
|
let lines;
|
|
119
157
|
if (this.role === 'receiver') {
|
|
120
|
-
const diskStr =
|
|
158
|
+
const diskStr = `${effectiveSpeed.toFixed(1)} MB/s`;
|
|
121
159
|
lines = [
|
|
122
160
|
'\x1b[1m\x1b[36mTeyyare [Download]\x1b[0m',
|
|
123
161
|
'',
|
|
@@ -126,7 +164,7 @@ export class ProgressReporter {
|
|
|
126
164
|
`${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
|
|
127
165
|
`\x1b[32m${percent}%\x1b[0m`,
|
|
128
166
|
'',
|
|
129
|
-
`network speed \x1b[1m${
|
|
167
|
+
`network speed \x1b[1m${networkSpeedMBs.toFixed(1)} MB/s\x1b[0m`,
|
|
130
168
|
`disk speed \x1b[1m${diskStr}\x1b[0m`,
|
|
131
169
|
`channels ${this.channels}`,
|
|
132
170
|
`ETA ${etaStr}`,
|
|
@@ -167,7 +205,8 @@ export class ProgressReporter {
|
|
|
167
205
|
process.stdout.write(lines.join('\n') + '\n');
|
|
168
206
|
this._linesRendered = lines.length;
|
|
169
207
|
} else {
|
|
170
|
-
|
|
208
|
+
const reportSpeed = (this.role === 'receiver' && networkSpeedMBs > 0) ? networkSpeedMBs : effectiveSpeed;
|
|
209
|
+
process.stdout.write(`[Teyyare] ${percent}% | ${reportSpeed.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks || this.chunksDone} | ETA: ${etaStr}\n`);
|
|
171
210
|
}
|
|
172
211
|
}
|
|
173
212
|
}
|
package/src/index.js
CHANGED
|
@@ -51,6 +51,10 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
|
51
51
|
const headerScratch = Buffer.allocUnsafe(HEADER_SIZE);
|
|
52
52
|
|
|
53
53
|
socket.on('data', (chunk) => {
|
|
54
|
+
this.bytesReceived = (this.bytesReceived || 0) + chunk.length;
|
|
55
|
+
if (this.onDataBytes) {
|
|
56
|
+
this.onDataBytes(chunk.length);
|
|
57
|
+
}
|
|
54
58
|
chunks.push(chunk);
|
|
55
59
|
bufferedLength += chunk.length;
|
|
56
60
|
|
|
@@ -180,11 +184,19 @@ TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
|
180
184
|
}
|
|
181
185
|
});
|
|
182
186
|
|
|
187
|
+
socket.on('end', () => {
|
|
188
|
+
socket.destroy();
|
|
189
|
+
});
|
|
190
|
+
|
|
183
191
|
socket.on('error', () => {
|
|
184
192
|
socket.destroy();
|
|
185
193
|
});
|
|
186
194
|
};
|
|
187
195
|
|
|
196
|
+
function isSocketReady(s) {
|
|
197
|
+
return Boolean(s && !s.destroyed && s.writable && !s.readableEnded && !s.writableEnded && s.readyState === 'open');
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
TCPTransport.prototype.connectChannels = async function(peer, channelCount = 4) {
|
|
189
201
|
if (channelCount <= 1) return;
|
|
190
202
|
const addr = this.peerAddresses.get(peer);
|
|
@@ -193,7 +205,7 @@ TCPTransport.prototype.connectChannels = async function(peer, channelCount = 4)
|
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
const pool = this._getPeerPool(peer);
|
|
196
|
-
const activeSockets = pool.filter(
|
|
208
|
+
const activeSockets = pool.filter(isSocketReady);
|
|
197
209
|
const needed = channelCount - activeSockets.length;
|
|
198
210
|
if (needed <= 0) return;
|
|
199
211
|
|
|
@@ -243,30 +255,75 @@ TCPTransport.prototype.connectChannels = async function(peer, channelCount = 4)
|
|
|
243
255
|
await new Promise((r) => setTimeout(r, 20));
|
|
244
256
|
};
|
|
245
257
|
|
|
258
|
+
const origGetOrCreate = TCPTransport.prototype._getOrCreateSocket;
|
|
259
|
+
TCPTransport.prototype._getOrCreateSocket = function(peer) {
|
|
260
|
+
if (!this.started) {
|
|
261
|
+
return Promise.reject(new Error(`[TCPTransport ${this.id}] Cannot connect: transport closed`));
|
|
262
|
+
}
|
|
263
|
+
return origGetOrCreate.call(this, peer).then((socket) => {
|
|
264
|
+
const pool = this._getPeerPool(peer);
|
|
265
|
+
if (!pool.includes(socket)) {
|
|
266
|
+
pool.push(socket);
|
|
267
|
+
}
|
|
268
|
+
return socket;
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
246
272
|
TCPTransport.prototype.sendv = function(peer, buffers) {
|
|
273
|
+
if (!this.started) return;
|
|
247
274
|
if (!this._channelIndex) this._channelIndex = new Map();
|
|
248
275
|
|
|
249
276
|
const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
|
|
277
|
+
if (pool && pool.length > 0) {
|
|
278
|
+
// Purge dead sockets from pool
|
|
279
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
280
|
+
if (!isSocketReady(pool[i])) {
|
|
281
|
+
pool.splice(i, 1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
if (pool && pool.length > 0) {
|
|
251
287
|
let idx = this._channelIndex.get(peer) || 0;
|
|
252
288
|
const poolLen = pool.length;
|
|
289
|
+
let bestSocket = null;
|
|
290
|
+
let bestWritableLength = Infinity;
|
|
291
|
+
let bestIndex = idx;
|
|
292
|
+
|
|
253
293
|
for (let attempts = 0; attempts < poolLen; attempts++) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
s.
|
|
258
|
-
|
|
259
|
-
|
|
294
|
+
const candidateIdx = (idx + attempts) % poolLen;
|
|
295
|
+
const s = pool[candidateIdx];
|
|
296
|
+
if (isSocketReady(s)) {
|
|
297
|
+
const qLen = s.writableLength || 0;
|
|
298
|
+
// Priority 1: Pick idle/drained socket immediately
|
|
299
|
+
if (qLen === 0) {
|
|
300
|
+
bestSocket = s;
|
|
301
|
+
bestIndex = candidateIdx;
|
|
302
|
+
break;
|
|
260
303
|
}
|
|
261
|
-
|
|
262
|
-
|
|
304
|
+
// Priority 2: Track least-buffered socket to prevent head-of-line convoy stalls
|
|
305
|
+
if (qLen < bestWritableLength) {
|
|
306
|
+
bestWritableLength = qLen;
|
|
307
|
+
bestSocket = s;
|
|
308
|
+
bestIndex = candidateIdx;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (bestSocket) {
|
|
314
|
+
this._channelIndex.set(peer, (bestIndex + 1) % poolLen);
|
|
315
|
+
bestSocket.cork();
|
|
316
|
+
for (let i = 0; i < buffers.length; i++) {
|
|
317
|
+
bestSocket.write(buffers[i]);
|
|
263
318
|
}
|
|
319
|
+
bestSocket.uncork();
|
|
320
|
+
return;
|
|
264
321
|
}
|
|
265
322
|
}
|
|
266
323
|
|
|
267
324
|
this._getOrCreateSocket(peer)
|
|
268
325
|
.then((socket) => {
|
|
269
|
-
if (
|
|
326
|
+
if (isSocketReady(socket)) {
|
|
270
327
|
socket.cork();
|
|
271
328
|
for (let i = 0; i < buffers.length; i++) {
|
|
272
329
|
socket.write(buffers[i]);
|
|
@@ -278,25 +335,56 @@ TCPTransport.prototype.sendv = function(peer, buffers) {
|
|
|
278
335
|
};
|
|
279
336
|
|
|
280
337
|
TCPTransport.prototype.send = function(peer, buffer) {
|
|
338
|
+
if (!this.started) return;
|
|
281
339
|
if (!this._channelIndex) this._channelIndex = new Map();
|
|
282
340
|
|
|
283
341
|
const pool = this._peerSocketPool ? this._peerSocketPool.get(peer) : null;
|
|
342
|
+
if (pool && pool.length > 0) {
|
|
343
|
+
// Purge dead sockets from pool
|
|
344
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
345
|
+
if (!isSocketReady(pool[i])) {
|
|
346
|
+
pool.splice(i, 1);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
284
351
|
if (pool && pool.length > 0) {
|
|
285
352
|
let idx = this._channelIndex.get(peer) || 0;
|
|
286
353
|
const poolLen = pool.length;
|
|
354
|
+
let bestSocket = null;
|
|
355
|
+
let bestWritableLength = Infinity;
|
|
356
|
+
let bestIndex = idx;
|
|
357
|
+
|
|
287
358
|
for (let attempts = 0; attempts < poolLen; attempts++) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
s.
|
|
292
|
-
|
|
359
|
+
const candidateIdx = (idx + attempts) % poolLen;
|
|
360
|
+
const s = pool[candidateIdx];
|
|
361
|
+
if (isSocketReady(s)) {
|
|
362
|
+
const qLen = s.writableLength || 0;
|
|
363
|
+
// Priority 1: Pick idle/drained socket immediately
|
|
364
|
+
if (qLen === 0) {
|
|
365
|
+
bestSocket = s;
|
|
366
|
+
bestIndex = candidateIdx;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
// Priority 2: Track least-buffered socket to prevent head-of-line convoy stalls
|
|
370
|
+
if (qLen < bestWritableLength) {
|
|
371
|
+
bestWritableLength = qLen;
|
|
372
|
+
bestSocket = s;
|
|
373
|
+
bestIndex = candidateIdx;
|
|
374
|
+
}
|
|
293
375
|
}
|
|
294
376
|
}
|
|
377
|
+
|
|
378
|
+
if (bestSocket) {
|
|
379
|
+
this._channelIndex.set(peer, (bestIndex + 1) % poolLen);
|
|
380
|
+
bestSocket.write(buffer);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
295
383
|
}
|
|
296
384
|
|
|
297
385
|
this._getOrCreateSocket(peer)
|
|
298
386
|
.then((socket) => {
|
|
299
|
-
if (
|
|
387
|
+
if (isSocketReady(socket)) {
|
|
300
388
|
socket.write(buffer);
|
|
301
389
|
}
|
|
302
390
|
})
|
|
@@ -305,6 +393,7 @@ TCPTransport.prototype.send = function(peer, buffer) {
|
|
|
305
393
|
|
|
306
394
|
const origClose = TCPTransport.prototype.close;
|
|
307
395
|
TCPTransport.prototype.close = async function() {
|
|
396
|
+
this.started = false;
|
|
308
397
|
if (this._peerSocketPool) {
|
|
309
398
|
for (const pool of this._peerSocketPool.values()) {
|
|
310
399
|
for (const socket of pool) {
|
|
@@ -503,6 +592,7 @@ export async function serve({
|
|
|
503
592
|
await s.abort();
|
|
504
593
|
}
|
|
505
594
|
replication.reset();
|
|
595
|
+
receiver.reset();
|
|
506
596
|
};
|
|
507
597
|
|
|
508
598
|
replication.onBatch(async (msg) => {
|
|
@@ -551,9 +641,18 @@ export async function serve({
|
|
|
551
641
|
}));
|
|
552
642
|
}
|
|
553
643
|
|
|
644
|
+
// Detect peer channels for optimal pipeline scaling
|
|
645
|
+
const pool = transport._peerSocketPool ? transport._peerSocketPool.get(entries[0].sourceNode || 1) : null;
|
|
646
|
+
const detectedChannels = pool && pool.length > 0 ? pool.length : 1;
|
|
647
|
+
const effectiveBatchBytes = maxBytes || ((chunkSize || 1024 * 1024) * (maxEntries || 8));
|
|
648
|
+
|
|
649
|
+
// Dynamically scale replication in-flight window for remote pull client
|
|
650
|
+
replication.maxInflightBytes = Math.max(128 * 1024 * 1024, detectedChannels * effectiveBatchBytes * 3);
|
|
651
|
+
replication.maxInflightBatches = Math.max(32, detectedChannels * 4);
|
|
652
|
+
|
|
554
653
|
const senderOpts = {
|
|
555
|
-
maxImmutableBuffers: 32,
|
|
556
|
-
maxConcurrentFlushes: 32
|
|
654
|
+
maxImmutableBuffers: Math.max(32, detectedChannels * 4),
|
|
655
|
+
maxConcurrentFlushes: Math.max(32, detectedChannels * 4)
|
|
557
656
|
};
|
|
558
657
|
if (chunkSize) senderOpts.chunkSize = chunkSize;
|
|
559
658
|
if (maxEntries) senderOpts.maxEntries = maxEntries;
|
|
@@ -647,20 +746,46 @@ export async function send({
|
|
|
647
746
|
await transport.connectChannels(2, channels);
|
|
648
747
|
}
|
|
649
748
|
|
|
749
|
+
const isRemote = host !== '127.0.0.1' && host !== 'localhost';
|
|
750
|
+
const defaultChunkSize = isRemote ? 2 * 1024 * 1024 : 1024 * 1024;
|
|
751
|
+
const defaultMaxEntries = 8;
|
|
752
|
+
const defaultMaxBytes = defaultChunkSize * defaultMaxEntries;
|
|
753
|
+
|
|
754
|
+
const effectiveChunkSize = pipelineOptions.chunkSize || defaultChunkSize;
|
|
755
|
+
const effectiveMaxEntries = pipelineOptions.maxEntries || defaultMaxEntries;
|
|
756
|
+
const effectiveMaxBytes = pipelineOptions.maxBytes || defaultMaxBytes;
|
|
757
|
+
|
|
758
|
+
const scaledInflightBytes = pipelineOptions.maxInflightBytes || Math.max(128 * 1024 * 1024, channels * effectiveMaxBytes * 3);
|
|
759
|
+
const scaledInflightBatches = pipelineOptions.maxInflightBatches || Math.max(32, channels * 4);
|
|
760
|
+
|
|
650
761
|
const replication = new ReplicationPipeline({
|
|
651
762
|
transport,
|
|
652
763
|
localNode: 1,
|
|
653
764
|
remoteNode: 2,
|
|
654
|
-
maxInflightBatches:
|
|
655
|
-
maxInflightBytes:
|
|
765
|
+
maxInflightBatches: scaledInflightBatches,
|
|
766
|
+
maxInflightBytes: scaledInflightBytes
|
|
656
767
|
});
|
|
657
768
|
|
|
658
769
|
const manifest = await TransferManifest.build(sourcePath);
|
|
659
770
|
|
|
771
|
+
const effectiveSenderOptions = {
|
|
772
|
+
chunkSize: effectiveChunkSize,
|
|
773
|
+
maxEntries: effectiveMaxEntries,
|
|
774
|
+
maxBytes: effectiveMaxBytes,
|
|
775
|
+
...pipelineOptions
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
if (!effectiveSenderOptions.maxImmutableBuffers && channels > 1) {
|
|
779
|
+
effectiveSenderOptions.maxImmutableBuffers = Math.max(32, channels * 4);
|
|
780
|
+
}
|
|
781
|
+
if (!effectiveSenderOptions.maxConcurrentFlushes && channels > 1) {
|
|
782
|
+
effectiveSenderOptions.maxConcurrentFlushes = effectiveSenderOptions.maxImmutableBuffers;
|
|
783
|
+
}
|
|
784
|
+
|
|
660
785
|
const sender = new SenderPipeline({
|
|
661
786
|
replication,
|
|
662
787
|
manifest,
|
|
663
|
-
options:
|
|
788
|
+
options: effectiveSenderOptions,
|
|
664
789
|
metrics,
|
|
665
790
|
progress,
|
|
666
791
|
knownResumeState
|
|
@@ -669,6 +794,7 @@ export async function send({
|
|
|
669
794
|
try {
|
|
670
795
|
await sender.transfer();
|
|
671
796
|
} finally {
|
|
797
|
+
if (progress) progress.stop();
|
|
672
798
|
await sender.abort();
|
|
673
799
|
metrics.stop();
|
|
674
800
|
await transport.close();
|
|
@@ -727,12 +853,15 @@ export async function pull({
|
|
|
727
853
|
await transport.connectChannels(2, channels);
|
|
728
854
|
}
|
|
729
855
|
|
|
856
|
+
const scaledInflightBytes = pipelineOptions.maxInflightBytes || Math.max(128 * 1024 * 1024, channels * effectiveMaxBytes * 3);
|
|
857
|
+
const scaledInflightBatches = pipelineOptions.maxInflightBatches || Math.max(32, channels * 4);
|
|
858
|
+
|
|
730
859
|
const replication = new ReplicationPipeline({
|
|
731
860
|
transport,
|
|
732
861
|
localNode: 1,
|
|
733
862
|
remoteNode: 2,
|
|
734
|
-
maxInflightBatches:
|
|
735
|
-
maxInflightBytes:
|
|
863
|
+
maxInflightBatches: scaledInflightBatches,
|
|
864
|
+
maxInflightBytes: scaledInflightBytes
|
|
736
865
|
});
|
|
737
866
|
|
|
738
867
|
const receiver = new ReceiverPipeline({
|
package/src/metrics/collector.js
CHANGED
package/src/receiver/pipeline.js
CHANGED
|
@@ -52,6 +52,18 @@ export class ReceiverPipeline {
|
|
|
52
52
|
this._batchQueue = [];
|
|
53
53
|
this._processingQueue = false;
|
|
54
54
|
|
|
55
|
+
this.networkBytesReceived = 0;
|
|
56
|
+
if (this.replication && this.replication.transport) {
|
|
57
|
+
const prevOnDataBytes = this.replication.transport.onDataBytes;
|
|
58
|
+
this.replication.transport.onDataBytes = (bytes) => {
|
|
59
|
+
if (prevOnDataBytes) prevOnDataBytes(bytes);
|
|
60
|
+
this.networkBytesReceived += bytes;
|
|
61
|
+
if (this.progress) {
|
|
62
|
+
this.progress.update({ networkBytesDone: this.networkBytesReceived });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
// Hook up Raptiye batch handler if autoHookBatch is true
|
|
56
68
|
if (this.replication && autoHookBatch) {
|
|
57
69
|
this.replication.onBatch(async (msg) => {
|
|
@@ -100,6 +112,24 @@ export class ReceiverPipeline {
|
|
|
100
112
|
}
|
|
101
113
|
}
|
|
102
114
|
|
|
115
|
+
reset() {
|
|
116
|
+
this.currentTransferId = null;
|
|
117
|
+
this.transferFinished = false;
|
|
118
|
+
this.transferError = null;
|
|
119
|
+
this._pendingTransferEnd = false;
|
|
120
|
+
this.expectedFileCount = null;
|
|
121
|
+
this._completedFileCount = 0;
|
|
122
|
+
this.completedFiles.clear();
|
|
123
|
+
this.activeFiles.clear();
|
|
124
|
+
this._pendingChunks.clear();
|
|
125
|
+
this._pendingFileEnds.clear();
|
|
126
|
+
this._batchQueue = [];
|
|
127
|
+
this._processingQueue = false;
|
|
128
|
+
this.networkBytesReceived = 0;
|
|
129
|
+
this._finishResolvers = [];
|
|
130
|
+
this._errorRejecters = [];
|
|
131
|
+
}
|
|
132
|
+
|
|
103
133
|
async waitForCompletion() {
|
|
104
134
|
if (this.transferError) throw this.transferError;
|
|
105
135
|
if (this.transferFinished) return;
|
|
@@ -421,7 +451,7 @@ export class ReceiverPipeline {
|
|
|
421
451
|
}
|
|
422
452
|
|
|
423
453
|
// 2. Sort by offset to form maximal contiguous runs
|
|
424
|
-
toWrite.sort((a, b) =>
|
|
454
|
+
toWrite.sort((a, b) => (a.meta.offset < b.meta.offset ? -1 : (a.meta.offset > b.meta.offset ? 1 : 0)));
|
|
425
455
|
|
|
426
456
|
let currentRun = [toWrite[0]];
|
|
427
457
|
let currentStartOffset = Number(toWrite[0].meta.offset);
|
|
@@ -484,9 +514,6 @@ export class ReceiverPipeline {
|
|
|
484
514
|
|
|
485
515
|
// 3. Update progress reporter
|
|
486
516
|
if (this.progress) {
|
|
487
|
-
const diskTimeSec = Math.max(0.001, (this.metrics ? this.metrics.diskWriteTimeMs : 1) / 1000);
|
|
488
|
-
const writtenMB = (this.metrics ? this.metrics.fileBytesWritten : 0) / (1024 * 1024);
|
|
489
|
-
const diskMBs = Number((writtenMB / diskTimeSec).toFixed(1));
|
|
490
517
|
for (const item of toWrite) {
|
|
491
518
|
file.maxChunkSize = Math.max(file.maxChunkSize || 0, item.meta.length);
|
|
492
519
|
}
|
|
@@ -496,8 +523,7 @@ export class ReceiverPipeline {
|
|
|
496
523
|
bytesDone: this.metrics ? this.metrics.fileBytesWritten : 0,
|
|
497
524
|
chunksDone: this.metrics ? this.metrics.chunksVerified : 0,
|
|
498
525
|
totalChunks: realTotalChunks > 0 ? realTotalChunks : undefined,
|
|
499
|
-
resumeHits: this.metrics ? this.metrics.resumeHits : 0
|
|
500
|
-
diskSpeedMBs: diskMBs
|
|
526
|
+
resumeHits: this.metrics ? this.metrics.resumeHits : 0
|
|
501
527
|
});
|
|
502
528
|
}
|
|
503
529
|
|
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
|
}
|