1function profileNow() {
2 return typeof performance !== 'undefined' && performance.now
3 ? performance.now()
4 : Date.now();
5}
6
7function createDetectorProfile() {
8 return { events: [] };
9}
10
11function recordProfileEvent(profile, event) {
12 if (!profile) return;
13 const normalized = {
14 engine: event.engine || 'unknown',
15 phase: event.phase || 'unknown',
16 ruleId: event.ruleId || 'unknown',
17 target: event.target || '',
18 ms: Number.isFinite(event.ms) ? event.ms : 0,
19 findings: Number.isFinite(event.findings) ? event.findings : 0,
20 };
21 if (event.detail) normalized.detail = event.detail;
22 if (Array.isArray(event.findingIds) && event.findingIds.length) {
23 normalized.findingIds = event.findingIds;
24 }
25 if (typeof profile === 'function') {
26 profile(normalized);
27 } else if (typeof profile.record === 'function') {
28 profile.record(normalized);
29 } else if (Array.isArray(profile.events)) {
30 profile.events.push(normalized);
31 } else if (Array.isArray(profile)) {
32 profile.push(normalized);
33 }
34}
35
36function extractFindingIds(findings) {
37 if (!Array.isArray(findings) || findings.length === 0) return [];
38 return [...new Set(findings.map(f => f?.id || f?.type || f?.antipattern).filter(Boolean))];
39}
40
41function profileFindings(profile, meta, callback) {
42 if (!profile) return callback();
43 const started = profileNow();
44 const findings = callback();
45 recordProfileEvent(profile, {
46 ...meta,
47 ms: profileNow() - started,
48 findings: Array.isArray(findings) ? findings.length : 0,
49 findingIds: extractFindingIds(findings),
50 });
51 return findings;
52}
53
54function profileStep(profile, meta, callback) {
55 if (!profile) return callback();
56 const started = profileNow();
57 try {
58 return callback();
59 } finally {
60 recordProfileEvent(profile, {
61 ...meta,
62 ms: profileNow() - started,
63 findings: 0,
64 });
65 }
66}
67
68async function profileFindingsAsync(profile, meta, callback) {
69 if (!profile) return callback();
70 const started = profileNow();
71 const findings = await callback();
72 recordProfileEvent(profile, {
73 ...meta,
74 ms: profileNow() - started,
75 findings: Array.isArray(findings) ? findings.length : 0,
76 findingIds: extractFindingIds(findings),
77 });
78 return findings;
79}
80
81async function profileStepAsync(profile, meta, callback) {
82 if (!profile) return callback();
83 const started = profileNow();
84 try {
85 return await callback();
86 } finally {
87 recordProfileEvent(profile, {
88 ...meta,
89 ms: profileNow() - started,
90 findings: 0,
91 });
92 }
93}
94
95function percentile(sortedValues, pct) {
96 if (!sortedValues.length) return 0;
97 const idx = Math.min(
98 sortedValues.length - 1,
99 Math.max(0, Math.ceil((pct / 100) * sortedValues.length) - 1),
100 );
101 return sortedValues[idx];
102}
103
104function summarizeDetectorProfile(profile) {
105 const events = Array.isArray(profile)
106 ? profile
107 : (Array.isArray(profile?.events) ? profile.events : []);
108 const groups = new Map();
109 for (const event of events) {
110 const key = [
111 event.engine || 'unknown',
112 event.phase || 'unknown',
113 event.ruleId || 'unknown',
114 event.target || '',
115 ].join('\u0000');
116 let group = groups.get(key);
117 if (!group) {
118 group = {
119 engine: event.engine || 'unknown',
120 phase: event.phase || 'unknown',
121 ruleId: event.ruleId || 'unknown',
122 target: event.target || '',
123 calls: 0,
124 totalMs: 0,
125 findings: 0,
126 samples: [],
127 };
128 groups.set(key, group);
129 }
130 const ms = Number.isFinite(event.ms) ? event.ms : 0;
131 group.calls += 1;
132 group.totalMs += ms;
133 group.findings += Number.isFinite(event.findings) ? event.findings : 0;
134 group.samples.push(ms);
135 }
136 return [...groups.values()]
137 .map(group => {
138 const samples = group.samples.sort((a, b) => a - b);
139 return {
140 engine: group.engine,
141 phase: group.phase,
142 ruleId: group.ruleId,
143 target: group.target,
144 calls: group.calls,
145 totalMs: Number(group.totalMs.toFixed(3)),
146 avgMs: Number((group.totalMs / group.calls).toFixed(3)),
147 p50: Number(percentile(samples, 50).toFixed(3)),
148 p95: Number(percentile(samples, 95).toFixed(3)),
149 findings: group.findings,
150 };
151 })
152 .sort((a, b) => b.totalMs - a.totalMs);
153}
154
155export {
156 profileNow,
157 createDetectorProfile,
158 recordProfileEvent,
159 extractFindingIds,
160 profileFindings,
161 profileStep,
162 profileFindingsAsync,
163 profileStepAsync,
164 percentile,
165 summarizeDetectorProfile,
166};