216 lines
5.5 KiB
Go
216 lines
5.5 KiB
Go
package profiler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
"log/slog"
|
|
"math"
|
|
)
|
|
|
|
// Profile represents performance metrics for an operation
|
|
type Profile struct {
|
|
Operation string `json:"operation"`
|
|
Duration time.Duration `json:"duration_ns"`
|
|
MemoryBefore uint64 `json:"memory_before_bytes"`
|
|
MemoryAfter uint64 `json:"memory_after_bytes"`
|
|
MemoryDelta int64 `json:"memory_delta_bytes"`
|
|
LinesCount int `json:"lines_count,omitempty"`
|
|
BytesCount int64 `json:"bytes_count,omitempty"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// String returns a human-readable representation of the profile
|
|
func (p *Profile) String() string {
|
|
return fmt.Sprintf("[%s] %s: duration=%v, memory_delta=%+dB, lines=%d, bytes=%d",
|
|
p.Timestamp.Format("15:04:05.000"),
|
|
p.Operation,
|
|
p.Duration,
|
|
p.MemoryDelta,
|
|
p.LinesCount,
|
|
p.BytesCount,
|
|
)
|
|
}
|
|
|
|
func safeMemoryDelta(after, before uint64) int64 {
|
|
if after > math.MaxInt64 || before > math.MaxInt64 {
|
|
if after >= before {
|
|
diff := after - before
|
|
if diff > math.MaxInt64 {
|
|
return math.MaxInt64
|
|
}
|
|
return int64(diff)
|
|
}
|
|
diff := before - after
|
|
if diff > math.MaxInt64 {
|
|
return -math.MaxInt64
|
|
}
|
|
return -int64(diff)
|
|
}
|
|
|
|
return int64(after) - int64(before)
|
|
}
|
|
|
|
// Profiler provides minimal performance profiling capabilities
|
|
type Profiler struct {
|
|
logger *slog.Logger
|
|
enabled bool
|
|
}
|
|
|
|
// New creates a new Profiler instance
|
|
func New(logger *slog.Logger, enabled bool) *Profiler {
|
|
return &Profiler{
|
|
logger: logger,
|
|
enabled: enabled,
|
|
}
|
|
}
|
|
|
|
// ProfileFunc profiles a function execution
|
|
func (p *Profiler) ProfileFunc(ctx context.Context, operation string, fn func() error) (*Profile, error) {
|
|
if !p.enabled {
|
|
return nil, fn()
|
|
}
|
|
|
|
profile := &Profile{
|
|
Operation: operation,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
var memBefore runtime.MemStats
|
|
runtime.ReadMemStats(&memBefore)
|
|
profile.MemoryBefore = memBefore.Alloc
|
|
|
|
start := time.Now()
|
|
err := fn()
|
|
profile.Duration = time.Since(start)
|
|
|
|
var memAfter runtime.MemStats
|
|
runtime.ReadMemStats(&memAfter)
|
|
profile.MemoryAfter = memAfter.Alloc
|
|
profile.MemoryDelta = safeMemoryDelta(memAfter.Alloc, memBefore.Alloc)
|
|
|
|
if p.logger != nil {
|
|
p.logger.InfoContext(ctx, "Performance profile", "profile", profile.String())
|
|
}
|
|
|
|
return profile, err
|
|
}
|
|
|
|
// ProfileFuncWithMetrics profiles a function execution and captures additional metrics
|
|
func (p *Profiler) ProfileFuncWithMetrics(ctx context.Context, operation string, fn func() (int, int64, error)) (*Profile, error) {
|
|
if !p.enabled {
|
|
_, _, err := fn()
|
|
return nil, err
|
|
}
|
|
|
|
profile := &Profile{
|
|
Operation: operation,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
var memBefore runtime.MemStats
|
|
runtime.ReadMemStats(&memBefore)
|
|
profile.MemoryBefore = memBefore.Alloc
|
|
|
|
start := time.Now()
|
|
lines, bytes, err := fn()
|
|
profile.Duration = time.Since(start)
|
|
profile.LinesCount = lines
|
|
profile.BytesCount = bytes
|
|
|
|
var memAfter runtime.MemStats
|
|
runtime.ReadMemStats(&memAfter)
|
|
profile.MemoryAfter = memAfter.Alloc
|
|
profile.MemoryDelta = safeMemoryDelta(memAfter.Alloc, memBefore.Alloc)
|
|
|
|
if p.logger != nil {
|
|
p.logger.InfoContext(ctx, "Performance profile", "profile", profile.String())
|
|
}
|
|
|
|
return profile, err
|
|
}
|
|
|
|
// Start begins timing an operation and returns a function to complete the profiling
|
|
func (p *Profiler) Start(ctx context.Context, operation string) func(lines int, bytes int64) *Profile {
|
|
if !p.enabled {
|
|
return func(int, int64) *Profile { return nil }
|
|
}
|
|
|
|
profile := &Profile{
|
|
Operation: operation,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
var memBefore runtime.MemStats
|
|
runtime.ReadMemStats(&memBefore)
|
|
profile.MemoryBefore = memBefore.Alloc
|
|
|
|
start := time.Now()
|
|
|
|
return func(lines int, bytes int64) *Profile {
|
|
profile.Duration = time.Since(start)
|
|
profile.LinesCount = lines
|
|
profile.BytesCount = bytes
|
|
|
|
var memAfter runtime.MemStats
|
|
runtime.ReadMemStats(&memAfter)
|
|
profile.MemoryAfter = memAfter.Alloc
|
|
profile.MemoryDelta = safeMemoryDelta(memAfter.Alloc, memBefore.Alloc)
|
|
|
|
if p.logger != nil {
|
|
p.logger.InfoContext(ctx, "Performance profile", "profile", profile.String())
|
|
}
|
|
|
|
return profile
|
|
}
|
|
}
|
|
|
|
var globalProfiler *Profiler
|
|
|
|
// IsProfilingEnabled checks if profiling is enabled via environment variables
|
|
func IsProfilingEnabled() bool {
|
|
if enabled, err := strconv.ParseBool(os.Getenv("GITHUB_MCP_PROFILING_ENABLED")); err == nil {
|
|
return enabled
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Init initializes the global profiler
|
|
func Init(logger *slog.Logger, enabled bool) {
|
|
globalProfiler = New(logger, enabled)
|
|
}
|
|
|
|
// InitFromEnv initializes the global profiler using environment variables
|
|
func InitFromEnv(logger *slog.Logger) {
|
|
globalProfiler = New(logger, IsProfilingEnabled())
|
|
}
|
|
|
|
// ProfileFunc profiles a function using the global profiler
|
|
func ProfileFunc(ctx context.Context, operation string, fn func() error) (*Profile, error) {
|
|
if globalProfiler == nil {
|
|
return nil, fn()
|
|
}
|
|
return globalProfiler.ProfileFunc(ctx, operation, fn)
|
|
}
|
|
|
|
// ProfileFuncWithMetrics profiles a function with metrics using the global profiler
|
|
func ProfileFuncWithMetrics(ctx context.Context, operation string, fn func() (int, int64, error)) (*Profile, error) {
|
|
if globalProfiler == nil {
|
|
_, _, err := fn()
|
|
return nil, err
|
|
}
|
|
return globalProfiler.ProfileFuncWithMetrics(ctx, operation, fn)
|
|
}
|
|
|
|
// Start begins timing using the global profiler
|
|
func Start(ctx context.Context, operation string) func(int, int64) *Profile {
|
|
if globalProfiler == nil {
|
|
return func(int, int64) *Profile { return nil }
|
|
}
|
|
return globalProfiler.Start(ctx, operation)
|
|
}
|