github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/performance/sysbench/testdef.go (about) 1 // Copyright 2022 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sysbench 16 17 import ( 18 "bytes" 19 "database/sql" 20 "fmt" 21 "log" 22 "math" 23 "os" 24 "os/exec" 25 "path" 26 "regexp" 27 "strconv" 28 "strings" 29 "testing" 30 31 "github.com/creasty/defaults" 32 "github.com/stretchr/testify/require" 33 yaml "gopkg.in/yaml.v3" 34 35 driver "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils/sql_server_driver" 36 ) 37 38 // TestDef is the top-level definition of tests to run. 39 type TestDef struct { 40 Tests []Script `yaml:"tests"` 41 } 42 43 // Config specifies default sysbench script arguments 44 type Config struct { 45 DbDriver string `yaml:"dbDriver"` 46 Host string `yaml:"host"` 47 Port string `yaml:"port"` 48 User string `yaml:"user"` 49 Password string `yaml:"password"` 50 RandType string `yaml:"randType"` 51 EventCnt int `yaml:"eventCnt"` 52 Time int `yaml:"time"` 53 Seed int `yaml:"seed"` 54 TableSize int `yaml:"tableSize"` 55 Histogram bool `yaml:"histogram"` 56 ScriptDir string `yaml:"scriptDir"` 57 Verbose bool `yaml:"verbose"` 58 Prepared bool `yaml:"prepared"` 59 } 60 61 func (c Config) WithScriptDir(dir string) Config { 62 c.ScriptDir = dir 63 return c 64 } 65 66 func (c Config) WithVerbose(v bool) Config { 67 c.Verbose = v 68 return c 69 } 70 71 func (c Config) WithPrepared(v bool) Config { 72 c.Prepared = v 73 return c 74 } 75 76 func (c Config) AsOpts() []string { 77 var ret []string 78 if c.DbDriver != "" { 79 ret = append(ret, fmt.Sprintf("--db-driver=%s", c.DbDriver)) 80 } 81 if c.Host != "" { 82 ret = append(ret, fmt.Sprintf("--mysql-host=%s", c.Host)) 83 } 84 if c.User != "" { 85 ret = append(ret, fmt.Sprintf("--mysql-user=%s", c.User)) 86 } 87 if c.Port != "" { 88 ret = append(ret, fmt.Sprintf("--mysql-port=%s", c.Port)) 89 } 90 if c.Password != "" { 91 ret = append(ret, fmt.Sprintf("--mysql-password=%s", c.Password)) 92 } 93 if c.RandType != "" { 94 ret = append(ret, fmt.Sprintf("--rand-type=%s", c.RandType)) 95 } 96 if c.EventCnt > 0 { 97 ret = append(ret, fmt.Sprintf("--events=%s", strconv.Itoa(c.EventCnt))) 98 } 99 if c.Time > 0 { 100 ret = append(ret, fmt.Sprintf("--time=%s", strconv.Itoa(c.Time))) 101 } 102 if c.Prepared { 103 ret = append(ret, fmt.Sprint("--db-ps-mode=auto")) 104 } 105 ret = append(ret, 106 fmt.Sprintf("--rand-seed=%s", strconv.Itoa(c.Seed)), 107 fmt.Sprintf("--table-size=%s", strconv.Itoa(c.TableSize)), 108 fmt.Sprintf("--histogram=%s", strconv.FormatBool(c.Histogram))) 109 110 return ret 111 } 112 113 // Script is a single test to run. The Repos and MultiRepos will be created, and 114 // any Servers defined within them will be started. The interactions and 115 // assertions defined in Conns will be run. 116 type Script struct { 117 Name string `yaml:"name"` 118 Repos []driver.TestRepo `yaml:"repos"` 119 Scripts []string `yaml:"scripts"` 120 121 // Skip the entire test with this reason. 122 Skip string `yaml:"skip"` 123 124 Results *Results 125 tmpdir string 126 scriptDir string 127 } 128 129 func (s *Script) UnmarshalYAML(unmarshal func(interface{}) error) error { 130 defaults.Set(s) 131 132 type plain Script 133 if err := unmarshal((*plain)(s)); err != nil { 134 return err 135 } 136 137 return nil 138 } 139 140 func ParseTestsFile(path string) (TestDef, error) { 141 contents, err := os.ReadFile(path) 142 if err != nil { 143 return TestDef{}, err 144 } 145 dec := yaml.NewDecoder(bytes.NewReader(contents)) 146 dec.KnownFields(true) 147 var res TestDef 148 err = dec.Decode(&res) 149 return res, err 150 } 151 152 func ParseConfig(path string) (Config, error) { 153 contents, err := os.ReadFile(path) 154 if err != nil { 155 return Config{}, err 156 } 157 dec := yaml.NewDecoder(bytes.NewReader(contents)) 158 dec.KnownFields(true) 159 var res Config 160 err = dec.Decode(&res) 161 return res, err 162 } 163 164 func MakeRepo(rs driver.RepoStore, r driver.TestRepo) (driver.Repo, error) { 165 repo, err := rs.MakeRepo(r.Name) 166 if err != nil { 167 return driver.Repo{}, err 168 } 169 return repo, nil 170 } 171 172 func MakeServer(dc driver.DoltCmdable, s *driver.Server) (*driver.SqlServer, error) { 173 if s == nil { 174 return nil, nil 175 } 176 opts := []driver.SqlServerOpt{driver.WithArgs(s.Args...)} 177 if s.Port != 0 { 178 opts = append(opts, driver.WithPort(s.Port)) 179 } 180 server, err := driver.StartSqlServer(dc, opts...) 181 if err != nil { 182 return nil, err 183 } 184 185 return server, nil 186 } 187 188 type Hist struct { 189 bins []float64 190 cnts []int 191 sum float64 192 cnt int 193 mn float64 194 md float64 195 v float64 196 } 197 198 func newHist() *Hist { 199 return &Hist{bins: make([]float64, 0), cnts: make([]int, 0)} 200 } 201 202 // add assumes bins are passed in increasing order before 203 // any statistics are evaluated 204 func (h *Hist) add(bin float64, cnt int) { 205 if h.mn != 0 || h.v != 0 || h.md != 0 { 206 panic("tried to edit finished histogram") 207 } 208 h.sum += bin * float64(cnt) 209 h.cnt += cnt 210 h.bins = append(h.bins, bin) 211 h.cnts = append(h.cnts, cnt) 212 } 213 214 // mean is a lossy mean based on histogram bucket sizes 215 func (h *Hist) mean() float64 { 216 if h.mn == 0 { 217 h.mn = math.Round(h.sum*1e3/(float64(h.cnt))) / 1e3 218 } 219 return h.mn 220 221 } 222 223 // median is a lossy median based on histogram bucket sizes 224 func (h *Hist) median() float64 { 225 if h.md == 0 { 226 mid := h.cnt / 2 227 var i int 228 var cnt int 229 for cnt < mid { 230 cnt += h.cnts[i] 231 i++ 232 } 233 h.md = h.bins[i] 234 } 235 return h.md 236 237 } 238 239 // variance is a lossy sum of least squares 240 func (h *Hist) variance() float64 { 241 if h.v == 0 { 242 ss := 0.0 243 for i := range h.cnts { 244 ss += float64(h.cnts[i]) * math.Pow(h.bins[i]-h.mean(), 2) 245 } 246 h.v = math.Round(ss*1e3/(float64(h.cnt-1))) / 1e3 247 } 248 return h.v 249 } 250 251 var histRe = regexp.MustCompile(`([0-9]+\.+[0-9]+)\s+.+\s+([1-9][0-9]*)\n`) 252 253 func (h *Hist) populate(buf []byte) error { 254 res := histRe.FindAllSubmatch(buf, -1) 255 var bin float64 256 var cnt int 257 var err error 258 if len(res) == 0 { 259 return fmt.Errorf("histogram not found") 260 } 261 for _, r := range res { 262 bin, err = strconv.ParseFloat(string(r[1]), 0) 263 if err != nil { 264 return fmt.Errorf("failed to parse bin: %s -> '%s' - '%s'; %s", r[0], r[1], r[2], err) 265 } 266 cnt, err = strconv.Atoi(string(r[2])) 267 if err != nil { 268 return fmt.Errorf("failed to parse cnt: %s -> '%s' - '%s'; %s", r[0], r[1], r[2], err) 269 } 270 h.add(bin, cnt) 271 } 272 return nil 273 } 274 275 func (r *Result) populateHistogram(buf []byte) error { 276 r.hist = newHist() 277 r.hist.populate(buf) 278 279 r.stddev = math.Sqrt(r.hist.variance()) 280 r.median = r.hist.median() 281 282 var err error 283 { 284 timeRe := regexp.MustCompile(`total time:\s+([0-9][0-9]*\.[0-9]+)s\n`) 285 res := timeRe.FindSubmatch(buf) 286 if len(res) == 0 { 287 return fmt.Errorf("time not found") 288 } 289 time, err := strconv.ParseFloat(string(res[1]), 0) 290 if err != nil { 291 return fmt.Errorf("failed to parse time: %s -> '%s' ; %s", res[0], res[1], err) 292 } 293 r.time = math.Round(time*1e3) / 1e3 294 } 295 { 296 itersRe := regexp.MustCompile(`total number of events:\s+([1-9][0-9]*)\n`) 297 res := itersRe.FindSubmatch(buf) 298 if len(res) == 0 { 299 return fmt.Errorf("time not found") 300 } 301 r.iters, err = strconv.Atoi(string(res[1])) 302 if err != nil { 303 return fmt.Errorf("failed to parse bin: %s -> '%s'; %s", res[0], res[1], err) 304 } 305 } 306 { 307 avgRe := regexp.MustCompile(`avg:\s+([0-9][0-9]*\.[0-9]+)\n`) 308 res := avgRe.FindSubmatch(buf) 309 if len(res) == 0 { 310 return fmt.Errorf("avg not found") 311 } 312 r.avg, err = strconv.ParseFloat(string(res[1]), 0) 313 if err != nil { 314 return fmt.Errorf("failed to parse avg: %s -> '%s'; %s", res[0], res[1], err) 315 } 316 } 317 return nil 318 } 319 320 func (r *Result) populateAvg(buf []byte) error { 321 panic("TODO") 322 } 323 324 type Result struct { 325 server string 326 detail string 327 test string 328 329 time float64 330 iters int 331 332 hist *Hist 333 avg float64 334 median float64 335 stddev float64 336 } 337 338 func newResult(server, test, detail string) *Result { 339 return &Result{ 340 server: server, 341 detail: detail, 342 test: test, 343 } 344 } 345 346 func (r *Result) String() string { 347 b := &strings.Builder{} 348 fmt.Fprintf(b, "result:\n") 349 b.WriteString("result:\n") 350 if r.test != "" { 351 fmt.Fprintf(b, "- test: '%s'\n", r.test) 352 } 353 if r.detail != "" { 354 fmt.Fprintf(b, "- detail: '%s'\n", r.detail) 355 } 356 if r.server != "" { 357 fmt.Fprintf(b, "- server: '%s'\n", r.server) 358 } 359 fmt.Fprintf(b, "- time: %.3f\n", r.time) 360 fmt.Fprintf(b, "- iters: %d\n", r.iters) 361 fmt.Fprintf(b, "- mean: %.3f\n", r.hist.mean()) 362 fmt.Fprintf(b, "- median: %.3f\n", r.median) 363 fmt.Fprintf(b, "- stddev: %.3f\n", r.stddev) 364 return b.String() 365 } 366 367 type Results struct { 368 Res []*Result 369 } 370 371 func (r *Results) Append(ir ...*Result) { 372 r.Res = append(r.Res, ir...) 373 } 374 375 func (r *Results) String() string { 376 b := strings.Builder{} 377 b.WriteString("Results:\n") 378 for _, x := range r.Res { 379 b.WriteString(x.String()) 380 } 381 return b.String() 382 } 383 384 func (r *Results) SqlDump() string { 385 b := strings.Builder{} 386 b.WriteString(`CREATE TABLE IF NOT EXISTS sysbench_results ( 387 test_name varchar(64), 388 detail varchar(64), 389 server varchar(64), 390 time double, 391 iters int, 392 avg double, 393 median double, 394 stdd double, 395 primary key (test_name, detail, server) 396 ); 397 `) 398 399 b.WriteString("insert into sysbench_results values\n") 400 for i, r := range r.Res { 401 if i > 0 { 402 b.WriteString(",\n ") 403 } 404 b.WriteString(fmt.Sprintf( 405 "('%s', '%s', '%s', %.3f, %d, %.3f, %.3f, %.3f)", 406 r.test, r.detail, r.server, r.time, r.iters, r.avg, r.median, r.stddev)) 407 } 408 b.WriteString(";\n") 409 410 return b.String() 411 } 412 413 func (test *Script) InitWithTmpDir(s string) { 414 test.tmpdir = s 415 test.Results = new(Results) 416 417 } 418 419 // Run executes an import configuration. Test parallelism makes 420 // runtimes resulting from this method unsuitable for reporting. 421 func (test *Script) Run(t *testing.T) { 422 if test.Skip != "" { 423 t.Skip(test.Skip) 424 } 425 426 conf, err := ParseConfig("testdata/default-config.yaml") 427 if err != nil { 428 require.NoError(t, err) 429 } 430 431 if _, err = os.Stat(test.scriptDir); err != nil { 432 require.NoError(t, err) 433 } 434 435 conf = conf.WithScriptDir(test.scriptDir) 436 437 tmpdir, err := os.MkdirTemp("", "repo-store-") 438 if err != nil { 439 require.NoError(t, err) 440 } 441 442 results := new(Results) 443 u, err := driver.NewDoltUser() 444 test.Results = results 445 test.InitWithTmpDir(tmpdir) 446 for _, r := range test.Repos { 447 var err error 448 switch { 449 case r.ExternalServer != nil: 450 panic("unsupported") 451 case r.Server != nil: 452 err = test.RunSqlServerTests(r, u, conf) 453 default: 454 panic("unsupported") 455 } 456 if err != nil { 457 require.NoError(t, err) 458 } 459 results.Append(test.Results.Res...) 460 } 461 462 fmt.Println(test.Results) 463 } 464 465 func modifyServerForImport(db *sql.DB) error { 466 _, err := db.Exec("CREATE DATABASE sbtest") 467 if err != nil { 468 return err 469 } 470 return nil 471 } 472 473 // RunExternalServerTests connects to a single externally provided server to run every test 474 func (test *Script) RunExternalServerTests(repoName string, s *driver.ExternalServer, conf Config) error { 475 conf.Port = strconv.Itoa(s.Port) 476 conf.Password = s.Password 477 return test.IterSysbenchScripts(conf, test.Scripts, func(script string, prep, run, clean *exec.Cmd) error { 478 log.Printf("starting scipt: %s", script) 479 480 db, err := driver.ConnectDB(s.User, s.Password, s.Name, s.Host, s.Port, nil) 481 if err != nil { 482 return err 483 } 484 defer db.Close() 485 defer clean.Run() 486 487 buf := new(bytes.Buffer) 488 prep.Stdout = buf 489 if err := prep.Run(); err != nil { 490 log.Println(buf) 491 return err 492 } 493 494 buf = new(bytes.Buffer) 495 run.Stdout = buf 496 err = run.Run() 497 if err != nil { 498 log.Println(buf) 499 return err 500 } 501 502 // TODO scrape histogram data 503 r := newResult(repoName, script, test.Name) 504 if conf.Histogram { 505 r.populateHistogram(buf.Bytes()) 506 } else { 507 r.populateAvg(buf.Bytes()) 508 } 509 test.Results.Append(r) 510 511 return clean.Run() 512 }) 513 } 514 515 // RunSqlServerTests creates a new repo and server for every import test. 516 func (test *Script) RunSqlServerTests(repo driver.TestRepo, user driver.DoltUser, conf Config) error { 517 return test.IterSysbenchScripts(conf, test.Scripts, func(script string, prep, run, clean *exec.Cmd) error { 518 log.Printf("starting scipt: %s", script) 519 //make a new server for every test 520 server, err := newServer(user, repo, conf) 521 if err != nil { 522 return err 523 } 524 defer server.GracefulStop() 525 526 db, err := server.DB(driver.Connection{User: "root", Pass: ""}) 527 if err != nil { 528 return err 529 } 530 531 err = modifyServerForImport(db) 532 if err != nil { 533 return err 534 } 535 536 if err := prep.Run(); err != nil { 537 return err 538 } 539 540 buf := new(bytes.Buffer) 541 run.Stdout = buf 542 err = run.Run() 543 if err != nil { 544 log.Println(buf) 545 return err 546 } 547 548 // TODO scrape histogram data 549 r := newResult(repo.Name, script, test.Name) 550 if conf.Histogram { 551 r.populateHistogram(buf.Bytes()) 552 } else { 553 r.populateAvg(buf.Bytes()) 554 } 555 test.Results.Append(r) 556 557 //if conf.Verbose { 558 // return nil 559 //} 560 return clean.Run() 561 }) 562 } 563 564 func newServer(u driver.DoltUser, r driver.TestRepo, conf Config) (*driver.SqlServer, error) { 565 rs, err := u.MakeRepoStore() 566 if err != nil { 567 return nil, err 568 } 569 // start dolt server 570 repo, err := MakeRepo(rs, r) 571 if err != nil { 572 return nil, err 573 } 574 if conf.Verbose { 575 log.Printf("database at: '%s'", repo.Dir) 576 } 577 server, err := MakeServer(repo, r.Server) 578 if err != nil { 579 return nil, err 580 } 581 if server != nil { 582 server.DBName = r.Name 583 } 584 return server, nil 585 } 586 587 const luaExt = ".lua" 588 589 // IterSysbenchScripts returns 3 executable commands for the given script path: prepare, run, cleanup 590 func (test *Script) IterSysbenchScripts(conf Config, scripts []string, cb func(name string, prep, run, clean *exec.Cmd) error) error { 591 newCmd := func(command, script string) *exec.Cmd { 592 cmd := exec.Command("sysbench") 593 cmd.Args = append(cmd.Args, conf.AsOpts()...) 594 cmd.Args = append(cmd.Args, script, command) 595 cmd.Stdout = os.Stdout 596 cmd.Stderr = os.Stderr 597 return cmd 598 } 599 600 for _, script := range scripts { 601 p := script 602 if strings.HasSuffix(script, luaExt) { 603 p = path.Join(conf.ScriptDir, script) 604 if _, err := os.Stat(p); err != nil { 605 return fmt.Errorf("failed to run script: '%s'", err) 606 } 607 } 608 prep := newCmd("prepare", p) 609 run := newCmd("run", p) 610 clean := newCmd("cleanup", p) 611 if err := cb(script, prep, run, clean); err != nil { 612 return err 613 } 614 } 615 return nil 616 } 617 618 func RunTestsFile(t *testing.T, path, scriptDir string) { 619 def, err := ParseTestsFile(path) 620 require.NoError(t, err) 621 for _, test := range def.Tests { 622 test.scriptDir = scriptDir 623 t.Run(test.Name, test.Run) 624 } 625 }