|
|
@@ -0,0 +1,86 @@
|
|
|
+package stage2
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "gorm.io/gorm/logger"
|
|
|
+)
|
|
|
+
|
|
|
+// 本文件是**双跑的代价计数**:本批一共打了多少次库、跑了多久。
|
|
|
+//
|
|
|
+// 为什么要明确统计而不是"感觉还行":
|
|
|
+// 活动域快照把每个 case 的库往返从 ~10 次抬到 ~22 次(六表 × 两侧)。
|
|
|
+// 如果只在"跑不动了"的时候才发现,那就分不清下面两种完全不同的情况:
|
|
|
+//
|
|
|
+// ① 读得对、只是慢 → 可以晚点优化(先让正确性成立)
|
|
|
+// ② 读错了、靠重试掩盖 → 必须立刻修,优化只会把错误藏得更深
|
|
|
+//
|
|
|
+// 所以把"总查询次数"与"总耗时"一起打进报告:它们是可以横向比较的数,
|
|
|
+// 加一条 case、改一个读取器,两个数都会动,动了就说明这次改动有代价。
|
|
|
+
|
|
|
+// Stats 是一次运行的代价计数。零值可用。
|
|
|
+type Stats struct {
|
|
|
+ mu sync.Mutex
|
|
|
+ queries int64
|
|
|
+ started time.Time
|
|
|
+ perCaseQ int64 // 当前 case 起始时的 queries 值,用来算"每 case 多少次"
|
|
|
+}
|
|
|
+
|
|
|
+var globalStats Stats //nolint:gochecknoglobals // 只在 harness 内使用,进程级计数最简单
|
|
|
+
|
|
|
+// Queries 返回至今累计的 SQL 执行次数(gorm `Trace` 被调用一次即一次)。
|
|
|
+func Queries() int64 {
|
|
|
+ globalStats.mu.Lock()
|
|
|
+ defer globalStats.mu.Unlock()
|
|
|
+ return globalStats.queries
|
|
|
+}
|
|
|
+
|
|
|
+// ResetStats 把计数归零并记下开始时间(每次全量跑开始时调一次)。
|
|
|
+func ResetStats() {
|
|
|
+ globalStats.mu.Lock()
|
|
|
+ defer globalStats.mu.Unlock()
|
|
|
+ globalStats.queries = 0
|
|
|
+ globalStats.started = time.Now()
|
|
|
+ globalStats.perCaseQ = 0
|
|
|
+}
|
|
|
+
|
|
|
+// sprintf 是 fmt.Sprintf 的薄封装(本文件只想 import fmt 一次)。
|
|
|
+func sprintf(format string, a ...any) string { return fmt.Sprintf(format, a...) }
|
|
|
+
|
|
|
+// StatsLine 返回可打印的一行"代价"说明。
|
|
|
+func StatsLine(cases int) string {
|
|
|
+ globalStats.mu.Lock()
|
|
|
+ q, st := globalStats.queries, globalStats.started
|
|
|
+ globalStats.mu.Unlock()
|
|
|
+ elapsed := time.Since(st)
|
|
|
+ per := float64(0)
|
|
|
+ if cases > 0 {
|
|
|
+ per = float64(q) / float64(cases)
|
|
|
+ }
|
|
|
+ return sprintf("代价:SQL 执行 %d 次 | 耗时 %s | 平均 %.1f 次/case | 平均 %s/case",
|
|
|
+ q, elapsed.Round(time.Millisecond), per,
|
|
|
+ (elapsed / time.Duration(maxInt(1, cases))).Round(time.Millisecond))
|
|
|
+}
|
|
|
+
|
|
|
+// countingLogger 是 gorm 的 logger 实现:**静默**,但每次 `Trace` 记一次。
|
|
|
+//
|
|
|
+// 为什么用 logger 而不是 gorm 的 callback:`Trace` 是 gorm v2 里每条 SQL 的必经之路
|
|
|
+// (Query/Row/Exec/Raw 都会走),挂在它上面**不会漏**某类语句;
|
|
|
+// 而 callback 要按 Query/Row/Create/Update/Delete 分别注册,漏一个就少算一类,
|
|
|
+// 那种"少算"在报告上表现为"次数偏低",读的人只会以为"没有额外开销"。
|
|
|
+type countingLogger struct{}
|
|
|
+
|
|
|
+func (countingLogger) LogMode(logger.LogLevel) logger.Interface { return countingLogger{} }
|
|
|
+func (countingLogger) Info(context.Context, string, ...interface{}) {}
|
|
|
+func (countingLogger) Warn(context.Context, string, ...interface{}) {}
|
|
|
+func (countingLogger) Error(context.Context, string, ...interface{}) {}
|
|
|
+
|
|
|
+func (countingLogger) Trace(_ context.Context, _ time.Time, fc func() (string, int64), _ error) {
|
|
|
+ globalStats.mu.Lock()
|
|
|
+ globalStats.queries++
|
|
|
+ globalStats.mu.Unlock()
|
|
|
+ _ = fc // 不执行:执行会真的把 SQL 渲染成字符串,白白变慢
|
|
|
+}
|