perfectionist-plugins 2.0.0 → 2.4.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/README.md +143 -121
- package/better-client.js +1093 -512
- package/hide-unowned-skins.js +130 -63
- package/package.json +36 -37
- package/perfectionist-utils.js +57 -4
- package/unlock-profile-skins.js +153 -67
- package/tools/betterclient-monitor.js +0 -269
package/unlock-profile-skins.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const version = "1.2.
|
|
1
|
+
export const version = "1.2.4";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "unlock-profile-skins";
|
|
4
4
|
const PICKER_BUTTON_SELECTOR = ".style-profile-skin-picker-button";
|
|
@@ -6,12 +6,14 @@ const CURRENT_SUMMONER_URI = "/lol-summoner/v1/current-summoner";
|
|
|
6
6
|
const INVENTORY_PATH = /^\/lol-champions\/v1\/inventories\/(\d+)\/champions$/;
|
|
7
7
|
const AUGMENT_INVENTORY_PATH = /^\/lol-inventory\/v2\/inventory\/SKIN_AUGMENT$/i;
|
|
8
8
|
const SUMMONER_TTL = 10 * 60 * 1000;
|
|
9
|
+
const SUMMONER_WAIT_MS = 1200;
|
|
9
10
|
const STATE_KEY = "perfectionist-plugins:state";
|
|
10
11
|
const FAKE_PURCHASE_DATE = 1577836800000;
|
|
11
12
|
const FAKE_RENTAL_END = 4102444800000;
|
|
12
13
|
const TIER_LABEL = "tier";
|
|
13
14
|
const GROUPING_OPTIONS = "lol-uikit-dropdown-option";
|
|
14
15
|
const HARVEST_WAIT_MS = 2000;
|
|
16
|
+
const INVENTORY_TEXT_CACHE_LIMIT = 2;
|
|
15
17
|
|
|
16
18
|
let filterActive = true;
|
|
17
19
|
let api = null;
|
|
@@ -22,6 +24,7 @@ let harvestPromise = null;
|
|
|
22
24
|
const augmentIds = new Set();
|
|
23
25
|
const pendingTargetLoads = new Set();
|
|
24
26
|
const variantIdRemap = new Map();
|
|
27
|
+
const inventoryTextCache = new Map();
|
|
25
28
|
|
|
26
29
|
function readDefaultEnabled() {
|
|
27
30
|
try {
|
|
@@ -196,17 +199,24 @@ function unlockDeep(node) {
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
const championNode = Array.isArray(node.skins);
|
|
203
|
+
if (!championNode) {
|
|
204
|
+
if (node.ownership && typeof node.ownership === "object") {
|
|
205
|
+
changed = unlockOwnership(node.ownership) || changed;
|
|
206
|
+
} else if (typeof node.contentId === "string" && Array.isArray(node.overlays)) {
|
|
207
|
+
node.ownership = synthesizedOwnership();
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
if (node.owned === false) {
|
|
211
|
+
node.owned = true;
|
|
212
|
+
changed = true;
|
|
213
|
+
}
|
|
208
214
|
}
|
|
209
|
-
const values = Array.isArray(node)
|
|
215
|
+
const values = Array.isArray(node)
|
|
216
|
+
? node
|
|
217
|
+
: Object.keys(node)
|
|
218
|
+
.filter((key) => key !== "ownership")
|
|
219
|
+
.map((key) => node[key]);
|
|
210
220
|
for (const value of values) {
|
|
211
221
|
if (value && typeof value === "object") changed = unlockDeep(value) || changed;
|
|
212
222
|
}
|
|
@@ -279,6 +289,7 @@ function ensureAugmentIds() {
|
|
|
279
289
|
let tierObserver = null;
|
|
280
290
|
let tierAppliedForOpen = false;
|
|
281
291
|
let tierDropdown = null;
|
|
292
|
+
let tierArmListener = null;
|
|
282
293
|
|
|
283
294
|
function findGroupingDropdown() {
|
|
284
295
|
for (const dd of document.querySelectorAll("lol-uikit-framed-dropdown")) {
|
|
@@ -312,6 +323,10 @@ function applyTierGrouping() {
|
|
|
312
323
|
function startTierGrouping() {
|
|
313
324
|
if (tierObserver) return;
|
|
314
325
|
tierObserver = new MutationObserver(() => {
|
|
326
|
+
if (!isPickerOpen()) {
|
|
327
|
+
stopTierGrouping();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
315
330
|
try {
|
|
316
331
|
applyTierGrouping();
|
|
317
332
|
} catch (error) {
|
|
@@ -334,12 +349,34 @@ function maybeRefreshSummoner() {
|
|
|
334
349
|
ensureSummonerId();
|
|
335
350
|
}
|
|
336
351
|
|
|
352
|
+
function whenSummonerKnown(timeout) {
|
|
353
|
+
if (summonerId !== null) return Promise.resolve(true);
|
|
354
|
+
ensureSummonerId();
|
|
355
|
+
const deadline = Date.now() + timeout;
|
|
356
|
+
return new Promise((resolve) => {
|
|
357
|
+
const step = () => {
|
|
358
|
+
if (summonerId !== null) return resolve(true);
|
|
359
|
+
if (Date.now() >= deadline) return resolve(false);
|
|
360
|
+
setTimeout(step, 40);
|
|
361
|
+
};
|
|
362
|
+
setTimeout(step, 0);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
337
366
|
function ensureSummonerId() {
|
|
338
367
|
if (summonerId !== null || summonerPending) return;
|
|
339
|
-
const get = api?.lcu?.get;
|
|
340
|
-
if (!get) return;
|
|
341
368
|
summonerPending = true;
|
|
342
|
-
|
|
369
|
+
let request;
|
|
370
|
+
try {
|
|
371
|
+
const get = api?.lcu?.get;
|
|
372
|
+
request = get
|
|
373
|
+
? get(CURRENT_SUMMONER_URI, { ttl: SUMMONER_TTL })
|
|
374
|
+
: (rawFetch || window.fetch)(CURRENT_SUMMONER_URI).then((response) => response.json());
|
|
375
|
+
} catch {
|
|
376
|
+
summonerPending = false;
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
Promise.resolve(request)
|
|
343
380
|
.then((summoner) => {
|
|
344
381
|
summonerId = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
|
|
345
382
|
})
|
|
@@ -348,13 +385,31 @@ function ensureSummonerId() {
|
|
|
348
385
|
summonerPending = false;
|
|
349
386
|
});
|
|
350
387
|
}
|
|
388
|
+
function rewriteInventoryText(raw) {
|
|
389
|
+
if (typeof raw !== "string") return raw;
|
|
390
|
+
if (inventoryTextCache.has(raw)) {
|
|
391
|
+
const cached = inventoryTextCache.get(raw);
|
|
392
|
+
inventoryTextCache.delete(raw);
|
|
393
|
+
inventoryTextCache.set(raw, cached);
|
|
394
|
+
return cached;
|
|
395
|
+
}
|
|
396
|
+
let rewritten = raw;
|
|
397
|
+
try {
|
|
398
|
+
const json = JSON.parse(raw);
|
|
399
|
+
if (unlockDeep(json)) rewritten = JSON.stringify(json);
|
|
400
|
+
} catch {}
|
|
401
|
+
inventoryTextCache.set(raw, rewritten);
|
|
402
|
+
while (inventoryTextCache.size > INVENTORY_TEXT_CACHE_LIMIT) {
|
|
403
|
+
inventoryTextCache.delete(inventoryTextCache.keys().next().value);
|
|
404
|
+
}
|
|
405
|
+
return rewritten;
|
|
406
|
+
}
|
|
351
407
|
|
|
352
408
|
function rewriteWebSocketFrame(text) {
|
|
353
409
|
if (!filterActive || typeof text !== "string") return null;
|
|
354
410
|
const targetFrame = text.indexOf("lol-champions/v1/inventories") !== -1;
|
|
355
411
|
const augmentFrame = text.indexOf("SKIN_AUGMENT") !== -1;
|
|
356
412
|
if (!targetFrame && !augmentFrame) return null;
|
|
357
|
-
if (!isPickerOpen()) return null;
|
|
358
413
|
let frame;
|
|
359
414
|
try {
|
|
360
415
|
frame = JSON.parse(text);
|
|
@@ -454,53 +509,72 @@ function installFetchFilter() {
|
|
|
454
509
|
|
|
455
510
|
const wrapped = function (input, init) {
|
|
456
511
|
const url = typeof input === "string" ? input : (input && input.url) || "";
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
if (filterActive &&
|
|
461
|
-
const mapped = remapVariantIds(
|
|
462
|
-
if (mapped !==
|
|
512
|
+
const inventory = pathIdOf(url) !== null;
|
|
513
|
+
const augmentRequest = isAugmentUrl(url);
|
|
514
|
+
let options = init;
|
|
515
|
+
if (filterActive && options && typeof options.body === "string") {
|
|
516
|
+
const mapped = remapVariantIds(options.body);
|
|
517
|
+
if (mapped !== options.body) options = { ...options, body: mapped };
|
|
463
518
|
}
|
|
464
|
-
const promise = original.call(this, input,
|
|
465
|
-
if (filterActive
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
519
|
+
const promise = original.call(this, input, options);
|
|
520
|
+
if (!filterActive || (!inventory && !augmentRequest)) return promise;
|
|
521
|
+
if (inventory) maybeRefreshSummoner();
|
|
522
|
+
|
|
523
|
+
return whenSummonerKnown(SUMMONER_WAIT_MS).then(() => {
|
|
524
|
+
const target = isTargetUrl(url);
|
|
525
|
+
if (!augmentRequest && !target) return promise;
|
|
526
|
+
if (target && augmentIds.size === 0) {
|
|
527
|
+
const latch = (async () => {
|
|
528
|
+
try {
|
|
529
|
+
const response = await promise;
|
|
530
|
+
if (response && response.ok && augmentIds.size === 0) {
|
|
531
|
+
const json = await response.clone().json().catch(() => null);
|
|
532
|
+
if (json) harvestFromPayload(json);
|
|
533
|
+
}
|
|
534
|
+
} catch {}
|
|
535
|
+
pendingTargetLoads.delete(latch);
|
|
536
|
+
})();
|
|
537
|
+
pendingTargetLoads.add(latch);
|
|
538
|
+
}
|
|
539
|
+
const ready = augmentRequest && augmentIds.size === 0 ? ensureAugmentIds() : Promise.resolve();
|
|
540
|
+
return ready
|
|
541
|
+
.then(() => promise)
|
|
542
|
+
.then((response) => {
|
|
543
|
+
if (!response || !response.ok) return response;
|
|
544
|
+
if (augmentRequest) {
|
|
545
|
+
return response
|
|
546
|
+
.clone()
|
|
547
|
+
.json()
|
|
548
|
+
.catch(() => null)
|
|
549
|
+
.then((json) => {
|
|
550
|
+
if (!json || !unlockAugmentInventory(json)) return response;
|
|
551
|
+
return new Response(JSON.stringify(json), {
|
|
552
|
+
status: response.status,
|
|
553
|
+
statusText: response.statusText,
|
|
554
|
+
headers: response.headers,
|
|
555
|
+
});
|
|
556
|
+
});
|
|
472
557
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
.json()
|
|
487
|
-
.catch(() => null)
|
|
488
|
-
.then((json) => {
|
|
489
|
-
if (!json) return response;
|
|
490
|
-
const changed = augment ? unlockAugmentInventory(json) : unlockDeep(json);
|
|
491
|
-
if (!changed) return response;
|
|
492
|
-
return new Response(JSON.stringify(json), {
|
|
493
|
-
status: response.status,
|
|
494
|
-
statusText: response.statusText,
|
|
495
|
-
headers: response.headers,
|
|
558
|
+
return response
|
|
559
|
+
.clone()
|
|
560
|
+
.text()
|
|
561
|
+
.catch(() => null)
|
|
562
|
+
.then((text) => {
|
|
563
|
+
if (text === null) return response;
|
|
564
|
+
const rewritten = rewriteInventoryText(text);
|
|
565
|
+
if (rewritten === text) return response;
|
|
566
|
+
return new Response(rewritten, {
|
|
567
|
+
status: response.status,
|
|
568
|
+
statusText: response.statusText,
|
|
569
|
+
headers: response.headers,
|
|
570
|
+
});
|
|
496
571
|
});
|
|
497
|
-
|
|
498
|
-
|
|
572
|
+
});
|
|
573
|
+
});
|
|
499
574
|
};
|
|
500
575
|
wrapped.__perfProfileFilter = true;
|
|
501
576
|
window.fetch = wrapped;
|
|
502
577
|
}
|
|
503
|
-
|
|
504
578
|
function installXhrFilter() {
|
|
505
579
|
const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
|
|
506
580
|
if (!proto || proto.open.__perfProfileFilter) return;
|
|
@@ -543,23 +617,24 @@ function installXhrFilter() {
|
|
|
543
617
|
});
|
|
544
618
|
pendingTargetLoads.add(latch);
|
|
545
619
|
}
|
|
546
|
-
if (textDescriptor && textDescriptor.get && (
|
|
620
|
+
if (textDescriptor && textDescriptor.get && (pathIdOf(url) !== null || augment)) {
|
|
547
621
|
const cache = { raw: null, gate: null, filtered: null };
|
|
548
|
-
const gateOpen = () => filterActive &&
|
|
622
|
+
const gateOpen = () => filterActive && (augment || isTargetUrl(url));
|
|
549
623
|
const rewriteBody = (json) => (augment ? unlockAugmentInventory(json) : unlockDeep(json));
|
|
550
624
|
const filteredText = () => {
|
|
551
|
-
const raw = textDescriptor.get.call(xhr);
|
|
552
625
|
const gate = gateOpen();
|
|
553
|
-
if (cache.filtered
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
626
|
+
if (cache.filtered !== null && cache.gate === gate) return cache.filtered;
|
|
627
|
+
const raw = textDescriptor.get.call(xhr);
|
|
628
|
+
cache.gate = gate;
|
|
629
|
+
cache.filtered = raw;
|
|
630
|
+
if (gate) {
|
|
631
|
+
if (augment) {
|
|
558
632
|
try {
|
|
559
633
|
const json = JSON.parse(raw);
|
|
560
|
-
|
|
561
|
-
if (rewritten !== null) cache.filtered = rewritten;
|
|
634
|
+
if (rewriteBody(json)) cache.filtered = JSON.stringify(json);
|
|
562
635
|
} catch {}
|
|
636
|
+
} else {
|
|
637
|
+
cache.filtered = rewriteInventoryText(raw);
|
|
563
638
|
}
|
|
564
639
|
}
|
|
565
640
|
return cache.filtered;
|
|
@@ -578,7 +653,7 @@ function installXhrFilter() {
|
|
|
578
653
|
});
|
|
579
654
|
}
|
|
580
655
|
}
|
|
581
|
-
if (augment && filterActive &&
|
|
656
|
+
if (augment && filterActive && augmentIds.size === 0) {
|
|
582
657
|
const sendArgs = args;
|
|
583
658
|
ensureAugmentIds().finally(() => {
|
|
584
659
|
try {
|
|
@@ -597,6 +672,7 @@ function installSourceFilter() {
|
|
|
597
672
|
installWebSocketFilter();
|
|
598
673
|
installFetchFilter();
|
|
599
674
|
installXhrFilter();
|
|
675
|
+
ensureSummonerId();
|
|
600
676
|
} catch (error) {
|
|
601
677
|
logError("could not install the source filter.", error);
|
|
602
678
|
}
|
|
@@ -617,11 +693,21 @@ function onEnable(utils) {
|
|
|
617
693
|
filterActive = true;
|
|
618
694
|
ensureSourceFilter();
|
|
619
695
|
ensureSummonerId();
|
|
620
|
-
|
|
696
|
+
tierArmListener = () => {
|
|
697
|
+
if (tierObserver) return;
|
|
698
|
+
setTimeout(() => {
|
|
699
|
+
if (tierArmListener && isPickerOpen()) startTierGrouping();
|
|
700
|
+
}, 0);
|
|
701
|
+
};
|
|
702
|
+
document.addEventListener("click", tierArmListener, true);
|
|
703
|
+
if (isPickerOpen()) startTierGrouping();
|
|
621
704
|
}
|
|
622
705
|
|
|
623
706
|
function onDisable() {
|
|
624
707
|
filterActive = false;
|
|
708
|
+
inventoryTextCache.clear();
|
|
709
|
+
if (tierArmListener) document.removeEventListener("click", tierArmListener, true);
|
|
710
|
+
tierArmListener = null;
|
|
625
711
|
stopTierGrouping();
|
|
626
712
|
api = null;
|
|
627
713
|
}
|
|
@@ -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
|
-
}
|