teyyare 0.2.1 → 0.2.3
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/bin/teyyare.js +8 -1
- package/package.json +1 -1
- package/src/cli/progress.js +72 -28
- package/src/index.js +126 -4
- package/src/receiver/pipeline.js +64 -3
- package/src/sender/pipeline.js +6 -3
package/bin/teyyare.js
CHANGED
|
@@ -50,6 +50,11 @@ async function main() {
|
|
|
50
50
|
process.exit(0);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
54
|
+
console.log('0.2.3');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
if (command === 'serve') {
|
|
54
59
|
let port = 7421;
|
|
55
60
|
let dir = process.cwd();
|
|
@@ -105,6 +110,7 @@ Backpressure: ${snap.backpressureCount}
|
|
|
105
110
|
Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
|
|
106
111
|
Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
107
112
|
`);
|
|
113
|
+
process.exit(0);
|
|
108
114
|
} else if (command === 'pull') {
|
|
109
115
|
const source = args[1];
|
|
110
116
|
const target = args[2] || './';
|
|
@@ -135,8 +141,9 @@ Backpressure: ${snap.backpressureCount}
|
|
|
135
141
|
Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
|
|
136
142
|
Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
137
143
|
`);
|
|
144
|
+
process.exit(0);
|
|
138
145
|
} else if (command === 'status') {
|
|
139
|
-
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.
|
|
146
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.3 status: ready');
|
|
140
147
|
} else {
|
|
141
148
|
console.error(`Unknown command: ${command}`);
|
|
142
149
|
printUsage();
|
package/package.json
CHANGED
package/src/cli/progress.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Throttled terminal progress renderer.
|
|
3
|
+
* Intelligent, accurate metrics for both Upload (send) and Download (pull) modes.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
export class ProgressReporter {
|
|
@@ -7,10 +8,12 @@ export class ProgressReporter {
|
|
|
7
8
|
* @param {object} [options]
|
|
8
9
|
* @param {number} [options.intervalMs=150]
|
|
9
10
|
* @param {boolean} [options.enabled=true]
|
|
11
|
+
* @param {'sender'|'receiver'} [options.role='sender']
|
|
10
12
|
*/
|
|
11
|
-
constructor({ intervalMs = 150, enabled = true } = {}) {
|
|
13
|
+
constructor({ intervalMs = 150, enabled = true, role = 'sender' } = {}) {
|
|
12
14
|
this.intervalMs = intervalMs;
|
|
13
15
|
this.enabled = enabled && Boolean(process.stdout.isTTY || process.env.CI !== 'true');
|
|
16
|
+
this.role = role;
|
|
14
17
|
|
|
15
18
|
this.fileName = '';
|
|
16
19
|
this.bytesDone = 0;
|
|
@@ -23,16 +26,20 @@ export class ProgressReporter {
|
|
|
23
26
|
this.muttafaQueue = 0;
|
|
24
27
|
this.raptiyeRTT = 0;
|
|
25
28
|
this.tcpPressure = 0;
|
|
29
|
+
this.resumeHits = 0;
|
|
30
|
+
this.diskSpeedMBs = 0;
|
|
26
31
|
|
|
27
32
|
this.startTime = Date.now();
|
|
28
33
|
this.lastRender = 0;
|
|
29
34
|
this._timer = null;
|
|
30
35
|
this._linesRendered = 0;
|
|
36
|
+
this._speedHistory = [];
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
start() {
|
|
34
40
|
if (!this.enabled) return;
|
|
35
41
|
this.startTime = Date.now();
|
|
42
|
+
this._speedHistory = [{ time: this.startTime, bytes: this.bytesDone }];
|
|
36
43
|
this._timer = setInterval(() => this.render(), this.intervalMs);
|
|
37
44
|
}
|
|
38
45
|
|
|
@@ -52,13 +59,15 @@ export class ProgressReporter {
|
|
|
52
59
|
if (state.bytesDone !== undefined) this.bytesDone = state.bytesDone;
|
|
53
60
|
if (state.totalBytes !== undefined) this.totalBytes = state.totalBytes;
|
|
54
61
|
if (state.chunksDone !== undefined) this.chunksDone = state.chunksDone;
|
|
55
|
-
if (state.totalChunks !== undefined) this.totalChunks = state.totalChunks;
|
|
62
|
+
if (state.totalChunks !== undefined && state.totalChunks > 0) this.totalChunks = state.totalChunks;
|
|
56
63
|
if (state.generationId !== undefined) this.generationId = state.generationId;
|
|
57
64
|
if (state.inflightGen !== undefined) this.inflightGen = state.inflightGen;
|
|
58
65
|
if (state.inflightBytes !== undefined) this.inflightBytes = state.inflightBytes;
|
|
59
66
|
if (state.muttafaQueue !== undefined) this.muttafaQueue = state.muttafaQueue;
|
|
60
67
|
if (state.raptiyeRTT !== undefined) this.raptiyeRTT = state.raptiyeRTT;
|
|
61
68
|
if (state.tcpPressure !== undefined) this.tcpPressure = state.tcpPressure;
|
|
69
|
+
if (state.resumeHits !== undefined) this.resumeHits = state.resumeHits;
|
|
70
|
+
if (state.diskSpeedMBs !== undefined) this.diskSpeedMBs = state.diskSpeedMBs;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
formatBytes(bytes) {
|
|
@@ -77,39 +86,75 @@ export class ProgressReporter {
|
|
|
77
86
|
render() {
|
|
78
87
|
if (!this.enabled) return;
|
|
79
88
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const elapsedSec = Math.max(0.001, (now - this.startTime) / 1000);
|
|
91
|
+
|
|
92
|
+
// Track sliding window (last 2 seconds) for smooth, responsive current speed
|
|
93
|
+
this._speedHistory.push({ time: now, bytes: this.bytesDone });
|
|
94
|
+
while (this._speedHistory.length > 2 && (now - this._speedHistory[0].time) > 2000) {
|
|
95
|
+
this._speedHistory.shift();
|
|
96
|
+
}
|
|
97
|
+
const oldest = this._speedHistory[0];
|
|
98
|
+
const windowSec = Math.max(0.001, (now - oldest.time) / 1000);
|
|
99
|
+
const windowBytes = Math.max(0, this.bytesDone - oldest.bytes);
|
|
100
|
+
|
|
101
|
+
const avgSpeedMBs = (this.bytesDone / (1024 * 1024)) / elapsedSec;
|
|
102
|
+
const currentSpeedMBs = windowSec >= 0.4 ? (windowBytes / (1024 * 1024)) / windowSec : avgSpeedMBs;
|
|
103
|
+
const effectiveSpeed = currentSpeedMBs > 0.05 ? currentSpeedMBs : avgSpeedMBs;
|
|
104
|
+
|
|
82
105
|
const percent = this.totalBytes > 0 ? ((this.bytesDone / this.totalBytes) * 100).toFixed(1) : '0.0';
|
|
83
106
|
|
|
84
107
|
let etaStr = '--:--';
|
|
85
|
-
if (
|
|
108
|
+
if (effectiveSpeed > 0 && this.totalBytes > this.bytesDone) {
|
|
86
109
|
const remainingBytes = this.totalBytes - this.bytesDone;
|
|
87
|
-
const remainingSec = Math.round(remainingBytes / (
|
|
110
|
+
const remainingSec = Math.round(remainingBytes / (effectiveSpeed * 1024 * 1024));
|
|
88
111
|
const mm = String(Math.floor(remainingSec / 60)).padStart(2, '0');
|
|
89
112
|
const ss = String(remainingSec % 60).padStart(2, '0');
|
|
90
113
|
etaStr = `${mm}:${ss}`;
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
let lines;
|
|
117
|
+
if (this.role === 'receiver') {
|
|
118
|
+
const diskStr = this.diskSpeedMBs > 0 ? `${this.diskSpeedMBs.toFixed(1)} MB/s` : `${effectiveSpeed.toFixed(1)} MB/s`;
|
|
119
|
+
lines = [
|
|
120
|
+
'\x1b[1m\x1b[36mTeyyare [Download]\x1b[0m',
|
|
121
|
+
'',
|
|
122
|
+
`\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
|
|
123
|
+
'',
|
|
124
|
+
`${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
|
|
125
|
+
`\x1b[32m${percent}%\x1b[0m`,
|
|
126
|
+
'',
|
|
127
|
+
`network speed \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
|
|
128
|
+
`disk speed \x1b[1m${diskStr}\x1b[0m`,
|
|
129
|
+
`ETA ${etaStr}`,
|
|
130
|
+
'',
|
|
131
|
+
`chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
|
|
132
|
+
`generation ${this.generationId}`,
|
|
133
|
+
`verified CRC32 ${this.chunksDone}`,
|
|
134
|
+
`resume hits ${this.resumeHits}`
|
|
135
|
+
];
|
|
136
|
+
} else {
|
|
137
|
+
lines = [
|
|
138
|
+
'\x1b[1m\x1b[36mTeyyare [Upload]\x1b[0m',
|
|
139
|
+
'',
|
|
140
|
+
`\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
|
|
141
|
+
'',
|
|
142
|
+
`${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
|
|
143
|
+
`\x1b[32m${percent}%\x1b[0m`,
|
|
144
|
+
'',
|
|
145
|
+
`throughput \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
|
|
146
|
+
`ETA ${etaStr}`,
|
|
147
|
+
'',
|
|
148
|
+
`chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
|
|
149
|
+
`generation ${this.generationId}`,
|
|
150
|
+
`inflight gen ${this.inflightGen}`,
|
|
151
|
+
`inflight bytes ${this.formatBytes(this.inflightBytes)}`,
|
|
152
|
+
'',
|
|
153
|
+
`muttafa queue ${this.muttafaQueue}`,
|
|
154
|
+
`raptiye RTT ${this.raptiyeRTT} ms`,
|
|
155
|
+
`TCP pressure ${this.tcpPressure}`
|
|
156
|
+
];
|
|
157
|
+
}
|
|
113
158
|
|
|
114
159
|
if (process.stdout.isTTY) {
|
|
115
160
|
if (this._linesRendered > 0) {
|
|
@@ -118,8 +163,7 @@ export class ProgressReporter {
|
|
|
118
163
|
process.stdout.write(lines.join('\n') + '\n');
|
|
119
164
|
this._linesRendered = lines.length;
|
|
120
165
|
} else {
|
|
121
|
-
|
|
122
|
-
process.stdout.write(`[Teyyare] ${percent}% | ${throughputMBs.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks} | ETA: ${etaStr}\n`);
|
|
166
|
+
process.stdout.write(`[Teyyare] ${percent}% | ${effectiveSpeed.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks || this.chunksDone} | ETA: ${etaStr}\n`);
|
|
123
167
|
}
|
|
124
168
|
}
|
|
125
169
|
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { TCPTransport, ReplicationPipeline } from 'raptiye';
|
|
8
|
+
import { readHeader, decodeMessage, HEADER_SIZE } from 'raptiye/src/protocol/wire.js';
|
|
8
9
|
import { SenderPipeline } from './sender/pipeline.js';
|
|
9
10
|
import { ReceiverPipeline } from './receiver/pipeline.js';
|
|
10
11
|
import { TransferManifest } from './manifest/manifest.js';
|
|
@@ -22,6 +23,116 @@ import {
|
|
|
22
23
|
unpackPullError
|
|
23
24
|
} from './core/protocol.js';
|
|
24
25
|
|
|
26
|
+
// High-performance streaming TCP framer patch for TCPTransport
|
|
27
|
+
// Eliminates O(N^2) Buffer.concat and ensures clean socket lifecycle management
|
|
28
|
+
TCPTransport.prototype._attachSocketReader = function(socket) {
|
|
29
|
+
socket.setNoDelay(true);
|
|
30
|
+
const chunks = [];
|
|
31
|
+
let bufferedLength = 0;
|
|
32
|
+
|
|
33
|
+
socket.on('data', (chunk) => {
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
bufferedLength += chunk.length;
|
|
36
|
+
|
|
37
|
+
while (bufferedLength >= HEADER_SIZE) {
|
|
38
|
+
let headerBuf = chunks[0];
|
|
39
|
+
if (headerBuf.length < HEADER_SIZE) {
|
|
40
|
+
const tempHeader = Buffer.allocUnsafe(HEADER_SIZE);
|
|
41
|
+
let off = 0;
|
|
42
|
+
for (let i = 0; off < HEADER_SIZE && i < chunks.length; i++) {
|
|
43
|
+
const c = chunks[i];
|
|
44
|
+
const toCopy = Math.min(c.length, HEADER_SIZE - off);
|
|
45
|
+
tempHeader.set(c.subarray(0, toCopy), off);
|
|
46
|
+
off += toCopy;
|
|
47
|
+
}
|
|
48
|
+
headerBuf = tempHeader;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let header;
|
|
52
|
+
try {
|
|
53
|
+
header = readHeader(headerBuf, 0);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
socket.destroy(err);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const totalLength = HEADER_SIZE + header.payloadLength;
|
|
60
|
+
if (bufferedLength < totalLength) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let rawFrame;
|
|
65
|
+
if (chunks[0].length >= totalLength) {
|
|
66
|
+
rawFrame = chunks[0].subarray(0, totalLength);
|
|
67
|
+
if (chunks[0].length === totalLength) {
|
|
68
|
+
chunks.shift();
|
|
69
|
+
} else {
|
|
70
|
+
chunks[0] = chunks[0].subarray(totalLength);
|
|
71
|
+
}
|
|
72
|
+
bufferedLength -= totalLength;
|
|
73
|
+
} else {
|
|
74
|
+
rawFrame = Buffer.allocUnsafe(totalLength);
|
|
75
|
+
let dstOff = 0;
|
|
76
|
+
let remaining = totalLength;
|
|
77
|
+
while (remaining > 0) {
|
|
78
|
+
const head = chunks[0];
|
|
79
|
+
if (head.length <= remaining) {
|
|
80
|
+
rawFrame.set(head, dstOff);
|
|
81
|
+
dstOff += head.length;
|
|
82
|
+
remaining -= head.length;
|
|
83
|
+
chunks.shift();
|
|
84
|
+
} else {
|
|
85
|
+
rawFrame.set(head.subarray(0, remaining), dstOff);
|
|
86
|
+
dstOff += remaining;
|
|
87
|
+
chunks[0] = head.subarray(remaining);
|
|
88
|
+
remaining = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
bufferedLength -= totalLength;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const frame = new Uint8Array(rawFrame.buffer, rawFrame.byteOffset, rawFrame.byteLength);
|
|
96
|
+
const msg = decodeMessage(frame, 0, true);
|
|
97
|
+
if (msg.sourceNode) {
|
|
98
|
+
this._outboundSockets.set(msg.sourceNode, socket);
|
|
99
|
+
}
|
|
100
|
+
if (this._messageHandler) {
|
|
101
|
+
this._messageHandler(msg.sourceNode, msg);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[TCPTransport ${this.id}] Error decoding frame:`, err.message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socket.on('close', () => {
|
|
110
|
+
this._inboundSockets.delete(socket);
|
|
111
|
+
for (const [peer, s] of this._outboundSockets.entries()) {
|
|
112
|
+
if (s === socket) {
|
|
113
|
+
this._outboundSockets.delete(peer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
socket.on('error', () => {
|
|
119
|
+
socket.destroy();
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Reset stalled inflight receipts on connection drop or before starting new transfer
|
|
124
|
+
ReplicationPipeline.prototype.reset = function(err = new Error('Replication pipeline reset')) {
|
|
125
|
+
for (const pending of this._pendingReceipts.values()) {
|
|
126
|
+
pending.reject(err);
|
|
127
|
+
}
|
|
128
|
+
this._pendingReceipts.clear();
|
|
129
|
+
this.inflightBatches = 0;
|
|
130
|
+
this.inflightBytes = 0;
|
|
131
|
+
this.stats.inflightBatches = 0;
|
|
132
|
+
this.stats.inflightBytes = 0;
|
|
133
|
+
this._notifyCapacity();
|
|
134
|
+
};
|
|
135
|
+
|
|
25
136
|
export {
|
|
26
137
|
SenderPipeline,
|
|
27
138
|
ReceiverPipeline,
|
|
@@ -108,16 +219,27 @@ export async function serve({
|
|
|
108
219
|
}
|
|
109
220
|
|
|
110
221
|
const manifest = await TransferManifest.build(resolved);
|
|
222
|
+
replication.reset();
|
|
111
223
|
const sender = new SenderPipeline({
|
|
112
224
|
replication,
|
|
113
225
|
manifest,
|
|
114
226
|
metrics
|
|
115
227
|
});
|
|
116
|
-
|
|
228
|
+
setImmediate(async () => {
|
|
229
|
+
try {
|
|
230
|
+
await sender.transfer();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
await submitSingleMutation(
|
|
233
|
+
replication,
|
|
234
|
+
Opcodes.PULL_ERROR,
|
|
235
|
+
packPullError({ errorCode: 500, message: err.message })
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
117
239
|
return;
|
|
118
240
|
}
|
|
119
241
|
|
|
120
|
-
await receiver.
|
|
242
|
+
await receiver.enqueueBatch(msg);
|
|
121
243
|
});
|
|
122
244
|
|
|
123
245
|
return {
|
|
@@ -152,7 +274,7 @@ export async function send({
|
|
|
152
274
|
knownResumeState = new Map()
|
|
153
275
|
}) {
|
|
154
276
|
const metrics = new MetricsCollector();
|
|
155
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
277
|
+
const progress = showProgress ? new ProgressReporter({ role: 'sender' }) : null;
|
|
156
278
|
|
|
157
279
|
// Ephemeral sender port
|
|
158
280
|
const senderPort = 10000 + Math.floor(Math.random() * 20000);
|
|
@@ -214,7 +336,7 @@ export async function pull({
|
|
|
214
336
|
showProgress = true
|
|
215
337
|
}) {
|
|
216
338
|
const metrics = new MetricsCollector();
|
|
217
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
339
|
+
const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
|
|
218
340
|
if (progress) progress.start();
|
|
219
341
|
|
|
220
342
|
const clientPort = 10000 + Math.floor(Math.random() * 20000);
|
package/src/receiver/pipeline.js
CHANGED
|
@@ -42,14 +42,58 @@ export class ReceiverPipeline {
|
|
|
42
42
|
this._finishResolvers = [];
|
|
43
43
|
this._errorRejecters = [];
|
|
44
44
|
|
|
45
|
+
// Processing queue to ensure strictly ordered, serialized generation processing
|
|
46
|
+
this._batchQueue = [];
|
|
47
|
+
this._processingQueue = false;
|
|
48
|
+
|
|
45
49
|
// Hook up Raptiye batch handler if autoHookBatch is true
|
|
46
50
|
if (this.replication && autoHookBatch) {
|
|
47
51
|
this.replication.onBatch(async (msg) => {
|
|
48
|
-
|
|
52
|
+
return this.enqueueBatch(msg);
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Enqueue incoming generation batch to guarantee strict FIFO sequential processing.
|
|
59
|
+
* @param {object} msg
|
|
60
|
+
* @returns {Promise<void>}
|
|
61
|
+
*/
|
|
62
|
+
enqueueBatch(msg) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
this._batchQueue.push({ msg, resolve, reject });
|
|
65
|
+
this._drainQueue();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _drainQueue() {
|
|
70
|
+
if (this._processingQueue) return;
|
|
71
|
+
this._processingQueue = true;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
while (this._batchQueue.length > 0) {
|
|
75
|
+
const item = this._batchQueue.shift();
|
|
76
|
+
try {
|
|
77
|
+
await this._processGenerationBatch(item.msg);
|
|
78
|
+
item.resolve();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
this.transferError = err;
|
|
81
|
+
for (const reject of this._errorRejecters) {
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
this._finishResolvers = [];
|
|
85
|
+
this._errorRejecters = [];
|
|
86
|
+
item.reject(err);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
this._processingQueue = false;
|
|
91
|
+
if (this._batchQueue.length > 0) {
|
|
92
|
+
this._drainQueue();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
async waitForCompletion() {
|
|
54
98
|
if (this.transferError) throw this.transferError;
|
|
55
99
|
if (this.transferFinished) return;
|
|
@@ -70,6 +114,12 @@ export class ReceiverPipeline {
|
|
|
70
114
|
this.metrics.generationBytes += (metaBuf.byteLength + payloadBuf.byteLength);
|
|
71
115
|
}
|
|
72
116
|
|
|
117
|
+
if (this.progress) {
|
|
118
|
+
this.progress.update({
|
|
119
|
+
generationId: msg.generationId || (this.metrics ? this.metrics.generations : 0)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
73
123
|
// Zero-copy unpacking of generation entries
|
|
74
124
|
const entries = unpackGeneration(metaBuf, payloadBuf);
|
|
75
125
|
const count = entries.length;
|
|
@@ -164,9 +214,12 @@ export class ReceiverPipeline {
|
|
|
164
214
|
});
|
|
165
215
|
|
|
166
216
|
if (this.progress) {
|
|
217
|
+
const fileSize = Number(info.fileSize);
|
|
218
|
+
const estimatedChunks = Math.max(1, Math.ceil(fileSize / (1024 * 1024)));
|
|
167
219
|
this.progress.update({
|
|
168
220
|
fileName: info.path,
|
|
169
|
-
totalBytes:
|
|
221
|
+
totalBytes: fileSize,
|
|
222
|
+
totalChunks: estimatedChunks
|
|
170
223
|
});
|
|
171
224
|
}
|
|
172
225
|
}
|
|
@@ -216,10 +269,18 @@ export class ReceiverPipeline {
|
|
|
216
269
|
}
|
|
217
270
|
|
|
218
271
|
if (this.progress) {
|
|
272
|
+
const diskTimeSec = Math.max(0.001, (this.metrics ? this.metrics.diskWriteTimeMs : 1) / 1000);
|
|
273
|
+
const writtenMB = (this.metrics ? this.metrics.fileBytesWritten : 0) / (1024 * 1024);
|
|
274
|
+
const diskMBs = Number((writtenMB / diskTimeSec).toFixed(1));
|
|
275
|
+
file.maxChunkSize = Math.max(file.maxChunkSize || 0, meta.length);
|
|
276
|
+
const realTotalChunks = file.maxChunkSize > 0 && file.fileSize > 0 ? Math.ceil(file.fileSize / file.maxChunkSize) : 0;
|
|
219
277
|
this.progress.update({
|
|
220
278
|
fileName: file.relativePath,
|
|
221
279
|
bytesDone: this.metrics ? this.metrics.fileBytesWritten : 0,
|
|
222
|
-
chunksDone: this.metrics ? this.metrics.chunksVerified : 0
|
|
280
|
+
chunksDone: this.metrics ? this.metrics.chunksVerified : 0,
|
|
281
|
+
totalChunks: realTotalChunks > 0 ? realTotalChunks : undefined,
|
|
282
|
+
resumeHits: this.metrics ? this.metrics.resumeHits : 0,
|
|
283
|
+
diskSpeedMBs: diskMBs
|
|
223
284
|
});
|
|
224
285
|
}
|
|
225
286
|
|
package/src/sender/pipeline.js
CHANGED
|
@@ -102,9 +102,10 @@ export class SenderPipeline {
|
|
|
102
102
|
this.replication = replication;
|
|
103
103
|
this.manifest = manifest;
|
|
104
104
|
this.chunkSize = options.chunkSize || 1024 * 1024;
|
|
105
|
-
this.maxEntries = options.maxEntries ||
|
|
106
|
-
this.maxBytes = options.maxBytes ||
|
|
107
|
-
this.maxImmutableBuffers = options.maxImmutableBuffers ||
|
|
105
|
+
this.maxEntries = options.maxEntries || 4;
|
|
106
|
+
this.maxBytes = options.maxBytes || 4 * 1024 * 1024;
|
|
107
|
+
this.maxImmutableBuffers = options.maxImmutableBuffers || 8;
|
|
108
|
+
this.maxConcurrentFlushes = options.maxConcurrentFlushes || Math.min(this.maxImmutableBuffers, 4);
|
|
108
109
|
this.metrics = metrics;
|
|
109
110
|
this.progress = progress;
|
|
110
111
|
this.knownResumeState = knownResumeState;
|
|
@@ -125,6 +126,8 @@ export class SenderPipeline {
|
|
|
125
126
|
maxEntries: this.maxEntries,
|
|
126
127
|
maxBytes: this.maxBytes,
|
|
127
128
|
maxImmutableBuffers: this.maxImmutableBuffers,
|
|
129
|
+
maxConcurrentFlushes: this.maxConcurrentFlushes,
|
|
130
|
+
ordered: true,
|
|
128
131
|
backpressurePolicy: BackpressurePolicy.WAIT,
|
|
129
132
|
onBackpressure: (active) => {
|
|
130
133
|
if (this.metrics && active) {
|