react-pref-guard 0.1.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/dist/index.d.mts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +1638 -0
- package/dist/index.mjs +1609 -0
- package/package.json +56 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1609 @@
|
|
|
1
|
+
// src/PerfProvider.tsx
|
|
2
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/collector.ts
|
|
5
|
+
var buffer = /* @__PURE__ */ new Map();
|
|
6
|
+
function collectMetric(metric2) {
|
|
7
|
+
const {
|
|
8
|
+
component,
|
|
9
|
+
phase,
|
|
10
|
+
actualDuration,
|
|
11
|
+
boundaryType
|
|
12
|
+
} = metric2;
|
|
13
|
+
let entry = buffer.get(component);
|
|
14
|
+
if (!entry) {
|
|
15
|
+
entry = {
|
|
16
|
+
renders: 0,
|
|
17
|
+
totalTime: 0,
|
|
18
|
+
maxTime: 0,
|
|
19
|
+
boundaryType,
|
|
20
|
+
phaseCounts: { mount: 0, update: 0 }
|
|
21
|
+
};
|
|
22
|
+
buffer.set(component, entry);
|
|
23
|
+
}
|
|
24
|
+
entry.renders += 1;
|
|
25
|
+
entry.totalTime += actualDuration;
|
|
26
|
+
entry.maxTime = Math.max(entry.maxTime, actualDuration);
|
|
27
|
+
if (phase === "mount" || phase === "update") {
|
|
28
|
+
entry.phaseCounts[phase] += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function flushMetrics() {
|
|
32
|
+
const now = performance.now();
|
|
33
|
+
const snapshot = [];
|
|
34
|
+
for (const [component, metric2] of buffer.entries()) {
|
|
35
|
+
snapshot.push({
|
|
36
|
+
component,
|
|
37
|
+
renders: metric2.renders,
|
|
38
|
+
avgTime: metric2.renders > 0 ? metric2.totalTime / metric2.renders : 0,
|
|
39
|
+
maxTime: metric2.maxTime,
|
|
40
|
+
boundaryType: metric2.boundaryType,
|
|
41
|
+
phaseCounts: {
|
|
42
|
+
mount: metric2.phaseCounts.mount,
|
|
43
|
+
update: metric2.phaseCounts.update
|
|
44
|
+
},
|
|
45
|
+
timestamp: now
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
buffer.clear();
|
|
49
|
+
return snapshot;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/issue-store/issueStore.ts
|
|
53
|
+
var store = /* @__PURE__ */ new Map();
|
|
54
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
55
|
+
function upsertIssue(issue) {
|
|
56
|
+
store.set(issue.id, issue);
|
|
57
|
+
notify();
|
|
58
|
+
}
|
|
59
|
+
function resolveIssue(id) {
|
|
60
|
+
const row = store.get(id);
|
|
61
|
+
if (!row) return;
|
|
62
|
+
row.status = "RESOLVED";
|
|
63
|
+
notify();
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
store.delete(id);
|
|
66
|
+
notify();
|
|
67
|
+
}, 1e4);
|
|
68
|
+
}
|
|
69
|
+
function subscribe(listener) {
|
|
70
|
+
listeners.add(listener);
|
|
71
|
+
listener(Array.from(store.values()));
|
|
72
|
+
return () => listeners.delete(listener);
|
|
73
|
+
}
|
|
74
|
+
function notify() {
|
|
75
|
+
const snapshot = Array.from(store.values());
|
|
76
|
+
listeners.forEach((l) => l(snapshot));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/warnings.ts
|
|
80
|
+
var activeIssues = /* @__PURE__ */ new Map();
|
|
81
|
+
var lastLogTime = /* @__PURE__ */ new Map();
|
|
82
|
+
var LOG_COOLDOWN = 3e4;
|
|
83
|
+
var RESOLVE_AFTER_MISSES = 3;
|
|
84
|
+
function fingerprint(component, issue) {
|
|
85
|
+
return `${component}:${issue.ruleId}:${issue.severity}`;
|
|
86
|
+
}
|
|
87
|
+
function shouldLog(fp) {
|
|
88
|
+
const last = lastLogTime.get(fp) || 0;
|
|
89
|
+
if (Date.now() - last < LOG_COOLDOWN) return false;
|
|
90
|
+
lastLogTime.set(fp, Date.now());
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function severityEmoji(sev) {
|
|
94
|
+
switch (sev) {
|
|
95
|
+
case "CRITICAL":
|
|
96
|
+
return "\u{1F4A5}";
|
|
97
|
+
case "HIGH":
|
|
98
|
+
return "\u{1F534}";
|
|
99
|
+
case "MEDIUM":
|
|
100
|
+
return "\u{1F7E1}";
|
|
101
|
+
case "LOW":
|
|
102
|
+
return "\u{1F535}";
|
|
103
|
+
default:
|
|
104
|
+
return "\u2139\uFE0F";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function severityColor(sev) {
|
|
108
|
+
switch (sev) {
|
|
109
|
+
case "CRITICAL":
|
|
110
|
+
return "#dc2626";
|
|
111
|
+
case "HIGH":
|
|
112
|
+
return "#ef4444";
|
|
113
|
+
case "MEDIUM":
|
|
114
|
+
return "#f59e0b";
|
|
115
|
+
case "LOW":
|
|
116
|
+
return "#3b82f6";
|
|
117
|
+
default:
|
|
118
|
+
return "#6b7280";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function showWarning(result) {
|
|
122
|
+
const seenThisBatch = /* @__PURE__ */ new Set();
|
|
123
|
+
for (const issue of result.issues) {
|
|
124
|
+
const fp = fingerprint(result.component, issue);
|
|
125
|
+
seenThisBatch.add(fp);
|
|
126
|
+
const state = activeIssues.get(fp);
|
|
127
|
+
if (!state) {
|
|
128
|
+
activeIssues.set(fp, { issue, missingCount: 0 });
|
|
129
|
+
upsertIssue({
|
|
130
|
+
id: fp,
|
|
131
|
+
component: result.component,
|
|
132
|
+
ruleId: issue.ruleId,
|
|
133
|
+
severity: issue.severity,
|
|
134
|
+
confidence: issue.confidence,
|
|
135
|
+
boundaryType: result.boundaryType,
|
|
136
|
+
status: "NEW",
|
|
137
|
+
reason: issue.reason,
|
|
138
|
+
lastSeen: Date.now()
|
|
139
|
+
});
|
|
140
|
+
if (shouldLog(fp)) {
|
|
141
|
+
logNewIssue(result, issue);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
state.missingCount = 0;
|
|
145
|
+
upsertIssue({
|
|
146
|
+
id: fp,
|
|
147
|
+
component: result.component,
|
|
148
|
+
ruleId: issue.ruleId,
|
|
149
|
+
severity: issue.severity,
|
|
150
|
+
// CRITICAL preserved
|
|
151
|
+
confidence: issue.confidence,
|
|
152
|
+
boundaryType: result.boundaryType,
|
|
153
|
+
status: "ACTIVE",
|
|
154
|
+
reason: issue.reason,
|
|
155
|
+
lastSeen: Date.now()
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const [fp, state] of activeIssues.entries()) {
|
|
160
|
+
if (!seenThisBatch.has(fp)) {
|
|
161
|
+
state.missingCount++;
|
|
162
|
+
if (state.missingCount >= RESOLVE_AFTER_MISSES) {
|
|
163
|
+
activeIssues.delete(fp);
|
|
164
|
+
resolveIssue(fp);
|
|
165
|
+
logResolvedIssue(result.component, state.issue);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function showCriticalAlert(result) {
|
|
171
|
+
for (const issue of result.issues) {
|
|
172
|
+
if (issue.severity !== "CRITICAL") continue;
|
|
173
|
+
const fp = fingerprint(result.component, issue);
|
|
174
|
+
if (!shouldLog(fp)) continue;
|
|
175
|
+
showCriticalOverlay(result.component, issue);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function logNewIssue(result, issue) {
|
|
179
|
+
console.groupCollapsed(
|
|
180
|
+
`%c\u26A1 PerfGuard \xB7 ${result.component}`,
|
|
181
|
+
`color:${severityColor(issue.severity)};font-weight:bold`
|
|
182
|
+
);
|
|
183
|
+
console.info(
|
|
184
|
+
`%c${severityEmoji(issue.severity)} ${issue.ruleId} (${issue.severity})`,
|
|
185
|
+
`color:${severityColor(issue.severity)}`
|
|
186
|
+
);
|
|
187
|
+
console.info("Confidence:", `${Math.round(issue.confidence * 100)}%`);
|
|
188
|
+
console.info("Reason:", issue.reason);
|
|
189
|
+
console.info("Boundary:", result.boundaryType);
|
|
190
|
+
console.groupCollapsed("\u{1F4CA} Metrics");
|
|
191
|
+
console.table(result.metrics);
|
|
192
|
+
console.groupEnd();
|
|
193
|
+
console.groupEnd();
|
|
194
|
+
}
|
|
195
|
+
function logResolvedIssue(component, issue) {
|
|
196
|
+
console.info(
|
|
197
|
+
`%c\u2705 PerfGuard \xB7 RESOLVED \xB7 ${component} \xB7 ${issue.ruleId}`,
|
|
198
|
+
"color:#16a34a;font-weight:bold"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
function showCriticalOverlay(component, issue) {
|
|
202
|
+
if (typeof document === "undefined") return;
|
|
203
|
+
const alert = document.createElement("div");
|
|
204
|
+
alert.style.cssText = `
|
|
205
|
+
position: fixed;
|
|
206
|
+
bottom: 20px;
|
|
207
|
+
right: 20px;
|
|
208
|
+
background: #dc2626;
|
|
209
|
+
color: white;
|
|
210
|
+
padding: 14px 18px;
|
|
211
|
+
border-radius: 8px;
|
|
212
|
+
box-shadow: 0 10px 20px rgba(0,0,0,0.35);
|
|
213
|
+
z-index: 10000;
|
|
214
|
+
font-family: monospace;
|
|
215
|
+
font-size: 13px;
|
|
216
|
+
max-width: 360px;
|
|
217
|
+
`;
|
|
218
|
+
alert.innerHTML = `
|
|
219
|
+
<strong>\u{1F4A5} PerfGuard \u2013 CRITICAL</strong><br/>
|
|
220
|
+
Component: ${component}<br/>
|
|
221
|
+
Rule: ${issue.ruleId}<br/>
|
|
222
|
+
Confidence: ${Math.round(issue.confidence * 100)}%
|
|
223
|
+
`;
|
|
224
|
+
document.body.appendChild(alert);
|
|
225
|
+
setTimeout(() => alert.remove(), 8e3);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/worker/createWorker.ts
|
|
229
|
+
function createAnalyzerWorker() {
|
|
230
|
+
const code = `
|
|
231
|
+
/* ===============================
|
|
232
|
+
PerfGuard \u2013 Enhanced Rule DSL Worker
|
|
233
|
+
=============================== */
|
|
234
|
+
|
|
235
|
+
const history = new Map();
|
|
236
|
+
const MAX_HISTORY = 10; // Increased for trend detection
|
|
237
|
+
|
|
238
|
+
const DOMINANT_RULES = new Set([
|
|
239
|
+
"BLOCKING_RENDER",
|
|
240
|
+
"SEVERE_PERF_REGRESSION",
|
|
241
|
+
"SUSPICIOUS_RENDER_LOOP",
|
|
242
|
+
"RENDER_TIME_CREEP",
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
let RULES = [];
|
|
246
|
+
|
|
247
|
+
/* -------------------------------
|
|
248
|
+
Utilities
|
|
249
|
+
-------------------------------- */
|
|
250
|
+
|
|
251
|
+
function hasDominantIssue(issues) {
|
|
252
|
+
return issues.some(issue => DOMINANT_RULES.has(issue.ruleId));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function record(snapshot) {
|
|
256
|
+
const list = history.get(snapshot.component) || [];
|
|
257
|
+
list.push(snapshot);
|
|
258
|
+
if (list.length > MAX_HISTORY) list.shift();
|
|
259
|
+
history.set(snapshot.component, list);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function calculateConfidenceWithCurrent(predicateFn, historyList, snapshot) {
|
|
263
|
+
if (historyList.length === 0) return 1;
|
|
264
|
+
|
|
265
|
+
const matches = historyList.filter(predicateFn).length;
|
|
266
|
+
const currentMatch = predicateFn(snapshot) ? 1 : 0;
|
|
267
|
+
|
|
268
|
+
return (matches + currentMatch) / (historyList.length + 1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function downgradeSeverity(severity, confidence, boundaryType) {
|
|
272
|
+
// CRITICAL is NEVER downgraded
|
|
273
|
+
if (severity === "CRITICAL") return "CRITICAL";
|
|
274
|
+
|
|
275
|
+
// INLINE components downgrade by ONE level only
|
|
276
|
+
if (boundaryType === "INLINE") {
|
|
277
|
+
if (severity === "HIGH") return "MEDIUM";
|
|
278
|
+
if (severity === "MEDIUM") return "LOW";
|
|
279
|
+
return "INFO";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// \u{1F4C9} Confidence-based softening
|
|
283
|
+
if (confidence < 0.7) return "LOW";
|
|
284
|
+
if (confidence < 0.85) return "MEDIUM";
|
|
285
|
+
|
|
286
|
+
return severity;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function buildPredicate({ field, operator, value }) {
|
|
290
|
+
switch (operator) {
|
|
291
|
+
case ">": return (s) => s[field] > value;
|
|
292
|
+
case "<": return (s) => s[field] < value;
|
|
293
|
+
case ">=": return (s) => s[field] >= value;
|
|
294
|
+
case "<=": return (s) => s[field] <= value;
|
|
295
|
+
case "===": return (s) => s[field] === value;
|
|
296
|
+
default: return () => false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function interpolateMessage(template, values) {
|
|
301
|
+
let result = template;
|
|
302
|
+
for (const [key, value] of Object.entries(values)) {
|
|
303
|
+
result = result.replace(
|
|
304
|
+
new RegExp(\`{\${key}}\`, 'g'),
|
|
305
|
+
typeof value === 'number' ? Math.round(value).toString() : value
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* -------------------------------
|
|
312
|
+
Trend Detection
|
|
313
|
+
-------------------------------- */
|
|
314
|
+
|
|
315
|
+
function detectTrend(historyList, field) {
|
|
316
|
+
if (historyList.length < 5) return { direction: 'stable', change: 0 };
|
|
317
|
+
|
|
318
|
+
// Use linear regression to detect trend
|
|
319
|
+
const values = historyList.map(s => s[field]);
|
|
320
|
+
const n = values.length;
|
|
321
|
+
const indices = Array.from({ length: n }, (_, i) => i);
|
|
322
|
+
|
|
323
|
+
const sumX = indices.reduce((a, b) => a + b, 0);
|
|
324
|
+
const sumY = values.reduce((a, b) => a + b, 0);
|
|
325
|
+
const sumXY = indices.reduce((sum, x, i) => sum + x * values[i], 0);
|
|
326
|
+
const sumX2 = indices.reduce((sum, x) => sum + x * x, 0);
|
|
327
|
+
|
|
328
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
329
|
+
const avgValue = sumY / n;
|
|
330
|
+
|
|
331
|
+
// Calculate percentage change
|
|
332
|
+
const percentChange = (slope * n / avgValue) * 100;
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
direction: slope > 0 ? 'increasing' : slope < 0 ? 'decreasing' : 'stable',
|
|
336
|
+
change: Math.abs(percentChange)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* -------------------------------
|
|
341
|
+
Rule Evaluation Engine
|
|
342
|
+
-------------------------------- */
|
|
343
|
+
|
|
344
|
+
function evaluate(snapshot) {
|
|
345
|
+
const issues = [];
|
|
346
|
+
const historyList = history.get(snapshot.component) || [];
|
|
347
|
+
const previous = historyList[historyList.length - 1];
|
|
348
|
+
|
|
349
|
+
/* ===============================
|
|
350
|
+
PASS 1: DOMINANT RULES ONLY
|
|
351
|
+
=============================== */
|
|
352
|
+
for (const rule of RULES) {
|
|
353
|
+
if (!DOMINANT_RULES.has(rule.id)) continue;
|
|
354
|
+
|
|
355
|
+
/* ----- Regression Rule ----- */
|
|
356
|
+
if (rule.regression && previous) {
|
|
357
|
+
const field = rule.regression.field;
|
|
358
|
+
const multiplier = rule.regression.multiplier;
|
|
359
|
+
|
|
360
|
+
if (snapshot[field] > previous[field] * multiplier) {
|
|
361
|
+
issues.push({
|
|
362
|
+
ruleId: rule.id,
|
|
363
|
+
confidence: 1,
|
|
364
|
+
severity: downgradeSeverity(
|
|
365
|
+
rule.baseSeverity,
|
|
366
|
+
1,
|
|
367
|
+
snapshot.boundaryType || "HOC"
|
|
368
|
+
),
|
|
369
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
370
|
+
prevValue: previous[field].toFixed(1),
|
|
371
|
+
currValue: snapshot[field].toFixed(1),
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ----- Trend Detection Rule ----- */
|
|
379
|
+
if (rule.trend && historyList.length >= 5) {
|
|
380
|
+
const trend = detectTrend(historyList, rule.trend.field);
|
|
381
|
+
|
|
382
|
+
if (
|
|
383
|
+
trend.direction === rule.trend.direction &&
|
|
384
|
+
trend.change >= rule.trend.threshold
|
|
385
|
+
) {
|
|
386
|
+
const confidence = Math.min(trend.change / 100, 1.0);
|
|
387
|
+
|
|
388
|
+
issues.push({
|
|
389
|
+
ruleId: rule.id,
|
|
390
|
+
confidence,
|
|
391
|
+
severity: downgradeSeverity(
|
|
392
|
+
rule.baseSeverity,
|
|
393
|
+
confidence,
|
|
394
|
+
snapshot.boundaryType || "HOC"
|
|
395
|
+
),
|
|
396
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
397
|
+
change: trend.change.toFixed(1),
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* ----- Predicate Rule ----- */
|
|
405
|
+
if (rule.predicate) {
|
|
406
|
+
const predicateFn = buildPredicate(rule.predicate);
|
|
407
|
+
const confidence = calculateConfidenceWithCurrent(
|
|
408
|
+
predicateFn,
|
|
409
|
+
historyList,
|
|
410
|
+
snapshot
|
|
411
|
+
);
|
|
412
|
+
const threshold = rule.confidenceThreshold || 0.6;
|
|
413
|
+
|
|
414
|
+
if (predicateFn(snapshot) && confidence >= threshold) {
|
|
415
|
+
issues.push({
|
|
416
|
+
ruleId: rule.id,
|
|
417
|
+
confidence,
|
|
418
|
+
severity: downgradeSeverity(
|
|
419
|
+
rule.baseSeverity,
|
|
420
|
+
confidence,
|
|
421
|
+
snapshot.boundaryType || "HOC"
|
|
422
|
+
),
|
|
423
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
424
|
+
confidence: (confidence * 100).toFixed(0),
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/* ===============================
|
|
432
|
+
If dominant issue exists \u2192 stop
|
|
433
|
+
=============================== */
|
|
434
|
+
if (issues.length) {
|
|
435
|
+
// Allow DEV_HINT rules only
|
|
436
|
+
for (const rule of RULES) {
|
|
437
|
+
if (!rule.id.startsWith("DEV_HINT")) continue;
|
|
438
|
+
if (!rule.predicate) continue;
|
|
439
|
+
|
|
440
|
+
const predicateFn = buildPredicate(rule.predicate);
|
|
441
|
+
const confidence = calculateConfidenceWithCurrent(
|
|
442
|
+
predicateFn,
|
|
443
|
+
historyList,
|
|
444
|
+
snapshot
|
|
445
|
+
);
|
|
446
|
+
const threshold = rule.confidenceThreshold || 0.6;
|
|
447
|
+
|
|
448
|
+
if (predicateFn(snapshot) && confidence >= threshold) {
|
|
449
|
+
issues.push({
|
|
450
|
+
ruleId: rule.id,
|
|
451
|
+
confidence,
|
|
452
|
+
severity: "INFO",
|
|
453
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
454
|
+
confidence: (confidence * 100).toFixed(0),
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
record(snapshot);
|
|
461
|
+
return issues;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ===============================
|
|
465
|
+
PASS 2: NORMAL RULES
|
|
466
|
+
=============================== */
|
|
467
|
+
for (const rule of RULES) {
|
|
468
|
+
|
|
469
|
+
/* ----- Regression Rule ----- */
|
|
470
|
+
if (rule.regression && previous) {
|
|
471
|
+
const field = rule.regression.field;
|
|
472
|
+
const multiplier = rule.regression.multiplier;
|
|
473
|
+
|
|
474
|
+
if (snapshot[field] > previous[field] * multiplier) {
|
|
475
|
+
issues.push({
|
|
476
|
+
ruleId: rule.id,
|
|
477
|
+
confidence: 1,
|
|
478
|
+
severity: downgradeSeverity(
|
|
479
|
+
rule.baseSeverity,
|
|
480
|
+
1,
|
|
481
|
+
snapshot.boundaryType || "HOC"
|
|
482
|
+
),
|
|
483
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
484
|
+
prevValue: previous[field].toFixed(1),
|
|
485
|
+
currValue: snapshot[field].toFixed(1),
|
|
486
|
+
}),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* ----- Trend Detection Rule ----- */
|
|
493
|
+
if (rule.trend && historyList.length >= 5) {
|
|
494
|
+
const trend = detectTrend(historyList, rule.trend.field);
|
|
495
|
+
|
|
496
|
+
if (
|
|
497
|
+
trend.direction === rule.trend.direction &&
|
|
498
|
+
trend.change >= rule.trend.threshold
|
|
499
|
+
) {
|
|
500
|
+
const confidence = Math.min(trend.change / 100, 1.0);
|
|
501
|
+
|
|
502
|
+
issues.push({
|
|
503
|
+
ruleId: rule.id,
|
|
504
|
+
confidence,
|
|
505
|
+
severity: downgradeSeverity(
|
|
506
|
+
rule.baseSeverity,
|
|
507
|
+
confidence,
|
|
508
|
+
snapshot.boundaryType || "HOC"
|
|
509
|
+
),
|
|
510
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
511
|
+
change: trend.change.toFixed(1),
|
|
512
|
+
}),
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* ----- Predicate Rule ----- */
|
|
519
|
+
if (rule.predicate) {
|
|
520
|
+
const predicateFn = buildPredicate(rule.predicate);
|
|
521
|
+
const confidence = calculateConfidenceWithCurrent(
|
|
522
|
+
predicateFn,
|
|
523
|
+
historyList,
|
|
524
|
+
snapshot
|
|
525
|
+
);
|
|
526
|
+
const threshold = rule.confidenceThreshold || 0.6;
|
|
527
|
+
|
|
528
|
+
if (predicateFn(snapshot) && confidence >= threshold) {
|
|
529
|
+
issues.push({
|
|
530
|
+
ruleId: rule.id,
|
|
531
|
+
confidence,
|
|
532
|
+
severity: downgradeSeverity(
|
|
533
|
+
rule.baseSeverity,
|
|
534
|
+
confidence,
|
|
535
|
+
snapshot.boundaryType || "HOC"
|
|
536
|
+
),
|
|
537
|
+
reason: interpolateMessage(rule.messageTemplate, {
|
|
538
|
+
confidence: (confidence * 100).toFixed(0),
|
|
539
|
+
}),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
record(snapshot);
|
|
546
|
+
return issues;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
/* -------------------------------
|
|
551
|
+
Worker Message Handler
|
|
552
|
+
-------------------------------- */
|
|
553
|
+
|
|
554
|
+
self.onmessage = (e) => {
|
|
555
|
+
const { type, payload } = e.data;
|
|
556
|
+
|
|
557
|
+
/* Init rules once */
|
|
558
|
+
if (type === "INIT_RULES") {
|
|
559
|
+
RULES = payload;
|
|
560
|
+
// console.log('[PerfGuard Worker] Rules loaded:', RULES.length);
|
|
561
|
+
self.postMessage({ type: "INIT_SUCCESS", count: RULES.length });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* Evaluate snapshots */
|
|
566
|
+
if (type === "EVALUATE") {
|
|
567
|
+
const results = [];
|
|
568
|
+
let hasCritical = false;
|
|
569
|
+
|
|
570
|
+
for (const snapshot of payload) {
|
|
571
|
+
const issues = evaluate(snapshot);
|
|
572
|
+
|
|
573
|
+
if (issues.length) {
|
|
574
|
+
const componentResult = {
|
|
575
|
+
component: snapshot.component,
|
|
576
|
+
boundaryType: snapshot.boundaryType || "HOC",
|
|
577
|
+
metrics: {
|
|
578
|
+
renders: snapshot.renders,
|
|
579
|
+
avgTime: snapshot.avgTime,
|
|
580
|
+
maxTime: snapshot.maxTime,
|
|
581
|
+
},
|
|
582
|
+
issues,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Check for critical issues
|
|
586
|
+
if (issues.some(i => i.severity === 'CRITICAL')) {
|
|
587
|
+
hasCritical = true;
|
|
588
|
+
componentResult.hasCritical = true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
results.push(componentResult);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// console.log('[PerfGuard Worker] Evaluation complete:', results.length, 'issues');
|
|
596
|
+
self.postMessage({
|
|
597
|
+
type: "RESULTS",
|
|
598
|
+
data: results,
|
|
599
|
+
hasCritical,
|
|
600
|
+
timestamp: Date.now()
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* Reset history */
|
|
605
|
+
if (type === "RESET") {
|
|
606
|
+
history.clear();
|
|
607
|
+
self.postMessage({ type: "RESET_SUCCESS" });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* Get stats */
|
|
611
|
+
if (type === "GET_STATS") {
|
|
612
|
+
const stats = {
|
|
613
|
+
componentsTracked: history.size,
|
|
614
|
+
totalSnapshots: Array.from(history.values()).reduce((sum, list) => sum + list.length, 0),
|
|
615
|
+
rulesLoaded: RULES.length,
|
|
616
|
+
};
|
|
617
|
+
self.postMessage({ type: "STATS", data: stats });
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
`;
|
|
621
|
+
return new Worker(
|
|
622
|
+
URL.createObjectURL(new Blob([code], { type: "application/javascript" }))
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/env.ts
|
|
627
|
+
var isDev = typeof process !== "undefined" ? process.env.NODE_ENV !== "production" : true;
|
|
628
|
+
|
|
629
|
+
// src/pref-engine/rules.ts
|
|
630
|
+
var PERF_RULES = [
|
|
631
|
+
// ========================================
|
|
632
|
+
// CRITICAL PERFORMANCE RULES
|
|
633
|
+
// ========================================
|
|
634
|
+
{
|
|
635
|
+
id: "BLOCKING_RENDER",
|
|
636
|
+
category: "PERFORMANCE",
|
|
637
|
+
baseSeverity: "CRITICAL",
|
|
638
|
+
predicate: {
|
|
639
|
+
field: "avgTime",
|
|
640
|
+
operator: ">",
|
|
641
|
+
value: 100
|
|
642
|
+
},
|
|
643
|
+
confidenceThreshold: 0.4,
|
|
644
|
+
messageTemplate: "CRITICAL: Component blocks UI thread for >100ms in {confidence}% of renders",
|
|
645
|
+
messageFields: ["confidence"],
|
|
646
|
+
docUrl: "https://web.dev/rail/"
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: "SLOW_RENDER",
|
|
650
|
+
category: "PERFORMANCE",
|
|
651
|
+
baseSeverity: "HIGH",
|
|
652
|
+
predicate: {
|
|
653
|
+
field: "avgTime",
|
|
654
|
+
operator: ">",
|
|
655
|
+
value: 16
|
|
656
|
+
},
|
|
657
|
+
confidenceThreshold: 0.6,
|
|
658
|
+
messageTemplate: "Average render time exceeded 16ms in {confidence}% of recent batches",
|
|
659
|
+
messageFields: ["confidence"]
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
id: "VERY_SLOW_RENDER",
|
|
663
|
+
category: "PERFORMANCE",
|
|
664
|
+
baseSeverity: "HIGH",
|
|
665
|
+
predicate: {
|
|
666
|
+
field: "avgTime",
|
|
667
|
+
operator: ">",
|
|
668
|
+
value: 50
|
|
669
|
+
},
|
|
670
|
+
confidenceThreshold: 0.5,
|
|
671
|
+
messageTemplate: "Critical: render time exceeded 50ms in {confidence}% of batches",
|
|
672
|
+
messageFields: ["confidence"]
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
id: "INCONSISTENT_PERFORMANCE",
|
|
676
|
+
category: "STABILITY",
|
|
677
|
+
baseSeverity: "MEDIUM",
|
|
678
|
+
predicate: {
|
|
679
|
+
field: "maxTime",
|
|
680
|
+
operator: ">",
|
|
681
|
+
value: 50
|
|
682
|
+
},
|
|
683
|
+
confidenceThreshold: 0.3,
|
|
684
|
+
messageTemplate: "Unstable performance: max render time spikes to >50ms",
|
|
685
|
+
messageFields: ["confidence"]
|
|
686
|
+
},
|
|
687
|
+
// ========================================
|
|
688
|
+
// RENDER OPTIMIZATION RULES
|
|
689
|
+
// ========================================
|
|
690
|
+
{
|
|
691
|
+
id: "EXCESSIVE_RENDERS",
|
|
692
|
+
category: "PERFORMANCE",
|
|
693
|
+
baseSeverity: "MEDIUM",
|
|
694
|
+
predicate: {
|
|
695
|
+
field: "renders",
|
|
696
|
+
operator: ">",
|
|
697
|
+
value: 20
|
|
698
|
+
},
|
|
699
|
+
confidenceThreshold: 0.6,
|
|
700
|
+
messageTemplate: "Component rendered excessively in {confidence}% of recent batches",
|
|
701
|
+
messageFields: ["confidence"]
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
id: "RENDER_THRASHING",
|
|
705
|
+
category: "PERFORMANCE",
|
|
706
|
+
baseSeverity: "HIGH",
|
|
707
|
+
predicate: {
|
|
708
|
+
field: "renders",
|
|
709
|
+
operator: ">",
|
|
710
|
+
value: 50
|
|
711
|
+
},
|
|
712
|
+
confidenceThreshold: 0.4,
|
|
713
|
+
messageTemplate: "Severe render thrashing detected: >50 renders in {confidence}% of batches",
|
|
714
|
+
messageFields: ["confidence"]
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
id: "SUSPICIOUS_RENDER_LOOP",
|
|
718
|
+
category: "RELIABILITY",
|
|
719
|
+
baseSeverity: "CRITICAL",
|
|
720
|
+
predicate: {
|
|
721
|
+
field: "renders",
|
|
722
|
+
operator: ">",
|
|
723
|
+
value: 100
|
|
724
|
+
},
|
|
725
|
+
confidenceThreshold: 0.2,
|
|
726
|
+
messageTemplate: "Potential infinite render loop: >100 renders detected",
|
|
727
|
+
messageFields: ["confidence"]
|
|
728
|
+
},
|
|
729
|
+
// ========================================
|
|
730
|
+
// REGRESSION DETECTION
|
|
731
|
+
// ========================================
|
|
732
|
+
{
|
|
733
|
+
id: "PERF_REGRESSION",
|
|
734
|
+
category: "PERFORMANCE",
|
|
735
|
+
baseSeverity: "HIGH",
|
|
736
|
+
regression: {
|
|
737
|
+
field: "avgTime",
|
|
738
|
+
multiplier: 1.3
|
|
739
|
+
},
|
|
740
|
+
messageTemplate: "Avg render time increased from {prevValue}ms to {currValue}ms",
|
|
741
|
+
messageFields: ["prevValue", "currValue"]
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
id: "SEVERE_PERF_REGRESSION",
|
|
745
|
+
category: "PERFORMANCE",
|
|
746
|
+
baseSeverity: "CRITICAL",
|
|
747
|
+
regression: {
|
|
748
|
+
field: "avgTime",
|
|
749
|
+
multiplier: 2
|
|
750
|
+
},
|
|
751
|
+
messageTemplate: "SEVERE: Performance degraded by 2x from {prevValue}ms to {currValue}ms",
|
|
752
|
+
messageFields: ["prevValue", "currValue"]
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
id: "RENDER_COUNT_SPIKE",
|
|
756
|
+
category: "STABILITY",
|
|
757
|
+
baseSeverity: "MEDIUM",
|
|
758
|
+
regression: {
|
|
759
|
+
field: "renders",
|
|
760
|
+
multiplier: 1.5
|
|
761
|
+
},
|
|
762
|
+
messageTemplate: "Render count increased from {prevValue} to {currValue}",
|
|
763
|
+
messageFields: ["prevValue", "currValue"]
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
id: "MAX_TIME_REGRESSION",
|
|
767
|
+
category: "STABILITY",
|
|
768
|
+
baseSeverity: "HIGH",
|
|
769
|
+
regression: {
|
|
770
|
+
field: "maxTime",
|
|
771
|
+
multiplier: 1.5
|
|
772
|
+
},
|
|
773
|
+
messageTemplate: "Peak render time degraded from {prevValue}ms to {currValue}ms",
|
|
774
|
+
messageFields: ["prevValue", "currValue"]
|
|
775
|
+
},
|
|
776
|
+
// ========================================
|
|
777
|
+
// USER EXPERIENCE RULES
|
|
778
|
+
// ========================================
|
|
779
|
+
{
|
|
780
|
+
id: "JANKY_ANIMATION",
|
|
781
|
+
category: "UX",
|
|
782
|
+
baseSeverity: "MEDIUM",
|
|
783
|
+
predicate: {
|
|
784
|
+
field: "avgTime",
|
|
785
|
+
operator: ">",
|
|
786
|
+
value: 16.67
|
|
787
|
+
// 60fps threshold
|
|
788
|
+
},
|
|
789
|
+
confidenceThreshold: 0.7,
|
|
790
|
+
messageTemplate: "Janky animations: unable to maintain 60fps in {confidence}% of renders",
|
|
791
|
+
messageFields: ["confidence"]
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
id: "POOR_INTERACTION_RESPONSE",
|
|
795
|
+
category: "UX",
|
|
796
|
+
baseSeverity: "HIGH",
|
|
797
|
+
predicate: {
|
|
798
|
+
field: "avgTime",
|
|
799
|
+
operator: ">",
|
|
800
|
+
value: 50
|
|
801
|
+
},
|
|
802
|
+
confidenceThreshold: 0.5,
|
|
803
|
+
messageTemplate: "Poor interaction response: >50ms delay in {confidence}% of renders",
|
|
804
|
+
messageFields: ["confidence"]
|
|
805
|
+
},
|
|
806
|
+
// ========================================
|
|
807
|
+
// MEMORY & LEAK DETECTION
|
|
808
|
+
// ========================================
|
|
809
|
+
{
|
|
810
|
+
id: "RENDER_TIME_CREEP",
|
|
811
|
+
category: "MEMORY",
|
|
812
|
+
baseSeverity: "MEDIUM",
|
|
813
|
+
trend: {
|
|
814
|
+
field: "avgTime",
|
|
815
|
+
direction: "increasing",
|
|
816
|
+
threshold: 20
|
|
817
|
+
// 20% increase over time
|
|
818
|
+
},
|
|
819
|
+
messageTemplate: "Gradual performance degradation: render time increasing over time",
|
|
820
|
+
confidenceThreshold: 0.6
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
id: "RENDER_COUNT_CREEP",
|
|
824
|
+
category: "MEMORY",
|
|
825
|
+
baseSeverity: "MEDIUM",
|
|
826
|
+
trend: {
|
|
827
|
+
field: "renders",
|
|
828
|
+
direction: "increasing",
|
|
829
|
+
threshold: 30
|
|
830
|
+
},
|
|
831
|
+
messageTemplate: "Render count steadily increasing - possible state management issue",
|
|
832
|
+
confidenceThreshold: 0.6
|
|
833
|
+
},
|
|
834
|
+
// ========================================
|
|
835
|
+
// STABILITY & RELIABILITY
|
|
836
|
+
// ========================================
|
|
837
|
+
{
|
|
838
|
+
id: "ERRATIC_PERFORMANCE",
|
|
839
|
+
category: "STABILITY",
|
|
840
|
+
baseSeverity: "MEDIUM",
|
|
841
|
+
predicate: {
|
|
842
|
+
field: "maxTime",
|
|
843
|
+
operator: ">",
|
|
844
|
+
value: 100
|
|
845
|
+
},
|
|
846
|
+
confidenceThreshold: 0.3,
|
|
847
|
+
messageTemplate: "Erratic performance spikes detected (>100ms)",
|
|
848
|
+
messageFields: ["confidence"]
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
id: "FIRST_RENDER_SLOW",
|
|
852
|
+
category: "UX",
|
|
853
|
+
baseSeverity: "MEDIUM",
|
|
854
|
+
predicate: {
|
|
855
|
+
field: "maxTime",
|
|
856
|
+
operator: ">",
|
|
857
|
+
value: 200
|
|
858
|
+
},
|
|
859
|
+
confidenceThreshold: 0.8,
|
|
860
|
+
messageTemplate: "Slow initial mount: first render >200ms",
|
|
861
|
+
messageFields: ["confidence"]
|
|
862
|
+
},
|
|
863
|
+
// ========================================
|
|
864
|
+
// PRODUCTION READINESS CHECKS
|
|
865
|
+
// ========================================
|
|
866
|
+
{
|
|
867
|
+
id: "PROD_READY_PERF",
|
|
868
|
+
category: "PERFORMANCE",
|
|
869
|
+
baseSeverity: "INFO",
|
|
870
|
+
predicate: {
|
|
871
|
+
field: "avgTime",
|
|
872
|
+
operator: "<",
|
|
873
|
+
value: 10
|
|
874
|
+
},
|
|
875
|
+
confidenceThreshold: 0.9,
|
|
876
|
+
messageTemplate: "\u2705 Component meets production performance standards (<10ms avg)",
|
|
877
|
+
messageFields: ["confidence"]
|
|
878
|
+
},
|
|
879
|
+
// ========================================
|
|
880
|
+
// HELPFUL HINTS (DEVELOPMENT)
|
|
881
|
+
// ========================================
|
|
882
|
+
{
|
|
883
|
+
id: "DEV_HINT_MEMOIZATION",
|
|
884
|
+
category: "PERFORMANCE",
|
|
885
|
+
baseSeverity: "INFO",
|
|
886
|
+
predicate: {
|
|
887
|
+
field: "renders",
|
|
888
|
+
operator: ">",
|
|
889
|
+
value: 15
|
|
890
|
+
},
|
|
891
|
+
confidenceThreshold: 0.5,
|
|
892
|
+
messageTemplate: "\u{1F4A1} Consider React.memo or useMemo for this component",
|
|
893
|
+
messageFields: ["confidence"]
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
id: "DEV_HINT_OPTIMIZATION",
|
|
897
|
+
category: "PERFORMANCE",
|
|
898
|
+
baseSeverity: "INFO",
|
|
899
|
+
predicate: {
|
|
900
|
+
field: "avgTime",
|
|
901
|
+
operator: ">",
|
|
902
|
+
value: 20
|
|
903
|
+
},
|
|
904
|
+
confidenceThreshold: 0.5,
|
|
905
|
+
messageTemplate: "\u{1F4A1} Component could benefit from optimization (>20ms avg)",
|
|
906
|
+
messageFields: ["confidence"]
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
function getRulesConfig() {
|
|
910
|
+
return PERF_RULES;
|
|
911
|
+
}
|
|
912
|
+
var RULE_GROUPS = {
|
|
913
|
+
CRITICAL_ONLY: PERF_RULES.filter((r) => r.baseSeverity === "CRITICAL"),
|
|
914
|
+
PERFORMANCE: PERF_RULES.filter((r) => r.category === "PERFORMANCE"),
|
|
915
|
+
UX: PERF_RULES.filter((r) => r.category === "UX"),
|
|
916
|
+
STABILITY: PERF_RULES.filter((r) => r.category === "STABILITY"),
|
|
917
|
+
MEMORY: PERF_RULES.filter((r) => r.category === "MEMORY"),
|
|
918
|
+
HINTS: PERF_RULES.filter((r) => r.id.startsWith("DEV_HINT") || r.id.startsWith("PROD_READY"))
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/PrevGuardPanel.tsx
|
|
922
|
+
import { useEffect, useState } from "react";
|
|
923
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
924
|
+
function PerfGuardPanel() {
|
|
925
|
+
const [issues, setIssues] = useState([]);
|
|
926
|
+
const [open, setOpen] = useState(false);
|
|
927
|
+
const [selectedIssue, setSelectedIssue] = useState(null);
|
|
928
|
+
useEffect(() => {
|
|
929
|
+
subscribe(setIssues);
|
|
930
|
+
}, []);
|
|
931
|
+
if (!issues.length) {
|
|
932
|
+
return /* @__PURE__ */ jsx("div", { style: panel, children: /* @__PURE__ */ jsxs("div", { style: header, children: [
|
|
933
|
+
/* @__PURE__ */ jsx("div", { style: headerTitle, children: "PerfGuard" }),
|
|
934
|
+
/* @__PURE__ */ jsx("div", { style: headerSubtitle, children: "No issues detected" })
|
|
935
|
+
] }) });
|
|
936
|
+
}
|
|
937
|
+
const criticalCount = issues.filter(
|
|
938
|
+
(i) => i.severity === "CRITICAL" && i.status === "ACTIVE"
|
|
939
|
+
).length;
|
|
940
|
+
const highCount = issues.filter(
|
|
941
|
+
(i) => i.severity === "HIGH" && i.status === "ACTIVE"
|
|
942
|
+
).length;
|
|
943
|
+
const activeCount = issues.filter(
|
|
944
|
+
(i) => i.status === "ACTIVE" || i.status === "NEW"
|
|
945
|
+
).length;
|
|
946
|
+
const getSeverityClass = (severity) => {
|
|
947
|
+
switch (severity) {
|
|
948
|
+
case "CRITICAL":
|
|
949
|
+
return "severity-critical";
|
|
950
|
+
case "HIGH":
|
|
951
|
+
return "severity-high";
|
|
952
|
+
case "MEDIUM":
|
|
953
|
+
return "severity-medium";
|
|
954
|
+
default:
|
|
955
|
+
return "severity-low";
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const sortedIssues = [...issues].sort((a, b) => {
|
|
959
|
+
if (a.status === b.status) return 0;
|
|
960
|
+
return a.status === "ACTIVE" ? -1 : 1;
|
|
961
|
+
});
|
|
962
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
963
|
+
/* @__PURE__ */ jsx("style", { children: styles }),
|
|
964
|
+
/* @__PURE__ */ jsxs("div", { style: panel, children: [
|
|
965
|
+
/* @__PURE__ */ jsxs(
|
|
966
|
+
"div",
|
|
967
|
+
{
|
|
968
|
+
style: header,
|
|
969
|
+
onClick: () => setOpen((o) => !o),
|
|
970
|
+
className: "perfguard-header",
|
|
971
|
+
children: [
|
|
972
|
+
/* @__PURE__ */ jsxs("div", { style: headerLeft, children: [
|
|
973
|
+
/* @__PURE__ */ jsxs("div", { style: iconWrapper, children: [
|
|
974
|
+
/* @__PURE__ */ jsx("span", { style: icon, children: "\u26A1" }),
|
|
975
|
+
criticalCount > 0 && /* @__PURE__ */ jsx("span", { style: criticalPing })
|
|
976
|
+
] }),
|
|
977
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
978
|
+
/* @__PURE__ */ jsxs("div", { style: headerTitle, children: [
|
|
979
|
+
"PerfGuard",
|
|
980
|
+
/* @__PURE__ */ jsx("span", { style: betaBadge, children: "BETA" })
|
|
981
|
+
] }),
|
|
982
|
+
/* @__PURE__ */ jsxs("div", { style: headerSubtitle, children: [
|
|
983
|
+
activeCount,
|
|
984
|
+
" active issue",
|
|
985
|
+
activeCount !== 1 ? "s" : ""
|
|
986
|
+
] })
|
|
987
|
+
] })
|
|
988
|
+
] }),
|
|
989
|
+
/* @__PURE__ */ jsxs("div", { style: headerRight, children: [
|
|
990
|
+
criticalCount > 0 && /* @__PURE__ */ jsxs("span", { style: criticalBadge, className: "critical-badge", children: [
|
|
991
|
+
criticalCount,
|
|
992
|
+
" Critical"
|
|
993
|
+
] }),
|
|
994
|
+
highCount > 0 && /* @__PURE__ */ jsxs("span", { style: highBadge, children: [
|
|
995
|
+
highCount,
|
|
996
|
+
" High"
|
|
997
|
+
] }),
|
|
998
|
+
/* @__PURE__ */ jsx(
|
|
999
|
+
"svg",
|
|
1000
|
+
{
|
|
1001
|
+
style: {
|
|
1002
|
+
...chevron,
|
|
1003
|
+
transform: open ? "rotate(180deg)" : "rotate(0deg)"
|
|
1004
|
+
},
|
|
1005
|
+
fill: "none",
|
|
1006
|
+
viewBox: "0 0 24 24",
|
|
1007
|
+
stroke: "currentColor",
|
|
1008
|
+
children: /* @__PURE__ */ jsx(
|
|
1009
|
+
"path",
|
|
1010
|
+
{
|
|
1011
|
+
strokeLinecap: "round",
|
|
1012
|
+
strokeLinejoin: "round",
|
|
1013
|
+
strokeWidth: 2,
|
|
1014
|
+
d: "M19 9l-7 7-7-7"
|
|
1015
|
+
}
|
|
1016
|
+
)
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
] })
|
|
1020
|
+
]
|
|
1021
|
+
}
|
|
1022
|
+
),
|
|
1023
|
+
open && /* @__PURE__ */ jsxs("div", { style: issuesContainer, className: "issues-container", children: [
|
|
1024
|
+
/* @__PURE__ */ jsx("div", { style: issuesList, children: sortedIssues.map((issue, idx) => /* @__PURE__ */ jsx(
|
|
1025
|
+
"div",
|
|
1026
|
+
{
|
|
1027
|
+
onClick: () => setSelectedIssue(
|
|
1028
|
+
selectedIssue === issue.id ? null : issue.id
|
|
1029
|
+
),
|
|
1030
|
+
style: issueCard(idx),
|
|
1031
|
+
className: `issue-card ${getSeverityClass(issue.severity)}`,
|
|
1032
|
+
children: /* @__PURE__ */ jsxs("div", { style: issueContent, children: [
|
|
1033
|
+
/* @__PURE__ */ jsxs("div", { style: issueMain, children: [
|
|
1034
|
+
/* @__PURE__ */ jsxs("div", { style: issueHeader, children: [
|
|
1035
|
+
/* @__PURE__ */ jsx(
|
|
1036
|
+
"span",
|
|
1037
|
+
{
|
|
1038
|
+
style: statusDot(issue.status),
|
|
1039
|
+
className: issue.status === "ACTIVE" ? "status-dot-pulse" : ""
|
|
1040
|
+
}
|
|
1041
|
+
),
|
|
1042
|
+
/* @__PURE__ */ jsx("span", { style: componentName, children: issue.component }),
|
|
1043
|
+
issue.status === "RESOLVED" ? /* @__PURE__ */ jsx("span", { style: statusBadgeResolved, children: "\u2713 Resolved" }) : /* @__PURE__ */ jsx(
|
|
1044
|
+
"span",
|
|
1045
|
+
{
|
|
1046
|
+
style: statusBadgeOpen,
|
|
1047
|
+
className: "status-badge-open",
|
|
1048
|
+
children: "\u25CF Open"
|
|
1049
|
+
}
|
|
1050
|
+
)
|
|
1051
|
+
] }),
|
|
1052
|
+
/* @__PURE__ */ jsx("div", { style: ruleId, children: issue.ruleId.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ") }),
|
|
1053
|
+
/* @__PURE__ */ jsxs("div", { style: issueMetrics, children: [
|
|
1054
|
+
/* @__PURE__ */ jsxs("div", { style: metric, children: [
|
|
1055
|
+
/* @__PURE__ */ jsx("span", { style: metricLabel, children: "Severity:" }),
|
|
1056
|
+
/* @__PURE__ */ jsx("span", { style: metricValue, children: issue.severity })
|
|
1057
|
+
] }),
|
|
1058
|
+
/* @__PURE__ */ jsxs("div", { style: metric, children: [
|
|
1059
|
+
/* @__PURE__ */ jsx("span", { style: metricLabel, children: "Confidence:" }),
|
|
1060
|
+
/* @__PURE__ */ jsxs("div", { style: confidenceWrapper, children: [
|
|
1061
|
+
/* @__PURE__ */ jsx("div", { style: confidenceBar, children: /* @__PURE__ */ jsx(
|
|
1062
|
+
"div",
|
|
1063
|
+
{
|
|
1064
|
+
style: {
|
|
1065
|
+
...confidenceProgress,
|
|
1066
|
+
width: `${issue.confidence * 100}%`
|
|
1067
|
+
},
|
|
1068
|
+
className: `confidence-progress ${getSeverityClass(
|
|
1069
|
+
issue.severity
|
|
1070
|
+
)}`
|
|
1071
|
+
}
|
|
1072
|
+
) }),
|
|
1073
|
+
/* @__PURE__ */ jsxs("span", { style: confidenceValue, children: [
|
|
1074
|
+
Math.round(issue.confidence * 100),
|
|
1075
|
+
"%"
|
|
1076
|
+
] })
|
|
1077
|
+
] })
|
|
1078
|
+
] })
|
|
1079
|
+
] }),
|
|
1080
|
+
selectedIssue === issue.id && /* @__PURE__ */ jsx("div", { style: issueDetails, className: "issue-details", children: /* @__PURE__ */ jsxs("div", { style: detailsContent, children: [
|
|
1081
|
+
/* @__PURE__ */ jsxs("span", { style: detailsText, children: [
|
|
1082
|
+
"Rule ID: ",
|
|
1083
|
+
issue.ruleId
|
|
1084
|
+
] }),
|
|
1085
|
+
/* @__PURE__ */ jsx("span", { style: detailsDescription, children: issue.reason })
|
|
1086
|
+
] }) })
|
|
1087
|
+
] }),
|
|
1088
|
+
/* @__PURE__ */ jsx(
|
|
1089
|
+
"svg",
|
|
1090
|
+
{
|
|
1091
|
+
style: {
|
|
1092
|
+
...expandIcon,
|
|
1093
|
+
transform: selectedIssue === issue.id ? "rotate(180deg)" : "rotate(0deg)"
|
|
1094
|
+
},
|
|
1095
|
+
fill: "none",
|
|
1096
|
+
viewBox: "0 0 24 24",
|
|
1097
|
+
stroke: "currentColor",
|
|
1098
|
+
children: /* @__PURE__ */ jsx(
|
|
1099
|
+
"path",
|
|
1100
|
+
{
|
|
1101
|
+
strokeLinecap: "round",
|
|
1102
|
+
strokeLinejoin: "round",
|
|
1103
|
+
strokeWidth: 2,
|
|
1104
|
+
d: "M19 9l-7 7-7-7"
|
|
1105
|
+
}
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
)
|
|
1109
|
+
] })
|
|
1110
|
+
},
|
|
1111
|
+
issue.id
|
|
1112
|
+
)) }),
|
|
1113
|
+
/* @__PURE__ */ jsxs("div", { style: footer, children: [
|
|
1114
|
+
/* @__PURE__ */ jsx("span", { style: footerText, children: "Last scan: just now" }),
|
|
1115
|
+
/* @__PURE__ */ jsx("button", { style: scanButton, children: "Run Scan" })
|
|
1116
|
+
] })
|
|
1117
|
+
] })
|
|
1118
|
+
] })
|
|
1119
|
+
] });
|
|
1120
|
+
}
|
|
1121
|
+
var styles = `
|
|
1122
|
+
@keyframes slideUp {
|
|
1123
|
+
from { transform: translateY(20px); opacity: 0; }
|
|
1124
|
+
to { transform: translateY(0); opacity: 1; }
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
@keyframes slideDown {
|
|
1128
|
+
from { max-height: 0; opacity: 0; }
|
|
1129
|
+
to { max-height: 600px; opacity: 1; }
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
@keyframes pulseGlow {
|
|
1133
|
+
0%, 100% { box-shadow: 0 0 15px rgba(239, 68, 68, 0.3); }
|
|
1134
|
+
50% { box-shadow: 0 0 25px rgba(239, 68, 68, 0.6); }
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
@keyframes ping {
|
|
1138
|
+
75%, 100% {
|
|
1139
|
+
transform: scale(2);
|
|
1140
|
+
opacity: 0;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
@keyframes pulse {
|
|
1145
|
+
0%, 100% { opacity: 1; }
|
|
1146
|
+
50% { opacity: 0.5; }
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.perfguard-header:hover {
|
|
1150
|
+
background: linear-gradient(to right, #1e293b, #0f172a);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.critical-badge {
|
|
1154
|
+
animation: pulseGlow 2s ease-in-out infinite;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.status-dot-pulse {
|
|
1158
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.status-badge-open {
|
|
1162
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.issues-container {
|
|
1166
|
+
animation: slideDown 0.3s ease-out;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.issue-card {
|
|
1170
|
+
animation: slideUp 0.3s ease-out;
|
|
1171
|
+
transition: all 0.2s ease;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.issue-card:hover {
|
|
1175
|
+
background-color: rgba(30, 41, 59, 0.5);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.issue-card.severity-critical {
|
|
1179
|
+
border-left: 4px solid rgba(239, 68, 68, 0.3);
|
|
1180
|
+
background-color: rgba(239, 68, 68, 0.1);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.issue-card.severity-high {
|
|
1184
|
+
border-left: 4px solid rgba(249, 115, 22, 0.3);
|
|
1185
|
+
background-color: rgba(249, 115, 22, 0.1);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.issue-card.severity-medium {
|
|
1189
|
+
border-left: 4px solid rgba(234, 179, 8, 0.3);
|
|
1190
|
+
background-color: rgba(234, 179, 8, 0.1);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.issue-card.severity-low {
|
|
1194
|
+
border-left: 4px solid rgba(59, 130, 246, 0.3);
|
|
1195
|
+
background-color: rgba(59, 130, 246, 0.1);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.issue-details {
|
|
1199
|
+
animation: slideDown 0.2s ease-out;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.confidence-progress {
|
|
1203
|
+
transition: width 0.5s ease-out;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.confidence-progress.severity-critical {
|
|
1207
|
+
background-color: #ef4444;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.confidence-progress.severity-high {
|
|
1211
|
+
background-color: #f97316;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.confidence-progress.severity-medium {
|
|
1215
|
+
background-color: #eab308;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.confidence-progress.severity-low {
|
|
1219
|
+
background-color: #3b82f6;
|
|
1220
|
+
}
|
|
1221
|
+
`;
|
|
1222
|
+
var panel = {
|
|
1223
|
+
position: "fixed",
|
|
1224
|
+
bottom: 16,
|
|
1225
|
+
right: 16,
|
|
1226
|
+
width: 480,
|
|
1227
|
+
fontFamily: "monospace",
|
|
1228
|
+
fontSize: 14,
|
|
1229
|
+
zIndex: 1e4,
|
|
1230
|
+
background: "#0f172a",
|
|
1231
|
+
borderRadius: 8,
|
|
1232
|
+
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
1233
|
+
border: "1px solid #334155",
|
|
1234
|
+
overflow: "hidden",
|
|
1235
|
+
animation: "slideUp 0.3s ease-out"
|
|
1236
|
+
};
|
|
1237
|
+
var header = {
|
|
1238
|
+
padding: "12px 16px",
|
|
1239
|
+
background: "linear-gradient(to right, #1e293b, #0f172a)",
|
|
1240
|
+
borderBottom: "1px solid #334155",
|
|
1241
|
+
cursor: "pointer",
|
|
1242
|
+
display: "flex",
|
|
1243
|
+
alignItems: "center",
|
|
1244
|
+
justifyContent: "space-between",
|
|
1245
|
+
transition: "background 0.2s ease"
|
|
1246
|
+
};
|
|
1247
|
+
var headerLeft = {
|
|
1248
|
+
display: "flex",
|
|
1249
|
+
alignItems: "center",
|
|
1250
|
+
gap: 12
|
|
1251
|
+
};
|
|
1252
|
+
var iconWrapper = {
|
|
1253
|
+
position: "relative"
|
|
1254
|
+
};
|
|
1255
|
+
var icon = {
|
|
1256
|
+
fontSize: 24
|
|
1257
|
+
};
|
|
1258
|
+
var criticalPing = {
|
|
1259
|
+
position: "absolute",
|
|
1260
|
+
top: -4,
|
|
1261
|
+
right: -4,
|
|
1262
|
+
width: 12,
|
|
1263
|
+
height: 12,
|
|
1264
|
+
background: "#ef4444",
|
|
1265
|
+
borderRadius: "50%",
|
|
1266
|
+
animation: "ping 1s cubic-bezier(0, 0, 0.2, 1) infinite"
|
|
1267
|
+
};
|
|
1268
|
+
var headerTitle = {
|
|
1269
|
+
fontWeight: "bold",
|
|
1270
|
+
color: "#ffffff",
|
|
1271
|
+
display: "flex",
|
|
1272
|
+
alignItems: "center",
|
|
1273
|
+
gap: 8
|
|
1274
|
+
};
|
|
1275
|
+
var betaBadge = {
|
|
1276
|
+
fontSize: 10,
|
|
1277
|
+
padding: "2px 8px",
|
|
1278
|
+
background: "rgba(59, 130, 246, 0.2)",
|
|
1279
|
+
color: "#60a5fa",
|
|
1280
|
+
borderRadius: 4,
|
|
1281
|
+
border: "1px solid rgba(59, 130, 246, 0.3)"
|
|
1282
|
+
};
|
|
1283
|
+
var headerSubtitle = {
|
|
1284
|
+
fontSize: 12,
|
|
1285
|
+
color: "#94a3b8"
|
|
1286
|
+
};
|
|
1287
|
+
var headerRight = {
|
|
1288
|
+
display: "flex",
|
|
1289
|
+
alignItems: "center",
|
|
1290
|
+
gap: 8
|
|
1291
|
+
};
|
|
1292
|
+
var criticalBadge = {
|
|
1293
|
+
padding: "4px 8px",
|
|
1294
|
+
fontSize: 12,
|
|
1295
|
+
background: "rgba(239, 68, 68, 0.2)",
|
|
1296
|
+
color: "#f87171",
|
|
1297
|
+
borderRadius: 4,
|
|
1298
|
+
border: "1px solid rgba(239, 68, 68, 0.3)"
|
|
1299
|
+
};
|
|
1300
|
+
var highBadge = {
|
|
1301
|
+
padding: "4px 8px",
|
|
1302
|
+
fontSize: 12,
|
|
1303
|
+
background: "rgba(249, 115, 22, 0.2)",
|
|
1304
|
+
color: "#fb923c",
|
|
1305
|
+
borderRadius: 4,
|
|
1306
|
+
border: "1px solid rgba(249, 115, 22, 0.3)"
|
|
1307
|
+
};
|
|
1308
|
+
var chevron = {
|
|
1309
|
+
width: 20,
|
|
1310
|
+
height: 20,
|
|
1311
|
+
color: "#94a3b8",
|
|
1312
|
+
transition: "transform 0.3s ease"
|
|
1313
|
+
};
|
|
1314
|
+
var issuesContainer = {
|
|
1315
|
+
maxHeight: 500,
|
|
1316
|
+
overflowY: "auto"
|
|
1317
|
+
};
|
|
1318
|
+
var issuesList = {
|
|
1319
|
+
borderTop: "1px solid #1e293b"
|
|
1320
|
+
};
|
|
1321
|
+
var issueCard = (idx) => ({
|
|
1322
|
+
padding: 16,
|
|
1323
|
+
cursor: "pointer",
|
|
1324
|
+
borderBottom: "1px solid #1e293b",
|
|
1325
|
+
animationDelay: `${idx * 50}ms`
|
|
1326
|
+
});
|
|
1327
|
+
var issueContent = {
|
|
1328
|
+
display: "flex",
|
|
1329
|
+
alignItems: "flex-start",
|
|
1330
|
+
justifyContent: "space-between",
|
|
1331
|
+
gap: 12
|
|
1332
|
+
};
|
|
1333
|
+
var issueMain = {
|
|
1334
|
+
flex: 1
|
|
1335
|
+
};
|
|
1336
|
+
var issueHeader = {
|
|
1337
|
+
display: "flex",
|
|
1338
|
+
alignItems: "center",
|
|
1339
|
+
gap: 8,
|
|
1340
|
+
marginBottom: 8
|
|
1341
|
+
};
|
|
1342
|
+
var statusDot = (status) => ({
|
|
1343
|
+
width: 8,
|
|
1344
|
+
height: 8,
|
|
1345
|
+
borderRadius: "50%",
|
|
1346
|
+
background: status === "OPEN" ? "#ef4444" : "#22c55e"
|
|
1347
|
+
});
|
|
1348
|
+
var componentName = {
|
|
1349
|
+
fontWeight: 600,
|
|
1350
|
+
color: "#ffffff"
|
|
1351
|
+
};
|
|
1352
|
+
var statusBadgeResolved = {
|
|
1353
|
+
padding: "2px 8px",
|
|
1354
|
+
fontSize: 12,
|
|
1355
|
+
borderRadius: 9999,
|
|
1356
|
+
background: "rgba(34, 197, 94, 0.2)",
|
|
1357
|
+
color: "#4ade80",
|
|
1358
|
+
border: "1px solid rgba(34, 197, 94, 0.3)"
|
|
1359
|
+
};
|
|
1360
|
+
var statusBadgeOpen = {
|
|
1361
|
+
padding: "2px 8px",
|
|
1362
|
+
fontSize: 12,
|
|
1363
|
+
borderRadius: 9999,
|
|
1364
|
+
background: "rgba(239, 68, 68, 0.2)",
|
|
1365
|
+
color: "#f87171",
|
|
1366
|
+
border: "1px solid rgba(239, 68, 68, 0.3)"
|
|
1367
|
+
};
|
|
1368
|
+
var ruleId = {
|
|
1369
|
+
fontSize: 14,
|
|
1370
|
+
color: "#cbd5e1",
|
|
1371
|
+
marginBottom: 8,
|
|
1372
|
+
fontWeight: 500
|
|
1373
|
+
};
|
|
1374
|
+
var issueMetrics = {
|
|
1375
|
+
display: "flex",
|
|
1376
|
+
alignItems: "center",
|
|
1377
|
+
gap: 16,
|
|
1378
|
+
fontSize: 12
|
|
1379
|
+
};
|
|
1380
|
+
var metric = {
|
|
1381
|
+
display: "flex",
|
|
1382
|
+
alignItems: "center",
|
|
1383
|
+
gap: 4
|
|
1384
|
+
};
|
|
1385
|
+
var metricLabel = {
|
|
1386
|
+
color: "#64748b"
|
|
1387
|
+
};
|
|
1388
|
+
var metricValue = {
|
|
1389
|
+
fontWeight: "bold",
|
|
1390
|
+
color: "#e2e8f0"
|
|
1391
|
+
};
|
|
1392
|
+
var confidenceWrapper = {
|
|
1393
|
+
display: "flex",
|
|
1394
|
+
alignItems: "center",
|
|
1395
|
+
gap: 4
|
|
1396
|
+
};
|
|
1397
|
+
var confidenceBar = {
|
|
1398
|
+
width: 64,
|
|
1399
|
+
height: 6,
|
|
1400
|
+
background: "#334155",
|
|
1401
|
+
borderRadius: 9999,
|
|
1402
|
+
overflow: "hidden"
|
|
1403
|
+
};
|
|
1404
|
+
var confidenceProgress = {
|
|
1405
|
+
height: "100%"
|
|
1406
|
+
};
|
|
1407
|
+
var confidenceValue = {
|
|
1408
|
+
color: "#cbd5e1",
|
|
1409
|
+
fontWeight: 500
|
|
1410
|
+
};
|
|
1411
|
+
var issueDetails = {
|
|
1412
|
+
marginTop: 12,
|
|
1413
|
+
paddingTop: 12,
|
|
1414
|
+
borderTop: "1px solid #334155"
|
|
1415
|
+
};
|
|
1416
|
+
var detailsContent = {
|
|
1417
|
+
display: "flex",
|
|
1418
|
+
flexDirection: "column",
|
|
1419
|
+
// justifyContent: "space-between",
|
|
1420
|
+
alignItems: "flex-start",
|
|
1421
|
+
gap: 2,
|
|
1422
|
+
fontSize: 12,
|
|
1423
|
+
color: "#94a3b8"
|
|
1424
|
+
};
|
|
1425
|
+
var detailsDescription = {
|
|
1426
|
+
color: "#b8b594ff"
|
|
1427
|
+
};
|
|
1428
|
+
var detailsText = {
|
|
1429
|
+
color: "#94a3b8"
|
|
1430
|
+
};
|
|
1431
|
+
var expandIcon = {
|
|
1432
|
+
width: 16,
|
|
1433
|
+
height: 16,
|
|
1434
|
+
color: "#64748b",
|
|
1435
|
+
transition: "transform 0.2s ease",
|
|
1436
|
+
flexShrink: 0,
|
|
1437
|
+
marginTop: 4
|
|
1438
|
+
};
|
|
1439
|
+
var footer = {
|
|
1440
|
+
padding: 12,
|
|
1441
|
+
background: "rgba(30, 41, 59, 0.5)",
|
|
1442
|
+
borderTop: "1px solid #334155",
|
|
1443
|
+
display: "flex",
|
|
1444
|
+
justifyContent: "space-between",
|
|
1445
|
+
alignItems: "center"
|
|
1446
|
+
};
|
|
1447
|
+
var footerText = {
|
|
1448
|
+
fontSize: 12,
|
|
1449
|
+
color: "#94a3b8"
|
|
1450
|
+
};
|
|
1451
|
+
var scanButton = {
|
|
1452
|
+
fontSize: 12,
|
|
1453
|
+
padding: "6px 12px",
|
|
1454
|
+
background: "#3b82f6",
|
|
1455
|
+
color: "#ffffff",
|
|
1456
|
+
border: "none",
|
|
1457
|
+
borderRadius: 4,
|
|
1458
|
+
cursor: "pointer",
|
|
1459
|
+
fontWeight: 500,
|
|
1460
|
+
fontFamily: "monospace",
|
|
1461
|
+
transition: "background 0.2s ease"
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// src/PerfProvider.tsx
|
|
1465
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1466
|
+
var worker = null;
|
|
1467
|
+
function PerfProvider({ children }) {
|
|
1468
|
+
const [stats, setStats] = useState2({ issues: 0, critical: 0 });
|
|
1469
|
+
if (!isDev) {
|
|
1470
|
+
return /* @__PURE__ */ jsx2(Fragment2, { children });
|
|
1471
|
+
}
|
|
1472
|
+
useEffect2(() => {
|
|
1473
|
+
try {
|
|
1474
|
+
worker = createAnalyzerWorker();
|
|
1475
|
+
const rules = getRulesConfig();
|
|
1476
|
+
worker.postMessage({
|
|
1477
|
+
type: "INIT_RULES",
|
|
1478
|
+
payload: rules
|
|
1479
|
+
});
|
|
1480
|
+
worker.onmessage = (e) => {
|
|
1481
|
+
const { type, data, hasCritical } = e.data;
|
|
1482
|
+
if (type === "INIT_SUCCESS") {
|
|
1483
|
+
console.log(`\u2705 [PerfGuard] Worker ready with ${e.data.count} rules`);
|
|
1484
|
+
}
|
|
1485
|
+
if (type === "RESULTS") {
|
|
1486
|
+
setStats((prev) => ({
|
|
1487
|
+
issues: prev.issues + data.length,
|
|
1488
|
+
critical: prev.critical + (hasCritical ? 1 : 0)
|
|
1489
|
+
}));
|
|
1490
|
+
data.forEach((result) => {
|
|
1491
|
+
showWarning(result);
|
|
1492
|
+
if (result.hasCritical) {
|
|
1493
|
+
showCriticalAlert(result);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
if (type === "STATS") {
|
|
1498
|
+
console.log("\u{1F4CA} [PerfGuard] Stats:", e.data.data);
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
worker.onerror = (err) => {
|
|
1502
|
+
console.error("[PerfGuard] Worker error:", err);
|
|
1503
|
+
};
|
|
1504
|
+
} catch (err) {
|
|
1505
|
+
console.warn("[PerfGuard] Worker failed to start", err);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const interval = setInterval(() => {
|
|
1509
|
+
const data = flushMetrics();
|
|
1510
|
+
if (data.length) {
|
|
1511
|
+
console.log(`[PerfGuard] Flushing ${data.length} snapshot(s)`);
|
|
1512
|
+
worker?.postMessage({
|
|
1513
|
+
type: "EVALUATE",
|
|
1514
|
+
payload: data
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}, 5e3);
|
|
1518
|
+
const statsInterval = setInterval(() => {
|
|
1519
|
+
worker?.postMessage({ type: "GET_STATS" });
|
|
1520
|
+
}, 3e4);
|
|
1521
|
+
return () => {
|
|
1522
|
+
clearInterval(interval);
|
|
1523
|
+
clearInterval(statsInterval);
|
|
1524
|
+
worker?.terminate();
|
|
1525
|
+
worker = null;
|
|
1526
|
+
};
|
|
1527
|
+
}, []);
|
|
1528
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1529
|
+
children,
|
|
1530
|
+
/* @__PURE__ */ jsx2(PerfGuardPanel, {})
|
|
1531
|
+
] });
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/withPerfGuard.tsx
|
|
1535
|
+
import { Profiler } from "react";
|
|
1536
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
1537
|
+
var DEFAULT_OPTIONS = {
|
|
1538
|
+
boundaryType: "INLINE",
|
|
1539
|
+
enabled: true
|
|
1540
|
+
};
|
|
1541
|
+
function withPerfGuard(Component, options) {
|
|
1542
|
+
const { boundaryType, enabled } = {
|
|
1543
|
+
...DEFAULT_OPTIONS,
|
|
1544
|
+
...options
|
|
1545
|
+
};
|
|
1546
|
+
const fallbackId = "AnonymousComponent";
|
|
1547
|
+
const name = Component.displayName || Component.name || `PerfGuard(${fallbackId})`;
|
|
1548
|
+
if (process.env.NODE_ENV === "production" || !enabled) {
|
|
1549
|
+
return Component;
|
|
1550
|
+
}
|
|
1551
|
+
const Guarded = (props) => {
|
|
1552
|
+
return /* @__PURE__ */ jsx3(
|
|
1553
|
+
Profiler,
|
|
1554
|
+
{
|
|
1555
|
+
id: name,
|
|
1556
|
+
onRender: (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
1557
|
+
collectMetric({
|
|
1558
|
+
component: id,
|
|
1559
|
+
phase,
|
|
1560
|
+
actualDuration,
|
|
1561
|
+
baseDuration,
|
|
1562
|
+
startTime,
|
|
1563
|
+
commitTime,
|
|
1564
|
+
boundaryType
|
|
1565
|
+
});
|
|
1566
|
+
},
|
|
1567
|
+
children: /* @__PURE__ */ jsx3(Component, { ...props })
|
|
1568
|
+
}
|
|
1569
|
+
);
|
|
1570
|
+
};
|
|
1571
|
+
Guarded.displayName = `withPerfGuard(${name})`;
|
|
1572
|
+
return Guarded;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/PerfProfiler.tsx
|
|
1576
|
+
import { Profiler as Profiler2 } from "react";
|
|
1577
|
+
import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
|
|
1578
|
+
function PerfProfiler({
|
|
1579
|
+
id,
|
|
1580
|
+
children,
|
|
1581
|
+
boundaryType = "INLINE"
|
|
1582
|
+
}) {
|
|
1583
|
+
if (!isDev) {
|
|
1584
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children });
|
|
1585
|
+
}
|
|
1586
|
+
return /* @__PURE__ */ jsx4(
|
|
1587
|
+
Profiler2,
|
|
1588
|
+
{
|
|
1589
|
+
id,
|
|
1590
|
+
onRender: (component, phase, actualDuration, baseDuration, startTime, commitTime) => {
|
|
1591
|
+
collectMetric({
|
|
1592
|
+
component,
|
|
1593
|
+
phase,
|
|
1594
|
+
actualDuration,
|
|
1595
|
+
baseDuration,
|
|
1596
|
+
startTime,
|
|
1597
|
+
commitTime,
|
|
1598
|
+
boundaryType
|
|
1599
|
+
});
|
|
1600
|
+
},
|
|
1601
|
+
children
|
|
1602
|
+
}
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
export {
|
|
1606
|
+
PerfProfiler,
|
|
1607
|
+
PerfProvider,
|
|
1608
|
+
withPerfGuard
|
|
1609
|
+
};
|