Skip to content

Commit 5aba344

Browse files
Add cpu-profile interactive command
1 parent 59fd18d commit 5aba344

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

doc/interactive-commands.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Both interfaces may serve at the same time. Both respond to simple text command,
1717
- `help`: shows a brief list of available commands
1818
- `status`: returns a detailed status summary of migration progress and configuration
1919
- `sup`: returns a brief status summary of migration progress
20+
- `cpu-profile`: returns a base64-encoded runtime/pprof CPU profile using a duration, default: `30s`. Comma-separated options `gzip` and/or `block` (blocked profile) may follow the profile duration
2021
- `coordinates`: returns recent (though not exactly up to date) binary log coordinates of the inspected server
2122
- `applier`: returns the hostname of the applier
2223
- `inspector`: returns the hostname of the inspector

go/logic/server.go

+68
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,30 @@ package logic
77

88
import (
99
"bufio"
10+
"bytes"
11+
"compress/gzip"
12+
"encoding/base64"
13+
"errors"
1014
"fmt"
1115
"io"
1216
"net"
1317
"os"
18+
"runtime"
19+
"runtime/pprof"
1420
"strconv"
1521
"strings"
1622
"sync/atomic"
23+
"time"
1724

1825
"github.com/github/gh-ost/go/base"
1926
)
2027

28+
var (
29+
ErrCPUProfilingBadOption = errors.New("unrecognized cpu profiling option")
30+
ErrCPUProfilingInProgress = errors.New("cpu profiling already in progress")
31+
defaultCPUProfileDuration = time.Second * 30
32+
)
33+
2134
type printStatusFunc func(PrintStatusRule, io.Writer)
2235

2336
// Server listens for requests on a socket file or via TCP
@@ -27,6 +40,7 @@ type Server struct {
2740
tcpListener net.Listener
2841
hooksExecutor *HooksExecutor
2942
printStatus printStatusFunc
43+
isCPUProfiling int64
3044
}
3145

3246
func NewServer(migrationContext *base.MigrationContext, hooksExecutor *HooksExecutor, printStatus printStatusFunc) *Server {
@@ -37,6 +51,53 @@ func NewServer(migrationContext *base.MigrationContext, hooksExecutor *HooksExec
3751
}
3852
}
3953

