Skip to content
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

WIP: Support performance trace #32973

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 15 additions & 5 deletions models/db/engine_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,33 @@ import (
"context"
"time"

"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"

"xorm.io/xorm/contexts"
)

type SlowQueryHook struct {
type EngineHook struct {
Threshold time.Duration
Logger log.Logger
}

var _ contexts.Hook = (*SlowQueryHook)(nil)
var _ contexts.Hook = (*EngineHook)(nil)

func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
return ctx, nil
}

func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
span := gtprof.GetContextSpan(c.Ctx)
if span != nil {
span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL)
span.End()
} else {
setting.PanicInDevOrTesting("span in database engine hook is nil")
}
if c.ExecuteTime >= h.Threshold {
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
// is being displayed (the function that ultimately wants to execute the query in the code)
Expand Down
2 changes: 1 addition & 1 deletion models/db/engine_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error {
xe.SetDefaultContext(ctx)

if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&SlowQueryHook{
xe.AddHook(&EngineHook{
Threshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"),
})
Expand Down
12 changes: 9 additions & 3 deletions modules/git/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -54,7 +55,7 @@ func logArgSanitize(arg string) string {
} else if filepath.IsAbs(arg) {
base := filepath.Base(arg)
dir := filepath.Dir(arg)
return filepath.Join(filepath.Base(dir), base)
return ".../" + filepath.Join(filepath.Base(dir), base)
}
return arg
}
Expand Down Expand Up @@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error {
timeout = defaultCommandExecutionTimeout
}

var desc string
cmdLogString := c.LogString()
callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
callerInfo = callerInfo[pos+1:]
}
// these logs are for debugging purposes only, so no guarantee of correctness or stability
desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString())
desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString)
log.Debug("git.Command: %s", desc)

_, span := gtprof.GetTracer().Start(c.parentContext, gtprof.TraceSpanGitRun)
defer span.End()
span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo)
span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString)

var ctx context.Context
var cancel context.CancelFunc
var finished context.CancelFunc
Expand Down
2 changes: 1 addition & 1 deletion modules/git/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ func TestCommandString(t *testing.T) {
assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString())

cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b")
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString())
assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString())
}
119 changes: 119 additions & 0 deletions modules/gtprof/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gtprof

import (
"context"
"fmt"
"sync"
"time"

"code.gitea.io/gitea/modules/util"
)

type contextKey struct {
name string
}

var ContextKeySpan = &contextKey{"span"}

type traceStarter interface {
start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal)
}

type traceSpanInternal interface {
end()
}

type TraceSpan struct {
// immutable
parent *TraceSpan
internalSpans []traceSpanInternal

// mutable, must be protected by mutex
mu sync.RWMutex
name string
startTime time.Time
endTime time.Time
attributes []TraceAttribute
children []*TraceSpan
}

type TraceAttribute struct {
Key string
Value TraceValue
}

type TraceValue struct {
v any
}

func (t *TraceValue) AsString() string {
return fmt.Sprint(t.v)
}

func (t *TraceValue) AsInt64() int64 {
v, _ := util.ToInt64(t.v)
return v
}

func (t *TraceValue) AsFloat64() float64 {
v, _ := util.ToFloat64(t.v)
return v
}

var globalTraceStarters []traceStarter

type Tracer struct {
starters []traceStarter
}

func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan {
s.mu.Lock()
defer s.mu.Unlock()

s.attributes = append(s.attributes, TraceAttribute{Key: key, Value: TraceValue{v: value}})
return s
}

func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) {
starters := t.starters
if starters == nil {
starters = globalTraceStarters
}
ts := &TraceSpan{name: spanName, startTime: time.Now()}
existingCtxSpan, _ := ctx.Value(ContextKeySpan).(*TraceSpan)
if existingCtxSpan != nil {
existingCtxSpan.mu.Lock()
existingCtxSpan.children = append(existingCtxSpan.children, ts)
existingCtxSpan.mu.Unlock()
ts.parent = existingCtxSpan
}
for internalSpanIdx, tsp := range starters {
var internalSpan traceSpanInternal
ctx, internalSpan = tsp.start(ctx, ts, internalSpanIdx)
ts.internalSpans = append(ts.internalSpans, internalSpan)
}
ctx = context.WithValue(ctx, ContextKeySpan, ts)
return ctx, ts
}

