betterpack
Version:
A Universal Node.js Package Manager CLI with automated agent capabilities
252 lines (206 loc) • 6.78 kB
JavaScript
class PerformanceTracker {
constructor(options = {}) {
this.options = {
trackingEnabled: options.trackingEnabled !== false,
sampleRate: options.sampleRate || 1.0, // 100% sampling
maxSamples: options.maxSamples || 1000,
...options,
}
this.metrics = new Map()
this.samples = []
this.timers = new Map()
}
startTimer(name, metadata = {}) {
if (!this.options.trackingEnabled) return
const timer = {
name,
startTime: process.hrtime.bigint(),
metadata,
}
this.timers.set(name, timer)
return timer
}
endTimer(name, additionalMetadata = {}) {
if (!this.options.trackingEnabled) return
const timer = this.timers.get(name)
if (!timer) {
console.warn(`[PerformanceTracker] Timer '${name}' not found`)
return
}
const endTime = process.hrtime.bigint()
const duration = Number(endTime - timer.startTime) / 1000000 // Convert to milliseconds
const sample = {
name: timer.name,
duration,
timestamp: new Date(),
metadata: { ...timer.metadata, ...additionalMetadata },
}
this.recordSample(sample)
this.timers.delete(name)
return sample
}
recordMetric(name, value, metadata = {}) {
if (!this.options.trackingEnabled) return
const sample = {
name,
value,
timestamp: new Date(),
metadata,
}
this.recordSample(sample)
}
recordSample(sample) {
// Apply sampling rate
if (Math.random() > this.options.sampleRate) {
return
}
this.samples.push(sample)
// Update metrics
if (!this.metrics.has(sample.name)) {
this.metrics.set(sample.name, {
name: sample.name,
count: 0,
sum: 0,
min: Number.POSITIVE_INFINITY,
max: Number.NEGATIVE_INFINITY,
avg: 0,
recent: [],
})
}
const metric = this.metrics.get(sample.name)
const value = sample.duration || sample.value || 0
metric.count++
metric.sum += value
metric.min = Math.min(metric.min, value)
metric.max = Math.max(metric.max, value)
metric.avg = metric.sum / metric.count
metric.recent.push({ value, timestamp: sample.timestamp })
// Keep only recent samples
if (metric.recent.length > 100) {
metric.recent = metric.recent.slice(-100)
}
// Cleanup old samples
if (this.samples.length > this.options.maxSamples) {
this.samples = this.samples.slice(-this.options.maxSamples)
}
}
getMetric(name) {
return this.metrics.get(name)
}
getAllMetrics() {
return Array.from(this.metrics.values())
}
getMetricsSummary() {
const summary = {}
for (const [name, metric] of this.metrics) {
summary[name] = {
count: metric.count,
avg: Math.round(metric.avg * 100) / 100,
min: Math.round(metric.min * 100) / 100,
max: Math.round(metric.max * 100) / 100,
recent: metric.recent.slice(-10), // Last 10 samples
}
}
return summary
}
// Convenience methods for common operations
trackTaskExecution(taskName, executor) {
return async (...args) => {
const timer = this.startTimer(`task_${taskName}`, { taskName })
try {
const result = await executor(...args)
this.endTimer(`task_${taskName}`, { status: "success" })
return result
} catch (error) {
this.endTimer(`task_${taskName}`, { status: "error", error: error.message })
throw error
}
}
}
trackAnalysis(analysisName, analyzer) {
return async (...args) => {
const timer = this.startTimer(`analysis_${analysisName}`, { analysisName })
try {
const result = await analyzer(...args)
this.endTimer(`analysis_${analysisName}`, {
status: "success",
resultSize: JSON.stringify(result).length,
})
return result
} catch (error) {
this.endTimer(`analysis_${analysisName}`, { status: "error", error: error.message })
throw error
}
}
}
trackDecision(decisionName, decisionMaker) {
return async (...args) => {
const timer = this.startTimer(`decision_${decisionName}`, { decisionName })
try {
const result = await decisionMaker(...args)
this.endTimer(`decision_${decisionName}`, {
status: "success",
confidence: result.confidence,
recommendationCount: result.recommendations?.length || 0,
})
return result
} catch (error) {
this.endTimer(`decision_${decisionName}`, { status: "error", error: error.message })
throw error
}
}
}
// Performance analysis
getSlowOperations(threshold = 1000) {
// Return operations that took longer than threshold (ms)
return this.samples.filter((sample) => (sample.duration || sample.value || 0) > threshold)
}
getErrorRate(metricName) {
const metric = this.metrics.get(metricName)
if (!metric) return 0
const errorSamples = this.samples.filter(
(sample) => sample.name === metricName && sample.metadata?.status === "error",
)
return metric.count > 0 ? (errorSamples.length / metric.count) * 100 : 0
}
getTrends(metricName, timeWindow = 60 * 60 * 1000) {
// Get trends for the last hour by default
const cutoffTime = new Date(Date.now() - timeWindow)
const recentSamples = this.samples.filter((sample) => sample.name === metricName && sample.timestamp > cutoffTime)
if (recentSamples.length < 2) {
return { trend: "insufficient_data", samples: recentSamples.length }
}
// Simple trend calculation
const values = recentSamples.map((s) => s.duration || s.value || 0)
const firstHalf = values.slice(0, Math.floor(values.length / 2))
const secondHalf = values.slice(Math.floor(values.length / 2))
const firstAvg = firstHalf.reduce((sum, val) => sum + val, 0) / firstHalf.length
const secondAvg = secondHalf.reduce((sum, val) => sum + val, 0) / secondHalf.length
const change = ((secondAvg - firstAvg) / firstAvg) * 100
let trend = "stable"
if (change > 10) trend = "increasing"
else if (change < -10) trend = "decreasing"
return {
trend,
change: Math.round(change * 100) / 100,
firstAvg: Math.round(firstAvg * 100) / 100,
secondAvg: Math.round(secondAvg * 100) / 100,
samples: recentSamples.length,
}
}
// Cleanup and maintenance
clearMetrics() {
this.metrics.clear()
this.samples = []
this.timers.clear()
}
exportMetrics() {
return {
timestamp: new Date(),
metrics: this.getAllMetrics(),
samples: this.samples,
summary: this.getMetricsSummary(),
}
}
}
module.exports = { PerformanceTracker }