perfectionist-plugins 2.0.0 → 2.4.1
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/README.md +123 -121
- package/better-client.js +370 -86
- package/hide-unowned-skins.js +194 -225
- package/package.json +36 -37
- package/perfectionist-utils.js +11 -131
- package/unlock-profile-skins.js +153 -67
- package/tools/betterclient-monitor.js +0 -269
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// BetterClient memory monitor.
|
|
3
|
-
//
|
|
4
|
-
// Samples every League/Riot process once per interval, records client phase
|
|
5
|
-
// (read straight from the LCU with the running client's own credentials) and
|
|
6
|
-
// writes a CSV you can chart. With --diff it compares two V8 heap snapshots and
|
|
7
|
-
// prints the constructors that grew the most.
|
|
8
|
-
//
|
|
9
|
-
// node tools/betterclient-monitor.mjs # live table + CSV
|
|
10
|
-
// node tools/betterclient-monitor.mjs --minutes 240 # long session
|
|
11
|
-
// node tools/betterclient-monitor.mjs --snapshots 15 # heap snapshots via CDP
|
|
12
|
-
// node tools/betterclient-monitor.mjs --diff a.heapsnapshot b.heapsnapshot
|
|
13
|
-
//
|
|
14
|
-
// Nothing is installed and nothing is written outside --out / --snapshot-dir.
|
|
15
|
-
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
-
import https from "node:https";
|
|
19
|
-
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const flag = (name, fallback) => {
|
|
22
|
-
const index = args.indexOf(`--${name}`);
|
|
23
|
-
if (index === -1) return fallback;
|
|
24
|
-
const value = args[index + 1];
|
|
25
|
-
return value && !value.startsWith("--") ? value : true;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const diffFiles = args.filter((entry) => entry.endsWith(".heapsnapshot"));
|
|
29
|
-
if (args.includes("--diff")) {
|
|
30
|
-
diffSnapshots(diffFiles[0], diffFiles[1]);
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const intervalSec = Number(flag("interval", 5));
|
|
35
|
-
const minutes = Number(flag("minutes", 0));
|
|
36
|
-
const out = String(flag("out", "betterclient-monitor.csv"));
|
|
37
|
-
const snapshotEvery = Number(flag("snapshots", 0));
|
|
38
|
-
const snapshotDir = String(flag("snapshot-dir", "heapsnapshots"));
|
|
39
|
-
const snapshotPrefix = String(flag("prefix", "betterclient"));
|
|
40
|
-
|
|
41
|
-
const NAMES = ["LeagueClient", "LeagueClientUx", "LeagueClientUxRender", "RiotClientServices", "RiotClientCrashHandler"];
|
|
42
|
-
|
|
43
|
-
function powershell(script) {
|
|
44
|
-
return execFileSync("powershell", ["-NoProfile", "-Command", script], { encoding: "utf8", windowsHide: true });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sampleProcesses() {
|
|
48
|
-
const filter = NAMES.map((name) => `$_.ProcessName -eq '${name}'`).join(" -or ");
|
|
49
|
-
const script = `Get-Process -ErrorAction SilentlyContinue | Where-Object { ${filter} } | ForEach-Object { "$($_.ProcessName)|$($_.Id)|$($_.WorkingSet64)|$($_.PrivateMemorySize64)|$($_.TotalProcessorTime.TotalSeconds)" }`;
|
|
50
|
-
const rows = powershell(script)
|
|
51
|
-
.split(/\r?\n/)
|
|
52
|
-
.map((line) => line.trim())
|
|
53
|
-
.filter(Boolean)
|
|
54
|
-
.map((line) => {
|
|
55
|
-
const [name, pid, workingSet, priv, cpu] = line.split("|");
|
|
56
|
-
return { name, pid: Number(pid), mb: Math.round(Number(workingSet) / 1048576), privateMb: Math.round(Number(priv) / 1048576), cpu: Number(cpu) };
|
|
57
|
-
});
|
|
58
|
-
return rows.sort((a, b) => a.pid - b.pid);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let lcu = null;
|
|
62
|
-
function discoverLcu() {
|
|
63
|
-
if (lcu) return lcu;
|
|
64
|
-
try {
|
|
65
|
-
const commandLine = powershell(`(Get-CimInstance Win32_Process -Filter "Name='LeagueClientUx.exe'").CommandLine`);
|
|
66
|
-
const port = /"?"--app-port=(\d+)/.exec(commandLine)?.[1];
|
|
67
|
-
const token = /"?"--remoting-auth-token=([\w-]+)/.exec(commandLine)?.[1];
|
|
68
|
-
if (port && token) lcu = { port: Number(port), token };
|
|
69
|
-
} catch {}
|
|
70
|
-
return lcu;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function lcuGet(path) {
|
|
74
|
-
const auth = discoverLcu();
|
|
75
|
-
if (!auth) return Promise.resolve(null);
|
|
76
|
-
return new Promise((resolve) => {
|
|
77
|
-
const request = https.request(
|
|
78
|
-
{
|
|
79
|
-
host: "127.0.0.1",
|
|
80
|
-
port: auth.port,
|
|
81
|
-
path,
|
|
82
|
-
method: "GET",
|
|
83
|
-
rejectUnauthorized: false,
|
|
84
|
-
headers: { Authorization: `Basic ${Buffer.from(`riot:${auth.token}`).toString("base64")}`, Accept: "application/json" },
|
|
85
|
-
timeout: 4000,
|
|
86
|
-
},
|
|
87
|
-
(response) => {
|
|
88
|
-
let body = "";
|
|
89
|
-
response.on("data", (chunk) => (body += chunk));
|
|
90
|
-
response.on("end", () => {
|
|
91
|
-
try {
|
|
92
|
-
resolve(JSON.parse(body));
|
|
93
|
-
} catch {
|
|
94
|
-
resolve(body);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
request.on("error", () => resolve(null));
|
|
100
|
-
request.on("timeout", () => {
|
|
101
|
-
request.destroy();
|
|
102
|
-
resolve(null);
|
|
103
|
-
});
|
|
104
|
-
request.end();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function cdpTarget() {
|
|
109
|
-
for (const port of [9222, 9223, 9229, 9333]) {
|
|
110
|
-
try {
|
|
111
|
-
const response = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(1500) });
|
|
112
|
-
if (!response.ok) continue;
|
|
113
|
-
const targets = await response.json();
|
|
114
|
-
const page = targets.find((t) => t.webSocketDebuggerUrl && /league|client|riot/i.test(t.url ?? "")) ?? targets.find((t) => t.webSocketDebuggerUrl);
|
|
115
|
-
if (page) return page.webSocketDebuggerUrl;
|
|
116
|
-
} catch {}
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function takeHeapSnapshot() {
|
|
122
|
-
const url = await cdpTarget();
|
|
123
|
-
if (!url) {
|
|
124
|
-
console.log("[snapshot] no CDP endpoint found - start the client with a remote debugging port to record heap snapshots");
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
if (!existsSync(snapshotDir)) mkdirSync(snapshotDir, { recursive: true });
|
|
128
|
-
const name = `${snapshotDir}/${snapshotPrefix}-${new Date().toISOString().replace(/[:.]/g, "-")}.heapsnapshot`;
|
|
129
|
-
const socket = new WebSocket(url);
|
|
130
|
-
const chunks = [];
|
|
131
|
-
let id = 0;
|
|
132
|
-
const send = (method, params = {}) => socket.send(JSON.stringify({ id: ++id, method, params }));
|
|
133
|
-
await new Promise((resolve, reject) => {
|
|
134
|
-
const timer = setTimeout(() => reject(new Error("snapshot timed out")), 180000);
|
|
135
|
-
socket.addEventListener("open", () => {
|
|
136
|
-
send("HeapProfiler.enable");
|
|
137
|
-
send("HeapProfiler.collectGarbage");
|
|
138
|
-
send("HeapProfiler.takeHeapSnapshot", { reportProgress: false, captureNumericValue: false });
|
|
139
|
-
});
|
|
140
|
-
socket.addEventListener("message", (event) => {
|
|
141
|
-
const message = JSON.parse(event.data);
|
|
142
|
-
if (message.method === "HeapProfiler.addHeapSnapshotChunk") chunks.push(message.params.chunk);
|
|
143
|
-
if (message.id === id) {
|
|
144
|
-
clearTimeout(timer);
|
|
145
|
-
if (message.error) reject(new Error(message.error.message));
|
|
146
|
-
else resolve();
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
socket.addEventListener("error", (error) => {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
reject(error);
|
|
152
|
-
});
|
|
153
|
-
}).finally(() => socket.close());
|
|
154
|
-
writeFileSync(name, chunks.join(""));
|
|
155
|
-
console.log(`[snapshot] wrote ${name} (${(chunks.join("").length / 1048576).toFixed(1)} MB)`);
|
|
156
|
-
return name;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function diffSnapshots(fromPath, toPath) {
|
|
160
|
-
if (!fromPath || !toPath) {
|
|
161
|
-
console.log("usage: node tools/betterclient-monitor.mjs --diff old.heapsnapshot new.heapsnapshot");
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
for (const path of [fromPath, toPath]) {
|
|
165
|
-
if (!existsSync(path)) {
|
|
166
|
-
console.log(`missing file: ${path}`);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const tally = (snapshot) => {
|
|
171
|
-
const nodeFields = snapshot.snapshot.meta.node_fields;
|
|
172
|
-
const nodeTypes = snapshot.snapshot.meta.node_types[nodeFields.indexOf("type")];
|
|
173
|
-
const nameOffset = nodeFields.indexOf("name");
|
|
174
|
-
const sizeOffset = nodeFields.indexOf("self_size");
|
|
175
|
-
const width = nodeFields.length;
|
|
176
|
-
const nodes = snapshot.nodes;
|
|
177
|
-
const strings = snapshot.strings;
|
|
178
|
-
const map = new Map();
|
|
179
|
-
for (let i = 0; i < nodes.length; i += width) {
|
|
180
|
-
const type = nodeTypes[nodes[i]];
|
|
181
|
-
const name = strings[nodes[i + nameOffset]] ?? type;
|
|
182
|
-
const key = `${type}:${name}`;
|
|
183
|
-
const entry = map.get(key) ?? { count: 0, bytes: 0 };
|
|
184
|
-
entry.count += 1;
|
|
185
|
-
entry.bytes += nodes[i + sizeOffset];
|
|
186
|
-
map.set(key, entry);
|
|
187
|
-
}
|
|
188
|
-
return map;
|
|
189
|
-
};
|
|
190
|
-
const before = tally(JSON.parse(readFileSync(fromPath, "utf8")));
|
|
191
|
-
const after = tally(JSON.parse(readFileSync(toPath, "utf8")));
|
|
192
|
-
const rows = [];
|
|
193
|
-
for (const [key, value] of after) {
|
|
194
|
-
const previous = before.get(key) ?? { count: 0, bytes: 0 };
|
|
195
|
-
rows.push({ key, count: value.count - previous.count, bytes: value.bytes - previous.bytes });
|
|
196
|
-
}
|
|
197
|
-
rows.sort((a, b) => b.bytes - a.bytes);
|
|
198
|
-
console.log(`\ntop growers between the two snapshots:`);
|
|
199
|
-
for (const row of rows.slice(0, 25)) {
|
|
200
|
-
if (row.bytes <= 0) break;
|
|
201
|
-
console.log(` +${(row.bytes / 1048576).toFixed(2)} MB ${String(row.count).padStart(7)} nodes ${row.key}`);
|
|
202
|
-
}
|
|
203
|
-
const total = rows.reduce((sum, row) => sum + row.bytes, 0);
|
|
204
|
-
console.log(`\nnet ${(total / 1048576).toFixed(1)} MB`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const started = Date.now();
|
|
208
|
-
const history = new Map();
|
|
209
|
-
writeFileSync(out, "timestamp,phase,pid,process,working_set_mb,private_mb,cpu_seconds,cpu_percent\n");
|
|
210
|
-
console.log(`writing ${out} - every ${intervalSec}s${minutes ? ` for ${minutes} min` : " until Ctrl+C"}`);
|
|
211
|
-
|
|
212
|
-
const lastCpu = new Map();
|
|
213
|
-
let lastCpuAt = Date.now();
|
|
214
|
-
let tick = 0;
|
|
215
|
-
|
|
216
|
-
async function loop() {
|
|
217
|
-
tick += 1;
|
|
218
|
-
let phase = "?";
|
|
219
|
-
try {
|
|
220
|
-
phase = await lcuGet("/lol-gameflow/v1/gameflow-phase");
|
|
221
|
-
} catch {}
|
|
222
|
-
const rows = sampleProcesses();
|
|
223
|
-
const now = Date.now();
|
|
224
|
-
const elapsed = (now - lastCpuAt) / 1000;
|
|
225
|
-
const parts = [];
|
|
226
|
-
for (const row of rows) {
|
|
227
|
-
const previous = lastCpu.get(row.pid);
|
|
228
|
-
const cpuPercent = previous !== undefined && elapsed > 0 ? Math.max(0, ((row.cpu - previous) / elapsed) * 100) : 0;
|
|
229
|
-
lastCpu.set(row.pid, row.cpu);
|
|
230
|
-
appendFileSync(out, `${new Date(now).toISOString()},${phase},${row.pid},${row.name},${row.mb},${row.privateMb},${row.cpu.toFixed(1)},${cpuPercent.toFixed(1)}\n`);
|
|
231
|
-
parts.push(`${row.name.replace("LeagueClient", "LC")}#${row.pid} ${row.mb}MB ${cpuPercent.toFixed(0)}%`);
|
|
232
|
-
const series = history.get(row.pid) ?? [];
|
|
233
|
-
series.push(row.mb);
|
|
234
|
-
history.set(row.pid, series);
|
|
235
|
-
}
|
|
236
|
-
lastCpuAt = now;
|
|
237
|
-
const totalMb = rows.reduce((sum, row) => sum + row.mb, 0);
|
|
238
|
-
console.log(`[${new Date(now).toLocaleTimeString()}] phase=${phase} total=${totalMb}MB | ${parts.join(" | ")}`);
|
|
239
|
-
|
|
240
|
-
if (snapshotEvery > 0 && tick % Math.max(1, Math.round((snapshotEvery * 60) / intervalSec)) === 0) {
|
|
241
|
-
await takeHeapSnapshot().catch((error) => console.log(`[snapshot] failed: ${error.message}`));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (minutes > 0 && now - started >= minutes * 60 * 1000) {
|
|
245
|
-
console.log("\ngrowth over the run:");
|
|
246
|
-
for (const [pid, series] of history) {
|
|
247
|
-
if (series.length < 2) continue;
|
|
248
|
-
const first = series[0];
|
|
249
|
-
const last = series[series.length - 1];
|
|
250
|
-
const delta = last - first;
|
|
251
|
-
const minutesRun = Math.max(0.1, (Date.now() - started) / 60000);
|
|
252
|
-
const perHour = (delta / minutesRun) * 60;
|
|
253
|
-
const row = rows.find((entry) => entry.pid === pid);
|
|
254
|
-
console.log(` ${row?.name ?? "process"}#${pid}: ${first}MB -> ${last}MB (${delta >= 0 ? "+" : ""}${delta}MB, ${perHour >= 0 ? "+" : ""}${perHour.toFixed(0)}MB/h)`);
|
|
255
|
-
if (perHour > 200) console.log(" ^ leak candidate - this process alone keeps growing");
|
|
256
|
-
}
|
|
257
|
-
console.log(`\nfull series in ${out}`);
|
|
258
|
-
process.exit(0);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
while (true) {
|
|
263
|
-
try {
|
|
264
|
-
await loop();
|
|
265
|
-
} catch (error) {
|
|
266
|
-
console.log(`sample failed: ${error.message}`);
|
|
267
|
-
}
|
|
268
|
-
await new Promise((resolve) => setTimeout(resolve, intervalSec * 1000));
|
|
269
|
-
}
|