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.
@@ -1,4 +1,4 @@
1
- export const version = "1.2.1";
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
- if (node.ownership && typeof node.ownership === "object") {
200
- changed = unlockOwnership(node.ownership) || changed;
201
- } else if (typeof node.contentId === "string" && Array.isArray(node.overlays)) {
202
- node.ownership = synthesizedOwnership();
203
- changed = true;
204
- }
205
- if (node.owned === false) {
206
- node.owned = true;
207
- changed = true;
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) ? node : Object.values(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
- get(CURRENT_SUMMONER_URI, { ttl: SUMMONER_TTL })
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 target = isTargetUrl(url);
458
- const augment = !target && isAugmentUrl(url);
459
- if (filterActive && pathIdOf(url) !== null) maybeRefreshSummoner();
460
- if (filterActive && init && typeof init.body === "string") {
461
- const mapped = remapVariantIds(init.body);
462
- if (mapped !== init.body) init = { ...init, body: 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, init);
465
- if (filterActive && target && augmentIds.size === 0) {
466
- const latch = (async () => {
467
- try {
468
- const response = await promise;
469
- if (response && response.ok && augmentIds.size === 0) {
470
- const json = await response.clone().json().catch(() => null);
471
- if (json) harvestFromPayload(json);
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
- } catch {}
474
- pendingTargetLoads.delete(latch);
475
- })();
476
- pendingTargetLoads.add(latch);
477
- }
478
- if (!filterActive || (!target && !augment) || !isPickerOpen()) return promise;
479
- const ready = augment && augmentIds.size === 0 ? ensureAugmentIds() : Promise.resolve();
480
- return ready
481
- .then(() => promise)
482
- .then((response) => {
483
- if (!response || !response.ok) return response;
484
- return response
485
- .clone()
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 && (target || augment)) {
620
+ if (textDescriptor && textDescriptor.get && (pathIdOf(url) !== null || augment)) {
547
621
  const cache = { raw: null, gate: null, filtered: null };
548
- const gateOpen = () => filterActive && isPickerOpen();
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 === null || cache.raw !== raw || cache.gate !== gate) {
554
- cache.raw = raw;
555
- cache.gate = gate;
556
- cache.filtered = raw;
557
- if (gate) {
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
- const rewritten = rewriteBody(json) ? JSON.stringify(json) : null;
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 && isPickerOpen() && augmentIds.size === 0) {
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
- startTierGrouping();
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
- }