Skip to content

Add PProf to admin pages and to gitea manager #22742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 34 commits into from
Closed
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5093d7d
Add PProf to admin pages
zeripath Feb 3, 2023
9cfcbb4
Update pprof.tmpl
zeripath Feb 4, 2023
f22aa21
Update locale_en-US.ini
zeripath Feb 4, 2023
b9989b1
Add FGProf handler
zeripath Feb 4, 2023
6451d01
wire in PProfFGProfile
zeripath Feb 4, 2023
351149b
Wire in FGProf
zeripath Feb 4, 2023
7941fbb
Update locale_en-US.ini
zeripath Feb 4, 2023
0ef07f6
Update pprof.tmpl
zeripath Feb 4, 2023
8d0d93e
Update pprof.go
zeripath Feb 4, 2023
768a960
Update pprof.go
zeripath Feb 4, 2023
82169ca
Update web.go
zeripath Feb 4, 2023
ba894a1
Update pprof.go
zeripath Feb 4, 2023
75d96e8
Update pprof.tmpl
zeripath Feb 4, 2023
381f12a
Update pprof.tmpl
zeripath Feb 4, 2023
2fc5d72
Update locale_en-US.ini
zeripath Feb 4, 2023
79756cd
placate lint
zeripath Feb 4, 2023
6247e72
Add pprof endpoints to manager too
zeripath Feb 4, 2023
d9eac18
add documentation
zeripath Feb 4, 2023
7decd93
Add output option
zeripath Feb 4, 2023
4d916b4
fix missing name
zeripath Feb 4, 2023
b9942cb
add trace and fix format
zeripath Feb 4, 2023
7c4be9c
fix format on stacktraces
zeripath Feb 4, 2023
6c2017d
fix trace
zeripath Feb 4, 2023
d37978e
Apply suggestions from code review
zeripath Feb 5, 2023
950c474
Merge remote-tracking branch 'origin/main' into add-pprof-to-admin-pages
zeripath Feb 5, 2023
2dbab58
as per delvh
zeripath Feb 5, 2023
2b523f1
include indent in WriteProcess
zeripath Feb 5, 2023
5925f47
as per delvh
zeripath Feb 5, 2023
91bc97a
remove space
zeripath Feb 5, 2023
61755a7
as per delvh
zeripath Feb 5, 2023
1400ab2
as per delvh
zeripath Feb 5, 2023
edebd86
add some comments
zeripath Feb 5, 2023
1ffe696
Merge remote-tracking branch 'origin/main' into add-pprof-to-admin-pages
zeripath Feb 19, 2023
92ac295
move trace help in to the ui form
zeripath Feb 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 175 additions & 10 deletions cmd/manager.go
Original file line number Diff line number Diff line change
@@ -4,7 +4,9 @@
package cmd

import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
@@ -26,6 +28,11 @@ var (
subcmdFlushQueues,
subcmdLogging,
subCmdProcesses,
subCmdCPUProfile,
subCmdFGProfile,
subCmdListNamedProfiles,
subCmdNamedProfile,
subCmdTrace,
},
}
subcmdShutdown = cli.Command{
@@ -95,15 +102,117 @@ var (
Name: "cancel",
Usage: "Process PID to cancel. (Only available for non-system processes.)",
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
Value: "-",
},
},
}
subCmdCPUProfile = cli.Command{
Name: "cpu-profile",
Usage: "Return PProf CPU profile",
Action: runCPUProfile,
Flags: []cli.Flag{
cli.DurationFlag{
Name: "duration",
Usage: "Duration to collect CPU Profile over",
Value: 30 * time.Second,
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
Value: "cpu-profile",
},
},
}
subCmdFGProfile = cli.Command{
Name: "fg-profile",
Usage: "Return PProf Full Go profile",
Action: runFGProfile,
Flags: []cli.Flag{
cli.DurationFlag{
Name: "duration",
Usage: "Duration to collect CPU Profile over",
Value: 30 * time.Second,
},
cli.StringFlag{
Name: "format",
Usage: "Format to return the profile in: pprof, folded",
Value: "pprof",
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
Value: "fg-profile",
},
},
}
subCmdNamedProfile = cli.Command{
Name: "named-profile",
Usage: "Return PProf named profile",
Action: runNamedProfile,
Flags: []cli.Flag{
cli.StringFlag{
Name: "name",
Usage: "Name of profile to run",
},
cli.IntFlag{
Name: "debug-level",
Usage: "Debug level for the profile",
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
},
},
}
subCmdListNamedProfiles = cli.Command{
Name: "list-named-profiles",
Usage: "Return PProf list of named profiles",
Action: runListNamedProfile,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "json",
Usage: "Output as json",
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
Value: "-",
},
},
}
subCmdTrace = cli.Command{
Name: "trace",
Usage: "Return PProf trace",
Action: runTrace,
Flags: []cli.Flag{
cli.DurationFlag{
Name: "duration",
Usage: "Duration to collect CPU Profile over",
Value: 30 * time.Second,
},
cli.StringFlag{
Name: "output,o",
Usage: "File to output to (set to \"-\" for stdout)",
Value: "trace",
},
},
}
)

