Compare commits
No commits in common. "5e38b63f10337e94124303737477894b8e9fabb9" and "1128eb644fd46901c700491cfa5e843f3c3dec77" have entirely different histories.
5e38b63f10
...
1128eb644f
@ -1,34 +0,0 @@
|
||||
servers:
|
||||
- listen: ":18080"
|
||||
static:
|
||||
- path: "/"
|
||||
root: "./testdata"
|
||||
index:
|
||||
- "index.html"
|
||||
proxy:
|
||||
- path: "/api"
|
||||
targets:
|
||||
- url: "http://127.0.0.1:18081"
|
||||
|
||||
monitoring:
|
||||
pprof:
|
||||
enabled: true
|
||||
path: "/debug/pprof"
|
||||
allow:
|
||||
- "127.0.0.1"
|
||||
|
||||
performance:
|
||||
file_cache:
|
||||
max_entries: 1000
|
||||
max_size: 104857600
|
||||
inactive: 60s
|
||||
|
||||
logging:
|
||||
format: "text"
|
||||
access:
|
||||
path: ""
|
||||
format: "json"
|
||||
sample_rate: 0.1
|
||||
error:
|
||||
path: ""
|
||||
level: "warn"
|
||||
@ -174,71 +174,13 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. 优化实施结果
|
||||
## 7. 原始数据文件
|
||||
|
||||
### Task A: 访问日志采样 (accesslog)
|
||||
|
||||
**实现**:
|
||||
- 新增 `logging.access.sample_rate` 配置(0.0~1.0)
|
||||
- 5xx 服务器错误始终记录,2xx/3xx/4xx 按采样率记录
|
||||
- 使用原子计数器实现无锁、零分配采样
|
||||
|
||||
**验证** (wrk 4 线程 × 200 连接,静态文件):
|
||||
- 未优化: `26,474 ns/op` latency, `13,398 B/op`
|
||||
- 采样 10%: `18,734 ns/op` latency, `4,631 B/op`
|
||||
- **收益: -29% latency, -65% allocations/op**
|
||||
|
||||
### Task B: 静态文件缓存优化 (handler)
|
||||
|
||||
**实现**:
|
||||
- `router.go` 始终启用 `FileInfoCache`,TTL 默认 2s
|
||||
- `FileInfoCache` 支持负缓存(缓存不存在的文件,避免重复 `os.Stat`)
|
||||
- 修复 `handleStandard` / `handleTryFiles` 中索引文件的 `fileCache` 查找缺失
|
||||
- 新增 `tryServeFromFileCache()` 辅助函数统一缓存命中逻辑
|
||||
|
||||
**验证** (wrk 4 线程 × 200 连接,`/` → `testdata/index.html`):
|
||||
- 未启用 fileCache: `~140k req/sec`, `~2.6GB alloc_space`
|
||||
- 启用并修复索引文件缓存后: `~242k req/sec`, `~4.6MB alloc_space`
|
||||
- **收益: +73% throughput, -99.8% alloc_space**
|
||||
|
||||
### Task C: RemoteAddr 字符串缓存 (netutil/logging/variable)
|
||||
|
||||
**实现**:
|
||||
- 新增 `netutil.FormatRemoteAddr()`,优先使用 `ctx.RemoteIP()`
|
||||
- IPv4 走零分配快速路径(手写 uint8 → ASCII)
|
||||
- IPv6 回退到 `addr.String()`,使用 1024 条目 LRU 缓存
|
||||
- `logging.LogAccess` 和 `variable.$remote_addr/$remote_port` 统一使用
|
||||
|
||||
**效果**:
|
||||
- 消除了 `net.JoinHostPort` 和 `net.IP.String` 在访问日志热路径的分配
|
||||
- 配合访问日志采样后,`LogAccess` 相关分配从 top 10 中消失
|
||||
|
||||
### 综合对比
|
||||
|
||||
| 指标 | 优化前 | 优化后 | 变化 |
|
||||
|------|--------|--------|------|
|
||||
| 静态文件 RPS | ~140k | **~242k** | **+73%** |
|
||||
| 静态文件 allocs | ~2.6 GB | **~4.6 MB** | **-99.8%** |
|
||||
| 访问日志 latency | 26.5 μs | 18.7 μs | -29% |
|
||||
| 访问日志 allocs | 13.4 KB/op | 4.6 KB/op | -65% |
|
||||
| CPU 热点 LogAccess | 16.36% cum | 未进入 top 10 | 消除 |
|
||||
| 内存热点 os.statNolog | 74.95% flat | 未出现 | 消除 |
|
||||
|
||||
## 8. 剩余优化机会
|
||||
|
||||
- **bufio.Reader/Writer 池化**: heap 中仍占主导,代理路径可优化
|
||||
- **连接池调优**: `net.Dialer.DialContext` 在代理路径仍有分配
|
||||
- **系统调用基线**: syscall 仍占 60%+ CPU,io_uring 可进一步挖掘
|
||||
|
||||
## 9. 原始数据文件
|
||||
|
||||
- `benchmarks/v0.4.0/pprof/v2/cpu-final.prof` — 优化后 CPU profile
|
||||
- `benchmarks/v0.4.0/pprof/v2/allocs-final.prof` — 优化后分配 profile
|
||||
- `benchmarks/v0.4.0/cpu-top-final.txt` — 优化后 CPU top 函数
|
||||
- `benchmarks/v0.4.0/allocs-top-final.txt` — 优化后分配 top 函数
|
||||
- `benchmarks/v0.4.0/pprof/cpu.prof` — 原始 CPU profile(保留)
|
||||
- `benchmarks/v0.4.0/pprof/allocs.prof` — 原始分配 profile(保留)
|
||||
- `benchmarks/v0.4.0/cpu-top.txt` — 原始 CPU top 函数
|
||||
- `benchmarks/v0.4.0/allocs-top.txt` — 原始分配 top 函数
|
||||
- `benchmarks/v0.4.0/pprof/cpu.prof` — CPU profile
|
||||
- `benchmarks/v0.4.0/pprof/allocs.prof` — 分配 profile
|
||||
- `benchmarks/v0.4.0/pprof/heap.prof` — 堆内存 profile
|
||||
- `benchmarks/v0.4.0/pprof/goroutine.prof` — Goroutine profile
|
||||
- `benchmarks/v0.4.0/cpu-top.txt` — CPU top 函数
|
||||
- `benchmarks/v0.4.0/allocs-top.txt` — 分配 top 函数
|
||||
- `benchmarks/v0.4.0/heap-top.txt` — 堆内存 top 函数
|
||||
- `benchmarks/v0.4.0/summary.txt` — 基准测试汇总
|
||||
|
||||
@ -352,57 +352,6 @@ func (h *StaticHandler) Handle(ctx *fasthttp.RequestCtx) {
|
||||
h.handleStandard(ctx, reqPath)
|
||||
}
|
||||
|
||||
// tryServeFromFileCache 尝试从文件缓存直接响应。
|
||||
// 命中缓存且文件未修改时直接写入响应并返回 true。
|
||||
func (h *StaticHandler) tryServeFromFileCache(ctx *fasthttp.RequestCtx, filePath string, info os.FileInfo) bool {
|
||||
if h.fileCache == nil {
|
||||
return false
|
||||
}
|
||||
entry, ok := h.fileCache.Get(filePath)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// TTL 验证(cacheTTL > 0 时启用)
|
||||
if h.cacheTTL > 0 && time.Since(entry.CachedAt) < h.cacheTTL {
|
||||
if isNotModified(ctx, entry.ETag, info.ModTime()) {
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
ctx.Response.SkipBody = true
|
||||
return true
|
||||
}
|
||||
ctx.Response.SetBody(entry.Data)
|
||||
ctx.Response.Header.SetContentType(entry.ContentType)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
return true
|
||||
}
|
||||
|
||||
// TTL 过期或未启用 TTL,验证文件新鲜度
|
||||
if entry.ModTime.Equal(info.ModTime()) {
|
||||
if isNotModified(ctx, entry.ETag, info.ModTime()) {
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
ctx.Response.SkipBody = true
|
||||
return true
|
||||
}
|
||||
if h.cacheTTL > 0 {
|
||||
h.fileCache.RefreshCachedAt(filePath)
|
||||
}
|
||||
ctx.Response.SetBody(entry.Data)
|
||||
ctx.Response.Header.SetContentType(entry.ContentType)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
return true
|
||||
}
|
||||
|
||||
// 文件已修改,删除缓存
|
||||
h.fileCache.Delete(filePath)
|
||||
return false
|
||||
}
|
||||
|
||||
// handleTryFiles 处理 try_files 逻辑。
|
||||
//
|
||||
// 按顺序尝试查找文件,支持 $uri 和 $uri/ 占位符。
|
||||
@ -440,9 +389,6 @@ func (h *StaticHandler) handleTryFiles(ctx *fasthttp.RequestCtx, reqPath string)
|
||||
continue
|
||||
}
|
||||
if !idxInfo.IsDir() {
|
||||
if h.tryServeFromFileCache(ctx, idxPath, idxInfo) {
|
||||
return
|
||||
}
|
||||
h.serveFile(ctx, idxPath, idxInfo, false)
|
||||
return
|
||||
}
|
||||
@ -586,9 +532,6 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
|
||||
idxPath := filepath.Join(filePath, idx)
|
||||
idxInfo, idxExists, _ := h.statWithCache(idxPath)
|
||||
if idxExists && !idxInfo.IsDir() {
|
||||
if h.tryServeFromFileCache(ctx, idxPath, idxInfo) {
|
||||
return
|
||||
}
|
||||
h.serveFile(ctx, idxPath, idxInfo, true)
|
||||
return
|
||||
}
|
||||
@ -608,9 +551,52 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string)
|
||||
return
|
||||
}
|
||||
|
||||
// Phase 2: 缓存查找 + TTL 验证,减少 os.ReadFile 调用
|
||||
if h.tryServeFromFileCache(ctx, filePath, info) {
|
||||
return
|
||||
// Phase 2: 缓存查找 + TTL 验证 // 在 serveFile 调用前检查缓存,减少 os.ReadFile 调用
|
||||
// 注意: CachedAt 迁移已在 FileCache.Get() 内部完成,确保并发安全
|
||||
if h.fileCache != nil {
|
||||
if entry, ok := h.fileCache.Get(filePath); ok {
|
||||
// TTL 验证(cacheTTL > 0 时启用)
|
||||
if h.cacheTTL > 0 && time.Since(entry.CachedAt) < h.cacheTTL {
|
||||
// TTL 内直接返回(无需验证 ModTime)
|
||||
// 使用缓存的 ETag,避免重新生成
|
||||
if isNotModified(ctx, entry.ETag, info.ModTime()) {
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
ctx.Response.SkipBody = true
|
||||
return
|
||||
}
|
||||
ctx.Response.SetBody(entry.Data)
|
||||
ctx.Response.Header.SetContentType(entry.ContentType)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
return
|
||||
}
|
||||
|
||||
// TTL 过期或未启用 TTL,验证文件新鲜度
|
||||
if entry.ModTime.Equal(info.ModTime()) {
|
||||
// 文件未修改,刷新 TTL 并返回
|
||||
// 使用缓存的 ETag,避免重新生成
|
||||
if isNotModified(ctx, entry.ETag, info.ModTime()) {
|
||||
ctx.Response.SetStatusCode(fasthttp.StatusNotModified)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
ctx.Response.SkipBody = true
|
||||
return
|
||||
}
|
||||
if h.cacheTTL > 0 {
|
||||
h.fileCache.RefreshCachedAt(filePath)
|
||||
}
|
||||
ctx.Response.SetBody(entry.Data)
|
||||
ctx.Response.Header.SetContentType(entry.ContentType)
|
||||
ctx.Response.Header.Set("ETag", entry.ETag)
|
||||
ctx.Response.Header.Set("Last-Modified", info.ModTime().UTC().Format(httpTimeFormat))
|
||||
return
|
||||
}
|
||||
|
||||
// 文件已修改,删除缓存继续处理
|
||||
h.fileCache.Delete(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: 缓存未命中,调用 serveFile 处理
|
||||
|
||||
@ -27,7 +27,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/valyala/fasthttp"
|
||||
"rua.plus/lolly/internal/config"
|
||||
"rua.plus/lolly/internal/netutil"
|
||||
"rua.plus/lolly/internal/variable"
|
||||
)
|
||||
|
||||
@ -143,7 +142,7 @@ func (l *Logger) LogAccess(ctx *fasthttp.RequestCtx, status int, size int64, dur
|
||||
// JSON 格式或空格式:输出结构化 JSON
|
||||
if l.accessFormat == formatJSON || l.accessFormat == "" {
|
||||
l.accessLog.Info().
|
||||
Str("remote_addr", netutil.FormatRemoteAddr(ctx)).
|
||||
Str("remote_addr", ctx.RemoteAddr().String()).
|
||||
Bytes("request", append(append(ctx.Method(), ' '), ctx.Path()...)).
|
||||
Int("status", status).
|
||||
Int64("body_bytes_sent", size).
|
||||
|
||||
@ -50,14 +50,12 @@ type AccessLog struct {
|
||||
// - *AccessLog: 访问日志中间件实例
|
||||
func New(cfg *config.LoggingConfig) *AccessLog {
|
||||
sampleRate := cfg.Access.SampleRate
|
||||
// sampleRate=0 明确表示禁用访问日志
|
||||
// sampleRate<0 或 >1 修正为 1.0(全量记录)
|
||||
if sampleRate < 0.0 || sampleRate > 1.0 {
|
||||
if sampleRate <= 0.0 || sampleRate > 1.0 {
|
||||
sampleRate = 1.0
|
||||
}
|
||||
|
||||
var sampleInterval uint64 = 1
|
||||
if sampleRate > 0.0 && sampleRate < 1.0 {
|
||||
if sampleRate < 1.0 {
|
||||
// 使用 1000 作为基数以提高精度,例如 0.123 -> 间隔约 8
|
||||
sampleInterval = uint64((1.0 / sampleRate) + 0.5)
|
||||
if sampleInterval < 1 {
|
||||
@ -83,21 +81,17 @@ func (a *AccessLog) Name() string {
|
||||
// shouldLog 判断当前请求是否应记录访问日志。
|
||||
//
|
||||
// 规则:
|
||||
// - 5xx 服务器错误始终记录(便于排查错误)
|
||||
// - sampleRate=0 时不记录 2xx/3xx/4xx
|
||||
// - 采样率为 1.0 时始终记录
|
||||
// - 其他情况按 sampleRate 采样
|
||||
// - 非 2xx 响应始终记录(便于排查错误)
|
||||
// - 2xx 响应按采样率决定是否记录
|
||||
//
|
||||
// 使用原子计数器实现无锁、零分配采样。
|
||||
func (a *AccessLog) shouldLog(status int) bool {
|
||||
// 5xx 服务器错误始终记录
|
||||
if status >= 500 {
|
||||
if a.sampleRate >= 1.0 {
|
||||
return true
|
||||
}
|
||||
if a.sampleRate == 0.0 {
|
||||
return false
|
||||
}
|
||||
if a.sampleRate >= 1.0 {
|
||||
// 非成功响应始终记录
|
||||
if status < 200 || status >= 300 {
|
||||
return true
|
||||
}
|
||||
// 确定性采样:每 sampleInterval 个请求记录一个
|
||||
|
||||
@ -91,24 +91,17 @@ func TestAccessLog_SampleRateAlwaysRecordErrors(t *testing.T) {
|
||||
al := New(&config.LoggingConfig{
|
||||
Access: config.AccessLogConfig{
|
||||
Format: "json",
|
||||
SampleRate: 0.0, // 理论上不采样成功请求,但 5xx 始终记录
|
||||
SampleRate: 0.0, // 理论上不采样成功请求,但错误始终记录
|
||||
},
|
||||
})
|
||||
|
||||
// 5xx 请求应始终记录
|
||||
for _, status := range []int{500, 502, 503, 504} {
|
||||
// 非 2xx 请求应始终记录
|
||||
for _, status := range []int{199, 300, 400, 500} {
|
||||
if !al.shouldLog(status) {
|
||||
t.Errorf("status %d should always be logged regardless of sample rate", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 2xx/3xx/4xx 请求按采样率(0% 不记录)
|
||||
for _, status := range []int{200, 301, 404} {
|
||||
if al.shouldLog(status) {
|
||||
t.Errorf("status %d should not be logged with sample_rate=0", status)
|
||||
}
|
||||
}
|
||||
|
||||
_ = al.Close()
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ package netutil
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
@ -111,87 +110,3 @@ func GetRemoteAddrIP(ctx *fasthttp.RequestCtx) net.IP {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// remoteAddrCache 缓存 RemoteAddr 字符串化结果,避免重复的 net.TCPAddr.String() 分配。
|
||||
type remoteAddrCache struct {
|
||||
mu sync.RWMutex
|
||||
entries map[string]string
|
||||
maxSize int
|
||||
}
|
||||
|
||||
var globalRemoteAddrCache = &remoteAddrCache{
|
||||
entries: make(map[string]string, 1024),
|
||||
maxSize: 1024,
|
||||
}
|
||||
|
||||
// FormatRemoteAddr 使用缓存格式化 RemoteAddr,避免重复的地址字符串分配。
|
||||
// 优先使用 ctx.RemoteIP() 获取 IP,对 IPv4 直接零分配格式化,IPv6 回退到 addr.String()。
|
||||
func FormatRemoteAddr(ctx *fasthttp.RequestCtx) string {
|
||||
ip := ctx.RemoteIP()
|
||||
if ip == nil {
|
||||
addr := ctx.RemoteAddr()
|
||||
if addr == nil {
|
||||
return "-"
|
||||
}
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
// 优先尝试 IPv4 快速路径(零分配)
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return formatIPv4(ipv4)
|
||||
}
|
||||
|
||||
// IPv6:尝试缓存
|
||||
ipStr := ip.String()
|
||||
globalRemoteAddrCache.mu.RLock()
|
||||
if cached, ok := globalRemoteAddrCache.entries[ipStr]; ok {
|
||||
globalRemoteAddrCache.mu.RUnlock()
|
||||
return cached
|
||||
}
|
||||
globalRemoteAddrCache.mu.RUnlock()
|
||||
|
||||
// 未命中缓存,回退到 addr.String()
|
||||
addr := ctx.RemoteAddr()
|
||||
if addr == nil {
|
||||
return "-"
|
||||
}
|
||||
result := addr.String()
|
||||
|
||||
globalRemoteAddrCache.mu.Lock()
|
||||
if len(globalRemoteAddrCache.entries) < globalRemoteAddrCache.maxSize {
|
||||
globalRemoteAddrCache.entries[ipStr] = result
|
||||
}
|
||||
globalRemoteAddrCache.mu.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
// formatIPv4 将 4 字节 IPv4 地址格式化为字符串(零分配)。
|
||||
func formatIPv4(ip net.IP) string {
|
||||
var buf [15]byte
|
||||
n := 0
|
||||
for i := 0; i < 4; i++ {
|
||||
if i > 0 {
|
||||
buf[n] = '.'
|
||||
n++
|
||||
}
|
||||
n += writeUint8(buf[n:], ip[i])
|
||||
}
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// writeUint8 将 uint8 写入 buf,返回写入的字节数。
|
||||
func writeUint8(buf []byte, v byte) int {
|
||||
if v >= 100 {
|
||||
buf[0] = byte('0' + v/100)
|
||||
buf[1] = byte('0' + (v/10)%10)
|
||||
buf[2] = byte('0' + v%10)
|
||||
return 3
|
||||
}
|
||||
if v >= 10 {
|
||||
buf[0] = byte('0' + v/10)
|
||||
buf[1] = byte('0' + v%10)
|
||||
return 2
|
||||
}
|
||||
buf[0] = byte('0' + v)
|
||||
return 1
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/valyala/fasthttp"
|
||||
"rua.plus/lolly/internal/netutil"
|
||||
)
|
||||
|
||||
// 内置变量常量
|
||||
@ -82,7 +81,11 @@ func init() {
|
||||
Name: VarRemoteAddr,
|
||||
Description: "客户端 IP 地址",
|
||||
Getter: func(ctx *fasthttp.RequestCtx) string {
|
||||
return netutil.FormatRemoteAddr(ctx)
|
||||
addr := ctx.RemoteAddr()
|
||||
if addr == nil {
|
||||
return "-"
|
||||
}
|
||||
return addr.String()
|
||||
},
|
||||
})
|
||||
|
||||
@ -95,8 +98,8 @@ func init() {
|
||||
if addr == nil {
|
||||
return "-"
|
||||
}
|
||||
// 解析地址获取端口,优先用 FormatRemoteAddr 缓存的结果
|
||||
s := netutil.FormatRemoteAddr(ctx)
|
||||
// 解析地址获取端口
|
||||
s := addr.String()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == ':' {
|
||||
return s[i+1:]
|
||||
|
||||
@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# bench-compare.sh — 比较两次基准测试结果,检测性能回归。
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/bench-compare.sh <old-summary> <new-summary>
|
||||
#
|
||||
# 返回码:
|
||||
# 0 — 无显著回归
|
||||
# 1 — 检测到显著回归(默认阈值: latency +10%, allocs +10%)
|
||||
#
|
||||
# 示例:
|
||||
# ./scripts/bench-compare.sh benchmarks/v0.4.0/summary.txt benchmarks/v0.5.0/summary.txt
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OLD="${1:-}"
|
||||
NEW="${2:-}"
|
||||
|
||||
if [[ -z "$OLD" || -z "$NEW" ]]; then
|
||||
echo "用法: $0 <old-summary> <new-summary>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$OLD" ]]; then
|
||||
echo "错误: 找不到旧摘要文件: $OLD" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$NEW" ]]; then
|
||||
echo "错误: 找不到新摘要文件: $NEW" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
LATENCY_THRESH="${LATENCY_THRESH:-10.0}"
|
||||
ALLOCS_THRESH="${ALLOCS_THRESH:-10.0}"
|
||||
RPS_THRESH="${RPS_THRESH:--10.0}"
|
||||
|
||||
echo "=== 基准比较 ==="
|
||||
printf "%-40s %12s %12s %12s\n" "Benchmark" "Old" "New" "Change%"
|
||||
echo "----------------------------------------------------------------------"
|
||||
|
||||
REGRESSION=0
|
||||
|
||||
# 提取并比较关键微基准(ns/op 和 B/op)
|
||||
# 格式: BenchmarkName-NN 1234 ns/op 567 B/op 890 allocs/op
|
||||
compare_metric() {
|
||||
local bench="$1"
|
||||
local metric="$2"
|
||||
local thresh="$3"
|
||||
local better_is_lower="${4:-1}"
|
||||
|
||||
local old_val new_val
|
||||
old_val=$(grep -E "^${bench}" "$OLD" | grep -oE "[0-9]+(\.[0-9]+)?[[:space:]]*${metric}" | head -1 | awk '{print $1}')
|
||||
new_val=$(grep -E "^${bench}" "$NEW" | grep -oE "[0-9]+(\.[0-9]+)?[[:space:]]*${metric}" | head -1 | awk '{print $1}')
|
||||
|
||||
if [[ -z "$old_val" || -z "$new_val" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if awk -v o="$old_val" 'BEGIN { exit (o == 0) ? 0 : 1 }'; then
|
||||
printf "%-40s %12s %12s %11s%%\n" "$bench ($metric)" "$old_val" "$new_val" "N/A"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local change
|
||||
change=$(awk -v o="$old_val" -v n="$new_val" 'BEGIN { printf "%.2f", ((n - o) / o) * 100 }')
|
||||
local abs_change
|
||||
abs_change=$(awk -v c="$change" 'BEGIN { printf "%.2f", c < 0 ? -c : c }')
|
||||
|
||||
printf "%-40s %12s %12s %11s%%\n" "$bench ($metric)" "$old_val" "$new_val" "$change"
|
||||
|
||||
if awk -v c="$abs_change" -v t="$thresh" 'BEGIN { exit (c > t) ? 0 : 1 }'; then
|
||||
if [[ "$better_is_lower" == "1" && $(awk -v c="$change" 'BEGIN { print (c > 0) ? 1 : 0 }') -eq 1 ]]; then
|
||||
echo " ⚠️ 回归警告: $bench $metric 增加 ${change}% (阈值 ${thresh}%)" >&2
|
||||
REGRESSION=1
|
||||
elif [[ "$better_is_lower" == "0" && $(awk -v c="$change" 'BEGIN { print (c < 0) ? 1 : 0 }') -eq 1 ]]; then
|
||||
echo " ⚠️ 回归警告: $bench $metric 降低 ${change}% (阈值 ${thresh}%)" >&2
|
||||
REGRESSION=1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 关键基准测试前缀列表(前缀匹配)
|
||||
BENCHMARKS=(
|
||||
"BenchmarkAccessLogProcess"
|
||||
"BenchmarkFileCacheGet"
|
||||
"BenchmarkProxyCacheGet"
|
||||
"BenchmarkStaticFile"
|
||||
"BenchmarkStaticIndex"
|
||||
"BenchmarkStaticTryFiles"
|
||||
"BenchmarkProxyForward"
|
||||
"BenchmarkProxyHostClient"
|
||||
"BenchmarkProxyWithMockBackend"
|
||||
"BenchmarkMiddlewareProcessChain"
|
||||
"BenchmarkMiddlewareChainExecution"
|
||||
"BenchmarkCompressionMiddleware"
|
||||
"BenchmarkDNSResolverLookupWithCache"
|
||||
)
|
||||
|
||||
for bench in "${BENCHMARKS[@]}"; do
|
||||
compare_metric "$bench" "ns/op" "$LATENCY_THRESH" 1
|
||||
compare_metric "$bench" "B/op" "$ALLOCS_THRESH" 1
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [[ "$REGRESSION" -eq 0 ]]; then
|
||||
echo "✅ 未检测到显著性能回归"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ 检测到性能回归,请检查上述警告"
|
||||
exit 1
|
||||
fi
|
||||
Loading…
x
Reference in New Issue
Block a user