@torrent-tv/proxy 2.62.0 → 2.63.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/CHANGELOG.md +8 -0
- package/bin/cli.js +520 -512
- package/package.json +1 -1
- package/services/delivery-probe.js +532 -480
- package/services/memory-report.js +120 -15
- package/services/piece-store/shared-piece-store.js +866 -791
- package/services/torrent-worker/worker.js +738 -706
- package/test/delivery-probe.test.js +213 -158
- package/test/memory-budget.test.js +89 -2
|
@@ -1,158 +1,213 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
|
|
4
|
-
import { allowedGap, readProbeState, PROBE_INTERVAL_MS, UNRELIABLE_LABEL } from "../services/delivery-probe.js";
|
|
5
|
-
|
|
6
|
-
const ORDERED = ["proxy", "proxy-control"];
|
|
7
|
-
const ALL = [...ORDERED, UNRELIABLE_LABEL];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {Record<string, number>} seen
|
|
11
|
-
* @param {object} [overrides]
|
|
12
|
-
*/
|
|
13
|
-
function state(seen, overrides = {}) {
|
|
14
|
-
const allowed = {};
|
|
15
|
-
for (const label of overrides.labels ?? ALL) {
|
|
16
|
-
allowed[label] = 3;
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
seq: 100,
|
|
20
|
-
seen,
|
|
21
|
-
labels: ALL,
|
|
22
|
-
echoes: 5,
|
|
23
|
-
echoAgeMs: 400,
|
|
24
|
-
allowed,
|
|
25
|
-
...overrides
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("every channel current reads as flowing", () => {
|
|
30
|
-
const { verdict } = readProbeState(state({ proxy: 100, "proxy-control": 99, "proxy-fast": 100 }));
|
|
31
|
-
assert.equal(verdict, "flowing");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("a lag shorter than the verdict window is still flowing", () => {
|
|
35
|
-
const behind = 100 - 3;
|
|
36
|
-
const { verdict } = readProbeState(
|
|
37
|
-
state({ proxy: behind, "proxy-control": behind, "proxy-fast": 100 })
|
|
38
|
-
);
|
|
39
|
-
assert.equal(verdict, "flowing");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("ordered channels behind while the unordered one keeps up names a stuck stream", () => {
|
|
43
|
-
const { verdict, detail } = readProbeState(
|
|
44
|
-
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 100 })
|
|
45
|
-
);
|
|
46
|
-
assert.equal(verdict, "stream-stuck");
|
|
47
|
-
// The numbers that produced the verdict must be in the line beside it.
|
|
48
|
-
assert.match(detail, /proxy=40\(gap 60 of 3\)/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("every channel behind names the association", () => {
|
|
52
|
-
const { verdict } = readProbeState(
|
|
53
|
-
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 })
|
|
54
|
-
);
|
|
55
|
-
assert.equal(verdict, "association-stopped");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("without the unordered channel the verdict says it cannot compare", () => {
|
|
59
|
-
const { verdict } = readProbeState(
|
|
60
|
-
state({ proxy: 40, "proxy-control": 41 }, { labels: ORDERED })
|
|
61
|
-
);
|
|
62
|
-
assert.equal(verdict, "ordered-behind-no-comparison");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("a stale echo means the reverse direction went too", () => {
|
|
66
|
-
const { verdict } = readProbeState(
|
|
67
|
-
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 }, { echoAgeMs: 30_000 })
|
|
68
|
-
);
|
|
69
|
-
assert.equal(verdict, "reverse-direction-gone");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("before the first echo nothing is claimed", () => {
|
|
73
|
-
const { verdict } = readProbeState(state({}, { echoes: 0, echoAgeMs: null }));
|
|
74
|
-
assert.equal(verdict, "no-echo-yet");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("a channel that has never reported counts as behind, not as unknown", () => {
|
|
78
|
-
const { verdict, detail } = readProbeState(
|
|
79
|
-
state({ "proxy-fast": 100 })
|
|
80
|
-
);
|
|
81
|
-
assert.equal(verdict, "stream-stuck");
|
|
82
|
-
assert.match(detail, /proxy=\?\(gap \? of 3\)/);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test("the allowance is the queue's own drain time, not a chosen number", () => {
|
|
86
|
-
// 8 MB queued at 8 MB/s is one second of draining; probes go twice a second,
|
|
87
|
-
// so two of them may legitimately be outstanding, plus the round trip.
|
|
88
|
-
assert.equal(
|
|
89
|
-
allowedGap({ queuedBytes: 8 * 1024 * 1024, bytesPerSecond: 8 * 1024 * 1024, rttMs: 0 }),
|
|
90
|
-
Math.ceil(1000 / PROBE_INTERVAL_MS)
|
|
91
|
-
);
|
|
92
|
-
// An empty queue still allows the one probe that is always in flight.
|
|
93
|
-
assert.equal(allowedGap({ queuedBytes: 0, bytesPerSecond: 8 * 1024 * 1024, rttMs: 0 }), 1);
|
|
94
|
-
// The round trip counts: the echo has to come back too.
|
|
95
|
-
assert.ok(
|
|
96
|
-
allowedGap({ queuedBytes: 0, bytesPerSecond: 1024, rttMs: 2000 }) >
|
|
97
|
-
allowedGap({ queuedBytes: 0, bytesPerSecond: 1024, rttMs: 0 })
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("with no rate measured nothing is claimed", () => {
|
|
102
|
-
assert.equal(allowedGap({ queuedBytes: 1024, bytesPerSecond: 0, rttMs: 10 }), null);
|
|
103
|
-
const { verdict } = readProbeState(
|
|
104
|
-
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 }, { allowed: {} })
|
|
105
|
-
);
|
|
106
|
-
assert.equal(verdict, "no-rate-yet");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("a burst big enough to explain the lag is not called a stopped association", () => {
|
|
110
|
-
// The 2026-08-26 false positive: all three channels at gap 7 while 150 Mbps
|
|
111
|
-
// crossed the association. 64 MB queued at 18 MB/s is three and a half
|
|
112
|
-
// seconds of draining, which is seven probe intervals.
|
|
113
|
-
const allowance = allowedGap({
|
|
114
|
-
queuedBytes: 64 * 1024 * 1024,
|
|
115
|
-
bytesPerSecond: 18 * 1024 * 1024,
|
|
116
|
-
rttMs: 16
|
|
117
|
-
});
|
|
118
|
-
assert.ok(allowance >= 7);
|
|
119
|
-
const allowed = Object.fromEntries(ALL.map((label) => [label, allowance]));
|
|
120
|
-
const { verdict } = readProbeState(
|
|
121
|
-
state({ proxy: 93, "proxy-control": 93, "proxy-fast": 93 }, { allowed })
|
|
122
|
-
);
|
|
123
|
-
assert.equal(verdict, "flowing");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test("the peer's own answering cadence counts toward the allowance", () => {
|
|
127
|
-
// Field case 2026-08-27: queues empty, 3.4 MB/s crossing, tab hidden so the
|
|
128
|
-
// browser echoed about once a second. Without the peer's cadence the
|
|
129
|
-
// allowance is one probe and every other line read `association-stopped`.
|
|
130
|
-
const withoutCadence = allowedGap({
|
|
131
|
-
queuedBytes: 0,
|
|
132
|
-
bytesPerSecond: 3.4 * 1024 * 1024,
|
|
133
|
-
rttMs: 9
|
|
134
|
-
});
|
|
135
|
-
assert.equal(withoutCadence, 1);
|
|
136
|
-
const withCadence = allowedGap({
|
|
137
|
-
queuedBytes: 0,
|
|
138
|
-
bytesPerSecond: 3.4 * 1024 * 1024,
|
|
139
|
-
rttMs: 9,
|
|
140
|
-
echoIntervalMs: 1000
|
|
141
|
-
});
|
|
142
|
-
assert.ok(withCadence >= 3, `a second of cadence must allow more than ${withCadence}`);
|
|
143
|
-
const allowed = Object.fromEntries(ALL.map((label) => [label, withCadence]));
|
|
144
|
-
const { verdict } = readProbeState(
|
|
145
|
-
state({ proxy: 98, "proxy-control": 98, "proxy-fast": 98 }, { allowed, echoAgeMs: 977 })
|
|
146
|
-
);
|
|
147
|
-
assert.equal(verdict, "flowing");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("a stale echo is judged against the peer's cadence, not a fixed half second", () => {
|
|
151
|
-
// The same numbers with the cadence unknown must still be able to say the
|
|
152
|
-
// reverse direction is gone — the bound rises with the cadence, it does not
|
|
153
|
-
// disappear.
|
|
154
|
-
const { verdict } = readProbeState(
|
|
155
|
-
state({ proxy: 99, "proxy-control": 99, "proxy-fast": 99 }, { echoAgeMs: 60_000, echoStaleMs: 2000 })
|
|
156
|
-
);
|
|
157
|
-
assert.equal(verdict, "reverse-direction-gone");
|
|
158
|
-
});
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { allowedGap, readProbeState, PROBE_INTERVAL_MS, UNRELIABLE_LABEL } from "../services/delivery-probe.js";
|
|
5
|
+
|
|
6
|
+
const ORDERED = ["proxy", "proxy-control"];
|
|
7
|
+
const ALL = [...ORDERED, UNRELIABLE_LABEL];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Record<string, number>} seen
|
|
11
|
+
* @param {object} [overrides]
|
|
12
|
+
*/
|
|
13
|
+
function state(seen, overrides = {}) {
|
|
14
|
+
const allowed = {};
|
|
15
|
+
for (const label of overrides.labels ?? ALL) {
|
|
16
|
+
allowed[label] = 3;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
seq: 100,
|
|
20
|
+
seen,
|
|
21
|
+
labels: ALL,
|
|
22
|
+
echoes: 5,
|
|
23
|
+
echoAgeMs: 400,
|
|
24
|
+
allowed,
|
|
25
|
+
...overrides
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("every channel current reads as flowing", () => {
|
|
30
|
+
const { verdict } = readProbeState(state({ proxy: 100, "proxy-control": 99, "proxy-fast": 100 }));
|
|
31
|
+
assert.equal(verdict, "flowing");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("a lag shorter than the verdict window is still flowing", () => {
|
|
35
|
+
const behind = 100 - 3;
|
|
36
|
+
const { verdict } = readProbeState(
|
|
37
|
+
state({ proxy: behind, "proxy-control": behind, "proxy-fast": 100 })
|
|
38
|
+
);
|
|
39
|
+
assert.equal(verdict, "flowing");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("ordered channels behind while the unordered one keeps up names a stuck stream", () => {
|
|
43
|
+
const { verdict, detail } = readProbeState(
|
|
44
|
+
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 100 })
|
|
45
|
+
);
|
|
46
|
+
assert.equal(verdict, "stream-stuck");
|
|
47
|
+
// The numbers that produced the verdict must be in the line beside it.
|
|
48
|
+
assert.match(detail, /proxy=40\(gap 60 of 3\)/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("every channel behind names the association", () => {
|
|
52
|
+
const { verdict } = readProbeState(
|
|
53
|
+
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 })
|
|
54
|
+
);
|
|
55
|
+
assert.equal(verdict, "association-stopped");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("without the unordered channel the verdict says it cannot compare", () => {
|
|
59
|
+
const { verdict } = readProbeState(
|
|
60
|
+
state({ proxy: 40, "proxy-control": 41 }, { labels: ORDERED })
|
|
61
|
+
);
|
|
62
|
+
assert.equal(verdict, "ordered-behind-no-comparison");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a stale echo means the reverse direction went too", () => {
|
|
66
|
+
const { verdict } = readProbeState(
|
|
67
|
+
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 }, { echoAgeMs: 30_000 })
|
|
68
|
+
);
|
|
69
|
+
assert.equal(verdict, "reverse-direction-gone");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("before the first echo nothing is claimed", () => {
|
|
73
|
+
const { verdict } = readProbeState(state({}, { echoes: 0, echoAgeMs: null }));
|
|
74
|
+
assert.equal(verdict, "no-echo-yet");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a channel that has never reported counts as behind, not as unknown", () => {
|
|
78
|
+
const { verdict, detail } = readProbeState(
|
|
79
|
+
state({ "proxy-fast": 100 })
|
|
80
|
+
);
|
|
81
|
+
assert.equal(verdict, "stream-stuck");
|
|
82
|
+
assert.match(detail, /proxy=\?\(gap \? of 3\)/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("the allowance is the queue's own drain time, not a chosen number", () => {
|
|
86
|
+
// 8 MB queued at 8 MB/s is one second of draining; probes go twice a second,
|
|
87
|
+
// so two of them may legitimately be outstanding, plus the round trip.
|
|
88
|
+
assert.equal(
|
|
89
|
+
allowedGap({ queuedBytes: 8 * 1024 * 1024, bytesPerSecond: 8 * 1024 * 1024, rttMs: 0 }),
|
|
90
|
+
Math.ceil(1000 / PROBE_INTERVAL_MS)
|
|
91
|
+
);
|
|
92
|
+
// An empty queue still allows the one probe that is always in flight.
|
|
93
|
+
assert.equal(allowedGap({ queuedBytes: 0, bytesPerSecond: 8 * 1024 * 1024, rttMs: 0 }), 1);
|
|
94
|
+
// The round trip counts: the echo has to come back too.
|
|
95
|
+
assert.ok(
|
|
96
|
+
allowedGap({ queuedBytes: 0, bytesPerSecond: 1024, rttMs: 2000 }) >
|
|
97
|
+
allowedGap({ queuedBytes: 0, bytesPerSecond: 1024, rttMs: 0 })
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("with no rate measured nothing is claimed", () => {
|
|
102
|
+
assert.equal(allowedGap({ queuedBytes: 1024, bytesPerSecond: 0, rttMs: 10 }), null);
|
|
103
|
+
const { verdict } = readProbeState(
|
|
104
|
+
state({ proxy: 40, "proxy-control": 41, "proxy-fast": 42 }, { allowed: {} })
|
|
105
|
+
);
|
|
106
|
+
assert.equal(verdict, "no-rate-yet");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("a burst big enough to explain the lag is not called a stopped association", () => {
|
|
110
|
+
// The 2026-08-26 false positive: all three channels at gap 7 while 150 Mbps
|
|
111
|
+
// crossed the association. 64 MB queued at 18 MB/s is three and a half
|
|
112
|
+
// seconds of draining, which is seven probe intervals.
|
|
113
|
+
const allowance = allowedGap({
|
|
114
|
+
queuedBytes: 64 * 1024 * 1024,
|
|
115
|
+
bytesPerSecond: 18 * 1024 * 1024,
|
|
116
|
+
rttMs: 16
|
|
117
|
+
});
|
|
118
|
+
assert.ok(allowance >= 7);
|
|
119
|
+
const allowed = Object.fromEntries(ALL.map((label) => [label, allowance]));
|
|
120
|
+
const { verdict } = readProbeState(
|
|
121
|
+
state({ proxy: 93, "proxy-control": 93, "proxy-fast": 93 }, { allowed })
|
|
122
|
+
);
|
|
123
|
+
assert.equal(verdict, "flowing");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("the peer's own answering cadence counts toward the allowance", () => {
|
|
127
|
+
// Field case 2026-08-27: queues empty, 3.4 MB/s crossing, tab hidden so the
|
|
128
|
+
// browser echoed about once a second. Without the peer's cadence the
|
|
129
|
+
// allowance is one probe and every other line read `association-stopped`.
|
|
130
|
+
const withoutCadence = allowedGap({
|
|
131
|
+
queuedBytes: 0,
|
|
132
|
+
bytesPerSecond: 3.4 * 1024 * 1024,
|
|
133
|
+
rttMs: 9
|
|
134
|
+
});
|
|
135
|
+
assert.equal(withoutCadence, 1);
|
|
136
|
+
const withCadence = allowedGap({
|
|
137
|
+
queuedBytes: 0,
|
|
138
|
+
bytesPerSecond: 3.4 * 1024 * 1024,
|
|
139
|
+
rttMs: 9,
|
|
140
|
+
echoIntervalMs: 1000
|
|
141
|
+
});
|
|
142
|
+
assert.ok(withCadence >= 3, `a second of cadence must allow more than ${withCadence}`);
|
|
143
|
+
const allowed = Object.fromEntries(ALL.map((label) => [label, withCadence]));
|
|
144
|
+
const { verdict } = readProbeState(
|
|
145
|
+
state({ proxy: 98, "proxy-control": 98, "proxy-fast": 98 }, { allowed, echoAgeMs: 977 })
|
|
146
|
+
);
|
|
147
|
+
assert.equal(verdict, "flowing");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a stale echo is judged against the peer's cadence, not a fixed half second", () => {
|
|
151
|
+
// The same numbers with the cadence unknown must still be able to say the
|
|
152
|
+
// reverse direction is gone — the bound rises with the cadence, it does not
|
|
153
|
+
// disappear.
|
|
154
|
+
const { verdict } = readProbeState(
|
|
155
|
+
state({ proxy: 99, "proxy-control": 99, "proxy-fast": 99 }, { echoAgeMs: 60_000, echoStaleMs: 2000 })
|
|
156
|
+
);
|
|
157
|
+
assert.equal(verdict, "reverse-direction-gone");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("bytes still arriving outrank the probe gaps — the cushion fill is not a wedge", () => {
|
|
161
|
+
// The field shape of 2026-08-28: every queue at 0 B, so the allowance is
|
|
162
|
+
// small, the probes are far behind because the browser is busy pulling two
|
|
163
|
+
// minutes of film, and the association is perfectly healthy. Before the peer's
|
|
164
|
+
// own byte counter was consulted this read as `association-stopped`, four
|
|
165
|
+
// times in the first two minutes of a session nobody was troubled by.
|
|
166
|
+
const state = {
|
|
167
|
+
seq: 88,
|
|
168
|
+
seen: { proxy: 78, "proxy-control": 78, "proxy-fast": 78 },
|
|
169
|
+
labels: ["proxy", "proxy-control", "proxy-fast"],
|
|
170
|
+
echoes: 40,
|
|
171
|
+
echoAgeMs: 1305,
|
|
172
|
+
allowed: { proxy: 9, "proxy-control": 9, "proxy-fast": 9 },
|
|
173
|
+
echoStaleMs: 6000,
|
|
174
|
+
peerBytesAdvancing: true
|
|
175
|
+
};
|
|
176
|
+
const reading = readProbeState(state);
|
|
177
|
+
assert.equal(reading.verdict, "flowing");
|
|
178
|
+
assert.match(reading.detail, /peerBytes=advancing/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("the same gaps with the peer's counter STILL are the wedge", () => {
|
|
182
|
+
// One field changes, and it is the one that says whether anything is
|
|
183
|
+
// arriving. This is the shape of a real freeze: probes behind, and the far
|
|
184
|
+
// end receiving nothing.
|
|
185
|
+
const reading = readProbeState({
|
|
186
|
+
seq: 88,
|
|
187
|
+
seen: { proxy: 78, "proxy-control": 78, "proxy-fast": 78 },
|
|
188
|
+
labels: ["proxy", "proxy-control", "proxy-fast"],
|
|
189
|
+
echoes: 40,
|
|
190
|
+
echoAgeMs: 1305,
|
|
191
|
+
allowed: { proxy: 9, "proxy-control": 9, "proxy-fast": 9 },
|
|
192
|
+
echoStaleMs: 6000,
|
|
193
|
+
peerBytesAdvancing: false
|
|
194
|
+
});
|
|
195
|
+
assert.equal(reading.verdict, "association-stopped");
|
|
196
|
+
assert.match(reading.detail, /peerBytes=still/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a browser that does not report its bytes is judged as before", () => {
|
|
200
|
+
// The term says nothing rather than guessing, and the rule falls back.
|
|
201
|
+
const reading = readProbeState({
|
|
202
|
+
seq: 88,
|
|
203
|
+
seen: { proxy: 78, "proxy-control": 78, "proxy-fast": 78 },
|
|
204
|
+
labels: ["proxy", "proxy-control", "proxy-fast"],
|
|
205
|
+
echoes: 40,
|
|
206
|
+
echoAgeMs: 1305,
|
|
207
|
+
allowed: { proxy: 9, "proxy-control": 9, "proxy-fast": 9 },
|
|
208
|
+
echoStaleMs: 6000,
|
|
209
|
+
peerBytesAdvancing: null
|
|
210
|
+
});
|
|
211
|
+
assert.equal(reading.verdict, "association-stopped");
|
|
212
|
+
assert.doesNotMatch(reading.detail, /peerBytes=/);
|
|
213
|
+
});
|
|
@@ -4,8 +4,12 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import { describeMemory } from "../services/memory-report.js";
|
|
5
5
|
import {
|
|
6
6
|
budgetForNewStore,
|
|
7
|
+
SharedPieceStore,
|
|
7
8
|
totalStoreBudgetBytes
|
|
8
9
|
} from "../services/piece-store/shared-piece-store.js";
|
|
10
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
9
13
|
|
|
10
14
|
const MEGABYTE = 1024 * 1024;
|
|
11
15
|
const GIGABYTE = 1024 * MEGABYTE;
|
|
@@ -50,12 +54,18 @@ test("the memory line says bytes, and names what it could not measure", () => {
|
|
|
50
54
|
availableBytes: 1900 * MEGABYTE,
|
|
51
55
|
availableMeasured: true,
|
|
52
56
|
stores: [
|
|
53
|
-
{
|
|
57
|
+
{
|
|
58
|
+
name: "a film",
|
|
59
|
+
residentBytes: 504 * MEGABYTE,
|
|
60
|
+
committedBytes: 504 * MEGABYTE,
|
|
61
|
+
spilledBytes: 0,
|
|
62
|
+
budgetBytes: 512 * MEGABYTE
|
|
63
|
+
}
|
|
54
64
|
]
|
|
55
65
|
});
|
|
56
66
|
// The figures the kernel's own kill line quoted, so the two can be compared.
|
|
57
67
|
assert.match(measured, /rss=2366MB/);
|
|
58
|
-
assert.match(measured, /1 torrent store\(s\) holding 504MB of 512MB allowed/);
|
|
68
|
+
assert.match(measured, /1 torrent store\(s\) holding 504MB, committed 504MB of 512MB allowed/);
|
|
59
69
|
assert.match(measured, /machine has 1900MB available$/);
|
|
60
70
|
|
|
61
71
|
const estimated = describeMemory({
|
|
@@ -67,3 +77,80 @@ test("the memory line says bytes, and names what it could not measure", () => {
|
|
|
67
77
|
assert.match(estimated, /no torrent stores/);
|
|
68
78
|
assert.match(estimated, /estimated — \/proc\/meminfo could not be read/);
|
|
69
79
|
});
|
|
80
|
+
|
|
81
|
+
test("holding and having taken are separate figures, and the line says both", () => {
|
|
82
|
+
// The shape that hid 650 MB on 2026-08-28: eighteen pieces held, sixty-four
|
|
83
|
+
// slots taken and never given back, and a line that only mentioned the first.
|
|
84
|
+
const line = describeMemory({
|
|
85
|
+
process: { rss: 893 * MEGABYTE, heapUsed: 29 * MEGABYTE, heapTotal: 34 * MEGABYTE, external: 11 * MEGABYTE, arrayBuffers: 7 * MEGABYTE },
|
|
86
|
+
availableBytes: 2287 * MEGABYTE,
|
|
87
|
+
availableMeasured: true,
|
|
88
|
+
anonymousBytes: 870 * MEGABYTE,
|
|
89
|
+
diskFreeBytes: 12000 * MEGABYTE,
|
|
90
|
+
stores: [
|
|
91
|
+
{
|
|
92
|
+
name: "a film",
|
|
93
|
+
residentBytes: 144 * MEGABYTE,
|
|
94
|
+
committedBytes: 512 * MEGABYTE,
|
|
95
|
+
spilledBytes: 2904 * MEGABYTE,
|
|
96
|
+
budgetBytes: 512 * MEGABYTE
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
assert.match(line, /holding 144MB, committed 512MB of 512MB allowed, 2904MB spilled to disk/);
|
|
101
|
+
assert.match(line, /anon=870MB/);
|
|
102
|
+
assert.match(line, /12000MB free on disk$/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("a thread's reading leaves out what belongs to the process", () => {
|
|
106
|
+
// `rss` and the kernel's rollup are process-wide and are read once, on the
|
|
107
|
+
// main thread. What the torrent worker can say — and the only place it can be
|
|
108
|
+
// said — is its own isolate, where the piece pool actually lives.
|
|
109
|
+
const line = describeMemory({
|
|
110
|
+
scope: "thread",
|
|
111
|
+
label: "torrent worker",
|
|
112
|
+
process: { rss: 893 * MEGABYTE, heapUsed: 12 * MEGABYTE, heapTotal: 20 * MEGABYTE, external: 530 * MEGABYTE, arrayBuffers: 512 * MEGABYTE },
|
|
113
|
+
stores: [
|
|
114
|
+
{
|
|
115
|
+
name: "a film",
|
|
116
|
+
residentBytes: 144 * MEGABYTE,
|
|
117
|
+
committedBytes: 512 * MEGABYTE,
|
|
118
|
+
spilledBytes: 0,
|
|
119
|
+
budgetBytes: 512 * MEGABYTE
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
assert.match(line, /^memory \(torrent worker\): heap=12MB\/20MB/);
|
|
124
|
+
assert.match(line, /arrayBuffers=512MB/);
|
|
125
|
+
assert.doesNotMatch(line, /rss=/);
|
|
126
|
+
assert.doesNotMatch(line, /machine has/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a store's allowance follows the machine, and never passes its reservation", async () => {
|
|
130
|
+
// The defect: a store created on an idle machine kept an idle machine's
|
|
131
|
+
// allowance for life and went on growing into memory the host no longer had.
|
|
132
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "budget-revision-"));
|
|
133
|
+
const store = new SharedPieceStore(1024, {
|
|
134
|
+
path: directory,
|
|
135
|
+
name: "revision",
|
|
136
|
+
memoryBytes: 64 * 1024
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const born = store.stats().budgetBytes;
|
|
140
|
+
assert.ok(born > 0);
|
|
141
|
+
|
|
142
|
+
// The machine fills up: the ceiling comes down and growth stops there.
|
|
143
|
+
const lowered = store.reviseGrowthCeiling(8 * 1024);
|
|
144
|
+
assert.ok(lowered.ceilingBytes < born, "a busier machine buys fewer slots");
|
|
145
|
+
assert.equal(store.stats().budgetBytes, lowered.ceilingBytes, "the line says what is allowed now");
|
|
146
|
+
|
|
147
|
+
// The machine empties again: the ceiling may rise, but never above the
|
|
148
|
+
// reservation, because `maxByteLength` was fixed from it and `grow()`
|
|
149
|
+
// cannot pass it.
|
|
150
|
+
const raised = store.reviseGrowthCeiling(1024 * 1024 * 1024);
|
|
151
|
+
assert.equal(raised.ceilingBytes, born, "the reservation is the hard limit");
|
|
152
|
+
} finally {
|
|
153
|
+
await new Promise((resolve) => store.destroy(resolve));
|
|
154
|
+
await rm(directory, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
});
|