func runShutdown(c *cli.Context) error {
func setupManager(c *cli.Context) (context.Context, context.CancelFunc) {
ctx, cancel := installSignals()
defer cancel()

setup("manager", c.Bool("debug"))
return ctx, cancel
}

func runShutdown(c *cli.Context) error {
ctx, cancel := setupManager(c)
defer cancel()

statusCode, msg := private.Shutdown(ctx)
switch statusCode {
case http.StatusInternalServerError:
@@ -115,10 +224,9 @@ func runShutdown(c *cli.Context) error {
}

func runRestart(c *cli.Context) error {
ctx, cancel := installSignals()
ctx, cancel := setupManager(c)
defer cancel()

setup("manager", c.Bool("debug"))
statusCode, msg := private.Restart(ctx)
switch statusCode {
case http.StatusInternalServerError:
@@ -130,10 +238,9 @@ func runRestart(c *cli.Context) error {
}

func runFlushQueues(c *cli.Context) error {
ctx, cancel := installSignals()
ctx, cancel := setupManager(c)
defer cancel()

setup("manager", c.Bool("debug"))
statusCode, msg := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
switch statusCode {
case http.StatusInternalServerError:
@@ -144,16 +251,74 @@ func runFlushQueues(c *cli.Context) error {
return nil
}

func runProcesses(c *cli.Context) error {
ctx, cancel := installSignals()
func determineOutput(c *cli.Context, defaultFilename string) (io.WriteCloser, error) {
out := os.Stdout
filename := c.String("output")
if filename == "" {
filename = defaultFilename
}
if filename != "-" {
var err error
out, err = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return nil, fail("Unable to open "+filename, err.Error())
}
fmt.Printf("Writing to %s\n", filename)
}
return out, nil
}

func wrapManagerPrivateFunc(c *cli.Context, defaultOutput string, fn func(ctx context.Context, out io.Writer) (int, string)) error {
ctx, cancel := setupManager(c)
defer cancel()

setup("manager", c.Bool("debug"))
statusCode, msg := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
out, err := determineOutput(c, defaultOutput)
if err != nil {
return err
}
defer out.Close()

statusCode, msg := fn(ctx, out)
switch statusCode {
case http.StatusInternalServerError:
return fail("InternalServerError", msg)
}

return nil
}

func runProcesses(c *cli.Context) error {
return wrapManagerPrivateFunc(c, "-", func(ctx context.Context, out io.Writer) (int, string) {
return private.Processes(ctx, out, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
})
}

func runCPUProfile(c *cli.Context) error {
return wrapManagerPrivateFunc(c, "cpu-profile", func(ctx context.Context, out io.Writer) (int, string) {
return private.CPUProfile(ctx, out, c.Duration("duration"))
})
}

func runFGProfile(c *cli.Context) error {
return wrapManagerPrivateFunc(c, "fg-profile", func(ctx context.Context, out io.Writer) (int, string) {
return private.FGProfile(ctx, out, c.Duration("duration"), c.String("format"))
})
}

func runNamedProfile(c *cli.Context) error {
return wrapManagerPrivateFunc(c, c.String("name")+"-profile", func(ctx context.Context, out io.Writer) (int, string) {
return private.NamedProfile(ctx, out, c.String("name"), c.Int("debug-level"))
})
}

func runListNamedProfile(c *cli.Context) error {
return wrapManagerPrivateFunc(c, "-", func(ctx context.Context, out io.Writer) (int, string) {
return private.ListNamedProfiles(ctx, out, c.Bool("json"))
})
}

func runTrace(c *cli.Context) error {
return wrapManagerPrivateFunc(c, "trace", func(ctx context.Context, out io.Writer) (int, string) {
return private.Trace(ctx, out, c.Duration("duration"))
})
}
23 changes: 23 additions & 0 deletions docs/content/doc/usage/command-line.en-us.md
Original file line number Diff line number Diff line change
@@ -526,6 +526,29 @@ Manage running server operations:
- `--stacktraces`: Show stacktraces for goroutines associated with processes
- `--json`: Output as json
- `--cancel PID`: Send cancel to process with PID. (Only for non-system processes.)
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)
- `cpu-profile`: Return the PProf CPU profile
- Options:
- `--duration`: Duration of time to run profile (default: 30s)
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)
- `fg-profile`: Returns the PProf Full Go profile
- Options:
- `--duration`: Duration of time to run profile (default: 30s)
- `--format`: Format of profile (default: pprof)
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)
- `list-named-profiles`: Returns a list of named profiles
- Options:
- `--json`: Set to true to return a json output
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)
- `named-profile`: Returns the output of a named profile
- Options:
- `--name`: Name of the profile
- `--debug-level`: Debug level for the profile
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)
- `trace`: Return the PProf trace
- Options:
- `--duration`: Duration of time to run profile (default: 30s)
- `--output filename`, `-o filename`: Filename to output to. (Set to `-` to use stdout.)

### dump-repo

2 changes: 1 addition & 1 deletion modules/context/private.go
Original file line number Diff line number Diff line change
@@ -47,7 +47,7 @@ var privateContextKey interface{} = "default_private_context"

// WithPrivateContext set up private context in request
func WithPrivateContext(req *http.Request, ctx *PrivateContext) *http.Request {
return req.WithContext(context.WithValue(req.Context(), privateContextKey, ctx))
return req.WithContext(context.WithValue(context.WithValue(req.Context(), privateContextKey, ctx), contextKey, ctx.Context))
}

// GetPrivateContext returns a context for Private routes
44 changes: 44 additions & 0 deletions modules/private/manager.go
Original file line number Diff line number Diff line change
@@ -230,3 +230,47 @@ func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces,
}
return http.StatusOK, ""
}

// CPUProfile returns a cpu profile from Gitea
func CPUProfile(ctx context.Context, out io.Writer, duration time.Duration) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/cpu-profile?duration=%s", url.QueryEscape(duration.String()))
return commonGet(ctx, out, reqURL)
}

