// Command stress plays many greedy AI-vs-AI games and reports the DAWG move generator's // speed and memory. It is a benchmark / regression tool for the production generator. package main import ( "flag" "fmt" "log" "os" "runtime" "strconv" "strings" "time" "gitea.iliadenisov.ru/developer/scrabble-solver/internal/dict" "gitea.iliadenisov.ru/developer/scrabble-solver/rules" "gitea.iliadenisov.ru/developer/scrabble-solver/scrabble" "gitea.iliadenisov.ru/developer/scrabble-solver/selfplay" ) func main() { games := flag.Int("games", 100, "games to play") flag.Parse() rs := rules.English() if !dict.EnglishAvailable() { log.Fatal("English dictionary not available: dawg/en_sowpods.dawg missing") } f, err := dict.EnglishDAWG() if err != nil { log.Fatalf("load dawg: %v", err) } gen := scrabble.NewDAWGGenerator(rs, f) structSize := fileSize(dict.DAWGCache()) runtime.GC() var m0 runtime.MemStats runtime.ReadMemStats(&m0) start := time.Now() var turns, plays, movesGen int var genTime time.Duration var score float64 for seed := 1; seed <= *games; seed++ { res := selfplay.PlayGame(rs, gen, scrabble.Both, int64(seed), nil) turns += res.Turns plays += res.Plays movesGen += res.MovesGenerated genTime += res.GenTime score += float64(res.Scores[0] + res.Scores[1]) } wall := time.Since(start) var m1 runtime.MemStats runtime.ReadMemStats(&m1) fmt.Printf("DAWG · English SOWPODS · %d games · board %dx%d · greedy self-play\n\n", *games, rs.Rows, rs.Cols) fmt.Printf(" structure size %s\n", humanBytes(structSize)) fmt.Printf(" turns / plays %d / %d\n", turns, plays) fmt.Printf(" moves generated %d\n", movesGen) fmt.Printf(" generation time %s (%.1f µs/turn)\n", genTime.Round(time.Millisecond), float64(genTime.Microseconds())/float64(turns)) fmt.Printf(" moves generated/sec %.0f\n", float64(movesGen)/genTime.Seconds()) fmt.Printf(" wall time %s\n", wall.Round(time.Millisecond)) fmt.Printf(" heap allocated %s (%d GC cycles)\n", humanBytes(int64(m1.TotalAlloc-m0.TotalAlloc)), m1.NumGC-m0.NumGC) fmt.Printf(" avg final game score %.1f\n", score/float64(*games)) fmt.Printf(" peak process RSS %s\n", humanKB(peakRSS())) } func fileSize(p string) int64 { if fi, err := os.Stat(p); err == nil { return fi.Size() } return 0 } func peakRSS() int64 { data, err := os.ReadFile("/proc/self/status") if err != nil { return 0 } for line := range strings.SplitSeq(string(data), "\n") { if rest, ok := strings.CutPrefix(line, "VmHWM:"); ok { if f := strings.Fields(rest); len(f) > 0 { kb, _ := strconv.ParseInt(f[0], 10, 64) return kb } } } return 0 } func humanBytes(n int64) string { switch { case n >= 1<<20: return fmt.Sprintf("%.2f MB", float64(n)/(1<<20)) case n >= 1<<10: return fmt.Sprintf("%.1f KB", float64(n)/(1<<10)) default: return fmt.Sprintf("%d B", n) } } func humanKB(kb int64) string { return humanBytes(kb * 1024) }