func (s *TraceSpan) End() {
s.mu.Lock()
s.endTime = time.Now()
s.mu.Unlock()

for _, tsp := range s.internalSpans {
tsp.end()
}
}

func GetTracer() *Tracer {
return &Tracer{}
}

func GetContextSpan(ctx context.Context) *TraceSpan {
ts, _ := ctx.Value(ContextKeySpan).(*TraceSpan)
return ts
}
66 changes: 66 additions & 0 deletions modules/gtprof/trace_builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gtprof

import (
"context"
"fmt"
"strings"
"time"

"code.gitea.io/gitea/modules/tailmsg"
)

type traceBuiltinStarter struct{}

type traceBuiltinSpan struct {
ts *TraceSpan

internalSpanIdx int
}

func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) {
t.ts.mu.RLock()
defer t.ts.mu.RUnlock()

out.WriteString(strings.Repeat(" ", indent))
out.WriteString(t.ts.name)
if t.ts.endTime.IsZero() {
out.WriteString(" duration: (not ended)")
} else {
out.WriteString(fmt.Sprintf(" duration: %.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds()))
}
out.WriteString("\n")
for _, a := range t.ts.attributes {
out.WriteString(strings.Repeat(" ", indent+2))
out.WriteString(a.Key)
out.WriteString(": ")
out.WriteString(a.Value.AsString())
out.WriteString("\n")
}
for _, c := range t.ts.children {
span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
span.toString(out, indent+2)
}
}

func (t *traceBuiltinSpan) end() {
if t.ts.parent == nil {
// FIXME: debug purpose only
// FIXME: it should distinguish between http response network lag and actual processing time
if len(t.ts.children) > 3 || t.ts.endTime.Sub(t.ts.startTime) > 100*time.Millisecond {
sb := &strings.Builder{}
t.toString(sb, 0)
tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
}
}
}

func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx}
}

func init() {
globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
}
19 changes: 19 additions & 0 deletions modules/gtprof/trace_const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gtprof

// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv

const (
TraceSpanHTTP = "http"
TraceSpanGitRun = "git-run"
TraceSpanDatabase = "database"
)

const (
TraceAttrFuncCaller = "func.caller"
TraceAttrDbSQL = "db.sql"
TraceAttrGitCommand = "git.command"
TraceAttrHTTPRoute = "http.route"
)
73 changes: 73 additions & 0 deletions modules/tailmsg/taillog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package tailmsg

import (
"sync"
"time"
)

type MsgRecord struct {
Time time.Time
Content string
}

type MsgRecorder interface {
Record(content string)
GetRecords() []*MsgRecord
}

type memoryMsgRecorder struct {
mu sync.RWMutex
msgs []*MsgRecord
limit int
}

// TODO: use redis for a clustered environment

func (m *memoryMsgRecorder) Record(content string) {
m.mu.Lock()
defer m.mu.Unlock()
m.msgs = append(m.msgs, &MsgRecord{
Time: time.Now(),
Content: content,
})
if len(m.msgs) > m.limit {
m.msgs = m.msgs[len(m.msgs)-m.limit:]
}
}

func (m *memoryMsgRecorder) GetRecords() []*MsgRecord {
m.mu.RLock()
defer m.mu.RUnlock()
ret := make([]*MsgRecord, len(m.msgs))
copy(ret, m.msgs)
return ret
}

func NewMsgRecorder(limit int) MsgRecorder {
return &memoryMsgRecorder{
limit: limit,
}
}

type Manager struct {
traceRecorder MsgRecorder
logRecorder MsgRecorder
}

func (m *Manager) GetTraceRecorder() MsgRecorder {
return m.traceRecorder
}

func (m *Manager) GetLogRecorder() MsgRecorder {
return m.logRecorder
}

var GetManager = sync.OnceValue(func() *Manager {
return &Manager{
traceRecorder: NewMsgRecorder(100),
logRecorder: NewMsgRecorder(1000),
}
})
3 changes: 2 additions & 1 deletion options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3367,6 +3367,8 @@ monitor.previous = Previous Time
monitor.execute_times = Executions
monitor.process = Running Processes
monitor.stacktrace = Stacktrace
monitor.trace = Trace
monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report
monitor.desc = Description
Expand All @@ -3375,7 +3377,6 @@ monitor.execute_time = Execution Time
monitor.last_execution_result = Result
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.queues = Queues
Expand Down
Loading
Loading