// FGProfile returns the full go profile from Gitea
func FGProfile(ctx context.Context, out io.Writer, duration time.Duration, format string) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/fgprof?duration=%s&format=%s", url.QueryEscape(duration.String()), url.QueryEscape(format))
return commonGet(ctx, out, reqURL)
}

// NamedProfile returns the named profile from Gitea
func NamedProfile(ctx context.Context, out io.Writer, name string, debugLevel int) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/profile?name=%s&debug=%d", url.QueryEscape(name), debugLevel)
return commonGet(ctx, out, reqURL)
}

// ListNamedProfiles returns a list of named profiles
func ListNamedProfiles(ctx context.Context, out io.Writer, json bool) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/list-profiles?json=%t", json)
return commonGet(ctx, out, reqURL)
}

// Trace returns a trace from Gitea
func Trace(ctx context.Context, out io.Writer, duration time.Duration) (int, string) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/trace?duration=%s", url.QueryEscape(duration.String()))
return commonGet(ctx, out, reqURL)
}

func commonGet(ctx context.Context, out io.Writer, reqURL string) (int, string) {
req := newInternalRequest(ctx, reqURL, "GET")
resp, err := req.Response()
if err != nil {
return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error())
}
defer resp.Body.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return http.StatusInternalServerError, err.Error()
}
return resp.StatusCode, ""
}
100 changes: 100 additions & 0 deletions modules/process/stacktraces_processlist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package process

import (
"bytes"
"fmt"
"io"
)

// WriteProcesses writes out processes to a provided writer
func WriteProcesses(out io.Writer, processes []*Process, processCount int, goroutineCount int64, indent string, flat bool) error {
if goroutineCount > 0 {
if _, err := fmt.Fprintf(out, "%sTotal Number of Goroutines: %d\n", indent, goroutineCount); err != nil {
return err
}
}
if _, err := fmt.Fprintf(out, "%sTotal Number of Processes: %d\n", indent, processCount); err != nil {
return err
}
if len(processes) > 0 {
if err := WriteProcess(out, processes[0], indent+" ", flat); err != nil {
return err
}
}
if len(processes) > 1 {
for _, process := range processes[1:] {
if _, err := fmt.Fprintf(out, "%s |\n", indent); err != nil {
return err
}
if err := WriteProcess(out, process, indent+" ", flat); err != nil {
return err
}
}
}
return nil
}

