profiler.mjs

  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};