nestjs-profiler 1.0.26 → 1.0.28
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 +286 -227
- package/controllers/profiler.controller.d.ts +15 -1
- package/controllers/profiler.controller.js +91 -4
- package/controllers/profiler.controller.js.map +1 -1
- package/interceptors/request-profiler.interceptor.d.ts +3 -1
- package/interceptors/request-profiler.interceptor.js +10 -5
- package/interceptors/request-profiler.interceptor.js.map +1 -1
- package/package.json +1 -1
- package/profiler.module.js +29 -2
- package/profiler.module.js.map +1 -1
- package/services/cron-explorer.service.d.ts +48 -0
- package/services/cron-explorer.service.js +260 -0
- package/services/cron-explorer.service.js.map +1 -0
- package/services/memory.service.d.ts +58 -0
- package/services/memory.service.js +223 -0
- package/services/memory.service.js.map +1 -0
- package/services/view.service.js +4 -0
- package/services/view.service.js.map +1 -1
- package/views/cron-jobs.html +635 -0
- package/views/layout.html +19 -1
- package/views/memory.html +380 -0
package/views/layout.html
CHANGED
|
@@ -204,6 +204,24 @@
|
|
|
204
204
|
</svg>
|
|
205
205
|
Code Quality
|
|
206
206
|
</a>
|
|
207
|
+
<a href="/__profiler/view/cron-jobs"
|
|
208
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ cronJobsActive }}}">
|
|
209
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ cronJobsIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
210
|
+
stroke="currentColor">
|
|
211
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
212
|
+
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
213
|
+
</svg>
|
|
214
|
+
Cron Jobs
|
|
215
|
+
</a>
|
|
216
|
+
<a href="/__profiler/view/memory"
|
|
217
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ memoryActive }}}">
|
|
218
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ memoryIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
219
|
+
stroke="currentColor">
|
|
220
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
221
|
+
d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18" />
|
|
222
|
+
</svg>
|
|
223
|
+
Memory
|
|
224
|
+
</a>
|
|
207
225
|
<a href="/__profiler/view/logs/live"
|
|
208
226
|
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ liveLogsActive }}}">
|
|
209
227
|
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ liveLogsIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
@@ -218,7 +236,7 @@
|
|
|
218
236
|
<div class="flex-shrink-0 flex bg-slate-800 p-4 items-center justify-between">
|
|
219
237
|
<div class="flex items-center">
|
|
220
238
|
<div class="ml-1">
|
|
221
|
-
<p class="text-xs font-medium text-white">v1.0.
|
|
239
|
+
<p class="text-xs font-medium text-white">v1.0.27</p>
|
|
222
240
|
<p class="text-xs font-medium text-slate-300">nestjs-profiler</p>
|
|
223
241
|
</div>
|
|
224
242
|
</div>
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
<script>const MEMORY_DATA = {{{ reportJson }}};</script>
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
@keyframes fade-in {
|
|
5
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
6
|
+
to { opacity: 1; transform: translateY(0); }
|
|
7
|
+
}
|
|
8
|
+
@keyframes pulse-red {
|
|
9
|
+
0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,.5); }
|
|
10
|
+
50% { box-shadow: 0 0 0 6px rgba(239,68,68,0); }
|
|
11
|
+
}
|
|
12
|
+
@keyframes pulse-amber {
|
|
13
|
+
0%,100% { box-shadow: 0 0 0 0 rgba(245,158,11,.4); }
|
|
14
|
+
50% { box-shadow: 0 0 0 6px rgba(245,158,11,0); }
|
|
15
|
+
}
|
|
16
|
+
.fade-in { animation: fade-in .3s ease; }
|
|
17
|
+
.pulse-red { animation: pulse-red 1.8s ease-in-out infinite; }
|
|
18
|
+
.pulse-amber { animation: pulse-amber 1.8s ease-in-out infinite; }
|
|
19
|
+
.chart-svg { width: 100%; height: 160px; }
|
|
20
|
+
.stat-card { transition: transform .15s, box-shadow .15s; }
|
|
21
|
+
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 20px rgba(0,0,0,.08); }
|
|
22
|
+
.score-ring { transform: rotate(-90deg); transform-origin: 50% 50%; }
|
|
23
|
+
</style>
|
|
24
|
+
|
|
25
|
+
<main class="flex-1 p-6 fade-in" id="memoryRoot">
|
|
26
|
+
|
|
27
|
+
<!-- Header row -->
|
|
28
|
+
<div class="flex items-center justify-between mb-6">
|
|
29
|
+
<div>
|
|
30
|
+
<h1 class="text-2xl font-bold text-gray-900">Memory Monitor</h1>
|
|
31
|
+
<p class="text-sm text-gray-500 mt-0.5">V8 heap statistics · rolling 10-minute window · auto-refreshes every 15 s</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="flex items-center gap-3">
|
|
34
|
+
<button id="btnGc"
|
|
35
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
|
|
36
|
+
title="Force garbage collection (requires --expose-gc)">
|
|
37
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
38
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
39
|
+
</svg>
|
|
40
|
+
Force GC
|
|
41
|
+
</button>
|
|
42
|
+
<button id="btnSnapshot"
|
|
43
|
+
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-indigo-300 bg-indigo-50 text-sm font-medium text-indigo-700 hover:bg-indigo-100 transition-colors">
|
|
44
|
+
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
45
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
|
46
|
+
</svg>
|
|
47
|
+
Heap Snapshot
|
|
48
|
+
</button>
|
|
49
|
+
<span id="refreshBadge" class="text-xs text-gray-400 tabular-nums"></span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Alerts -->
|
|
54
|
+
<div id="alertsSection"></div>
|
|
55
|
+
|
|
56
|
+
<!-- Stat cards row -->
|
|
57
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
58
|
+
<!-- Heap Used -->
|
|
59
|
+
<div class="stat-card bg-white rounded-xl border border-gray-200 p-4">
|
|
60
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Heap Used</p>
|
|
61
|
+
<p id="statHeapUsed" class="text-2xl font-bold text-gray-900">—</p>
|
|
62
|
+
<p id="statHeapUsedPct" class="text-xs text-gray-400 mt-0.5">— of heap total</p>
|
|
63
|
+
</div>
|
|
64
|
+
<!-- Heap Total -->
|
|
65
|
+
<div class="stat-card bg-white rounded-xl border border-gray-200 p-4">
|
|
66
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Heap Total</p>
|
|
67
|
+
<p id="statHeapTotal" class="text-2xl font-bold text-gray-900">—</p>
|
|
68
|
+
<p id="statRss" class="text-xs text-gray-400 mt-0.5">— RSS</p>
|
|
69
|
+
</div>
|
|
70
|
+
<!-- Detached Contexts -->
|
|
71
|
+
<div class="stat-card bg-white rounded-xl border border-gray-200 p-4">
|
|
72
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Detached Contexts</p>
|
|
73
|
+
<p id="statDetached" class="text-2xl font-bold text-gray-900">—</p>
|
|
74
|
+
<p class="text-xs text-gray-400 mt-0.5">V8 leak indicator</p>
|
|
75
|
+
</div>
|
|
76
|
+
<!-- Heap Limit -->
|
|
77
|
+
<div class="stat-card bg-white rounded-xl border border-gray-200 p-4">
|
|
78
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Heap Limit</p>
|
|
79
|
+
<p id="statHeapLimit" class="text-2xl font-bold text-gray-900">—</p>
|
|
80
|
+
<p id="statLimitPct" class="text-xs text-gray-400 mt-0.5">— used</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Chart + Leak Score -->
|
|
85
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
|
|
86
|
+
|
|
87
|
+
<!-- Heap chart (2/3 width) -->
|
|
88
|
+
<div class="lg:col-span-2 bg-white rounded-xl border border-gray-200 p-4">
|
|
89
|
+
<div class="flex items-center justify-between mb-3">
|
|
90
|
+
<h2 class="text-sm font-semibold text-gray-700">Heap Used — last <span id="chartSamples">0</span> samples</h2>
|
|
91
|
+
<span id="trendBadge" class="text-xs px-2.5 py-1 rounded-full font-medium">—</span>
|
|
92
|
+
</div>
|
|
93
|
+
<svg id="heapChart" class="chart-svg" viewBox="0 0 600 160" preserveAspectRatio="none">
|
|
94
|
+
<defs>
|
|
95
|
+
<linearGradient id="heapGrad" x1="0" y1="0" x2="0" y2="1">
|
|
96
|
+
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.3"/>
|
|
97
|
+
<stop offset="100%" stop-color="#6366f1" stop-opacity="0.02"/>
|
|
98
|
+
</linearGradient>
|
|
99
|
+
</defs>
|
|
100
|
+
<text x="300" y="85" text-anchor="middle" fill="#9ca3af" font-size="13">Collecting samples…</text>
|
|
101
|
+
</svg>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Leak Score (1/3 width) -->
|
|
105
|
+
<div class="bg-white rounded-xl border border-gray-200 p-4 flex flex-col items-center justify-center">
|
|
106
|
+
<h2 class="text-sm font-semibold text-gray-700 mb-3 self-start">Leak Score</h2>
|
|
107
|
+
<div class="relative w-36 h-36">
|
|
108
|
+
<svg viewBox="0 0 36 36" class="w-full h-full -rotate-90">
|
|
109
|
+
<circle cx="18" cy="18" r="15.915" fill="none" stroke="#e5e7eb" stroke-width="3"/>
|
|
110
|
+
<circle id="scoreArc" cx="18" cy="18" r="15.915" fill="none" stroke="#10b981"
|
|
111
|
+
stroke-width="3" stroke-linecap="round"
|
|
112
|
+
stroke-dasharray="0 100" stroke-dashoffset="0"
|
|
113
|
+
style="transition: stroke-dasharray .6s ease, stroke .4s ease;"/>
|
|
114
|
+
</svg>
|
|
115
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
|
116
|
+
<span id="scoreNumber" class="text-3xl font-bold text-gray-900">0</span>
|
|
117
|
+
<span class="text-xs text-gray-400">/ 100</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="mt-4 w-full space-y-2">
|
|
121
|
+
<div class="flex justify-between text-xs">
|
|
122
|
+
<span class="text-gray-500">Growth rate</span>
|
|
123
|
+
<span id="scoreGrowth" class="font-medium text-gray-700">0 pts</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="flex justify-between text-xs">
|
|
126
|
+
<span class="text-gray-500">Detached contexts</span>
|
|
127
|
+
<span id="scoreDetached" class="font-medium text-gray-700">0 pts</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="flex justify-between text-xs">
|
|
130
|
+
<span class="text-gray-500">Sustained growth</span>
|
|
131
|
+
<span id="scoreSustained" class="font-medium text-gray-700">0 pts</span>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<!-- Top memory-heavy requests -->
|
|
138
|
+
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden mb-6">
|
|
139
|
+
<div class="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
140
|
+
<h2 class="text-sm font-semibold text-gray-700">Top Memory-Heavy Requests</h2>
|
|
141
|
+
<span class="text-xs text-gray-400">heap delta per request (last 50)</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div id="requestsTable">
|
|
144
|
+
<p class="text-sm text-gray-400 px-5 py-6 text-center">No requests recorded yet.</p>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- GC toast -->
|
|
149
|
+
<div id="toast" class="fixed bottom-6 right-6 z-50 hidden max-w-xs bg-gray-900 text-white text-sm rounded-xl px-4 py-3 shadow-2xl"></div>
|
|
150
|
+
|
|
151
|
+
</main>
|
|
152
|
+
|
|
153
|
+
<script>
|
|
154
|
+
(function () {
|
|
155
|
+
|
|
156
|
+
const MB = 1024 * 1024;
|
|
157
|
+
const fmt = b => (b / MB).toFixed(2) + ' MB';
|
|
158
|
+
const fmtDelta = b => (b >= 0 ? '+' : '') + (b / 1024).toFixed(1) + ' KB';
|
|
159
|
+
|
|
160
|
+
// ── Toast ────────────────────────────────────────────────────────────────────
|
|
161
|
+
function toast(msg, color) {
|
|
162
|
+
const el = document.getElementById('toast');
|
|
163
|
+
el.textContent = msg;
|
|
164
|
+
el.style.background = color || '#111827';
|
|
165
|
+
el.classList.remove('hidden');
|
|
166
|
+
setTimeout(() => el.classList.add('hidden'), 4000);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Alerts ───────────────────────────────────────────────────────────────────
|
|
170
|
+
function renderAlerts(alerts) {
|
|
171
|
+
const sec = document.getElementById('alertsSection');
|
|
172
|
+
if (!alerts || !alerts.length) { sec.innerHTML = ''; return; }
|
|
173
|
+
sec.innerHTML = alerts.map(a =>
|
|
174
|
+
'<div class="flex items-center gap-2 mb-3 bg-red-50 border border-red-200 text-red-800 text-sm px-4 py-2.5 rounded-lg">' +
|
|
175
|
+
'<svg class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>' +
|
|
176
|
+
a + '</div>'
|
|
177
|
+
).join('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Trend badge ──────────────────────────────────────────────────────────────
|
|
181
|
+
function trendBadge(trend) {
|
|
182
|
+
const el = document.getElementById('trendBadge');
|
|
183
|
+
if (trend === 'likely_leak') {
|
|
184
|
+
el.textContent = '⚠ Likely Leak'; el.className = 'text-xs px-2.5 py-1 rounded-full font-medium bg-red-100 text-red-700 pulse-red';
|
|
185
|
+
} else if (trend === 'growing') {
|
|
186
|
+
el.textContent = '↑ Growing'; el.className = 'text-xs px-2.5 py-1 rounded-full font-medium bg-amber-100 text-amber-700';
|
|
187
|
+
} else {
|
|
188
|
+
el.textContent = '✓ Stable'; el.className = 'text-xs px-2.5 py-1 rounded-full font-medium bg-green-100 text-green-700';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Score ring ───────────────────────────────────────────────────────────────
|
|
193
|
+
function renderScore(score, breakdown) {
|
|
194
|
+
const arc = document.getElementById('scoreArc');
|
|
195
|
+
arc.setAttribute('stroke-dasharray', score + ' ' + (100 - score));
|
|
196
|
+
arc.setAttribute('stroke', score >= 60 ? '#ef4444' : score >= 30 ? '#f59e0b' : '#10b981');
|
|
197
|
+
document.getElementById('scoreNumber').textContent = score;
|
|
198
|
+
document.getElementById('scoreNumber').style.color = score >= 60 ? '#ef4444' : score >= 30 ? '#d97706' : '#111827';
|
|
199
|
+
document.getElementById('scoreGrowth').textContent = (breakdown.growthRate ?? 0) + ' pts';
|
|
200
|
+
document.getElementById('scoreDetached').textContent = (breakdown.detachedContexts ?? 0) + ' pts';
|
|
201
|
+
document.getElementById('scoreSustained').textContent = (breakdown.sustained ?? 0) + ' pts';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Stat cards ───────────────────────────────────────────────────────────────
|
|
205
|
+
function renderStats(report) {
|
|
206
|
+
const c = report.current;
|
|
207
|
+
const heapPct = c.heapTotal > 0 ? ((c.heapUsed / c.heapTotal) * 100).toFixed(1) : 0;
|
|
208
|
+
const limitPct = c.heapSizeLimit > 0 ? ((c.heapUsed / c.heapSizeLimit) * 100).toFixed(1) : 0;
|
|
209
|
+
|
|
210
|
+
document.getElementById('statHeapUsed').textContent = fmt(c.heapUsed);
|
|
211
|
+
document.getElementById('statHeapUsedPct').textContent = heapPct + '% of heap total';
|
|
212
|
+
document.getElementById('statHeapTotal').textContent = fmt(c.heapTotal);
|
|
213
|
+
document.getElementById('statRss').textContent = fmt(c.rss) + ' RSS';
|
|
214
|
+
|
|
215
|
+
const detEl = document.getElementById('statDetached');
|
|
216
|
+
detEl.textContent = c.detachedContexts;
|
|
217
|
+
detEl.className = 'text-2xl font-bold ' + (c.detachedContexts > 5 ? 'text-red-600' : c.detachedContexts > 0 ? 'text-amber-600' : 'text-gray-900');
|
|
218
|
+
|
|
219
|
+
document.getElementById('statHeapLimit').textContent = fmt(c.heapSizeLimit);
|
|
220
|
+
document.getElementById('statLimitPct').textContent = limitPct + '% used';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Heap chart ───────────────────────────────────────────────────────────────
|
|
224
|
+
function renderChart(samples) {
|
|
225
|
+
const svg = document.getElementById('heapChart');
|
|
226
|
+
document.getElementById('chartSamples').textContent = samples.length;
|
|
227
|
+
if (samples.length < 2) { return; }
|
|
228
|
+
|
|
229
|
+
const W = 600, H = 160, PAD = 10;
|
|
230
|
+
const values = samples.map(s => s.heapUsed);
|
|
231
|
+
const minV = Math.min(...values);
|
|
232
|
+
const maxV = Math.max(...values);
|
|
233
|
+
const range = maxV - minV || 1;
|
|
234
|
+
|
|
235
|
+
const x = i => PAD + (i / (samples.length - 1)) * (W - PAD * 2);
|
|
236
|
+
const y = v => H - PAD - ((v - minV) / range) * (H - PAD * 2 - 20);
|
|
237
|
+
|
|
238
|
+
const pts = samples.map((s, i) => x(i) + ',' + y(s.heapUsed)).join(' ');
|
|
239
|
+
const areaBottom = 'L' + x(samples.length - 1) + ',' + (H - PAD) + ' L' + x(0) + ',' + (H - PAD) + ' Z';
|
|
240
|
+
|
|
241
|
+
// Axis labels
|
|
242
|
+
const labelMin = (minV / MB).toFixed(1) + ' MB';
|
|
243
|
+
const labelMax = (maxV / MB).toFixed(1) + ' MB';
|
|
244
|
+
|
|
245
|
+
svg.innerHTML =
|
|
246
|
+
'<defs>' +
|
|
247
|
+
'<linearGradient id="heapGrad" x1="0" y1="0" x2="0" y2="1">' +
|
|
248
|
+
'<stop offset="0%" stop-color="#6366f1" stop-opacity="0.3"/>' +
|
|
249
|
+
'<stop offset="100%" stop-color="#6366f1" stop-opacity="0.02"/>' +
|
|
250
|
+
'</linearGradient>' +
|
|
251
|
+
'</defs>' +
|
|
252
|
+
// Grid lines
|
|
253
|
+
[0.25, 0.5, 0.75].map(f => {
|
|
254
|
+
const gy = PAD + f * (H - PAD * 2 - 20);
|
|
255
|
+
return '<line x1="' + PAD + '" y1="' + gy + '" x2="' + (W - PAD) + '" y2="' + gy + '" stroke="#f3f4f6" stroke-width="1"/>';
|
|
256
|
+
}).join('') +
|
|
257
|
+
// Area fill
|
|
258
|
+
'<path d="M' + pts.split(' ').map((p, i) => (i === 0 ? '' : 'L') + p).join(' ') + areaBottom + '" fill="url(#heapGrad)"/>' +
|
|
259
|
+
// Line
|
|
260
|
+
'<polyline points="' + pts + '" fill="none" stroke="#6366f1" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>' +
|
|
261
|
+
// Current dot
|
|
262
|
+
'<circle cx="' + x(samples.length - 1) + '" cy="' + y(values[values.length - 1]) + '" r="4" fill="#6366f1" stroke="white" stroke-width="2"/>' +
|
|
263
|
+
// Y-axis labels
|
|
264
|
+
'<text x="' + (PAD + 2) + '" y="' + (PAD + 12) + '" font-size="9" fill="#9ca3af">' + labelMax + '</text>' +
|
|
265
|
+
'<text x="' + (PAD + 2) + '" y="' + (H - PAD - 4) + '" font-size="9" fill="#9ca3af">' + labelMin + '</text>';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Requests table ───────────────────────────────────────────────────────────
|
|
269
|
+
function renderRequests(reqs) {
|
|
270
|
+
const el = document.getElementById('requestsTable');
|
|
271
|
+
if (!reqs || !reqs.length) {
|
|
272
|
+
el.innerHTML = '<p class="text-sm text-gray-400 px-5 py-6 text-center">No requests recorded yet.</p>';
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const rows = reqs.slice(0, 15).map(r => {
|
|
276
|
+
const deltaColor = r.heapDeltaBytes > 1 * 1024 * 1024 ? 'text-red-600 font-semibold'
|
|
277
|
+
: r.heapDeltaBytes > 100 * 1024 ? 'text-amber-600 font-medium'
|
|
278
|
+
: r.heapDeltaBytes < 0 ? 'text-green-600'
|
|
279
|
+
: 'text-gray-700';
|
|
280
|
+
const methodBg = { GET: 'bg-blue-100 text-blue-700', POST: 'bg-green-100 text-green-700',
|
|
281
|
+
PUT: 'bg-yellow-100 text-yellow-700', DELETE: 'bg-red-100 text-red-700',
|
|
282
|
+
PATCH: 'bg-purple-100 text-purple-700' }[r.method] || 'bg-gray-100 text-gray-600';
|
|
283
|
+
return '<tr class="hover:bg-gray-50">' +
|
|
284
|
+
'<td class="px-5 py-3 whitespace-nowrap"><span class="text-xs px-2 py-0.5 rounded font-mono font-medium ' + methodBg + '">' + r.method + '</span></td>' +
|
|
285
|
+
'<td class="px-4 py-3 font-mono text-xs text-gray-700 max-w-xs truncate">' + r.url + '</td>' +
|
|
286
|
+
'<td class="px-4 py-3 text-xs ' + deltaColor + ' tabular-nums">' + fmtDelta(r.heapDeltaBytes) + '</td>' +
|
|
287
|
+
'<td class="px-4 py-3 text-xs text-gray-500 tabular-nums">' + r.durationMs + ' ms</td>' +
|
|
288
|
+
'<td class="px-4 py-3 text-xs text-gray-400 tabular-nums">' + new Date(r.timestamp).toLocaleTimeString() + '</td>' +
|
|
289
|
+
'</tr>';
|
|
290
|
+
}).join('');
|
|
291
|
+
el.innerHTML =
|
|
292
|
+
'<table class="min-w-full divide-y divide-gray-100">' +
|
|
293
|
+
'<thead class="bg-gray-50"><tr>' +
|
|
294
|
+
'<th class="px-5 py-3 text-left text-xs font-semibold text-gray-500">Method</th>' +
|
|
295
|
+
'<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500">URL</th>' +
|
|
296
|
+
'<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500">Heap Delta</th>' +
|
|
297
|
+
'<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500">Duration</th>' +
|
|
298
|
+
'<th class="px-4 py-3 text-left text-xs font-semibold text-gray-500">Time</th>' +
|
|
299
|
+
'</tr></thead>' +
|
|
300
|
+
'<tbody class="divide-y divide-gray-50">' + rows + '</tbody>' +
|
|
301
|
+
'</table>';
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Render from data ─────────────────────────────────────────────────────────
|
|
305
|
+
function render(data) {
|
|
306
|
+
renderAlerts(data.alerts);
|
|
307
|
+
renderStats(data);
|
|
308
|
+
trendBadge(data.trend);
|
|
309
|
+
renderScore(data.leakScore, data.leakScoreBreakdown);
|
|
310
|
+
renderChart(data.samples);
|
|
311
|
+
renderRequests(data.topMemoryRequests);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Initial render ───────────────────────────────────────────────────────────
|
|
315
|
+
render(MEMORY_DATA);
|
|
316
|
+
|
|
317
|
+
// ── Auto-refresh every 15 s ──────────────────────────────────────────────────
|
|
318
|
+
let countdown = 15;
|
|
319
|
+
const badge = document.getElementById('refreshBadge');
|
|
320
|
+
const tick = setInterval(() => {
|
|
321
|
+
countdown--;
|
|
322
|
+
badge.textContent = 'refresh in ' + countdown + ' s';
|
|
323
|
+
if (countdown <= 0) {
|
|
324
|
+
countdown = 15;
|
|
325
|
+
fetch('/__profiler/api/memory')
|
|
326
|
+
.then(r => r.json())
|
|
327
|
+
.then(data => render(data))
|
|
328
|
+
.catch(() => {});
|
|
329
|
+
}
|
|
330
|
+
}, 1000);
|
|
331
|
+
|
|
332
|
+
// ── GC button ────────────────────────────────────────────────────────────────
|
|
333
|
+
document.getElementById('btnGc').addEventListener('click', () => {
|
|
334
|
+
fetch('/__profiler/api/memory/gc', { method: 'POST' })
|
|
335
|
+
.then(r => r.json())
|
|
336
|
+
.then(d => {
|
|
337
|
+
toast(d.message, d.success ? '#059669' : '#dc2626');
|
|
338
|
+
if (d.success) {
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
fetch('/__profiler/api/memory').then(r => r.json()).then(render);
|
|
341
|
+
}, 500);
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
.catch(() => toast('GC request failed', '#dc2626'));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ── Snapshot button — streams file back as browser download ─────────────────
|
|
348
|
+
document.getElementById('btnSnapshot').addEventListener('click', () => {
|
|
349
|
+
const btn = document.getElementById('btnSnapshot');
|
|
350
|
+
btn.disabled = true;
|
|
351
|
+
btn.textContent = 'Generating…';
|
|
352
|
+
|
|
353
|
+
fetch('/__profiler/api/memory/snapshot', { method: 'POST' })
|
|
354
|
+
.then(res => {
|
|
355
|
+
if (!res.ok) return res.json().then(d => Promise.reject(d.message || 'Snapshot failed'));
|
|
356
|
+
const cd = res.headers.get('Content-Disposition') || '';
|
|
357
|
+
const match = cd.match(/filename="?([^"]+)"?/);
|
|
358
|
+
const fileName = match ? match[1] : `Heap-${Date.now()}.heapsnapshot`;
|
|
359
|
+
return res.blob().then(blob => ({ blob, fileName }));
|
|
360
|
+
})
|
|
361
|
+
.then(({ blob, fileName }) => {
|
|
362
|
+
const url = URL.createObjectURL(blob);
|
|
363
|
+
const a = document.createElement('a');
|
|
364
|
+
a.href = url;
|
|
365
|
+
a.download = fileName;
|
|
366
|
+
document.body.appendChild(a);
|
|
367
|
+
a.click();
|
|
368
|
+
document.body.removeChild(a);
|
|
369
|
+
URL.revokeObjectURL(url);
|
|
370
|
+
toast('Snapshot downloaded — load it in Chrome DevTools → Memory → Load', '#4f46e5');
|
|
371
|
+
})
|
|
372
|
+
.catch(msg => toast(typeof msg === 'string' ? msg : 'Snapshot failed', '#dc2626'))
|
|
373
|
+
.finally(() => {
|
|
374
|
+
btn.disabled = false;
|
|
375
|
+
btn.innerHTML = '<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg> Heap Snapshot';
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
})();
|
|
380
|
+
</script>
|