github.com/distbuild/reclient@v0.0.0-20240401075343-3de72e395564/internal/pkg/stats/stats_test.go (about) 1 // Copyright 2023 Google LLC 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 stats 16 17 import ( 18 "fmt" 19 "os" 20 "path/filepath" 21 "reflect" 22 "strconv" 23 "strings" 24 "testing" 25 "time" 26 27 "cloud.google.com/go/bigquery" 28 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 29 "github.com/google/go-cmp/cmp" 30 "github.com/google/go-cmp/cmp/cmpopts" 31 "google.golang.org/protobuf/testing/protocmp" 32 "google.golang.org/protobuf/types/known/timestamppb" 33 34 lpb "github.com/bazelbuild/reclient/api/log" 35 stpb "github.com/bazelbuild/reclient/api/stat" 36 spb "github.com/bazelbuild/reclient/api/stats" 37 38 cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command" 39 ) 40 41 var ( 42 minfo = machineInfo() 43 cmpStatsOpts = []cmp.Option{protocmp.Transform(), protocmp.SortRepeatedFields(&stpb.Stat{}, "counts_by_value"), protocmp.SortRepeatedFields(&spb.Stats{}, "stats")} 44 cmpStatsIgnoreBuildLatency = protocmp.IgnoreFields(&spb.Stats{}, "build_latency") // This is invalid if no proxy execution times provided. 45 recordsToWrite = []*lpb.LogRecord{ 46 &lpb.LogRecord{ 47 RemoteMetadata: &lpb.RemoteMetadata{ 48 CacheHit: true, 49 }, 50 }, 51 } 52 proxyInfosToWrite = []*lpb.ProxyInfo{&lpb.ProxyInfo{ 53 EventTimes: map[string]*cpb.TimeInterval{ 54 "Event": &cpb.TimeInterval{}, 55 }, 56 }} 57 st = &Stats{ 58 NumRecords: 1, 59 Stats: map[string]*Stat{}, 60 } 61 statsToWrite = &Stats{ 62 NumRecords: 5, 63 Stats: map[string]*Stat{ 64 "empty": &Stat{}, 65 "b": &Stat{ 66 Count: 3, 67 CountByValue: map[string]int64{ 68 "v1": 4, 69 "v2": 5, 70 }, 71 }, 72 "a": &Stat{ 73 Count: 6, 74 Median: 10, 75 Percentile75: 11, 76 Percentile85: 12, 77 Percentile95: 13, 78 Average: 9.8, 79 Outlier1: &stpb.Outlier{CommandId: "foo", Value: 15}, 80 Outlier2: &stpb.Outlier{CommandId: "foo", Value: 14}, 81 }, 82 }, 83 ProxyInfos: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 84 EventTimes: map[string]*cpb.TimeInterval{ 85 "Event": &cpb.TimeInterval{}, 86 }, 87 }}, 88 } 89 ) 90 91 // TODO (b/269614799): Test refactor/clean up 92 93 func TestStatsToProto(t *testing.T) { 94 st := &Stats{ 95 NumRecords: 5, 96 Stats: map[string]*Stat{ 97 "empty": &Stat{}, 98 "b": &Stat{ 99 Count: 3, 100 CountByValue: map[string]int64{ 101 "v1": 4, 102 "v2": 5, 103 }, 104 }, 105 "a": &Stat{ 106 Count: 6, 107 Median: 10, 108 Percentile75: 11, 109 Percentile85: 12, 110 Percentile95: 13, 111 Average: 9.8, 112 Outlier1: &stpb.Outlier{CommandId: "foo", Value: 15}, 113 Outlier2: &stpb.Outlier{CommandId: "foo", Value: 14}, 114 }, 115 }, 116 ProxyInfos: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 117 EventTimes: map[string]*cpb.TimeInterval{ 118 "Event": &cpb.TimeInterval{}, 119 }, 120 }}, 121 cacheHits: 10, 122 minProxyExecStart: 23.5, 123 maxProxyExecEnd: 50.75, 124 } 125 expected := &spb.Stats{ 126 NumRecords: 5, 127 Stats: []*stpb.Stat{ 128 &stpb.Stat{ 129 Name: "a", 130 Count: 6, 131 Median: 10, 132 Percentile75: 11, 133 Percentile85: 12, 134 Percentile95: 13, 135 Average: 9.8, 136 Outliers: []*stpb.Outlier{ 137 &stpb.Outlier{CommandId: "foo", Value: 15}, 138 &stpb.Outlier{CommandId: "foo", Value: 14}, 139 }, 140 }, 141 &stpb.Stat{ 142 Name: "b", 143 Count: 3, 144 CountsByValue: []*stpb.Stat_Value{ 145 &stpb.Stat_Value{Name: "v1", Count: 4}, 146 &stpb.Stat_Value{Name: "v2", Count: 5}, 147 }, 148 }, 149 }, 150 MachineInfo: minfo, 151 ProxyInfo: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 152 EventTimes: map[string]*cpb.TimeInterval{ 153 "Event": &cpb.TimeInterval{}, 154 }, 155 }}, 156 BuildCacheHitRatio: 2, 157 BuildLatency: 27.25, 158 } 159 if diff := cmp.Diff(expected, st.ToProto(), protocmp.Transform()); diff != "" { 160 t.Errorf("ToProto returned diff in result: (-want +got)\n%s", diff) 161 } 162 } 163 164 func TestWriteStats(t *testing.T) { 165 st := &Stats{ 166 NumRecords: 5, 167 Stats: map[string]*Stat{ 168 "empty": &Stat{}, 169 "b": &Stat{ 170 Count: 3, 171 CountByValue: map[string]int64{ 172 "v1": 4, 173 "v2": 5, 174 }, 175 }, 176 "a": &Stat{ 177 Count: 6, 178 Median: 10, 179 Percentile75: 11, 180 Percentile85: 12, 181 Percentile95: 13, 182 Average: 9.8, 183 Outlier1: &stpb.Outlier{CommandId: "foo", Value: 15}, 184 Outlier2: &stpb.Outlier{CommandId: "foo", Value: 14}, 185 }, 186 }, 187 ProxyInfos: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 188 EventTimes: map[string]*cpb.TimeInterval{ 189 "Event": &cpb.TimeInterval{}, 190 }, 191 }}, 192 } 193 outDir := t.TempDir() 194 WriteStats(st.ToProto(), outDir) 195 196 got := fileContent(t, filepath.Join(outDir, "rbe_metrics.txt")) 197 198 if len(strings.Fields(got)) <= 2 { 199 t.Errorf("WriteStats did not generate multiline output, got: \n%s", got) 200 } 201 } 202 203 func fileContent(t *testing.T, path string) string { 204 t.Helper() 205 b, err := os.ReadFile(path) 206 if err != nil { 207 t.Fatalf("Failed to open %v", path) 208 } 209 return string(b) 210 } 211 212 func TestBoolStats(t *testing.T) { 213 recs := []*lpb.LogRecord{ 214 &lpb.LogRecord{ 215 RemoteMetadata: &lpb.RemoteMetadata{ 216 CacheHit: true, 217 }, 218 LocalMetadata: &lpb.LocalMetadata{ 219 ValidCacheHit: true, 220 ExecutedLocally: false, 221 UpdatedCache: false, 222 }, 223 }, 224 &lpb.LogRecord{ 225 RemoteMetadata: &lpb.RemoteMetadata{ 226 CacheHit: true, 227 }, 228 LocalMetadata: &lpb.LocalMetadata{ 229 ValidCacheHit: true, 230 ExecutedLocally: false, 231 UpdatedCache: false, 232 }, 233 }, 234 &lpb.LogRecord{ 235 RemoteMetadata: &lpb.RemoteMetadata{ 236 CacheHit: false, 237 }, 238 LocalMetadata: &lpb.LocalMetadata{ 239 ValidCacheHit: false, 240 ExecutedLocally: true, 241 UpdatedCache: false, 242 }, 243 }, 244 &lpb.LogRecord{ 245 RemoteMetadata: &lpb.RemoteMetadata{ 246 CacheHit: false, 247 }, 248 LocalMetadata: &lpb.LocalMetadata{ 249 ValidCacheHit: false, 250 ExecutedLocally: true, 251 UpdatedCache: true, 252 }, 253 }, 254 &lpb.LogRecord{ 255 RemoteMetadata: &lpb.RemoteMetadata{ 256 CacheHit: true, 257 }, 258 LocalMetadata: &lpb.LocalMetadata{ 259 ValidCacheHit: false, 260 ExecutedLocally: true, 261 UpdatedCache: false, 262 }, 263 }, 264 } 265 s := NewFromRecords(recs, nil) 266 wantStats := &spb.Stats{ 267 NumRecords: 5, 268 Stats: []*stpb.Stat{ 269 &stpb.Stat{ 270 Name: "CompletionStatus", 271 CountsByValue: []*stpb.Stat_Value{ 272 { 273 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 274 Count: 5, 275 }, 276 }, 277 }, 278 &stpb.Stat{ 279 Name: "LocalMetadata.ExecutedLocally", 280 Count: 3, 281 }, 282 &stpb.Stat{ 283 Name: "LocalMetadata.UpdatedCache", 284 Count: 1, 285 }, 286 &stpb.Stat{ 287 Name: "LocalMetadata.ValidCacheHit", 288 Count: 2, 289 }, 290 &stpb.Stat{ 291 Name: "RemoteMetadata.CacheHit", 292 Count: 3, 293 }, 294 }, 295 MachineInfo: minfo, 296 ProxyInfo: []*lpb.ProxyInfo{}, 297 } 298 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 299 t.Errorf("Boolean stat returned diff in result: (-want +got)\n%s", diff) 300 } 301 } 302 303 func TestEnumStats(t *testing.T) { 304 recs := []*lpb.LogRecord{ 305 &lpb.LogRecord{ 306 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_REMOTE_ERROR}, 307 CompletionStatus: lpb.CompletionStatus_STATUS_REMOTE_FAILURE, 308 }, 309 &lpb.LogRecord{ 310 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_SUCCESS}, 311 CompletionStatus: lpb.CompletionStatus_STATUS_REMOTE_EXECUTION, 312 }, 313 &lpb.LogRecord{ 314 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_CACHE_HIT}, 315 CompletionStatus: lpb.CompletionStatus_STATUS_CACHE_HIT, 316 }, 317 &lpb.LogRecord{ 318 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_REMOTE_ERROR}, 319 CompletionStatus: lpb.CompletionStatus_STATUS_REMOTE_FAILURE, 320 }, 321 &lpb.LogRecord{ 322 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_SUCCESS}, 323 CompletionStatus: lpb.CompletionStatus_STATUS_LOCAL_EXECUTION, 324 }, 325 &lpb.LogRecord{ 326 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_SUCCESS}, 327 CompletionStatus: lpb.CompletionStatus_STATUS_RACING_LOCAL, 328 }, 329 &lpb.LogRecord{ 330 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_TIMEOUT}, 331 CompletionStatus: lpb.CompletionStatus_STATUS_TIMEOUT, 332 }, 333 &lpb.LogRecord{ 334 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_SUCCESS}, 335 CompletionStatus: lpb.CompletionStatus_STATUS_RACING_REMOTE, 336 }, 337 &lpb.LogRecord{ 338 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_CACHE_HIT}, 339 CompletionStatus: lpb.CompletionStatus_STATUS_CACHE_HIT, 340 }, 341 &lpb.LogRecord{ 342 Result: &cpb.CommandResult{Status: cpb.CommandResultStatus_NON_ZERO_EXIT}, 343 CompletionStatus: lpb.CompletionStatus_STATUS_NON_ZERO_EXIT, 344 }, 345 } 346 s := NewFromRecords(recs, nil) 347 wantStats := &spb.Stats{ 348 NumRecords: 10, 349 Stats: []*stpb.Stat{ 350 &stpb.Stat{ 351 Name: "CompletionStatus", 352 CountsByValue: []*stpb.Stat_Value{ 353 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_CACHE_HIT.String(), Count: 2}, 354 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_NON_ZERO_EXIT.String(), Count: 1}, 355 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_REMOTE_FAILURE.String(), Count: 2}, 356 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_REMOTE_EXECUTION.String(), Count: 1}, 357 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_LOCAL_EXECUTION.String(), Count: 1}, 358 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_RACING_LOCAL.String(), Count: 1}, 359 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_RACING_REMOTE.String(), Count: 1}, 360 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_TIMEOUT.String(), Count: 1}, 361 }, 362 }, 363 &stpb.Stat{ 364 Name: "Result.Status", 365 CountsByValue: []*stpb.Stat_Value{ 366 &stpb.Stat_Value{Name: "CACHE_HIT", Count: 2}, 367 &stpb.Stat_Value{Name: "NON_ZERO_EXIT", Count: 1}, 368 &stpb.Stat_Value{Name: "REMOTE_ERROR", Count: 2}, 369 &stpb.Stat_Value{Name: "SUCCESS", Count: 4}, 370 &stpb.Stat_Value{Name: "TIMEOUT", Count: 1}, 371 }, 372 }, 373 }, 374 MachineInfo: minfo, 375 ProxyInfo: []*lpb.ProxyInfo{}, 376 BuildCacheHitRatio: 0.2, 377 } 378 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 379 t.Errorf("Enum stat returned diff in result: (-want +got)\n%s", diff) 380 } 381 } 382 383 func TestInvocationIDs(t *testing.T) { 384 // Maps in Go have random iteration order, which is a good test for our stats. 385 invIDs := []string{"inv1", "inv2"} 386 var recs []*lpb.LogRecord 387 for i := 0; i < 5; i++ { 388 recs = append(recs, &lpb.LogRecord{ 389 Command: &cpb.Command{Identifiers: &cpb.Identifiers{ 390 InvocationId: invIDs[i%2], 391 }}, 392 }) 393 } 394 s := NewFromRecords(recs, nil) 395 wantStats := &spb.Stats{ 396 NumRecords: 5, 397 InvocationIds: invIDs, 398 MachineInfo: minfo, 399 ProxyInfo: []*lpb.ProxyInfo{}, 400 Stats: []*stpb.Stat{ 401 { 402 Name: "CompletionStatus", 403 CountsByValue: []*stpb.Stat_Value{ 404 { 405 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 406 Count: 5, 407 }, 408 }, 409 }, 410 }, 411 } 412 strSliceCmp := cmpopts.SortSlices(func(a, b string) bool { return a < b }) 413 if diff := cmp.Diff(wantStats, s.ToProto(), protocmp.Transform(), strSliceCmp, cmpStatsIgnoreBuildLatency); diff != "" { 414 t.Errorf("Int stat returned diff in result: (-want +got)\n%s", diff) 415 } 416 } 417 418 func TestIntStats(t *testing.T) { 419 // Maps in Go have random iteration order, which is a good test for our stats. 420 m := make(map[string]int) 421 for i := 1; i <= 100; i++ { 422 m[fmt.Sprint(i)] = i 423 } 424 var recs []*lpb.LogRecord 425 for n, v := range m { 426 recs = append(recs, &lpb.LogRecord{ 427 Command: &cpb.Command{Identifiers: &cpb.Identifiers{CommandId: n}}, 428 RemoteMetadata: &lpb.RemoteMetadata{NumInputFiles: int32(v)}, 429 }) 430 } 431 s := NewFromRecords(recs, nil) 432 wantStats := &spb.Stats{ 433 NumRecords: 100, 434 Stats: []*stpb.Stat{ 435 { 436 Name: "CompletionStatus", 437 CountsByValue: []*stpb.Stat_Value{ 438 { 439 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 440 Count: 100, 441 }, 442 }, 443 }, 444 &stpb.Stat{ 445 Name: "RemoteMetadata.NumInputFiles", 446 Average: 50.5, 447 Count: 5050, 448 Median: 51, 449 Percentile75: 76, 450 Percentile85: 86, 451 Percentile95: 96, 452 Outliers: []*stpb.Outlier{ 453 &stpb.Outlier{CommandId: "100", Value: 100}, 454 &stpb.Outlier{CommandId: "99", Value: 99}, 455 }, 456 }, 457 }, 458 MachineInfo: minfo, 459 ProxyInfo: []*lpb.ProxyInfo{}, 460 } 461 if diff := cmp.Diff(wantStats, s.ToProto(), protocmp.Transform(), cmpStatsIgnoreBuildLatency); diff != "" { 462 t.Errorf("Int stat returned diff in result: (-want +got)\n%s", diff) 463 } 464 } 465 466 func TestLabelsAggregation(t *testing.T) { 467 var recs []*lpb.LogRecord 468 for i := 1; i <= 10; i++ { 469 recs = append(recs, &lpb.LogRecord{ 470 Command: &cpb.Command{Identifiers: &cpb.Identifiers{CommandId: "c_" + fmt.Sprint(i)}}, 471 LocalMetadata: &lpb.LocalMetadata{ 472 Labels: map[string]string{ 473 "lang": "c++", 474 "action": "compile", 475 }, 476 }, 477 RemoteMetadata: &lpb.RemoteMetadata{NumInputFiles: 2}, 478 }) 479 } 480 for i := 1; i <= 20; i++ { 481 recs = append(recs, &lpb.LogRecord{ 482 Command: &cpb.Command{Identifiers: &cpb.Identifiers{CommandId: "l_" + fmt.Sprint(i)}}, 483 LocalMetadata: &lpb.LocalMetadata{ 484 Labels: map[string]string{ 485 "lang": "c++", 486 "action": "link", 487 }, 488 }, 489 RemoteMetadata: &lpb.RemoteMetadata{NumInputFiles: 5}, 490 }) 491 } 492 s := NewFromRecords(recs, nil) 493 wantStats := &spb.Stats{ 494 NumRecords: 30, 495 Stats: []*stpb.Stat{ 496 &stpb.Stat{ 497 Name: "RemoteMetadata.NumInputFiles", 498 Average: 4, 499 Count: 120, 500 Median: 5, 501 Percentile75: 5, 502 Percentile85: 5, 503 Percentile95: 5, 504 Outliers: []*stpb.Outlier{ 505 &stpb.Outlier{CommandId: "l_1", Value: 5}, 506 &stpb.Outlier{CommandId: "l_2", Value: 5}, 507 }, 508 }, 509 &stpb.Stat{ 510 Name: "[action=compile,lang=c++].RemoteMetadata.NumInputFiles", 511 Average: 2, 512 Count: 20, 513 Median: 2, 514 Percentile75: 2, 515 Percentile85: 2, 516 Percentile95: 2, 517 Outliers: []*stpb.Outlier{ 518 &stpb.Outlier{CommandId: "c_1", Value: 2}, 519 &stpb.Outlier{CommandId: "c_2", Value: 2}, 520 }, 521 }, 522 &stpb.Stat{ 523 Name: "[action=link,lang=c++].RemoteMetadata.NumInputFiles", 524 Average: 5, 525 Count: 100, 526 Median: 5, 527 Percentile75: 5, 528 Percentile85: 5, 529 Percentile95: 5, 530 Outliers: []*stpb.Outlier{ 531 &stpb.Outlier{CommandId: "l_1", Value: 5}, 532 &stpb.Outlier{CommandId: "l_2", Value: 5}, 533 }, 534 }, 535 { 536 Name: "CompletionStatus", 537 CountsByValue: []*stpb.Stat_Value{ 538 { 539 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 540 Count: 30, 541 }, 542 }, 543 }, 544 { 545 Name: "[action=compile,lang=c++].CompletionStatus", 546 CountsByValue: []*stpb.Stat_Value{ 547 { 548 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 549 Count: 10, 550 }, 551 }, 552 }, 553 { 554 Name: "[action=link,lang=c++].CompletionStatus", 555 CountsByValue: []*stpb.Stat_Value{ 556 { 557 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 558 Count: 20, 559 }, 560 }, 561 }, 562 }, 563 MachineInfo: minfo, 564 ProxyInfo: []*lpb.ProxyInfo{}, 565 } 566 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 567 t.Errorf("Stat returned diff in result: (-want +got)\n%s", diff) 568 } 569 } 570 571 func TestVerification(t *testing.T) { 572 recs := []*lpb.LogRecord{ 573 &lpb.LogRecord{ 574 LocalMetadata: &lpb.LocalMetadata{ 575 Verification: &lpb.Verification{ 576 Mismatches: []*lpb.Verification_Mismatch{ 577 &lpb.Verification_Mismatch{ 578 Path: "foo.o", 579 RemoteDigests: []string{"aaa/1"}, 580 LocalDigest: "bbb/1", 581 }, 582 &lpb.Verification_Mismatch{ 583 Path: "foo.d", 584 RemoteDigests: []string{"aaa/2"}, 585 LocalDigest: "bbb/2", 586 }, 587 }, 588 TotalMismatches: 2, 589 TotalVerified: 2, 590 }, 591 }, 592 }, 593 &lpb.LogRecord{ 594 LocalMetadata: &lpb.LocalMetadata{ 595 Verification: &lpb.Verification{ 596 Mismatches: []*lpb.Verification_Mismatch{ 597 &lpb.Verification_Mismatch{ 598 Path: "bar.o", 599 RemoteDigests: []string{"aaa/3"}, 600 LocalDigest: "bbb/3", 601 }, 602 &lpb.Verification_Mismatch{ 603 Path: "bar.d", 604 RemoteDigests: []string{"aaa/4"}, 605 LocalDigest: "bbb/4", 606 }, 607 }, 608 TotalMismatches: 2, 609 TotalVerified: 2, 610 }, 611 }, 612 }, 613 &lpb.LogRecord{ 614 LocalMetadata: &lpb.LocalMetadata{ 615 Verification: &lpb.Verification{ 616 Mismatches: []*lpb.Verification_Mismatch{ 617 &lpb.Verification_Mismatch{ 618 Path: "bla.txt", 619 RemoteDigests: []string{"aaa/5"}, 620 LocalDigest: "bbb/5", 621 }, 622 }, 623 TotalMismatches: 1, 624 TotalVerified: 2, 625 }, 626 }, 627 }, 628 } 629 s := NewFromRecords(recs, nil) 630 wantStats := &spb.Stats{ 631 NumRecords: 3, 632 Stats: []*stpb.Stat{ 633 { 634 Name: "CompletionStatus", 635 CountsByValue: []*stpb.Stat_Value{ 636 { 637 Name: "STATUS_UNKNOWN", 638 Count: 3, 639 }, 640 }, 641 }, 642 &stpb.Stat{ 643 Name: "LocalMetadata.Verification.TotalMismatches", 644 Count: 5, 645 Median: 2, 646 Percentile75: 2, 647 Percentile85: 2, 648 Percentile95: 2, 649 Average: 5.0 / 3.0, 650 Outliers: []*stpb.Outlier{ 651 &stpb.Outlier{Value: 2}, 652 &stpb.Outlier{Value: 2}, 653 }, 654 }, 655 &stpb.Stat{ 656 Name: "LocalMetadata.Verification.TotalVerified", 657 Count: 6, 658 Median: 2, 659 Percentile75: 2, 660 Percentile85: 2, 661 Percentile95: 2, 662 Average: 6.0 / 3.0, 663 Outliers: []*stpb.Outlier{ 664 &stpb.Outlier{Value: 2}, 665 &stpb.Outlier{Value: 2}, 666 }, 667 }, 668 }, 669 Verification: &lpb.Verification{ 670 Mismatches: []*lpb.Verification_Mismatch{ 671 &lpb.Verification_Mismatch{ 672 Path: "bar.d", 673 RemoteDigests: []string{"aaa/4"}, 674 LocalDigest: "bbb/4", 675 }, 676 &lpb.Verification_Mismatch{ 677 Path: "bar.o", 678 RemoteDigests: []string{"aaa/3"}, 679 LocalDigest: "bbb/3", 680 }, 681 &lpb.Verification_Mismatch{ 682 Path: "bla.txt", 683 RemoteDigests: []string{"aaa/5"}, 684 LocalDigest: "bbb/5", 685 }, 686 &lpb.Verification_Mismatch{ 687 Path: "foo.d", 688 RemoteDigests: []string{"aaa/2"}, 689 LocalDigest: "bbb/2", 690 }, 691 &lpb.Verification_Mismatch{ 692 Path: "foo.o", 693 RemoteDigests: []string{"aaa/1"}, 694 LocalDigest: "bbb/1", 695 }, 696 }, 697 TotalMismatches: 5, 698 TotalVerified: 6, 699 }, 700 MachineInfo: minfo, 701 ProxyInfo: []*lpb.ProxyInfo{}, 702 } 703 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 704 t.Errorf("Int stat returned diff in result: (-want +got)\n%s", diff) 705 } 706 } 707 708 func TestTimeStats(t *testing.T) { 709 // Maps in Go have random iteration order, which is a good test for our stats. 710 m := make(map[string]int) 711 for i := 0; i < 100; i++ { 712 m[fmt.Sprint(i)] = i 713 } 714 from, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") 715 fromPb := command.TimeToProto(from) 716 var recs []*lpb.LogRecord 717 for n, v := range m { 718 recs = append(recs, &lpb.LogRecord{ 719 Command: &cpb.Command{Identifiers: &cpb.Identifiers{CommandId: n}}, 720 RemoteMetadata: &lpb.RemoteMetadata{ 721 EventTimes: map[string]*cpb.TimeInterval{ 722 "Foo": &cpb.TimeInterval{ 723 From: fromPb, 724 To: command.TimeToProto(from.Add(time.Duration(v) * time.Millisecond)), 725 }, 726 }, 727 }, 728 }) 729 } 730 s := NewFromRecords(recs, nil) 731 wantStats := &spb.Stats{ 732 NumRecords: 100, 733 Stats: []*stpb.Stat{ 734 { 735 Name: "CompletionStatus", 736 CountsByValue: []*stpb.Stat_Value{ 737 { 738 Name: lpb.CompletionStatus_STATUS_UNKNOWN.String(), 739 Count: 100, 740 }, 741 }, 742 }, 743 &stpb.Stat{ 744 Name: "RemoteMetadata.EventTimes.FooMillis", 745 Count: 100, 746 Average: 49.5, 747 Median: 50, 748 Percentile75: 75, 749 Percentile85: 85, 750 Percentile95: 95, 751 Outliers: []*stpb.Outlier{ 752 &stpb.Outlier{CommandId: "99", Value: 99}, 753 &stpb.Outlier{CommandId: "98", Value: 98}, 754 }, 755 }, 756 }, 757 MachineInfo: minfo, 758 ProxyInfo: []*lpb.ProxyInfo{}, 759 } 760 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 761 t.Errorf("Time stat returned diff in result: (-want +got)\n%s", diff) 762 } 763 } 764 765 func TestTimeStatsDoNotAggregatePartial(t *testing.T) { 766 from, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") 767 fromPb := command.TimeToProto(from) 768 recs := []*lpb.LogRecord{ 769 &lpb.LogRecord{ 770 RemoteMetadata: &lpb.RemoteMetadata{ 771 EventTimes: map[string]*cpb.TimeInterval{"Foo": &cpb.TimeInterval{From: fromPb}}, 772 }, 773 }, 774 &lpb.LogRecord{ 775 RemoteMetadata: &lpb.RemoteMetadata{ 776 EventTimes: map[string]*cpb.TimeInterval{"Foo": &cpb.TimeInterval{From: fromPb}}, 777 }, 778 }, 779 &lpb.LogRecord{ 780 RemoteMetadata: &lpb.RemoteMetadata{ 781 EventTimes: map[string]*cpb.TimeInterval{ 782 "Foo": &cpb.TimeInterval{From: fromPb, To: fromPb}, 783 }, 784 }, 785 }, 786 &lpb.LogRecord{ 787 RemoteMetadata: &lpb.RemoteMetadata{ 788 EventTimes: map[string]*cpb.TimeInterval{ 789 "Foo": &cpb.TimeInterval{ 790 From: fromPb, 791 To: command.TimeToProto(from.Add(2 * time.Microsecond)), 792 }, 793 }, 794 }, 795 }, 796 &lpb.LogRecord{ 797 RemoteMetadata: &lpb.RemoteMetadata{ 798 EventTimes: map[string]*cpb.TimeInterval{ 799 "Foo": &cpb.TimeInterval{ 800 From: fromPb, 801 To: command.TimeToProto(from.Add(2 * time.Millisecond)), 802 }, 803 }, 804 }, 805 }, 806 } 807 s := NewFromRecords(recs, nil) 808 809 wantStats := &spb.Stats{ 810 NumRecords: 5, 811 Stats: []*stpb.Stat{ 812 { 813 Name: "CompletionStatus", 814 CountsByValue: []*stpb.Stat_Value{ 815 { 816 Name: "STATUS_UNKNOWN", 817 Count: 5, 818 }, 819 }, 820 }, 821 &stpb.Stat{ 822 Name: "RemoteMetadata.EventTimes.FooMillis", 823 Count: 3, 824 Average: 2.0 / 3.0, 825 Median: 0, 826 Percentile75: 2, 827 Percentile85: 2, 828 Percentile95: 2, 829 Outliers: []*stpb.Outlier{ 830 &stpb.Outlier{Value: 2}, 831 }, 832 }, 833 }, 834 MachineInfo: minfo, 835 ProxyInfo: []*lpb.ProxyInfo{}, 836 } 837 if diff := cmp.Diff(wantStats, s.ToProto(), append(cmpStatsOpts, cmpStatsIgnoreBuildLatency)...); diff != "" { 838 t.Errorf("Time stat returned diff in result: (-want +got)\n%s", diff) 839 } 840 } 841 842 func TestBandwidthStats(t *testing.T) { 843 testCases := []struct { 844 name string 845 stat *Stats 846 wantDown string 847 wantUp string 848 }{ 849 { 850 name: "Test bytes", 851 stat: &Stats{ 852 Stats: map[string]*Stat{ 853 "RemoteMetadata.RealBytesDownloaded": &Stat{Count: 999}, 854 "RemoteMetadata.RealBytesUploaded": &Stat{Count: 999}, 855 }, 856 }, 857 wantDown: "999 B", 858 wantUp: "999 B", 859 }, 860 { 861 name: "Test kilo-bytes", 862 stat: &Stats{ 863 Stats: map[string]*Stat{ 864 "RemoteMetadata.RealBytesDownloaded": &Stat{Count: 2520}, 865 "RemoteMetadata.RealBytesUploaded": &Stat{Count: 2520}, 866 }, 867 }, 868 wantDown: "2.52 KB", 869 wantUp: "2.52 KB", 870 }, 871 { 872 name: "Test mega-bytes", 873 stat: &Stats{ 874 Stats: map[string]*Stat{ 875 "RemoteMetadata.RealBytesDownloaded": &Stat{Count: 1000 * 1100}, 876 "RemoteMetadata.RealBytesUploaded": &Stat{Count: 1000 * 1100}, 877 }, 878 }, 879 wantDown: "1.10 MB", 880 wantUp: "1.10 MB", 881 }, 882 { 883 name: "Test giga-bytes", 884 stat: &Stats{ 885 Stats: map[string]*Stat{ 886 "RemoteMetadata.RealBytesDownloaded": &Stat{Count: 1000 * 1100 * 2000}, 887 "RemoteMetadata.RealBytesUploaded": &Stat{Count: 1000 * 1100 * 2000}, 888 }, 889 }, 890 wantDown: "2.20 GB", 891 wantUp: "2.20 GB", 892 }, 893 } 894 895 for _, tc := range testCases { 896 t.Run(tc.name, func(t *testing.T) { 897 gotDown, gotUp := BandwidthStats(tc.stat.ToProto()) 898 if gotDown != tc.wantDown || gotUp != tc.wantUp { 899 t.Fatalf("BandwidthStats() returned incorrect response, gotDown=%v, wantDown=%v, gotUp=%v, wantUp=%v", gotDown, tc.wantDown, gotUp, tc.wantUp) 900 } 901 }) 902 } 903 } 904 905 func TestStatsProtoSaver(t *testing.T) { 906 original := &spb.Stats{ 907 NumRecords: 5, 908 Stats: []*stpb.Stat{ 909 &stpb.Stat{ 910 Name: "a", 911 Count: 6, 912 Median: 10, 913 Percentile75: 11, 914 Percentile85: 12, 915 Percentile95: 13, 916 Average: 9.8, 917 Outliers: []*stpb.Outlier{ 918 &stpb.Outlier{CommandId: "foo", Value: 15}, 919 &stpb.Outlier{CommandId: "foo", Value: 14}, 920 }, 921 }, 922 &stpb.Stat{ 923 Name: "b", 924 Count: 3, 925 CountsByValue: []*stpb.Stat_Value{ 926 &stpb.Stat_Value{Name: "v1", Count: 4}, 927 &stpb.Stat_Value{Name: "v2", Count: 5}, 928 }, 929 }, 930 }, 931 MachineInfo: minfo, 932 ProxyInfo: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 933 EventTimes: map[string]*cpb.TimeInterval{ 934 "Event": &cpb.TimeInterval{ 935 From: timestamppb.New(time.Unix(1676388339, 0)), 936 }, 937 }, 938 }}, 939 } 940 vs := &ProtoSaver{original} 941 got, id, err := vs.Save() 942 if err != nil { 943 t.Errorf("Save() returned error: %v", err) 944 } 945 want := map[string]bigquery.Value{ 946 "machine_info": map[string]interface{}{ 947 "arch": minfo.Arch, 948 "num_cpu": strconv.FormatInt(minfo.NumCpu, 10), 949 "os_family": minfo.OsFamily, 950 "ram_mbs": strconv.FormatInt(minfo.RamMbs, 10), 951 }, 952 "num_records": string("5"), 953 "proxy_info": []interface{}{ 954 map[string]interface{}{ 955 "event_times": []interface{}{ 956 map[string]interface{}{ 957 "key": string("Event"), 958 "value": map[string]interface{}{"from": string("2023-02-14T15:25:39Z")}, 959 }, 960 }, 961 }, 962 }, 963 "stats": []interface{}{ 964 map[string]interface{}{ 965 "average": float64(9.8), 966 "count": string("6"), 967 "median": string("10"), 968 "name": string("a"), 969 "outliers": []interface{}{ 970 map[string]interface{}{"command_id": string("foo"), "value": string("15")}, 971 map[string]interface{}{"command_id": string("foo"), "value": string("14")}, 972 }, 973 "percentile75": string("11"), 974 "percentile85": string("12"), 975 "percentile95": string("13"), 976 }, 977 map[string]interface{}{"count": string("3"), "counts_by_value": []interface{}{ 978 map[string]interface{}{"count": string("4"), "name": string("v1")}, 979 map[string]interface{}{"count": string("5"), "name": string("v2")}, 980 }, "name": string("b")}, 981 }, 982 } 983 if len(id) == 0 { 984 t.Errorf("Save() returned zero length id") 985 } 986 987 if diff := cmp.Diff(want, got); diff != "" { 988 t.Errorf("Save() returned diff in result: (-want +got)\n%s", diff) 989 } 990 } 991 992 // BenchmarkProtoSaver measures the runtime of saving a large stats proto as a bq compatible map 993 func BenchmarkProtoSaver(b *testing.B) { 994 testNow := time.Unix(1676388339, 0) 995 proxyEvents := map[string]*cpb.TimeInterval{} 996 flags := map[string]string{} 997 metrics := map[string]*lpb.Metric{} 998 statArr := []*stpb.Stat{} 999 for i := 0; i < 1000; i++ { 1000 proxyEvents[fmt.Sprintf("Event%d", i)] = &cpb.TimeInterval{ 1001 From: timestamppb.New(testNow.Add(time.Duration(i) * time.Minute)), 1002 To: timestamppb.New(testNow.Add(time.Duration(i+1) * time.Minute)), 1003 } 1004 flags[fmt.Sprintf("flag_%d", i)] = fmt.Sprintf("value_%d", i) 1005 metrics[fmt.Sprintf("flag_%d_bool", i)] = &lpb.Metric{Value: &lpb.Metric_BoolValue{i%2 == 0}} 1006 metrics[fmt.Sprintf("flag_%d_double", i)] = &lpb.Metric{Value: &lpb.Metric_DoubleValue{float64(i) + 0.5}} 1007 metrics[fmt.Sprintf("flag_%d_int64", i)] = &lpb.Metric{Value: &lpb.Metric_Int64Value{int64(i)}} 1008 statArr = append(statArr, &stpb.Stat{ 1009 Name: "a", 1010 Count: int64(6 * i), 1011 Median: int64(10 + i), 1012 Percentile75: int64(11 + i), 1013 Percentile85: int64(12 + i), 1014 Percentile95: int64(13 + i), 1015 Average: 9.8 + float64(i), 1016 Outliers: []*stpb.Outlier{ 1017 &stpb.Outlier{CommandId: "foo", Value: int64(15 + i)}, 1018 &stpb.Outlier{CommandId: "foo", Value: int64(14 + i)}, 1019 }, 1020 }) 1021 } 1022 original := &spb.Stats{ 1023 NumRecords: 5, 1024 Stats: statArr, 1025 MachineInfo: minfo, 1026 ProxyInfo: []*lpb.ProxyInfo{&lpb.ProxyInfo{ 1027 EventTimes: proxyEvents, 1028 Metrics: metrics, 1029 Flags: flags, 1030 }}, 1031 } 1032 for n := 0; n < b.N; n++ { 1033 (&ProtoSaver{original}).Save() 1034 } 1035 } 1036 1037 func TestCompletionStats(t *testing.T) { 1038 tests := []struct { 1039 name string 1040 stats *spb.Stats 1041 want string 1042 }{ 1043 { 1044 name: "OneStatus", 1045 stats: &spb.Stats{ 1046 Stats: []*stpb.Stat{ 1047 &stpb.Stat{ 1048 Name: "CompletionStatus", 1049 CountsByValue: []*stpb.Stat_Value{ 1050 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_REMOTE_EXECUTION.String(), Count: 4}, 1051 }, 1052 }, 1053 }, 1054 }, 1055 want: "4 remote executions", 1056 }, 1057 { 1058 name: "MultipleStatuses", 1059 stats: &spb.Stats{ 1060 Stats: []*stpb.Stat{ 1061 &stpb.Stat{ 1062 Name: "CompletionStatus", 1063 CountsByValue: []*stpb.Stat_Value{ 1064 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_REMOTE_EXECUTION.String(), Count: 4}, 1065 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_LOCAL_EXECUTION.String(), Count: 3}, 1066 &stpb.Stat_Value{Name: lpb.CompletionStatus_STATUS_LOCAL_FALLBACK.String(), Count: 1}, 1067 }, 1068 }, 1069 }, 1070 }, 1071 want: "4 remote executions, 1 local fallback, 3 local executions", 1072 }, 1073 { 1074 name: "NoStatuses", 1075 stats: &spb.Stats{}, 1076 want: "", 1077 }, 1078 } 1079 for _, tc := range tests { 1080 tc := tc 1081 t.Run(tc.name, func(t *testing.T) { 1082 t.Parallel() 1083 got := CompletionStats(tc.stats) 1084 1085 if diff := cmp.Diff(tc.want, got); diff != "" { 1086 t.Errorf("CompletionStats(%v) returned diff in result: (-want +got)\n%s", tc.stats, diff) 1087 } 1088 }) 1089 } 1090 } 1091 1092 func TestFromSeriesToProto(t *testing.T) { 1093 tests := []struct { 1094 testName string 1095 name string 1096 rawValues []int64 1097 want *stpb.Stat 1098 }{ 1099 {testName: "no rawValues", name: "foo", rawValues: []int64{}, want: &stpb.Stat{Name: "foo", Median: 0, Percentile75: 0, Percentile85: 0, Percentile95: 0, Average: 0.0}}, 1100 {testName: "same rawValues", name: "foo", rawValues: []int64{1, 1, 1, 1, 1}, want: &stpb.Stat{Name: "foo", Median: 1, Percentile75: 1, Percentile85: 1, Percentile95: 1, Average: 1.0}}, 1101 {testName: "in-order rawValues", name: "foo", rawValues: []int64{1, 2, 3, 4, 5}, want: &stpb.Stat{Name: "foo", Median: 3, Percentile75: 4, Percentile85: 5, Percentile95: 5, Average: 3.0}}, 1102 {testName: "off-order rawValues", name: "foo", rawValues: []int64{5, 2, 3, 1, 4}, want: &stpb.Stat{Name: "foo", Median: 3, Percentile75: 4, Percentile85: 5, Percentile95: 5, Average: 3.0}}, 1103 } 1104 for _, tt := range tests { 1105 t.Run(tt.name, func(t *testing.T) { 1106 got := FromSeriesToProto(tt.name, tt.rawValues) 1107 if !reflect.DeepEqual(got, tt.want) { 1108 t.Errorf("%v FromSeriesToProto() = %v, want %v", tt.testName, got, tt.want) 1109 } 1110 }) 1111 } 1112 } 1113 1114 func TestWriteFromRecords(t *testing.T) { 1115 type args struct { 1116 recs []*lpb.LogRecord 1117 pInfo []*lpb.ProxyInfo 1118 wantStats *spb.Stats 1119 } 1120 tests := []struct { 1121 name string 1122 args args 1123 }{ 1124 {name: "recs is nil", args: args{nil, nil, &spb.Stats{}}}, 1125 {name: "recs is not nil", args: args{recordsToWrite, proxyInfosToWrite, NewFromRecords(recordsToWrite, proxyInfosToWrite).ToProto()}}, 1126 } 1127 for _, tt := range tests { 1128 t.Run(tt.name, func(t *testing.T) { 1129 wantDir := t.TempDir() 1130 WriteStats(tt.args.wantStats, wantDir) 1131 want := fileContent(t, filepath.Join(wantDir, "rbe_metrics.txt")) 1132 1133 gotDir := t.TempDir() 1134 WriteFromRecords(tt.args.recs, tt.args.pInfo, gotDir) 1135 got := fileContent(t, filepath.Join(gotDir, "rbe_metrics.txt")) 1136 if diff := cmp.Diff(want, got); diff != "" { 1137 t.Errorf("TestAggregateRecordsToFiles generate diff in file content: (-want +got)\n", diff) 1138 } 1139 }) 1140 } 1141 }