@torrent-tv/proxy 2.99.21 → 2.100.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/bin/cli.js CHANGED
@@ -89,7 +89,7 @@ program
89
89
  .option("--no-port-mapping", "Disable automatic UPnP/NAT-PMP port mapping")
90
90
  .option("--delivery-sink", "Serve /api/delivery-sink, a torrent-free byte stream for delivery testing")
91
91
  .option("--pin-preset <name>", "Encode every software picture at this libx264 preset only, for runs that must be compared at one quality (off by default)")
92
- .option("--profile-start <seconds>", "Profile the main thread's CPU for this many seconds from the first source opened, written beside the state (off by default)")
92
+ .option("--profile-start <seconds>", "Profile the CPU of the main thread and the torrent thread for this many seconds from the first source opened, written beside the state (off by default)")
93
93
  .option(
94
94
  "--send-chunk-bytes <bytes>",
95
95
  "Size of one data-channel message when sending a response body (0 = whatever the body read hands over, ~64KB). For measuring whether the delivery wedge depends on that size."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.99.21",
3
+ "version": "2.100.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -1099,7 +1099,8 @@ export async function startProxyServer({
1099
1099
  seconds: profileStartSeconds,
1100
1100
  dir: stateDir || "",
1101
1101
  log: (line) => logger.info(line),
1102
- mayKeep: (bytes) => diagnostics?.mayKeep?.({ what: "a CPU profile", bytes }) ?? true
1102
+ mayKeep: (bytes) => diagnostics?.mayKeep?.({ what: "a CPU profile", bytes }) ?? true,
1103
+ torrentThread: (seconds) => torrentPool.cpuProfile?.(seconds) ?? Promise.resolve(null)
1103
1104
  });
