@hyperbrowser/agent
Version:
Hyperbrowsers Web Agent
266 lines (265 loc) • 8.64 kB
JavaScript
;
/**
* Performance tracking utilities for A11y DOM extraction
* Provides hierarchical timing with minimal overhead
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PerformanceTracker = void 0;
exports.getGlobalTracker = getGlobalTracker;
exports.setGlobalTracker = setGlobalTracker;
exports.withTiming = withTiming;
exports.withTimingSync = withTimingSync;
/**
* Performance tracker with hierarchical timing support
*/
class PerformanceTracker {
constructor(name = 'root') {
this.timerStack = [];
this.timerMap = new Map();
this.rootEntry = {
name,
startTime: performance.now(),
children: [],
};
this.currentEntry = this.rootEntry;
}
/**
* Start a named timer
*/
startTimer(name, metadata) {
const entry = {
name,
startTime: performance.now(),
children: [],
metadata,
};
// Add to current entry's children
this.currentEntry.children.push(entry);
// Push to stack and update current
this.timerStack.push(this.currentEntry);
this.currentEntry = entry;
// Store in map for quick access
this.timerMap.set(name, entry);
}
/**
* Stop a named timer and return duration
*/
stopTimer(name) {
const entry = this.timerMap.get(name);
if (!entry) {
console.warn(`[Performance] Timer '${name}' not found`);
return 0;
}
// If this timer is not the current one, something went wrong
if (this.currentEntry !== entry) {
console.warn(`[Performance] Timer '${name}' stopped out of order (current: ${this.currentEntry.name})`);
}
entry.endTime = performance.now();
entry.duration = entry.endTime - entry.startTime;
// Pop back to parent
if (this.timerStack.length > 0) {
this.currentEntry = this.timerStack.pop();
}
return entry.duration;
}
/**
* Record a single point-in-time mark
*/
mark(name, metadata) {
const entry = {
name,
startTime: performance.now(),
endTime: performance.now(),
duration: 0,
children: [],
metadata,
};
this.currentEntry.children.push(entry);
}
/**
* End the root timer
*/
finish() {
if (!this.rootEntry.endTime) {
this.rootEntry.endTime = performance.now();
this.rootEntry.duration = this.rootEntry.endTime - this.rootEntry.startTime;
}
}
/**
* Get flattened timing data
*/
getFlatTimings() {
const flat = [];
const traverse = (entry, depth = 0) => {
if (entry.duration !== undefined) {
flat.push({
name: ' '.repeat(depth) + entry.name,
duration: entry.duration,
metadata: entry.metadata,
});
}
for (const child of entry.children) {
traverse(child, depth + 1);
}
};
traverse(this.rootEntry);
return flat;
}
/**
* Build hierarchical breakdown object
*/
buildBreakdown(entry) {
if (entry.children.length === 0) {
return entry.duration ?? 0;
}
const breakdown = {};
// If we have both duration and children, include total
if (entry.duration !== undefined) {
breakdown.total = entry.duration;
}
// Add children
for (const child of entry.children) {
breakdown[child.name] = this.buildBreakdown(child);
}
return breakdown;
}
/**
* Detect bottlenecks based on timing data
*/
detectBottlenecks(totalTime) {
const bottlenecks = [];
const threshold = {
critical: 200, // > 200ms
high: 100, // > 100ms
medium: 50, // > 50ms
};
const traverse = (entry, path = '') => {
if (!entry.duration)
return;
const fullPath = path ? `${path} > ${entry.name}` : entry.name;
const percentage = (entry.duration / totalTime) * 100;
let severity = 'low';
let recommendation = '';
// Determine severity
if (entry.duration > threshold.critical) {
severity = 'critical';
recommendation = `Operation takes ${entry.duration.toFixed(0)}ms. Consider optimization.`;
}
else if (entry.duration > threshold.high) {
severity = 'high';
recommendation = `Operation takes ${entry.duration.toFixed(0)}ms. May benefit from optimization.`;
}
else if (entry.duration > threshold.medium && percentage > 10) {
severity = 'medium';
recommendation = `Represents ${percentage.toFixed(1)}% of total time.`;
}
// Check metadata for bottleneck hints
if (entry.metadata?.sequential && entry.metadata?.parallelizable) {
severity = severity === 'low' ? 'high' : severity;
recommendation += ' Could be parallelized.';
}
if (entry.metadata?.cdpCallCount && entry.metadata.cdpCallCount > 10) {
severity = severity === 'low' ? 'medium' : severity;
recommendation += ` Makes ${entry.metadata.cdpCallCount} CDP calls.`;
}
if (severity !== 'low') {
bottlenecks.push({
operation: fullPath,
time: entry.duration,
percentage,
severity,
recommendation,
});
}
// Recurse through children
for (const child of entry.children) {
traverse(child, fullPath);
}
};
traverse(this.rootEntry);
// Sort by time descending
bottlenecks.sort((a, b) => b.time - a.time);
return bottlenecks;
}
/**
* Generate performance report
*/
getReport() {
this.finish();
const totalTime = this.rootEntry.duration ?? 0;
const breakdown = this.buildBreakdown(this.rootEntry);
const bottlenecks = this.detectBottlenecks(totalTime);
return {
totalTime,
breakdown,
bottlenecks,
rawTimings: [this.rootEntry],
};
}
/**
* Export as JSON
*/
toJSON() {
return JSON.stringify(this.getReport(), null, 2);
}
/**
* Format report as human-readable string
*/
formatReport() {
const report = this.getReport();
const lines = [];
lines.push(`\n[A11y Performance] Total time: ${report.totalTime.toFixed(0)}ms\n`);
// Top bottlenecks
if (report.bottlenecks.length > 0) {
lines.push('Top Bottlenecks:');
report.bottlenecks.slice(0, 5).forEach((b, i) => {
const icon = b.severity === 'critical' ? '🔴' : b.severity === 'high' ? '⚠️ ' : '🔶';
lines.push(` ${i + 1}. ${icon} ${b.operation.padEnd(40)} ${b.time.toFixed(0)}ms ${b.percentage.toFixed(1)}% → ${b.recommendation}`);
});
lines.push('');
}
// Flat timing breakdown
lines.push('Timing Breakdown:');
const flatTimings = this.getFlatTimings();
for (const { name, duration } of flatTimings) {
const percentage = ((duration / report.totalTime) * 100).toFixed(1);
lines.push(` ${name.padEnd(50)} ${duration.toFixed(0)}ms (${percentage}%)`);
}
return lines.join('\n');
}
}
exports.PerformanceTracker = PerformanceTracker;
/**
* Global tracker instance (optional convenience)
*/
let globalTracker = null;
function getGlobalTracker() {
return globalTracker;
}
function setGlobalTracker(tracker) {
globalTracker = tracker;
}
/**
* Convenience wrapper for timing async operations
*/
async function withTiming(tracker, name, fn, metadata) {
tracker.startTimer(name, metadata);
try {
return await fn();
}
finally {
tracker.stopTimer(name);
}
}
/**
* Convenience wrapper for timing sync operations
*/
function withTimingSync(tracker, name, fn, metadata) {
tracker.startTimer(name, metadata);
try {
return fn();
}
finally {
tracker.stopTimer(name);
}
}