Files
github--github-mcp-server/internal/profiler/profiler.go
Matt Holloway 2621dbefd9 Add actions job log buffer and profiler (#866)
* add sliding window for actions logs

* refactor: fix sliding

* remove trim content

* only use up to 1mb of memory for logs

* update to tail lines in second pass

* add better memory usage calculation

* increase window size to 5MB

* update test

* update vers

* undo vers change

* add incremental memory tracking

* use ring buffer

* remove unused ctx param

* remove manual GC clear

* fix cca feedback

* extract ring buffer logic to new package

* handle log content processing errors and use correct param for maxjobloglines

* fix tailing

* account for if tailLines exceeds window size

* add profiling thats reusable

* remove profiler testing

* refactor profiler: introduce safeMemoryDelta for accurate memory delta calculations

* linter fixes

* Update pkg/buffer/buffer.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* use flag for maxJobLogLines

* add param passing for context window size

* refactor: rename contextWindowSize to contentWindowSize for consistency

* fix: use tailLines if bigger but only if <= 5000

* fix: limit tailLines to a maximum of 500 for log content download

* Update cmd/github-mcp-server/main.go

Co-authored-by: Adam Holt <omgitsads@github.com>

* Update cmd/github-mcp-server/main.go

Co-authored-by: Adam Holt <omgitsads@github.com>

* move profiler to internal/

* update actions test with new profiler location

* fix: adjust buffer size limits

* make line buffer 1028kb

* fix mod path

* change test to use same buffer size as normal use

* improve test for non-sliding window implementation to not count empty lines

* make test memory measurement more accurate

* remove impossible conditional

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Adam Holt <omgitsads@github.com>
2025-08-19 14:02:16 +01:00

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