1104
1105
  startProcessorReport({
1105
1106
  log: (line) => logger.info(line),
@@ -1,17 +1,23 @@
1
1
  /**
2
- * @file A CPU profile of the main thread at a cold start, asked for by the
3
- * operator and never taken otherwise (torrent-tv/meta#177).
2
+ * @file CPU profiles of the main thread and of the torrent thread, asked for
3
+ * by the operator and never taken otherwise (torrent-tv/meta#177, #178).
4
4
  *
5
5
  * At a cold start on the addon host (#166 run 12) the picture's first input
6
6
  * waited 29.5 s, 20.2 s of it inside a budget revision, while the main thread
7
7
  * ran at 83-108 % of a core; what it was doing is not in any log. A profile of
8
8
  * that thread names it.
9
9
  *
10
+ * The torrent thread is profiled beside it. After a seek on Home Assistant
11
+ * (r60, #178) a soundtrack's input waited 23.3 s, every read of held bytes took
12
+ * 1.5-6 s in that thread, its loop ran up to 2.7 s late and it drew three
13
+ * quarters of a core; what it spent them on is in no log either.
14
+ *
10
15
  * In-process, through `node:inspector`: the sampling profiler runs beside the
11
16
  * thread and does not stop it — unlike the debugger that stopped every thread
12
- * of this process on 2026-09-05 (torrent-tv/meta#77). It begins at the first
13
- * source opened, which is where a cold start begins, runs for the seconds the
14
- * operator named, and writes `cpu-start-<time>.cpuprofile` beside the log,
17
+ * of this process on 2026-09-05 (torrent-tv/meta#77). Both begin at the first
18
+ * source opened, which is where a cold start begins, run for the seconds the
19
+ * operator named, and are written by this thread under the one storage budget
20
+ * as `cpu-start-<time>.cpuprofile` (main) and `cpu-torrent-<time>.cpuprofile`,
15
21
  * readable in Chrome's DevTools (Performance, "Load profile").
16
22
  */
17
23
 
@@ -19,50 +25,72 @@ import { Session } from "node:inspector/promises";
19
25
  import { writeFile } from "node:fs/promises";
20
26
  import path from "node:path";
21
27
 
28
+ /**
29
+ * Profile the thread this runs on for `seconds`.
30
+ *
31
+ * @param {number} seconds
32
+ * @returns {Promise<object>} The profile, as `Profiler.stop` gives it.
33
+ */
34
+ export async function profileThisThread(seconds) {
35
+ const session = new Session();
36
+ session.connect();
37
+ try {
38
+ await session.post("Profiler.enable");
39
+ await session.post("Profiler.start");
40
+ await new Promise((resolve) => {
41
+ const timer = setTimeout(resolve, seconds * 1000);
42
+ if (typeof timer.unref === "function") timer.unref();
43
+ });
44
+ const { profile } = await session.post("Profiler.stop");
45
+ return profile;
46
+ } finally {
47
+ session.disconnect();
48
+ }
49
+ }
50
+
22
51
  /**
23
52
  * @param {object} params
24
53
  * @param {number} params.seconds - How long to profile; nothing happens unless positive.
25
54
  * @param {string} params.dir - Where to write the profile.
26
55
  * @param {(line: string) => void} params.log
27
56
  * @param {(bytes: number) => boolean} [params.mayKeep] - Whether the storage budget lets it be kept.
57
+ * @param {(seconds: number) => Promise<string | null>} [params.torrentThread] - The torrent thread's
58
+ * profile over the same seconds, as JSON text, or null when it could not be taken.
28
59
  * @returns {{ begin: () => void, active: () => boolean }}
29
60
  */
30
- export function startupCpuProfile({ seconds, dir, log, mayKeep = () => true }) {
61
+ export function startupCpuProfile({ seconds, dir, log, mayKeep = () => true, torrentThread = null }) {
31
62
  let begun = false;
32
- let running = false;
63
+ let running = 0;
33
64
  const wanted = Number.isFinite(seconds) && seconds > 0 && typeof dir === "string" && dir.length > 0;
34
- const take = async () => {
35
- const session = new Session();
36
- session.connect();
65
+ const keep = async (name, text) => {
66
+ const bytes = Buffer.byteLength(text);
67
+ if (!mayKeep(bytes)) {
68
+ log(`cpu profile: ${name} not kept, ${bytes} bytes refused by the storage budget`);
69
+ return;
70
+ }
71
+ const file = path.join(dir, `cpu-${name}-${new Date().toISOString().replace(/[:.]/g, "-")}.cpuprofile`);
72
+ await writeFile(file, text);
73
+ log(`cpu profile: written to ${file} (${bytes} bytes)`);
74
+ };
75
+ const take = async (name, profile) => {
76
+ running += 1;
37
77
  try {
38
- await session.post("Profiler.enable");
39
- await session.post("Profiler.start");
40
- running = true;
41
- log(`cpu profile: the main thread is profiled for ${seconds}s from the first source opened`);
42
- await new Promise((resolve) => {
43
- const timer = setTimeout(resolve, seconds * 1000);
44
- if (typeof timer.unref === "function") timer.unref();
45
- });
46
- const { profile } = await session.post("Profiler.stop");
47
- const text = JSON.stringify(profile);
48
- if (!mayKeep(Buffer.byteLength(text))) {
49
- log(`cpu profile: not kept, ${Buffer.byteLength(text)} bytes refused by the storage budget`);
50
- return;
51
- }
52
- const file = path.join(dir, `cpu-start-${new Date().toISOString().replace(/[:.]/g, "-")}.cpuprofile`);
53
- await writeFile(file, text);
54
- log(`cpu profile: written to ${file} (${Buffer.byteLength(text)} bytes)`);
78
+ const text = await profile();
79
+ if (text) await keep(name, text);
80
+ } catch (error) {
81
+ log(`cpu profile: ${name} failed — ${error.message}`);
55
82
  } finally {
56
- running = false;
57
- session.disconnect();
83
+ running -= 1;
58
84
  }
59
85
  };
60
86
  return {
61
87
  begin() {
62
88
  if (!wanted || begun) return;
63
89
  begun = true;
64
- void take().catch((error) => log(`cpu profile: failed — ${error.message}`));
90
+ log(`cpu profile: the main thread${torrentThread ? " and the torrent thread are" : " is"} profiled for ${seconds}s from the first source opened`);
91
+ void take("start", async () => JSON.stringify(await profileThisThread(seconds)));
92
+ if (torrentThread) void take("torrent", () => torrentThread(seconds));
65
93
  },
66
- active: () => running
94
+ active: () => running > 0
67
95
  };
68
96
  }
@@ -744,6 +744,17 @@ export class TorrentWorkerClient {
744
744
  return this.wholeFileBytes ?? 0;
745
745
  }
746
746
 
747
+ /**
748
+ * A processor profile of the torrent thread over `seconds`, as JSON text.
749
+ *
750
+ * @param {number} seconds
751
+ * @returns {Promise<string | null>}
752
+ */
753
+ async cpuProfile(seconds) {
754
+ const text = await this.#caller.call(Command.CPU_PROFILE, { seconds });
755
+ return typeof text === "string" ? text : null;
756
+ }
757
+
747
758
  /**
748
759
  * Say how much disk the spilled pieces on this thread may take between them.
749
760
  *
@@ -278,6 +278,11 @@ export class WorkerTorrentPool {
278
278
  *
279
279
  * @returns {Promise<{ infoHash: string, progress: number, bytes: number }[]>}
280
280
  */
281
+ /** A processor profile of the torrent thread, as JSON text (`TorrentWorkerClient.cpuProfile`). */
282
+ async cpuProfile(seconds) {
283
+ return this.#client.cpuProfile(seconds);
284
+ }
285
+
281
286
  async heldTorrents() {
282
287
  const answer = await this.#client.heldTorrents();
283
288
  return Array.isArray(answer?.held) ? answer.held : [];
@@ -107,6 +107,8 @@ export const Command = {
107
107
  SPILL_ALLOWANCE: "spill-allowance",
108
108
  WHOLE_FILES_ALLOWANCE: "whole-files-allowance",
109
109
  MEMORY_ALLOWANCE: "memory-allowance",
110
+ /** Profile this thread's processor for some seconds and answer with the profile as JSON text (opt-in, torrent-tv/meta#178). */
111
+ CPU_PROFILE: "cpu-profile",
110
112
  DESTROY_ALL: "destroy-all"
111
113
  };
112
114
 
@@ -39,6 +39,7 @@ import { wholeFileStats } from "./whole-file-stats.js";
39
39
  import { describeHeldObjects, readUtpOpenCount } from "./held-objects.js";
40
40
  import { destroyedTorrents } from "../destroyed-torrents.js";
41
41
  import { startMemoryReport, WORKER_MEMORY_SAMPLE_MS } from "../../storage/memory-report.js";
42
+ import { profileThisThread } from "../../storage/cpu-profile.js";
42
43
  import { forwardLogsTo, logger } from "../../../utils/logger.js";
43
44
 
44
45
  /**
@@ -603,6 +604,12 @@ async function runCommand(command, params, id) {
603
604
  return after.bytes;
604
605
  }
605
606
 
607
+ case Command.CPU_PROFILE: {
608
+ // The operator's profile of this thread, written by the main thread under
609
+ // the storage budget there (`storage/cpu-profile.js`).
610
+ return JSON.stringify(await profileThisThread(Number(params.seconds)));
611
+ }
612
+
606
613
  case Command.DESTROY_ALL: {
607
614
  torrentsByKey.clear();
608
615
  resolvedTorrents.clear();