54+
func (this *Server) runCPUProfile(args string) (string, error) {
55+
if atomic.LoadInt64(&this.isCPUProfiling) > 0 {
56+
return "", ErrCPUProfilingInProgress
57+
}
58+
59+
duration := defaultCPUProfileDuration
60+
61+
var err error
62+
var useGzip bool
63+
if args != "" {
64+
s := strings.Split(args, ",")
65+
// a duration string must be the 1st field, if any
66+
if duration, err = time.ParseDuration(s[0]); err != nil {
67+
return "", err
68+
}
69+
for _, arg := range s[1:] {
70+
switch arg {
71+
case "block", "blocked", "blocking":
72+
runtime.SetBlockProfileRate(1)
73+
defer runtime.SetBlockProfileRate(0)
74+
case "gzip":
75+
useGzip = true
76+
default:
77+
return "", ErrCPUProfilingBadOption
78+
}
79+
}
80+
}
81+
82+
atomic.StoreInt64(&this.isCPUProfiling, 1)
83+
defer atomic.StoreInt64(&this.isCPUProfiling, 0)
84+
85+
var buf bytes.Buffer
86+
var writer io.Writer = &buf
87+
if useGzip {
88+
writer = gzip.NewWriter(&buf)
89+
}
90+
if err = pprof.StartCPUProfile(writer); err != nil {
91+
return "", err
92+
}
93+
94+
time.Sleep(duration)
95+
pprof.StopCPUProfile()
96+
this.migrationContext.Log.Infof("Captured %d byte runtime/pprof CPU profile (gzip=%v)", buf.Len(), useGzip)
97+
98+
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
99+
}
100+
40101
func (this *Server) BindSocketFile() (err error) {
41102
if this.migrationContext.ServeSocketFile == "" {
42103
return nil
@@ -144,6 +205,7 @@ func (this *Server) applyServerCommand(command string, writer *bufio.Writer) (pr
144205
fmt.Fprint(writer, `available commands:
145206
status # Print a detailed status message
146207
sup # Print a short status message
208+
cpu-profile=<options> # Print a base64-encoded runtime/pprof CPU profile using a duration, default: 30s. Comma-separated options 'gzip' and/or 'block' (blocked profile) may follow the profile duration
147209
coordinates # Print the currently inspected coordinates
148210
applier # Print the hostname of the applier
149211
inspector # Print the hostname of the inspector
@@ -169,6 +231,12 @@ help # This message
169231
return ForcePrintStatusOnlyRule, nil
170232
case "info", "status":
171233
return ForcePrintStatusAndHintRule, nil
234+
case "cpu-profile":
235+
profile, err := this.runCPUProfile(arg)
236+
if err == nil {
237+
fmt.Fprintln(writer, profile)
238+
}
239+
return NoPrintStatusRule, err
172240
case "coordinates":
173241
{
174242
if argIsQuestion || arg == "" {

go/logic/server_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package logic
2+
3+
import (
4+
"encoding/base64"
5+
"testing"
6+
"time"
7+
8+
"github.com/github/gh-ost/go/base"
9+
"github.com/openark/golib/tests"
10+
)
11+
12+
func TestServerRunCPUProfile(t *testing.T) {
13+
t.Parallel()
14+
15+
t.Run("failed already running", func(t *testing.T) {
16+
s := &Server{isCPUProfiling: 1}
17+
profile, err := s.runCPUProfile("15ms")
18+
tests.S(t).ExpectEquals(err, ErrCPUProfilingInProgress)
19+
tests.S(t).ExpectEquals(profile, "")
20+
})
21+
22+
t.Run("failed bad duration", func(t *testing.T) {
23+
s := &Server{isCPUProfiling: 0}
24+
profile, err := s.runCPUProfile("should-fail")
25+
tests.S(t).ExpectNotNil(err)
26+
tests.S(t).ExpectEquals(profile, "")
27+
})
28+
29+
t.Run("failed bad option", func(t *testing.T) {
30+
s := &Server{isCPUProfiling: 0}
31+
profile, err := s.runCPUProfile("10ms,badoption")
32+
tests.S(t).ExpectEquals(err, ErrCPUProfilingBadOption)
33+
tests.S(t).ExpectEquals(profile, "")
34+
})
35+
36+
t.Run("success", func(t *testing.T) {
37+
s := &Server{
38+
isCPUProfiling: 0,
39+
migrationContext: base.NewMigrationContext(),
40+
}
41+
defaultCPUProfileDuration = time.Millisecond * 10
42+
profile, err := s.runCPUProfile("")
43+
tests.S(t).ExpectNil(err)
44+
tests.S(t).ExpectNotEquals(profile, "")
45+
46+
data, err := base64.StdEncoding.DecodeString(profile)
47+
tests.S(t).ExpectNil(err)
48+
tests.S(t).ExpectNotEquals(len(data), 0)
49+
tests.S(t).ExpectEquals(s.isCPUProfiling, int64(0))
50+
})
51+
52+
t.Run("success with block", func(t *testing.T) {
53+
s := &Server{
54+
isCPUProfiling: 0,
55+
migrationContext: base.NewMigrationContext(),
56+
}
57+
profile, err := s.runCPUProfile("10ms,block")
58+
tests.S(t).ExpectNil(err)
59+
tests.S(t).ExpectNotEquals(profile, "")
60+
61+
data, err := base64.StdEncoding.DecodeString(profile)
62+
tests.S(t).ExpectNil(err)
63+
tests.S(t).ExpectNotEquals(len(data), 0)
64+
tests.S(t).ExpectEquals(s.isCPUProfiling, int64(0))
65+
})
66+
67+
t.Run("success with block and gzip", func(t *testing.T) {
68+
s := &Server{
69+
isCPUProfiling: 0,
70+
migrationContext: base.NewMigrationContext(),
71+
}
72+
profile, err := s.runCPUProfile("10ms,block,gzip")
73+
tests.S(t).ExpectNil(err)
74+
tests.S(t).ExpectNotEquals(profile, "")
75+
76+
data, err := base64.StdEncoding.DecodeString(profile)
77+
tests.S(t).ExpectNil(err)
78+
tests.S(t).ExpectNotEquals(len(data), 0)
79+
tests.S(t).ExpectEquals(s.isCPUProfiling, int64(0))
80+
})
81+
}

0 commit comments

Comments
 (0)