teyyare 0.2.1 → 0.2.2
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 +1 -1
- package/package.json +1 -1
- package/src/cli/progress.js +72 -28
- package/src/index.js +14 -4
- package/src/receiver/pipeline.js +64 -3
- package/src/sender/pipeline.js +4 -1
package/bin/teyyare.js
CHANGED
|
@@ -136,7 +136,7 @@ Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
|
|
|
136
136
|
Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
|
|
137
137
|
`);
|
|
138
138
|
} else if (command === 'status') {
|
|
139
|
-
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.
|
|
139
|
+
console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.2 status: ready');
|
|
140
140
|
} else {
|
|
141
141
|
console.error(`Unknown command: ${command}`);
|
|
142
142
|
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
|
@@ -113,11 +113,21 @@ export async function serve({
|
|
|
113
113
|
manifest,
|
|
114
114
|
metrics
|
|
115
115
|
});
|
|
116
|
-
|
|
116
|
+
setImmediate(async () => {
|
|
117
|
+
try {
|
|
118
|
+
await sender.transfer();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
await submitSingleMutation(
|
|
121
|
+
replication,
|
|
122
|
+
Opcodes.PULL_ERROR,
|
|
123
|
+
packPullError({ errorCode: 500, message: err.message })
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
117
127
|
return;
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
await receiver.
|
|
130
|
+
await receiver.enqueueBatch(msg);
|
|
121
131
|
});
|
|
122
132
|
|
|
123
133
|
return {
|
|
@@ -152,7 +162,7 @@ export async function send({
|
|
|
152
162
|
knownResumeState = new Map()
|
|
153
163
|
}) {
|
|
154
164
|
const metrics = new MetricsCollector();
|
|
155
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
165
|
+
const progress = showProgress ? new ProgressReporter({ role: 'sender' }) : null;
|
|
156
166
|
|
|
157
167
|
// Ephemeral sender port
|
|
158
168
|
const senderPort = 10000 + Math.floor(Math.random() * 20000);
|
|
@@ -214,7 +224,7 @@ export async function pull({
|
|
|
214
224
|
showProgress = true
|
|
215
225
|
}) {
|
|
216
226
|
const metrics = new MetricsCollector();
|
|
217
|
-
const progress = showProgress ? new ProgressReporter() : null;
|
|
227
|
+
const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
|
|
218
228
|
if (progress) progress.start();
|
|
219
229
|
|
|
220
230
|
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
|
@@ -104,7 +104,8 @@ export class SenderPipeline {
|
|
|
104
104
|
this.chunkSize = options.chunkSize || 1024 * 1024;
|
|
105
105
|
this.maxEntries = options.maxEntries || 8;
|
|
106
106
|
this.maxBytes = options.maxBytes || 8 * 1024 * 1024;
|
|
107
|
-
this.maxImmutableBuffers = options.maxImmutableBuffers ||
|
|
107
|
+
this.maxImmutableBuffers = options.maxImmutableBuffers || 8;
|
|
108
|
+
this.maxConcurrentFlushes = options.maxConcurrentFlushes || 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) {
|