// WriteProcess writes out a process to a provided writer
func WriteProcess(out io.Writer, process *Process, indent string, flat bool) error {
sb := &bytes.Buffer{}
if flat {
if process.ParentPID != "" {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\t\tType: %s\n", indent, process.PID, process.Type)
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s:%s\tType: %s\n", indent, process.ParentPID, process.PID, process.Type)
}
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\tType: %s\n", indent, process.PID, process.Type)
}
indent += "| "

_, _ = fmt.Fprintf(sb, "%sDescription: %s\n", indent, process.Description)
_, _ = fmt.Fprintf(sb, "%sStart: %s\n", indent, process.Start)

if len(process.Stacks) > 0 {
_, _ = fmt.Fprintf(sb, "%sGoroutines:\n", indent)
for _, stack := range process.Stacks {
indent := indent + " "
_, _ = fmt.Fprintf(sb, "%s+ Description: %s", indent, stack.Description)
if stack.Count > 1 {
_, _ = fmt.Fprintf(sb, "* %d", stack.Count)
}
_, _ = fmt.Fprintf(sb, "\n")
indent += "| "
if len(stack.Labels) > 0 {
_, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value)

if len(stack.Labels) > 1 {
for _, label := range stack.Labels[1:] {
_, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value)
}
}
_, _ = fmt.Fprintf(sb, "\n")
}
_, _ = fmt.Fprintf(sb, "%sStack:\n", indent)
indent += " "
for _, entry := range stack.Entry {
_, _ = fmt.Fprintf(sb, "%s+ %s\n", indent, entry.Function)
_, _ = fmt.Fprintf(sb, "%s| %s:%d\n", indent, entry.File, entry.Line)
}
}
}
if _, err := out.Write(sb.Bytes()); err != nil {
return err
}
sb.Reset()
if len(process.Children) > 0 {
if _, err := fmt.Fprintf(out, "%sChildren:\n", indent); err != nil {
return err
}
for _, child := range process.Children {
if err := WriteProcess(out, child, indent+" ", flat); err != nil {
return err
}
}
}
return nil
}
34 changes: 34 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
@@ -2954,6 +2954,7 @@ monitor.previous = Previous Time
monitor.execute_times = Executions
monitor.process = Running Processes
monitor.stacktrace = Stacktraces
monitor.stacktrace.download_stacktrace = Download Stacktrace
monitor.goroutines = %d Goroutines
monitor.desc = Description
monitor.start = Start Time
@@ -2963,6 +2964,39 @@ monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss
monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
monitor.process.children = Children
monitor.pprof = PProf Profiles
monitor.pprof.description = PProf profiles provide runtime profiling data.
monitor.pprof.description_2 = Text formats can be read directly but most data is in the format expected by the golang pprof visualization tool
monitor.pprof.download = Download
monitor.pprof.duration = Duration
monitor.pprof.duration_placeholder = e.g. 30s
monitor.pprof.duration_invalid = Invalid duration - duration must be a golang duration string
monitor.pprof.cpuprofile = CPU Profile
monitor.pprof.cpuprofile.description = CPU profile determines where Gitea spends its time while actively consuming CPU cycles
monitor.pprof.fgprof = Full Go Profile
monitor.pprof.fgprof.description = Full Go profile provides wall clock profiling combining the CPU Profile with IO time
monitor.pprof.fgprof.format = Format
monitor.pprof.named_profiles = Named Profiles
monitor.pprof.named_profiles.description = Go provides a number of named profiles for other profiling
monitor.pprof.named_profiles.name = Name
monitor.pprof.named_profiles.debug = Format
monitor.pprof.named_profiles.format_pprof = pprof
monitor.pprof.named_profiles.format_text = text
monitor.pprof.named_profiles.format_goroutine = custom/goroutine
monitor.pprof.stacktrace.description = Stacktraces provides stacktraces for all current goroutines mapped with Gitea's internal processes
monitor.pprof.stacktrace.flat = Do not nest processes under their parents
monitor.pprof.stacktrace.no-system = Do not include go-routines associated with system processes
monitor.pprof.stacktrace.format = Format
monitor.pprof.trace = Trace
monitor.pprof.trace.description = Trace provides tracing that can be used by the go tracing tool
monitor.queues = Queues
monitor.queue = Queue: %s
monitor.queue.name = Name
6 changes: 6 additions & 0 deletions routers/private/internal.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/admin"

