diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 8ac6280c96..da4ddc2717 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -136,7 +136,6 @@ func (s *Service) Start(ctx context.Context) error { user.RegisterRouter, info.RegisterRouter, clusterinfo.RegisterRouter, - profiling.RegisterRouter, logsearch.RegisterRouter, slowquery.RegisterRouter, statement.RegisterRouter, diff --git a/pkg/apiserver/profiling/clientmap.go b/pkg/apiserver/profiling/clientmap.go new file mode 100644 index 0000000000..5aaa18aac6 --- /dev/null +++ b/pkg/apiserver/profiling/clientmap.go @@ -0,0 +1,115 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package profiling + +import ( + "fmt" + "net/http" + "time" + + "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" + "github.com/pingcap/tidb-dashboard/pkg/apiserver/profiling/fetcher" + "github.com/pingcap/tidb-dashboard/pkg/config" + "github.com/pingcap/tidb-dashboard/pkg/pd" + "github.com/pingcap/tidb-dashboard/pkg/tidb" + "github.com/pingcap/tidb-dashboard/pkg/tiflash" + "github.com/pingcap/tidb-dashboard/pkg/tikv" +) + +const ( + maxProfilingTimeout = time.Minute * 5 +) + +type clientMap map[model.NodeKind]fetcher.Client + +func (fm *clientMap) Get(kind model.NodeKind) (fetcher.Client, error) { + f, ok := (*fm)[kind] + if !ok { + return nil, fmt.Errorf("unsupported target %s", kind) + } + return f, nil +} + +func newClientMap( + tikvHTTPClient *tikv.Client, + tiflashHTTPClient *tiflash.Client, + tidbHTTPClient *tidb.Client, + pdHTTPClient *pd.Client, + config *config.Config, +) *clientMap { + return &clientMap{ + model.NodeKindTiKV: &tikvClient{ + client: tikvHTTPClient, + }, + model.NodeKindTiFlash: &tiflashClient{ + client: tiflashHTTPClient, + }, + model.NodeKindTiDB: &tidbClient{ + client: tidbHTTPClient, + }, + model.NodeKindPD: &pdClient{ + client: pdHTTPClient, + statusAPIHTTPScheme: config.GetClusterHTTPScheme(), + }, + } +} + +// tikv +var _ fetcher.Client = (*tikvClient)(nil) + +type tikvClient struct { + client *tikv.Client +} + +func (f *tikvClient) Fetch(op *fetcher.ClientFetchOptions) ([]byte, error) { + return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.IP, op.Port, op.Path) +} + +// tiflash +var _ fetcher.Client = (*tiflashClient)(nil) + +type tiflashClient struct { + client *tiflash.Client +} + +func (f *tiflashClient) Fetch(op *fetcher.ClientFetchOptions) ([]byte, error) { + return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.IP, op.Port, op.Path) +} + +// tidb +var _ fetcher.Client = (*tidbClient)(nil) + +type tidbClient struct { + client *tidb.Client +} + +func (f *tidbClient) Fetch(op *fetcher.ClientFetchOptions) ([]byte, error) { + return f.client.WithStatusAPIAddress(op.IP, op.Port).WithStatusAPITimeout(maxProfilingTimeout).SendGetRequest(op.Path) +} + +// pd +var _ fetcher.Client = (*pdClient)(nil) + +type pdClient struct { + client *pd.Client + statusAPIHTTPScheme string +} + +func (f *pdClient) Fetch(op *fetcher.ClientFetchOptions) ([]byte, error) { + baseURL := fmt.Sprintf("%s://%s:%d", f.statusAPIHTTPScheme, op.IP, op.Port) + f.client.WithBeforeRequest(func(req *http.Request) { + req.Header.Add("PD-Allow-follower-handle", "true") + }) + return f.client.WithTimeout(maxProfilingTimeout).WithBaseURL(baseURL).SendGetRequest(op.Path) +} diff --git a/pkg/apiserver/profiling/fetcher.go b/pkg/apiserver/profiling/fetcher.go deleted file mode 100644 index c532a21e2a..0000000000 --- a/pkg/apiserver/profiling/fetcher.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2021 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// See the License for the specific language governing permissions and -// limitations under the License. - -package profiling - -import ( - "fmt" - "net/http" - "time" - - "go.uber.org/fx" - - "github.com/pingcap/tidb-dashboard/pkg/config" - "github.com/pingcap/tidb-dashboard/pkg/pd" - "github.com/pingcap/tidb-dashboard/pkg/tidb" - "github.com/pingcap/tidb-dashboard/pkg/tiflash" - "github.com/pingcap/tidb-dashboard/pkg/tikv" -) - -const ( - maxProfilingTimeout = time.Minute * 5 -) - -type fetchOptions struct { - ip string - port int - path string -} - -type profileFetcher interface { - fetch(op *fetchOptions) ([]byte, error) -} - -type fetchers struct { - tikv profileFetcher - tiflash profileFetcher - tidb profileFetcher - pd profileFetcher -} - -var newFetchers = fx.Provide(func( - tikvClient *tikv.Client, - tidbClient *tidb.Client, - pdClient *pd.Client, - tiflashClient *tiflash.Client, - config *config.Config, -) *fetchers { - return &fetchers{ - tikv: &tikvFetcher{ - client: tikvClient, - }, - tiflash: &tiflashFetcher{ - client: tiflashClient, - }, - tidb: &tidbFetcher{ - client: tidbClient, - }, - pd: &pdFetcher{ - client: pdClient, - statusAPIHTTPScheme: config.GetClusterHTTPScheme(), - }, - } -}) - -type tikvFetcher struct { - client *tikv.Client -} - -func (f *tikvFetcher) fetch(op *fetchOptions) ([]byte, error) { - return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path) -} - -type tiflashFetcher struct { - client *tiflash.Client -} - -func (f *tiflashFetcher) fetch(op *fetchOptions) ([]byte, error) { - return f.client.WithTimeout(maxProfilingTimeout).SendGetRequest(op.ip, op.port, op.path) -} - -type tidbFetcher struct { - client *tidb.Client -} - -func (f *tidbFetcher) fetch(op *fetchOptions) ([]byte, error) { - return f.client.WithStatusAPIAddress(op.ip, op.port).WithStatusAPITimeout(maxProfilingTimeout).SendGetRequest(op.path) -} - -type pdFetcher struct { - client *pd.Client - statusAPIHTTPScheme string -} - -func (f *pdFetcher) fetch(op *fetchOptions) ([]byte, error) { - baseURL := fmt.Sprintf("%s://%s:%d", f.statusAPIHTTPScheme, op.ip, op.port) - f.client.WithBeforeRequest(func(req *http.Request) { - req.Header.Add("PD-Allow-follower-handle", "true") - }) - return f.client.WithTimeout(maxProfilingTimeout).WithBaseURL(baseURL).SendGetRequest(op.path) -} diff --git a/pkg/apiserver/profiling/fetcher/client.go b/pkg/apiserver/profiling/fetcher/client.go new file mode 100644 index 0000000000..8145aa374c --- /dev/null +++ b/pkg/apiserver/profiling/fetcher/client.go @@ -0,0 +1,24 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +type ClientFetchOptions struct { + IP string + Port int + Path string +} + +type Client interface { + Fetch(op *ClientFetchOptions) ([]byte, error) +} diff --git a/pkg/apiserver/profiling/fetcher/fetcher.go b/pkg/apiserver/profiling/fetcher/fetcher.go new file mode 100644 index 0000000000..85968cbf34 --- /dev/null +++ b/pkg/apiserver/profiling/fetcher/fetcher.go @@ -0,0 +1,59 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +import ( + "time" + + "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" +) + +type ProfileFetchOptions struct { + Duration time.Duration +} + +type ProfilerFetcher interface { + Fetch(op *ProfileFetchOptions) ([]byte, error) +} + +type Fetcher interface { + Fetch(client Client, target *model.RequestTargetNode, op *ProfileFetchOptions) ([]byte, error) +} + +type profilerFetcher struct { + fetcher Fetcher + client Client + target *model.RequestTargetNode +} + +func (p *profilerFetcher) Fetch(op *ProfileFetchOptions) ([]byte, error) { + return p.fetcher.Fetch(p.client, p.target, op) +} + +type Factory struct { + client Client + target *model.RequestTargetNode +} + +func (ff *Factory) Create(fetcher Fetcher) ProfilerFetcher { + return &profilerFetcher{ + fetcher: fetcher, + client: ff.client, + target: ff.target, + } +} + +func NewFetcherFactory(client Client, target *model.RequestTargetNode) *Factory { + return &Factory{client: client, target: target} +} diff --git a/pkg/apiserver/profiling/fetcher/flamegraph.go b/pkg/apiserver/profiling/fetcher/flamegraph.go new file mode 100644 index 0000000000..effd7d77ca --- /dev/null +++ b/pkg/apiserver/profiling/fetcher/flamegraph.go @@ -0,0 +1,29 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package fetcher + +import ( + "fmt" + + "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" +) + +var _ Fetcher = (*FlameGraph)(nil) + +type FlameGraph struct{} + +func (f *FlameGraph) Fetch(client Client, target *model.RequestTargetNode, op *ProfileFetchOptions) ([]byte, error) { + path := fmt.Sprintf("/debug/pprof/profile?seconds=%d", op.Duration) + return client.Fetch(&ClientFetchOptions{IP: target.IP, Port: target.Port, Path: path}) +} diff --git a/pkg/apiserver/profiling/pprof.go b/pkg/apiserver/profiling/fetcher/pprof.go similarity index 57% rename from pkg/apiserver/profiling/pprof.go rename to pkg/apiserver/profiling/fetcher/pprof.go index 920ab9e227..ab70acd7ef 100644 --- a/pkg/apiserver/profiling/pprof.go +++ b/pkg/apiserver/profiling/fetcher/pprof.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package profiling +package fetcher import ( "flag" @@ -20,10 +20,8 @@ import ( "io/ioutil" "os" "strconv" - "sync" "time" - "github.com/goccy/go-graphviz" "github.com/google/pprof/driver" "github.com/google/pprof/profile" @@ -31,86 +29,56 @@ import ( ) var ( - _ driver.Fetcher = (*fetcher)(nil) - mu sync.Mutex + _ driver.Fetcher = (*pprofFetcher)(nil) + _ Fetcher = (*Pprof)(nil) ) -type pprofOptions struct { - duration uint - // frequency uint - fileNameWithoutExt string - - target *model.RequestTargetNode - fetcher *profileFetcher +type Pprof struct { + FileNameWithoutExt string } -func fetchPprofSVG(op *pprofOptions) (string, error) { - f, err := fetchPprof(op, "dot") - if err != nil { - return "", fmt.Errorf("failed to get DOT output from file: %v", err) - } - - b, err := ioutil.ReadFile(f) +func (p *Pprof) Fetch(client Client, target *model.RequestTargetNode, op *ProfileFetchOptions) (d []byte, err error) { + tmpfile, err := ioutil.TempFile("", p.FileNameWithoutExt) if err != nil { - return "", fmt.Errorf("failed to get DOT output from file: %v", err) - } - - tmpfile, err := ioutil.TempFile("", op.fileNameWithoutExt) - if err != nil { - return "", fmt.Errorf("failed to create temp file: %v", err) + return d, fmt.Errorf("failed to create temp file: %v", err) } defer tmpfile.Close() - tmpPath := fmt.Sprintf("%s.%s", tmpfile.Name(), "svg") - - g := graphviz.New() - mu.Lock() - defer mu.Unlock() - graph, err := graphviz.ParseBytes(b) - if err != nil { - return "", fmt.Errorf("failed to parse DOT file: %v", err) - } - - if err := g.RenderFilename(graph, graphviz.SVG, tmpPath); err != nil { - return "", fmt.Errorf("failed to render SVG: %v", err) - } - return tmpPath, nil -} - -type flagSet struct { - *flag.FlagSet - args []string -} - -func fetchPprof(op *pprofOptions, format string) (string, error) { - tmpfile, err := ioutil.TempFile("", op.fileNameWithoutExt) - if err != nil { - return "", fmt.Errorf("failed to create temp file: %v", err) - } - defer tmpfile.Close() + format := "dot" tmpPath := fmt.Sprintf("%s.%s", tmpfile.Name(), format) format = "-" + format args := []string{ format, // prevent printing stdout "-output", "dummy", - "-seconds", strconv.Itoa(int(op.duration)), + "-seconds", strconv.Itoa(int(op.Duration)), } - address := fmt.Sprintf("%s:%d", op.target.IP, op.target.Port) + address := fmt.Sprintf("%s:%d", target.IP, target.Port) args = append(args, address) f := &flagSet{ FlagSet: flag.NewFlagSet("pprof", flag.PanicOnError), args: args, } if err := driver.PProf(&driver.Options{ - Fetch: &fetcher{profileFetcher: op.fetcher, target: op.target}, + Fetch: &pprofFetcher{client: client, target: target}, Flagset: f, UI: &blankPprofUI{}, Writer: &oswriter{output: tmpPath}, }); err != nil { - return "", fmt.Errorf("failed to generate profile report: %v", err) + return d, fmt.Errorf("failed to generate profile report: %v", err) } - return tmpPath, nil + + d, err = ioutil.ReadFile(tmpPath) + if err != nil { + return d, fmt.Errorf("failed to get DOT output from file: %v", err) + } + + return +} + +type flagSet struct { + *flag.FlagSet + args []string } func (f *flagSet) StringList(o, d, c string) *[]*string { @@ -139,16 +107,16 @@ func (o *oswriter) Open(name string) (io.WriteCloser, error) { return f, err } -type fetcher struct { - target *model.RequestTargetNode - profileFetcher *profileFetcher +type pprofFetcher struct { + client Client + target *model.RequestTargetNode } -func (f *fetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) { +func (f *pprofFetcher) Fetch(src string, duration, timeout time.Duration) (*profile.Profile, string, error) { secs := strconv.Itoa(int(duration / time.Second)) url := "/debug/pprof/profile?seconds=" + secs - resp, err := (*f.profileFetcher).fetch(&fetchOptions{ip: f.target.IP, port: f.target.Port, path: url}) + resp, err := f.client.Fetch(&ClientFetchOptions{IP: f.target.IP, Port: f.target.Port, Path: url}) if err != nil { return nil, url, err } diff --git a/pkg/apiserver/profiling/flamegraph.go b/pkg/apiserver/profiling/flamegraph.go deleted file mode 100644 index 003e7b0a44..0000000000 --- a/pkg/apiserver/profiling/flamegraph.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2021 PingCAP, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// See the License for the specific language governing permissions and -// limitations under the License. - -package profiling - -import ( - "fmt" - "io" - "io/ioutil" - "os" - - "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" -) - -type flameGraphOptions struct { - duration uint - // frequency uint - fileNameWithoutExt string - - target *model.RequestTargetNode - fetcher *profileFetcher -} - -func fetchFlameGraphSVG(op *flameGraphOptions) (string, error) { - path := fmt.Sprintf("/debug/pprof/profile?seconds=%d", op.duration) - resp, err := (*op.fetcher).fetch(&fetchOptions{ip: op.target.IP, port: op.target.Port, path: path}) - if err != nil { - return "", err - } - svgFilePath, err := writePprofRsSVG(resp, op.fileNameWithoutExt) - if err != nil { - return "", err - } - return svgFilePath, nil -} - -func writePprofRsSVG(body []byte, fileNameWithoutExt string) (string, error) { - file, err := ioutil.TempFile("", fileNameWithoutExt) - if err != nil { - return "", fmt.Errorf("failed to create temp file: %v", err) - } - _, err = io.WriteString(file, string(body)) - if err != nil { - return "", fmt.Errorf("failed to write temp file: %v", err) - } - svgFilePath := file.Name() + ".svg" - err = os.Rename(file.Name(), svgFilePath) - if err != nil { - return "", fmt.Errorf("failed to write SVG from temp file: %v", err) - } - return svgFilePath, nil -} diff --git a/pkg/apiserver/profiling/model.go b/pkg/apiserver/profiling/model.go index f03e97b860..cdb7065b58 100644 --- a/pkg/apiserver/profiling/model.go +++ b/pkg/apiserver/profiling/model.go @@ -72,11 +72,11 @@ type Task struct { ctx context.Context cancel context.CancelFunc taskGroup *TaskGroup - fetchers *fetchers + clientMap *clientMap } // NewTask creates a new profiling task. -func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTargetNode, fts *fetchers) *Task { +func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTargetNode, cm *clientMap) *Task { ctx, cancel := context.WithCancel(ctx) return &Task{ TaskModel: &TaskModel{ @@ -88,13 +88,13 @@ func NewTask(ctx context.Context, taskGroup *TaskGroup, target model.RequestTarg ctx: ctx, cancel: cancel, taskGroup: taskGroup, - fetchers: fts, + clientMap: cm, } } func (t *Task) run() { fileNameWithoutExt := fmt.Sprintf("profiling_%d_%d_%s", t.TaskGroupID, t.ID, t.Target.FileName()) - svgFilePath, err := profileAndWriteSVG(t.ctx, t.fetchers, &t.Target, fileNameWithoutExt, t.taskGroup.ProfileDurationSecs) + svgFilePath, err := profileAndWriteSVG(t.ctx, t.clientMap, &t.Target, fileNameWithoutExt, t.taskGroup.ProfileDurationSecs) if err != nil { t.Error = err.Error() t.State = TaskStateError diff --git a/pkg/apiserver/profiling/module.go b/pkg/apiserver/profiling/module.go index a76c7c44ab..046274ad46 100644 --- a/pkg/apiserver/profiling/module.go +++ b/pkg/apiserver/profiling/module.go @@ -1,5 +1,26 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + package profiling -import "go.uber.org/fx" +import ( + "go.uber.org/fx" +) -var Module = fx.Options(newFetchers, newService) +var Module = fx.Options( + fx.Provide( + newClientMap, + newService, + ), + fx.Invoke(registerRouter), +) diff --git a/pkg/apiserver/profiling/profile.go b/pkg/apiserver/profiling/profile.go index 4f94c92467..9f31d73960 100644 --- a/pkg/apiserver/profiling/profile.go +++ b/pkg/apiserver/profiling/profile.go @@ -16,21 +16,35 @@ package profiling import ( "context" "fmt" + "time" + + "github.com/goccy/go-graphviz" "github.com/pingcap/tidb-dashboard/pkg/apiserver/model" + "github.com/pingcap/tidb-dashboard/pkg/apiserver/profiling/fetcher" ) -func profileAndWriteSVG(ctx context.Context, fts *fetchers, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint) (string, error) { +func profileAndWriteSVG(ctx context.Context, cm *clientMap, target *model.RequestTargetNode, fileNameWithoutExt string, profileDurationSecs uint) (string, error) { + c, err := cm.Get(target.Kind) + if err != nil { + return "", err + } + + ff := fetcher.NewFetcherFactory(c, target) + + var f fetcher.ProfilerFetcher + var w Writer switch target.Kind { - case model.NodeKindTiKV: - return fetchFlameGraphSVG(&flameGraphOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tikv}) - case model.NodeKindTiFlash: - return fetchFlameGraphSVG(&flameGraphOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tiflash}) - case model.NodeKindTiDB: - return fetchPprofSVG(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.tidb}) - case model.NodeKindPD: - return fetchPprofSVG(&pprofOptions{duration: profileDurationSecs, fileNameWithoutExt: fileNameWithoutExt, target: target, fetcher: &fts.pd}) + case model.NodeKindTiKV, model.NodeKindTiFlash: + f = ff.Create(&fetcher.FlameGraph{}) + w = &fileWriter{fileNameWithoutExt: fileNameWithoutExt, ext: "svg"} + case model.NodeKindTiDB, model.NodeKindPD: + f = ff.Create(&fetcher.Pprof{FileNameWithoutExt: fileNameWithoutExt}) + w = &graphvizWriter{fileNameWithoutExt: fileNameWithoutExt, ext: graphviz.SVG} default: return "", fmt.Errorf("unsupported target %s", target) } + + p := newProfiler(f, w) + return p.Profile(&profileOptions{Duration: time.Duration(profileDurationSecs)}) } diff --git a/pkg/apiserver/profiling/profiler.go b/pkg/apiserver/profiling/profiler.go new file mode 100644 index 0000000000..576170e76e --- /dev/null +++ b/pkg/apiserver/profiling/profiler.go @@ -0,0 +1,45 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package profiling + +import ( + "time" + + "github.com/pingcap/tidb-dashboard/pkg/apiserver/profiling/fetcher" +) + +type profiler struct { + fetcher fetcher.ProfilerFetcher + writer Writer +} + +func newProfiler(fetcher fetcher.ProfilerFetcher, writer Writer) *profiler { + return &profiler{ + fetcher: fetcher, + writer: writer, + } +} + +type profileOptions struct { + Duration time.Duration +} + +func (p *profiler) Profile(op *profileOptions) (string, error) { + resp, err := p.fetcher.Fetch(&fetcher.ProfileFetchOptions{Duration: op.Duration}) + if err != nil { + return "", err + } + + return p.writer.Write(resp) +} diff --git a/pkg/apiserver/profiling/router.go b/pkg/apiserver/profiling/router.go index 470b8577a9..b8c46be2f3 100644 --- a/pkg/apiserver/profiling/router.go +++ b/pkg/apiserver/profiling/router.go @@ -30,7 +30,7 @@ import ( ) // Register register the handlers to the service. -func RegisterRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { +func registerRouter(r *gin.RouterGroup, auth *user.AuthService, s *Service) { endpoint := r.Group("/profiling") endpoint.GET("/group/list", auth.MWAuthRequired(), s.getGroupList) endpoint.POST("/group/start", auth.MWAuthRequired(), s.handleStartGroup) diff --git a/pkg/apiserver/profiling/service.go b/pkg/apiserver/profiling/service.go index ad3d4a08f2..bde274f9fc 100644 --- a/pkg/apiserver/profiling/service.go +++ b/pkg/apiserver/profiling/service.go @@ -62,14 +62,14 @@ type Service struct { sessionCh chan *StartRequestSession lastTaskGroup *TaskGroup tasks sync.Map - fetchers *fetchers + clientMap *clientMap } -var newService = fx.Provide(func(lc fx.Lifecycle, p ServiceParams, fts *fetchers) (*Service, error) { +func newService(lc fx.Lifecycle, p ServiceParams, cm *clientMap) (*Service, error) { if err := autoMigrate(p.LocalStore); err != nil { return nil, err } - s := &Service{params: p, fetchers: fts} + s := &Service{params: p, clientMap: cm} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { s.wg.Add(1) @@ -86,7 +86,7 @@ var newService = fx.Provide(func(lc fx.Lifecycle, p ServiceParams, fts *fetchers }) return s, nil -}) +} func (s *Service) serviceLoop(ctx context.Context) { cfgCh := s.params.ConfigManager.NewPushChannel() @@ -159,7 +159,7 @@ func (s *Service) startGroup(ctx context.Context, req *StartRequest) (*TaskGroup tasks := make([]*Task, 0, len(req.Targets)) for _, target := range req.Targets { - t := NewTask(ctx, taskGroup, target, s.fetchers) + t := NewTask(ctx, taskGroup, target, s.clientMap) s.params.LocalStore.Create(t.TaskModel) s.tasks.Store(t.ID, t) tasks = append(tasks, t) diff --git a/pkg/apiserver/profiling/writer.go b/pkg/apiserver/profiling/writer.go new file mode 100644 index 0000000000..0feb9eb6e9 --- /dev/null +++ b/pkg/apiserver/profiling/writer.go @@ -0,0 +1,83 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package profiling + +import ( + "fmt" + "io/ioutil" + "os" + "sync" + + "github.com/goccy/go-graphviz" +) + +var mu sync.Mutex + +type Writer interface { + Write(p []byte) (string, error) +} + +type fileWriter struct { + fileNameWithoutExt string + ext string +} + +func (w *fileWriter) Write(p []byte) (string, error) { + f, err := ioutil.TempFile("", w.fileNameWithoutExt) + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.Write(p) + if err != nil { + return "", fmt.Errorf("failed to write temp file: %v", err) + } + + path := fmt.Sprintf("%s.%s", f.Name(), w.ext) + err = os.Rename(f.Name(), path) + if err != nil { + return "", fmt.Errorf("failed to write %s from temp file: %v", w.ext, err) + } + + return path, nil +} + +type graphvizWriter struct { + fileNameWithoutExt string + ext graphviz.Format +} + +func (w *graphvizWriter) Write(b []byte) (string, error) { + tmpfile, err := ioutil.TempFile("", w.fileNameWithoutExt) + if err != nil { + return "", fmt.Errorf("failed to create temp file: %v", err) + } + defer tmpfile.Close() + tmpPath := fmt.Sprintf("%s.%s", tmpfile.Name(), w.ext) + + g := graphviz.New() + mu.Lock() + defer mu.Unlock() + graph, err := graphviz.ParseBytes(b) + if err != nil { + return "", fmt.Errorf("failed to parse DOT file: %v", err) + } + + if err := g.RenderFilename(graph, w.ext, tmpPath); err != nil { + return "", fmt.Errorf("failed to render %s: %v", w.ext, err) + } + + return tmpPath, nil +} diff --git a/ui/lib/apps/InstanceProfiling/pages/Detail.tsx b/ui/lib/apps/InstanceProfiling/pages/Detail.tsx index ababbc0b45..b465bf57bf 100644 --- a/ui/lib/apps/InstanceProfiling/pages/Detail.tsx +++ b/ui/lib/apps/InstanceProfiling/pages/Detail.tsx @@ -32,9 +32,7 @@ function mapData(data) { return data } -function isFinished(data) { - return data?.task_group_status?.state === 2 -} +const isFinished = (state: number) => state === 2 export default function Page() { const { t } = useTranslation() @@ -43,7 +41,7 @@ export default function Page() { const { data: respData, isLoading, error } = useClientRequestWithPolling( (reqConfig) => client.getInstance().getProfilingGroupDetail(id, reqConfig), { - shouldPoll: (data) => !isFinished(data), + shouldPoll: (data) => !isFinished(data?.task_group_status?.state!), } ) @@ -101,6 +99,9 @@ export default function Page() { const handleRowClick = usePersistFn( async (rec, _idx, _ev: React.MouseEvent) => { + if (!isFinished(rec.state)) { + return + } const res = await client .getInstance() .getActionToken(rec.id, 'single_view')