diff --git a/utils/coreutils/profiler.go b/utils/coreutils/profiler.go new file mode 100644 index 000000000..8dab71712 --- /dev/null +++ b/utils/coreutils/profiler.go @@ -0,0 +1,87 @@ +package coreutils + +import ( + "errors" + "fmt" + "os" + "runtime/pprof" + "time" + + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" +) + +const ( + defaultInterval = time.Second + defaultRepetitions = 3 +) + +type Profiler struct { + interval time.Duration + repetitions uint +} + +type ProfilerOption func(*Profiler) + +func NewProfiler(opts ...ProfilerOption) *Profiler { + profiler := &Profiler{ + interval: defaultInterval, + repetitions: defaultRepetitions, + } + for _, opt := range opts { + opt(profiler) + } + return profiler +} + +func WithInterval(interval time.Duration) ProfilerOption { + return func(p *Profiler) { + p.interval = interval + } +} + +func WithRepetitions(repetitions uint) ProfilerOption { + return func(p *Profiler) { + p.repetitions = repetitions + } +} + +func (p *Profiler) ThreadDump() (output string, err error) { + var outputFilePath string + if outputFilePath, err = p.threadDumpToFile(); err != nil { + return + } + defer func() { + err = errors.Join(err, os.Remove(outputFilePath)) + }() + return p.convertFileToString(outputFilePath) +} + +func (p *Profiler) threadDumpToFile() (outputFilePath string, err error) { + outputFile, err := fileutils.CreateTempFile() + if err != nil { + return + } + defer func() { + err = errors.Join(err, outputFile.Close()) + }() + + for i := 0; i < int(p.repetitions); i++ { + fmt.Fprintf(outputFile, "=== Thread dump #%d ===\n", i) + prof := pprof.Lookup("goroutine") + if err = prof.WriteTo(outputFile, 1); err != nil { + return + } + time.Sleep(p.interval) + } + return outputFile.Name(), nil +} + +func (p *Profiler) convertFileToString(outputFilePath string) (output string, err error) { + var outputBytes []byte + outputBytes, err = os.ReadFile(outputFilePath) + if err != nil { + return + } + output = string(outputBytes) + return +} diff --git a/utils/coreutils/profiler_test.go b/utils/coreutils/profiler_test.go new file mode 100644 index 000000000..3aff8f6d1 --- /dev/null +++ b/utils/coreutils/profiler_test.go @@ -0,0 +1,57 @@ +package coreutils + +import ( + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestThreadDump(t *testing.T) { + // Create default profiler + profiler := NewProfiler() + + // Start a thread that sleeps + go func() { + dummyZzzz() + }() + + // Run thread dump + output, err := profiler.ThreadDump() + assert.NoError(t, err) + + // Check results + assert.Contains(t, output, "Thread dump #0") + assert.Contains(t, output, "Thread dump #1") + assert.Contains(t, output, "Thread dump #2") + assert.Contains(t, output, "dummyZzzz") +} + +func TestThreadInterval(t *testing.T) { + // Create profiler with 10 repetitions and 10ms intervals + var expectedRepetitions uint = 10 + var expectedInterval = 10 * time.Millisecond + profiler := NewProfiler(WithInterval(expectedInterval), WithRepetitions(expectedRepetitions)) + + // Check that the required values are set + assert.Equal(t, profiler.interval, expectedInterval) + assert.Equal(t, profiler.repetitions, expectedRepetitions) + + // start measure the time + start := time.Now() + + // Run thread dump + output, err := profiler.ThreadDump() + assert.NoError(t, err) + + // Ensure duration less than 1 second + assert.WithinDuration(t, start, time.Now(), time.Second) + + // Ensure 10 repetitions + assert.Contains(t, output, "Thread dump #"+strconv.FormatUint(uint64(expectedRepetitions)-1, 10)) +} + +func dummyZzzz() { + time.Sleep(time.Second) +}