"gitea.com/go-chi/binding"
chi_middleware "github.com/go-chi/chi/v5/middleware"
@@ -75,6 +76,11 @@ func Routes() *web.Route {
r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
r.Post("/manager/remove-logger/{group}/{name}", RemoveLogger)
r.Get("/manager/processes", Processes)
r.Get("/manager/cpu-profile", admin.PProfCPUProfile)
r.Get("/manager/profile", admin.PProfNamedProfile)
r.Get("/manager/fgprof", admin.PProfFGProfile)
r.Get("/manager/list-profiles", ListProfiles)
r.Get("/manager/trace", admin.Trace)
r.Post("/mail/send", SendEmail)
r.Post("/restore_repo", RestoreRepo)

106 changes: 24 additions & 82 deletions routers/private/manager_process.go
Original file line number Diff line number Diff line change
@@ -4,11 +4,10 @@
package private

import (
"bytes"
"fmt"
"io"
"net/http"
"runtime"
"runtime/pprof"
"time"

"code.gitea.io/gitea/modules/context"
@@ -60,7 +59,7 @@ func Processes(ctx *context.PrivateContext) {
ctx.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
ctx.Resp.WriteHeader(http.StatusOK)

if err := writeProcesses(ctx.Resp, processes, processCount, goroutineCount, "", flat); err != nil {
if err := process_module.WriteProcesses(ctx.Resp, processes, processCount, goroutineCount, "", flat); err != nil {
log.Error("Unable to write out process stacktrace: %v", err)
if !ctx.Written() {
ctx.JSON(http.StatusInternalServerError, private.Response{
@@ -71,90 +70,33 @@ func Processes(ctx *context.PrivateContext) {
}
}

func writeProcesses(out io.Writer, processes []*process_module.Process, processCount int, goroutineCount int64, indent string, flat bool) error {
if goroutineCount > 0 {
if _, err := fmt.Fprintf(out, "%sTotal Number of Goroutines: %d\n", indent, goroutineCount); err != nil {
return err
}
}
if _, err := fmt.Fprintf(out, "%sTotal Number of Processes: %d\n", indent, processCount); err != nil {
return err
}
if len(processes) > 0 {
if err := writeProcess(out, processes[0], " ", flat); err != nil {
return err
}
}
if len(processes) > 1 {
for _, process := range processes[1:] {
if _, err := fmt.Fprintf(out, "%s | \n", indent); err != nil {
return err
}
if err := writeProcess(out, process, " ", flat); err != nil {
return err
}
// ListProfiles lists the available named pprof profiles
func ListProfiles(ctx *context.PrivateContext) {
json := ctx.FormBool("json")
profiles := pprof.Profiles()
if json {
names := make([]string, len(profiles))
for _, profile := range profiles {
names = append(names, profile.Name())
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"Names": names,
})
}
return nil
}

func writeProcess(out io.Writer, process *process_module.Process, indent string, flat bool) error {
sb := &bytes.Buffer{}
if flat {
if process.ParentPID != "" {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\t\tType: %s\n", indent, process.PID, process.Type)
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s:%s\tType: %s\n", indent, process.ParentPID, process.PID, process.Type)
ctx.Status(http.StatusOK)
for _, profile := range profiles {
if _, err := ctx.Resp.Write([]byte(profile.Name())); err != nil {
log.Error("Unable to write out profile name: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
} else {
_, _ = fmt.Fprintf(sb, "%s+ PID: %s\tType: %s\n", indent, process.PID, process.Type)
}
indent += "| "

_, _ = fmt.Fprintf(sb, "%sDescription: %s\n", indent, process.Description)
_, _ = fmt.Fprintf(sb, "%sStart: %s\n", indent, process.Start)

if len(process.Stacks) > 0 {
_, _ = fmt.Fprintf(sb, "%sGoroutines:\n", indent)
for _, stack := range process.Stacks {
indent := indent + " "
_, _ = fmt.Fprintf(sb, "%s+ Description: %s", indent, stack.Description)
if stack.Count > 1 {
_, _ = fmt.Fprintf(sb, "* %d", stack.Count)
}
_, _ = fmt.Fprintf(sb, "\n")
indent += "| "
if len(stack.Labels) > 0 {
_, _ = fmt.Fprintf(sb, "%sLabels: %q:%q", indent, stack.Labels[0].Name, stack.Labels[0].Value)

if len(stack.Labels) > 1 {
for _, label := range stack.Labels[1:] {
_, _ = fmt.Fprintf(sb, ", %q:%q", label.Name, label.Value)
}
}
_, _ = fmt.Fprintf(sb, "\n")
}
_, _ = fmt.Fprintf(sb, "%sStack:\n", indent)
indent += " "
for _, entry := range stack.Entry {
_, _ = fmt.Fprintf(sb, "%s+ %s\n", indent, entry.Function)
_, _ = fmt.Fprintf(sb, "%s| %s:%d\n", indent, entry.File, entry.Line)
}
}
}
if _, err := out.Write(sb.Bytes()); err != nil {
return err
}
sb.Reset()
if len(process.Children) > 0 {
if _, err := fmt.Fprintf(out, "%sChildren:\n", indent); err != nil {
return err
}
for _, child := range process.Children {
if err := writeProcess(out, child, indent+" ", flat); err != nil {
return err
}
if _, err := ctx.Resp.Write([]byte("\n")); err != nil {
log.Error("Unable to write out profile name: %v", err)
ctx.Error(http.StatusInternalServerError, err.Error())
return
Comment on lines +79 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we writing the names twice?

}
}
return nil
ctx.Resp.Flush()
}
12 changes: 12 additions & 0 deletions routers/web/admin/admin.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@ import (
"fmt"
"net/http"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"time"

activities_model "code.gitea.io/gitea/models/activities"
@@ -163,6 +165,8 @@ func Monitor(ctx *context.Context) {
ctx.Data["Entries"] = cron.ListTasks()
ctx.Data["Queues"] = queue.GetManager().ManagedQueues()

ctx.Data["Profiles"] = pprof.Profiles()

ctx.HTML(http.StatusOK, tplMonitor)
}

@@ -182,6 +186,14 @@ func GoroutineStacktrace(ctx *context.Context) {

ctx.Data["GoroutineCount"] = goroutineCount
ctx.Data["ProcessCount"] = processCount
sb := new(strings.Builder)

if err := process.WriteProcesses(sb, processStacks, processCount, goroutineCount, "", false); err != nil {
ctx.ServerError("WriteProcesses", err)
return
}

ctx.Data["StacktraceString"] = sb.String()

ctx.HTML(http.StatusOK, tplStacktrace)
}
187 changes: 187 additions & 0 deletions routers/web/admin/pprof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package admin

import (
"fmt"
"net/http"
"runtime/pprof"
"runtime/trace"
"strconv"
"time"

"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"

"github.com/felixge/fgprof"
)

// PProfProcessStacktrace returns the stacktrace similar to GoroutineStacktrace but without rendering it
func PProfProcessStacktrace(ctx *context.Context) {
flat := ctx.FormBool("flat")
noSystem := ctx.FormBool("no-system")

format := ctx.FormString("format")
jsonFormat := format == "json"

start := time.Now()
filename := "process-stacktrace-" + strconv.FormatInt(start.Unix(), 10)
if jsonFormat {
filename += ".json"
}

processStacks, processCount, goroutineCount, err := process.GetManager().ProcessStacktraces(flat, noSystem)
if err != nil {
ctx.ServerError("ProcessStacktraces", err)
}

ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: filename,
LastModified: start,
})

if jsonFormat {
ctx.JSON(http.StatusOK, map[string]interface{}{
"TotalNumberOfGoroutines": goroutineCount,
"TotalNumberOfProcesses": processCount,
"Processes": processStacks,
})
return
}

if err := process.WriteProcesses(ctx.Resp, processStacks, processCount, goroutineCount, "", flat); err != nil {
ctx.ServerError("WriteProcesses", err)
}
}

// PProfFGProfile returns the Full Go Profile from fgprof
func PProfFGProfile(ctx *context.Context) {
durationStr := ctx.FormString("duration")
duration := 30 * time.Second
if durationStr != "" {
var err error
duration, err = time.ParseDuration(durationStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.pprof.duration_invalid"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor")
return
}
}

format := fgprof.Format(ctx.FormString("format"))
if format != fgprof.FormatFolded {
format = fgprof.FormatPprof
}

start := time.Now()

ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: "fgprof-profile-" + strconv.FormatInt(start.Unix(), 10),
LastModified: start,
})

fn := fgprof.Start(ctx.Resp, format)

select {
case <-time.After(duration):
case <-ctx.Done():
}

err := fn()
if err != nil {
ctx.ServerError("fgprof.Write", err)
}
}

// PProfCPUProfile returns the PProf CPU Profile
func PProfCPUProfile(ctx *context.Context) {
durationStr := ctx.FormString("duration")
duration := 30 * time.Second
if durationStr != "" {
var err error
duration, err = time.ParseDuration(durationStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.pprof.duration_invalid"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor")
return
}
}

start := time.Now()

ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: "cpu-profile-" + strconv.FormatInt(start.Unix(), 10),
LastModified: start,
})

err := pprof.StartCPUProfile(ctx.Resp)
if err != nil {
ctx.ServerError("StartCPUProfile", err)
return
}

select {
case <-time.After(duration):
case <-ctx.Done():
}
pprof.StopCPUProfile()
}

// PProfNamedProfile returns the PProf Profile
func PProfNamedProfile(ctx *context.Context) {
name := ctx.FormString("name")
profile := pprof.Lookup(name)
if profile == nil {
ctx.ServerError(fmt.Sprintf("pprof.Lookup(%s)", name), fmt.Errorf("missing profile: %s", name))
return
}

debug := ctx.FormInt("debug")

start := time.Now()

ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: name + "-profile-" + strconv.FormatInt(start.Unix(), 10),
LastModified: start,
})
if err := profile.WriteTo(ctx.Resp, debug); err != nil {
ctx.ServerError(fmt.Sprintf("PProfNamedProfile(%s).WriteTo", name), err)
return
}
}

// Trace returns a trace
func Trace(ctx *context.Context) {
durationStr := ctx.FormString("duration")
duration := 30 * time.Second
if durationStr != "" {
var err error
duration, err = time.ParseDuration(durationStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.pprof.duration_invalid"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor")
return
}
}

start := time.Now()

ctx.SetServeHeaders(&context.ServeHeaderOptions{
Filename: "trace-" + strconv.FormatInt(start.Unix(), 10),
LastModified: start,
})

err := trace.Start(ctx.Resp)
if err != nil {
ctx.ServerError("StartCPUProfile", err)
return
}

select {
case <-time.After(duration):
case <-ctx.Done():
}
trace.Stop()
}
5 changes: 5 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
@@ -527,6 +527,11 @@ func RegisterRoutes(m *web.Route) {
m.Group("/monitor", func() {
m.Get("", admin.Monitor)
m.Get("/stacktrace", admin.GoroutineStacktrace)
m.Get("/cpu-profile", admin.PProfCPUProfile)
m.Get("/profile", admin.PProfNamedProfile)
m.Get("/fgprof", admin.PProfFGProfile)
m.Get("/stacktrace-profile", admin.PProfProcessStacktrace)
m.Get("/trace", admin.Trace)
m.Post("/cancel/{pid}", admin.MonitorCancel)
m.Group("/queue/{qid}", func() {
m.Get("", admin.Queue)
1 change: 1 addition & 0 deletions templates/admin/monitor.tmpl
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@
</div>

{{template "admin/process" .}}
{{template "admin/pprof" .}}
</div>
</div>
<div class="ui small basic delete modal">
96 changes: 96 additions & 0 deletions templates/admin/pprof.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<h4 class="ui top attached header">
{{.locale.Tr "admin.monitor.pprof"}}
</h4>
<div class="ui attached segment">
<p>{{.locale.Tr "admin.monitor.pprof.description"}}</p>
<p>{{.locale.Tr "admin.monitor.pprof.description_2"}}</p>
<h4>{{.locale.Tr "admin.monitor.pprof.cpuprofile"}}</h4>
<form action="{{AppSubUrl}}/admin/monitor/cpu-profile">
<div class="ui form">
<div class="help">{{.locale.Tr "admin.monitor.pprof.cpuprofile.description"}}</div>
<div class="field">
<label for="pprof-cpuprofile-duration">{{.locale.Tr "admin.monitor.pprof.duration"}}</label>
<input id="pprof-cpuprofile-duration" type="text" name="duration" placeholder="{{.locale.Tr "admin.monitor.pprof.duration_placeholder"}}">
</div>
<input class="ui button basic" type="submit" value="{{.locale.Tr "admin.monitor.pprof.download"}}">
</div>
</form>
<h4>{{.locale.Tr "admin.monitor.pprof.fgprof"}}</h4>
<form action="{{AppSubUrl}}/admin/monitor/fgprof">
<div class="ui form">
<div class="help">{{.locale.Tr "admin.monitor.pprof.fgprof.description"}}</div>
<div class="field">
<label for="pprof-fgprof-duration">{{.locale.Tr "admin.monitor.pprof.duration"}}</label>
<input id="pprof-fgprof-duration" type="text" name="duration" placeholder="{{.locale.Tr "admin.monitor.pprof.duration_placeholder"}}">
</div>
<div class="field">
<label for="pprof-fgprof-format">{{.locale.Tr "admin.monitor.pprof.fgprof.format"}}</label>
<select class="ui search dropdown" id="pprof-fgprof-format" name="format">
<option value="pprof" selected>pprof</option>
<option value="folded">folded</option>
</select>
</div>
<input class="ui button basic" type="submit" value="{{.locale.Tr "admin.monitor.pprof.download"}}">
</div>
</form>
<h4>{{.locale.Tr "admin.monitor.stacktrace"}}</h4>
<form action="{{AppSubUrl}}/admin/monitor/stacktrace-profile">
<div class="ui form">
<div class="help">{{.locale.Tr "admin.monitor.pprof.stacktrace.description"}}</div>
<div class="inline field">
<div class="ui checkbox">
<input id="pprof-stacktrace-flat" type="checkbox" tabindex="0" class="hidden" name="flat">
<label for="pprof-stacktrace-flat">{{.locale.Tr "admin.monitor.pprof.stacktrace.flat"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input id="pprof-stacktrace-no-system" type="checkbox" tabindex="0" class="hidden" name="no-system">
<label for="pprof-stacktrace-no-system">{{.locale.Tr "admin.monitor.pprof.stacktrace.no-system"}}</label>
</div>
</div>
<div class="field">
<label for="pprof-stacktrace-format">{{.locale.Tr "admin.monitor.pprof.stacktrace.format"}}</label>
<select class="ui search dropdown" id="pprof-stacktrace-format" name="format">
<option value="text" selected>text</option>
<option value="json">json</option>
</select>
</div>
<input class="ui button basic" type="submit" value="{{.locale.Tr "admin.monitor.pprof.download"}}">
</div>
</form>
<h4>{{.locale.Tr "admin.monitor.pprof.named_profiles"}}</h4>
<form action="{{AppSubUrl}}/admin/monitor/profile">
<div class="ui form">
<div class="help">{{.locale.Tr "admin.monitor.pprof.named_profiles.description"}}</div>
<div class="field">
<label for="pprof-named_profiles-name">{{.locale.Tr "admin.monitor.pprof.named_profiles.name"}}</label>
<select class="ui search dropdown" id="pprof-named_profiles-name" name="name">
{{range .Profiles}}
<option value="{{.Name}}">{{.Name}}</option>
{{end}}
</select>
</div>
<div class="field">
<label for="pprof-named_profiles-debug">{{$.locale.Tr "admin.monitor.pprof.named_profiles.debug"}}</label>
<select class="ui search dropdown" id="pprof-named_profiles-debug" name="format">
<option value="0" selected>{{.locale.Tr "admin.monitor.pprof.named_profiles.format_pprof"}}</option>
<option value="1">{{.locale.Tr "admin.monitor.pprof.named_profiles.format_text"}}</option>
<option value="2">{{.locale.Tr "admin.monitor.pprof.named_profiles.format_goroutine"}}</option>
</select>
</div>
<input class="ui button basic" type="submit" value="{{$.locale.Tr "admin.monitor.pprof.download"}}">
</div>
</form>
<h4>{{.locale.Tr "admin.monitor.pprof.trace"}}</h4>
<form action="{{AppSubUrl}}/admin/monitor/trace">
<div class="ui form">
<div class="help">{{.locale.Tr "admin.monitor.pprof.trace.description"}}</div>
<div class="field">
<label for="pprof-trace-duration">{{.locale.Tr "admin.monitor.pprof.duration"}}</label>
<input id="pprof-trace-duration" type="text" name="duration" placeholder="{{.locale.Tr "admin.monitor.pprof.duration_placeholder"}}">
</div>
<input class="ui button basic" type="submit" value="{{.locale.Tr "admin.monitor.pprof.download"}}">
</div>
</form>
</div>
11 changes: 11 additions & 0 deletions templates/admin/stacktrace.tmpl
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
{{.locale.Tr "admin.monitor.stacktrace"}}: {{.locale.Tr "admin.monitor.goroutines" .GoroutineCount}}
<div class="ui right">
<a class="ui primary tiny button" href="{{AppSubUrl}}/admin/monitor">{{.locale.Tr "admin.monitor"}}</a>
<a class="ui tiny button" id="download-stacktrace">{{.locale.Tr "admin.monitor.stacktrace.download_stacktrace"}}</a>
</div>
</h4>
<div class="ui attached segment">
@@ -18,6 +19,16 @@
</div>
</div>
</div>
<div class="ui small basic modal" id="download-stacktrace-modal">
<div class="ui icon header">
{{svg "octicon-download" 16 "close inside"}}
{{.locale.Tr "admin.monitor.stacktrace.download_stacktrace"}}
</div>
{{template "base/delete_modal_actions" .}}
<div class="hide" id="stacktrace-to-download">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just:

  1. User click the download button
  2. Download from a HTTP URL directly

?

Then no need that complex dialog nor the JS code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Then they get a different stacktrace.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean , could there be no stacktrace on the UI? Just some links to help to collect problems.

The complex UI doesn't help users. As an end user, they should just download and report the stacktrace file.

The end users could do nothing even if they see the stacktrace on the UI.....

Copy link
Contributor

@wxiaoguang wxiaoguang Feb 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, when I was using GitLab, they have an all-in-one tool. It collects everything into a file, and what I need to do is just using the tool and sending the generated file to them, then they can help to resolve problems.

Making the diagnosis system too complex doesn't benefit end users IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean , could there be no stacktrace on the UI? Just some links to help to collect problems.

I'm going to add a direct download stacktrace profile below to get the stacktraces without seeing them.

The complex UI doesn't help users. As an end user, they should just download and report the stacktrace file.

The pretty stacktrace helps me and SEVERAL bugs have been solved using it. It has helped me a large number of times already.

The end users could do nothing even if they see the stacktrace on the UI.....

Not every user is incapable and whilst the purpose of this UI is to help us to help users we should enable users and developers to help themselves. A pretty UI can be helpful for us to solve issues in a way that the opaque stacktrace format is not.

Copy link
Contributor

@wxiaoguang wxiaoguang Feb 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean , could there be no stacktrace on the UI? Just some links to help to collect problems.

I'm going to add a direct download stacktrace profile below to get the stacktraces without seeing them.

If it means more complex, then it's not necessary IMO. My initial idea is about keeping the system simple but complete and useful.

While I do not think keeping the downloaded stacktrace file exactly the same as the UI list is meaningful -- everytime you refresh the page, you get a different stacktrace ..... Even if they are different, they are all helpful for resolving problem equally, and maybe sometimes the UI shown list is not helpful but the downloaded is helpful in case the downloaded one catches the problem, everything is possible.

Copy link
Contributor Author

@zeripath zeripath Feb 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fundamentally it's really important to be able to download the stacktrace that you have seen. Given these are dynamic it's possible that you may actually only see the issue on the stacktrace that you have and if you try get another the issue will be gone.

We need the download.


FYI, when I was using GitLab, they have an all-in-one tool. It collects everything into a file, and what I need to do is just using the tool and sending the generated file to them, then they can help to resolve problems.

Making the diagnosis system too complex doesn't benefit end users IMO.

If I knew what we generally needed I would do that - but in general we don't need cpu-profiles etc.

We do need a way of reading the logs and I will get to that but ... let's get general routes in and then we can have pared back routes to get the common things. Right now we have no easy way of getting pprof profiles from users and that fundamentally limits our ability to use them for diagnostics. This PR makes that a hell of a lot easier.

{{.StacktraceString}}
</div>
</div>
<div class="ui small basic delete modal">
<div class="ui icon header">
{{svg "octicon-x" 16 "close inside"}}
14 changes: 14 additions & 0 deletions web_src/js/features/admin/common.js
Original file line number Diff line number Diff line change
@@ -215,4 +215,18 @@ export function initAdminCommon() {
});
});
}

$('#download-stacktrace').on('click', () => {
$('#download-stacktrace-modal').modal({
closable: false,
onApprove() {
const textToSave = document.getElementById('stacktrace-to-download').innerText;
const hiddenElement = document.createElement('a');
hiddenElement.href = `data:attachment/text,${encodeURI(textToSave)}`;
hiddenElement.target = '_blank';
hiddenElement.download = 'stacktraces.txt';
hiddenElement.click();
}
}).modal('show');
});
}