Skip to content

Commit 004446d

Browse files
(feature) add a more detailed summary about the data transfer at the end
1 parent 94f1f5c commit 004446d

File tree

4 files changed

+127
-11
lines changed

4 files changed

+127
-11
lines changed

src/commands/recv.js

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { hardExitIfEnabled } from "../env/hard-exit.js";
1212
import { createLogger, getLogger, setGlobalLogger } from "../util/logger.js";
1313
import { sanitizeFilename } from "../util/sanitize.js";
1414
import { attachRawLogger } from "./raw_tap.js";
15-
import { humanBytes } from "../util/format.js";
15+
import { humanBytes, formatDuration } from "../util/format.js";
16+
import { formatSummary, humanThroughput } from "../util/transfer-summary.js";
1617
import { formatProgressLine } from "../util/progress.js";
1718
import { closeTransport, waitForPeerClose } from "../util/rtc.js";
1819

@@ -80,7 +81,7 @@ export async function run(outDir, opts, ctx = {}) {
8081

8182
let totalBytes = 0;
8283
let written = 0;
83-
let startedAt = 0;
84+
let startedAt = null;
8485

8586
const sink = makeSniffingSink({
8687
outToStdout,
@@ -159,8 +160,46 @@ export async function run(outDir, opts, ctx = {}) {
159160
await sink.close();
160161
await waitForPeerClose(rtc, 1500);
161162

162-
process.stderr.write("\nDone • " + humanBytes(written) + "\n");
163+
const completedAt = Date.now();
164+
const durationMs = startedAt != null ? Math.max(0, completedAt - startedAt) : 0;
165+
const throughputStr = humanThroughput(written, durationMs);
166+
const announced = Number.isFinite(stats.announced) ? stats.announced : null;
167+
let announcedValue = "—";
168+
if (announced != null) {
169+
const delta = announced - written;
170+
const deltaSuffix =
171+
delta === 0
172+
? ` (Δ ${humanBytes(0)})`
173+
: ` (Δ ${delta > 0 ? "+" : "-"}${humanBytes(Math.abs(delta))})`;
174+
announcedValue = `${humanBytes(announced)}${deltaSuffix}`;
175+
}
176+
177+
const destinationParts = [];
178+
if (stats.label) destinationParts.push(stats.label);
179+
if (stats.filePath) {
180+
destinationParts.push(stats.filePath);
181+
} else if (outToStdout) {
182+
destinationParts.push("stdout");
183+
}
184+
const destinationInfo =
185+
destinationParts.length > 0
186+
? destinationParts.join(" → ")
187+
: outToStdout
188+
? "stdout"
189+
: "(unknown)";
190+
191+
const summary = formatSummary({
192+
rows: [
193+
{ label: "Bytes", value: humanBytes(written) },
194+
{ label: "Duration", value: formatDuration(durationMs) },
195+
{ label: "Throughput", value: throughputStr },
196+
{ label: "Announced", value: announcedValue },
197+
{ label: "Destination", value: destinationInfo },
198+
],
199+
});
200+
process.stderr.write(summary + "\n");
163201
// Return a structured result for programmatic use
202+
164203
const stats = sink.getStats?.() || {};
165204
return {
166205
bytesWritten: written,

src/commands/send.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { hardExitIfEnabled } from "../env/hard-exit.js";
1616
import { createLogger, getLogger, setGlobalLogger } from "../util/logger.js";
1717
import { sanitizeFilename } from "../util/sanitize.js";
1818
import { attachRawLogger } from "./raw_tap.js";
19-
import { humanBytes } from "../util/format.js";
19+
import { humanBytes, formatDuration } from "../util/format.js";
20+
import { formatSummary, humanThroughput } from "../util/transfer-summary.js";
2021
import { formatProgressLine } from "../util/progress.js";
2122
import { closeTransport, waitForPeerClose } from "../util/rtc.js";
2223
import { MAX_STREAM_CHUNK_BYTES } from "../transfer/constants.js";
@@ -85,22 +86,27 @@ export async function run(paths, opts, ctx = {}) {
8586
sendNameHint = opts?.name ? String(opts.name) : `${stem}.tar`;
8687
getLogger().debug(`send: tarball name set to ${sendNameHint}`);
8788
}
88-
// Progress UI
89-
let startedAt = null;
89+
// Progress UI + metrics
90+
const commandStartedAt = Date.now();
91+
let transferStartedAt = null;
92+
let lastProgressAt = null;
9093
let lastTick = 0;
9194
function onProgress(sent, total) {
9295
getLogger().debug(`OnProgress: sent=${sent} total=${total}`);
9396
const now = Date.now();
94-
if (startedAt == null && sent > 0) {
95-
startedAt = now;
97+
if (transferStartedAt == null && sent > 0) {
98+
transferStartedAt = now;
99+
}
100+
if (sent > 0 || (total != null && sent >= total)) {
101+
lastProgressAt = now;
96102
}
97103
if (now - lastTick < 120 && sent !== total) return;
98104
lastTick = now;
99105
if (process.stderr.isTTY) {
100106
const msg = formatProgressLine({
101107
doneBytes: sent,
102108
totalBytes: total,
103-
startedAt: startedAt ?? now,
109+
startedAt: transferStartedAt ?? commandStartedAt,
104110
});
105111
readline.clearLine(process.stderr, 0);
106112
readline.cursorTo(process.stderr, 0);
@@ -122,6 +128,7 @@ export async function run(paths, opts, ctx = {}) {
122128
// Attach low-level logger BEFORE wiring auth/stream
123129
const detachTap = attachRawLogger(rtc, { label: "low" });
124130
getLogger().debug("send: RTC connected");
131+
let peerCloseObservedAt = null;
125132
try {
126133
signal.send?.({ type: "telemetry", event: "ice-connected", sessionId });
127134
getLogger().debug("send: announced telemetry ice-connected");
@@ -166,7 +173,7 @@ export async function run(paths, opts, ctx = {}) {
166173
assumeYes: !!opts.yes,
167174
});
168175
}
169-
176+
lastProgressAt = Date.now();
170177
try {
171178
onProgress(totalBytes, totalBytes);
172179
} catch {}
@@ -201,11 +208,13 @@ export async function run(paths, opts, ctx = {}) {
201208
} else if (finAckResult === "nack") {
202209
finAckError = "send: receiver reported failure while acknowledging FIN";
203210
getLogger().info(finAckError);
211+
peerCloseObservedAt = Date.now();
204212
}
205213
}
206214

207215
if (finAckResult !== "peer-close") {
208216
const peerCloseResult = await waitForPeerClose(rtc, peerCloseTimeoutMs);
217+
peerCloseObservedAt = Date.now();
209218
if (
210219
peerCloseResult === "timeout" &&
211220
Number.isFinite(peerCloseTimeoutMs) &&
@@ -223,7 +232,22 @@ export async function run(paths, opts, ctx = {}) {
223232
throw err;
224233
}
225234

226-
process.stderr.write("\nDone • " + humanBytes(totalBytes) + "\n");
235+
const completedAt = peerCloseObservedAt ?? Date.now();
236+
const dataStart = transferStartedAt ?? commandStartedAt;
237+
const dataEnd = lastProgressAt ?? completedAt;
238+
const totalDurationMs = Math.max(0, completedAt - commandStartedAt);
239+
const dataDurationMs = Math.max(0, dataEnd - dataStart);
240+
const handshakeWaitMs = Math.max(0, dataStart - commandStartedAt);
241+
const peerCloseWaitMs = Math.max(0, completedAt - dataEnd);
242+
const summary = formatSummary({
243+
rows: [
244+
{ label: "Bytes", value: humanBytes(totalBytes) },
245+
{ label: "Duration", value: formatDuration(totalDurationMs) },
246+
{ label: "Throughput", value: humanThroughput(totalBytes, dataDurationMs) },
247+
{ label: "Handshake wait", value: formatDuration(handshakeWaitMs) },
248+
],
249+
});
250+
process.stderr.write(summary + "\n");
227251
// --- Silent workaround on success
228252
} finally {
229253
// Hard, handler-safe close sequence

src/util/format.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,28 @@ export function formatETA(s) {
1515
sec = Math.floor(s % 60);
1616
return `${m}:${sec.toString().padStart(2, "0")}`;
1717
}
18+
19+
export function formatDuration(ms) {
20+
if (!Number.isFinite(ms)) return "—";
21+
const clamped = Math.max(0, Number(ms));
22+
if (clamped < 1) return "0 ms";
23+
if (clamped < 1000) return `${Math.round(clamped)} ms`;
24+
25+
const totalSeconds = clamped / 1000;
26+
if (totalSeconds < 60) {
27+
const precision = totalSeconds < 10 ? 2 : 1;
28+
const trimmed = Number(totalSeconds.toFixed(precision)).toString();
29+
return `${trimmed} s`;
30+
}
31+
32+
const parts = [];
33+
const hours = Math.floor(totalSeconds / 3600);
34+
const minutes = Math.floor((totalSeconds % 3600) / 60);
35+
const seconds = Math.floor(totalSeconds % 60);
36+
37+
if (hours) parts.push(`${hours}h`);
38+
if (minutes) parts.push(`${minutes}m`);
39+
if (seconds || parts.length === 0) parts.push(`${seconds}s`);
40+
41+
return parts.join(" ");
42+
}

src/util/transfer-summary.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { humanBytes } from "./format.js";
2+
3+
export function humanThroughput(bytes, durationMs) {
4+
const totalBytes = Number(bytes);
5+
const ms = Number(durationMs);
6+
if (!Number.isFinite(totalBytes) || totalBytes < 0) return "—";
7+
if (!Number.isFinite(ms) || ms <= 0) return "—";
8+
const seconds = ms / 1000;
9+
if (seconds <= 0) return "—";
10+
const perSecond = totalBytes / seconds;
11+
if (!Number.isFinite(perSecond)) return "—";
12+
return `${humanBytes(perSecond)}/s`;
13+
}
14+
15+
export function formatSummary({ heading = "Done", rows = [] } = {}) {
16+
const lines = ["", String(heading)];
17+
for (const row of rows) {
18+
if (!row) continue;
19+
if (typeof row === "string") {
20+
lines.push(row);
21+
continue;
22+
}
23+
const label = row.label ?? "";
24+
const value = row.value ?? "";
25+
lines.push(` ${String(label)}: ${String(value)}`);
26+
}
27+
return lines.join("\n");
28+
}

0 commit comments

Comments
 (0)