github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/networkQuality.go (about) 1 /* 2 * This file is part of Go Responsiveness. 3 * 4 * Go Responsiveness is free software: you can redistribute it and/or modify it under 5 * the terms of the GNU General Public License as published by the Free Software Foundation, 6 * either version 2 of the License, or (at your option) any later version. 7 * Go Responsiveness is distributed in the hope that it will be useful, but WITHOUT ANY 8 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 9 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 * 11 * You should have received a copy of the GNU General Public License along 12 * with Go Responsiveness. If not, see <https://www.gnu.org/licenses/>. 13 */ 14 15 package main 16 17 import ( 18 "bytes" 19 "context" 20 "flag" 21 "fmt" 22 "net/url" 23 "os" 24 "runtime/pprof" 25 "strings" 26 "time" 27 28 "github.com/network-quality/goresponsiveness/ccw" 29 "github.com/network-quality/goresponsiveness/config" 30 "github.com/network-quality/goresponsiveness/constants" 31 "github.com/network-quality/goresponsiveness/datalogger" 32 "github.com/network-quality/goresponsiveness/debug" 33 "github.com/network-quality/goresponsiveness/direction" 34 "github.com/network-quality/goresponsiveness/executor" 35 "github.com/network-quality/goresponsiveness/extendedstats" 36 "github.com/network-quality/goresponsiveness/lgc" 37 "github.com/network-quality/goresponsiveness/probe" 38 "github.com/network-quality/goresponsiveness/qualityattenuation" 39 "github.com/network-quality/goresponsiveness/rpm" 40 "github.com/network-quality/goresponsiveness/series" 41 "github.com/network-quality/goresponsiveness/stabilizer" 42 "github.com/network-quality/goresponsiveness/timeoutat" 43 "github.com/network-quality/goresponsiveness/utilities" 44 ) 45 46 var ( 47 // Variables to hold CLI arguments. 48 configHost = flag.String( 49 "config", 50 constants.DefaultConfigHost, 51 "name/IP of responsiveness configuration server.", 52 ) 53 configPort = flag.Int( 54 "port", 55 constants.DefaultPortNumber, 56 "port number on which to access responsiveness configuration server.", 57 ) 58 configPath = flag.String( 59 "path", 60 "config", 61 "path on the server to the configuration endpoint.", 62 ) 63 configURL = flag.String( 64 "url", 65 "", 66 "configuration URL (takes precedence over other configuration parts)", 67 ) 68 debugCliFlag = flag.Bool( 69 "debug", 70 constants.DefaultDebug, 71 "Enable debugging.", 72 ) 73 rpmtimeout = flag.Int( 74 "rpm.timeout", 75 constants.DefaultTestTime, 76 "Maximum time (in seconds) to spend calculating RPM (i.e., total test time.).", 77 ) 78 rpmmad = flag.Int( 79 "rpm.mad", 80 constants.SpecParameterCliOptionsDefaults.Mad, 81 "Moving average distance -- number of intervals considered during stability calculations.", 82 ) 83 rpmid = flag.Int( 84 "rpm.id", 85 constants.SpecParameterCliOptionsDefaults.Id, 86 "Duration of the interval between re-evaluating the network conditions (in seconds).", 87 ) 88 rpmtmp = flag.Uint( 89 "rpm.tmp", 90 constants.SpecParameterCliOptionsDefaults.Tmp, 91 "Percent of measurements to trim when calculating statistics about network conditions (between 0 and 100).", 92 ) 93 rpmsdt = flag.Float64( 94 "rpm.sdt", 95 constants.SpecParameterCliOptionsDefaults.Sdt, 96 "Cutoff in the standard deviation of measured values about network conditions between unstable and stable.", 97 ) 98 rpmmnp = flag.Int( 99 "rpm.mnp", 100 constants.SpecParameterCliOptionsDefaults.Mnp, 101 "Maximimum number of parallel connections to establish when attempting to reach working conditions.", 102 ) 103 rpmmps = flag.Int( 104 "rpm.mps", 105 constants.SpecParameterCliOptionsDefaults.Mps, 106 "Maximimum number of probes to send per second.", 107 ) 108 rpmptc = flag.Float64( 109 "rpm.ptc", 110 constants.SpecParameterCliOptionsDefaults.Ptc, 111 "Percentage of the (discovered) total network capacity that probes are allowed to consume.", 112 ) 113 rpmp = flag.Int( 114 "rpm.p", 115 constants.SpecParameterCliOptionsDefaults.P, 116 "Percentile of results to consider when calculating responsiveness.", 117 ) 118 119 sslKeyFileName = flag.String( 120 "ssl-key-file", 121 "", 122 "Store the per-session SSL key files in this file.", 123 ) 124 profile = flag.String( 125 "profile", 126 "", 127 "Enable client runtime profiling and specify storage location. Disabled by default.", 128 ) 129 calculateExtendedStats = flag.Bool( 130 "extended-stats", 131 false, 132 "Enable the collection and display of extended statistics -- may not be available on certain platforms.", 133 ) 134 printQualityAttenuation = flag.Bool( 135 "quality-attenuation", 136 false, 137 "Print quality attenuation information.", 138 ) 139 dataLoggerBaseFileName = flag.String( 140 "logger-filename", 141 "", 142 "Store granular information about tests results in files with this basename. Time and information type will be appended (before the first .) to create separate log files. Disabled by default.", 143 ) 144 connectToAddr = flag.String( 145 "connect-to", 146 "", 147 "address (hostname or IP) to connect to (overriding DNS). Disabled by default.", 148 ) 149 insecureSkipVerify = flag.Bool( 150 "insecure-skip-verify", 151 constants.DefaultInsecureSkipVerify, 152 "Enable server certificate validation.", 153 ) 154 prometheusStatsFilename = flag.String( 155 "prometheus-stats-filename", 156 "", 157 "If filename specified, prometheus stats will be written. If specified file exists, it will be overwritten.", 158 ) 159 showVersion = flag.Bool( 160 "version", 161 false, 162 "Show version.", 163 ) 164 calculateRelativeRpm = flag.Bool( 165 "relative-rpm", 166 false, 167 "Calculate a relative RPM.", 168 ) 169 withL4S = flag.Bool("with-l4s", false, "Use L4S (with default TCP prague congestion control algorithm.)") 170 withL4SAlgorithm = flag.String("with-l4s-algorithm", "", "Use L4S (with specified congestion control algorithm.)") 171 172 parallelTestExecutionPolicy = constants.DefaultTestExecutionPolicy 173 ) 174 175 func main() { 176 // Add one final command-line argument 177 flag.BoolFunc("rpm.parallel", "Parallel test execution policy.", func(value string) error { 178 if value != "true" { 179 return fmt.Errorf("-parallel can only be used to enable parallel test execution policy") 180 } 181 parallelTestExecutionPolicy = executor.Parallel 182 return nil 183 }) 184 185 flag.Parse() 186 187 if *showVersion { 188 fmt.Fprintf(os.Stdout, "goresponsiveness %s\n", utilities.GitVersion) 189 os.Exit(0) 190 } 191 192 var debugLevel debug.DebugLevel = debug.Error 193 194 if *debugCliFlag { 195 debugLevel = debug.Debug 196 } 197 198 specParameters, err := rpm.SpecParametersFromArguments(*rpmtimeout, *rpmmad, *rpmid, 199 *rpmtmp, *rpmsdt, *rpmmnp, *rpmmps, *rpmptc, *rpmp, parallelTestExecutionPolicy) 200 if err != nil { 201 fmt.Fprintf( 202 os.Stderr, 203 "Error: There was an error configuring the test with user-supplied parameters: %v\n", 204 err, 205 ) 206 os.Exit(1) 207 } 208 209 if debug.IsDebug(debugLevel) { 210 fmt.Printf("Running the test according to the following spec parameters:\n%v\n", specParameters.ToString()) 211 } 212 213 var configHostPort string 214 215 // if user specified a full URL, use that and set the various parts we need out of it 216 if len(*configURL) > 0 { 217 parsedURL, err := url.ParseRequestURI(*configURL) 218 if err != nil { 219 fmt.Printf("Error: Could not parse %q: %s", *configURL, err) 220 os.Exit(1) 221 } 222 223 *configHost = parsedURL.Hostname() 224 *configPath = parsedURL.Path 225 // We don't explicitly care about configuring the *configPort. 226 configHostPort = parsedURL.Host // host or host:port 227 } else { 228 configHostPort = fmt.Sprintf("%s:%d", *configHost, *configPort) 229 } 230 231 // This is the overall operating context of the program. All other 232 // contexts descend from this one. Canceling this one cancels all 233 // the others. 234 operatingCtx, operatingCtxCancel := context.WithCancel(context.Background()) 235 236 config := &config.Config{ 237 ConnectToAddr: *connectToAddr, 238 } 239 240 if *calculateExtendedStats && !extendedstats.ExtendedStatsAvailable() { 241 *calculateExtendedStats = false 242 fmt.Fprintf( 243 os.Stderr, 244 "Warning: Calculation of extended statistics was requested but is not supported on this platform.\n", 245 ) 246 } 247 248 var sslKeyFileConcurrentWriter *ccw.ConcurrentWriter = nil 249 if *sslKeyFileName != "" { 250 if sslKeyFileHandle, err := os.OpenFile(*sslKeyFileName, os.O_RDWR|os.O_CREATE, os.FileMode(0o600)); err != nil { 251 fmt.Printf("Could not open the requested SSL key logging file for writing: %v!\n", err) 252 sslKeyFileConcurrentWriter = nil 253 } else { 254 if err = utilities.SeekForAppend(sslKeyFileHandle); err != nil { 255 fmt.Printf("Could not seek to the end of the SSL key logging file: %v!\n", err) 256 sslKeyFileConcurrentWriter = nil 257 } else { 258 if debug.IsDebug(debugLevel) { 259 fmt.Printf("Doing SSL key logging through file %v\n", *sslKeyFileName) 260 } 261 sslKeyFileConcurrentWriter = ccw.NewConcurrentFileWriter(sslKeyFileHandle) 262 defer sslKeyFileHandle.Close() 263 } 264 } 265 } 266 267 var congestionControlChosen *string = nil 268 if *withL4S || *withL4SAlgorithm != "" { 269 congestionControlChosen = &constants.DefaultL4SCongestionControlAlgorithm 270 if *withL4SAlgorithm != "" { 271 congestionControlChosen = withL4SAlgorithm 272 } 273 } 274 275 if congestionControlChosen != nil && debug.IsDebug(debugLevel) { 276 fmt.Printf("Doing congestion control with the %v algorithm.\n", *congestionControlChosen) 277 } 278 279 if err := config.Get(configHostPort, *configPath, *insecureSkipVerify, 280 sslKeyFileConcurrentWriter); err != nil { 281 fmt.Fprintf(os.Stderr, "%s\n", err) 282 os.Exit(1) 283 } 284 if err := config.IsValid(); err != nil { 285 fmt.Fprintf( 286 os.Stderr, 287 "Error: Invalid configuration returned from %s: %v\n", 288 config.Source, 289 err, 290 ) 291 os.Exit(1) 292 } 293 if debug.IsDebug(debugLevel) { 294 fmt.Printf("Configuration: %s\n", config) 295 } 296 297 downloadDirection := direction.Direction{} 298 uploadDirection := direction.Direction{} 299 300 // User wants to log data 301 if *dataLoggerBaseFileName != "" { 302 var err error = nil 303 unique := time.Now().UTC().Format("01-02-2006-15-04-05") 304 305 dataLoggerDownloadThroughputFilename := utilities.FilenameAppend( 306 *dataLoggerBaseFileName, 307 "-throughput-download-"+unique, 308 ) 309 dataLoggerUploadThroughputFilename := utilities.FilenameAppend( 310 *dataLoggerBaseFileName, 311 "-throughput-upload-"+unique, 312 ) 313 314 dataLoggerDownloadGranularThroughputFilename := utilities.FilenameAppend( 315 *dataLoggerBaseFileName, 316 "-throughput-download-granular-"+unique, 317 ) 318 319 dataLoggerUploadGranularThroughputFilename := utilities.FilenameAppend( 320 *dataLoggerBaseFileName, 321 "-throughput-upload-granular-"+unique, 322 ) 323 324 dataLoggerSelfFilename := utilities.FilenameAppend(*dataLoggerBaseFileName, "-self-"+unique) 325 dataLoggerForeignFilename := utilities.FilenameAppend( 326 *dataLoggerBaseFileName, 327 "-foreign-"+unique, 328 ) 329 330 selfProbeDataLogger, err := datalogger.CreateCSVDataLogger[probe.ProbeDataPoint]( 331 dataLoggerSelfFilename, 332 ) 333 if err != nil { 334 fmt.Fprintf( 335 os.Stderr, 336 "Warning: Could not create the file for storing self probe results (%s). Disabling functionality.\n", 337 dataLoggerSelfFilename, 338 ) 339 selfProbeDataLogger = nil 340 } 341 uploadDirection.SelfProbeDataLogger = selfProbeDataLogger 342 downloadDirection.SelfProbeDataLogger = selfProbeDataLogger 343 344 foreignProbeDataLogger, err := datalogger.CreateCSVDataLogger[probe.ProbeDataPoint]( 345 dataLoggerForeignFilename, 346 ) 347 if err != nil { 348 fmt.Fprintf( 349 os.Stderr, 350 "Warning: Could not create the file for storing foreign probe results (%s). Disabling functionality.\n", 351 dataLoggerForeignFilename, 352 ) 353 foreignProbeDataLogger = nil 354 } 355 uploadDirection.ForeignProbeDataLogger = foreignProbeDataLogger 356 downloadDirection.ForeignProbeDataLogger = foreignProbeDataLogger 357 358 downloadDirection.ThroughputDataLogger, err = datalogger.CreateCSVDataLogger[rpm.ThroughputDataPoint]( 359 dataLoggerDownloadThroughputFilename, 360 ) 361 if err != nil { 362 fmt.Fprintf( 363 os.Stderr, 364 "Warning: Could not create the file for storing download throughput results (%s). Disabling functionality.\n", 365 dataLoggerDownloadThroughputFilename, 366 ) 367 downloadDirection.ThroughputDataLogger = nil 368 } 369 uploadDirection.ThroughputDataLogger, err = datalogger.CreateCSVDataLogger[rpm.ThroughputDataPoint]( 370 dataLoggerUploadThroughputFilename, 371 ) 372 if err != nil { 373 fmt.Fprintf( 374 os.Stderr, 375 "Warning: Could not create the file for storing upload throughput results (%s). Disabling functionality.\n", 376 dataLoggerUploadThroughputFilename, 377 ) 378 uploadDirection.ThroughputDataLogger = nil 379 } 380 381 downloadDirection.GranularThroughputDataLogger, err = datalogger.CreateCSVDataLogger[rpm.GranularThroughputDataPoint]( 382 dataLoggerDownloadGranularThroughputFilename, 383 ) 384 if err != nil { 385 fmt.Fprintf( 386 os.Stderr, 387 "Warning: Could not create the file for storing download granular throughput results (%s). Disabling functionality.\n", 388 dataLoggerDownloadGranularThroughputFilename, 389 ) 390 downloadDirection.GranularThroughputDataLogger = nil 391 } 392 uploadDirection.GranularThroughputDataLogger, err = datalogger.CreateCSVDataLogger[rpm.GranularThroughputDataPoint]( 393 dataLoggerUploadGranularThroughputFilename, 394 ) 395 if err != nil { 396 fmt.Fprintf( 397 os.Stderr, 398 "Warning: Could not create the file for storing upload granular throughput results (%s). Disabling functionality.\n", 399 dataLoggerUploadGranularThroughputFilename, 400 ) 401 uploadDirection.GranularThroughputDataLogger = nil 402 } 403 404 } 405 // If, for some reason, the data loggers are nil, make them Null Data Loggers so that we don't have conditional 406 // code later. 407 if downloadDirection.SelfProbeDataLogger == nil { 408 downloadDirection.SelfProbeDataLogger = datalogger.CreateNullDataLogger[probe.ProbeDataPoint]() 409 } 410 if uploadDirection.SelfProbeDataLogger == nil { 411 uploadDirection.SelfProbeDataLogger = datalogger.CreateNullDataLogger[probe.ProbeDataPoint]() 412 } 413 414 if downloadDirection.ForeignProbeDataLogger == nil { 415 downloadDirection.ForeignProbeDataLogger = datalogger.CreateNullDataLogger[probe.ProbeDataPoint]() 416 } 417 if uploadDirection.ForeignProbeDataLogger == nil { 418 uploadDirection.ForeignProbeDataLogger = datalogger.CreateNullDataLogger[probe.ProbeDataPoint]() 419 } 420 421 if downloadDirection.ThroughputDataLogger == nil { 422 downloadDirection.ThroughputDataLogger = datalogger.CreateNullDataLogger[rpm.ThroughputDataPoint]() 423 } 424 if uploadDirection.ThroughputDataLogger == nil { 425 uploadDirection.ThroughputDataLogger = datalogger.CreateNullDataLogger[rpm.ThroughputDataPoint]() 426 } 427 428 if downloadDirection.GranularThroughputDataLogger == nil { 429 downloadDirection.GranularThroughputDataLogger = 430 datalogger.CreateNullDataLogger[rpm.GranularThroughputDataPoint]() 431 } 432 if uploadDirection.GranularThroughputDataLogger == nil { 433 uploadDirection.GranularThroughputDataLogger = 434 datalogger.CreateNullDataLogger[rpm.GranularThroughputDataPoint]() 435 } 436 437 /* 438 * Create (and then, ironically, name) two anonymous functions that, when invoked, 439 * will create load-generating connections for upload/download 440 */ 441 downloadDirection.CreateLgdc = func() lgc.LoadGeneratingConnection { 442 lgd := lgc.NewLoadGeneratingConnectionDownload(config.Urls.LargeUrl, 443 sslKeyFileConcurrentWriter, config.ConnectToAddr, *insecureSkipVerify, congestionControlChosen) 444 return &lgd 445 } 446 uploadDirection.CreateLgdc = func() lgc.LoadGeneratingConnection { 447 lgu := lgc.NewLoadGeneratingConnectionUpload(config.Urls.UploadUrl, 448 sslKeyFileConcurrentWriter, config.ConnectToAddr, *insecureSkipVerify, congestionControlChosen) 449 return &lgu 450 } 451 452 downloadDirection.DirectionDebugging = debug.NewDebugWithPrefix(debugLevel, "download") 453 downloadDirection.ProbeDebugging = debug.NewDebugWithPrefix(debugLevel, "download probe") 454 455 uploadDirection.DirectionDebugging = debug.NewDebugWithPrefix(debugLevel, "upload") 456 uploadDirection.ProbeDebugging = debug.NewDebugWithPrefix(debugLevel, "upload probe") 457 458 downloadDirection.Lgcc = lgc.NewLoadGeneratingConnectionCollection() 459 uploadDirection.Lgcc = lgc.NewLoadGeneratingConnectionCollection() 460 461 uploadDirection.ExtendedStatsEligible = true 462 downloadDirection.ExtendedStatsEligible = true 463 464 generateSelfProbeConfiguration := func() probe.ProbeConfiguration { 465 return probe.ProbeConfiguration{ 466 URL: config.Urls.SmallUrl, 467 ConnectToAddr: config.ConnectToAddr, 468 InsecureSkipVerify: *insecureSkipVerify, 469 CongestionControl: congestionControlChosen, 470 } 471 } 472 473 generateForeignProbeConfiguration := func() probe.ProbeConfiguration { 474 return probe.ProbeConfiguration{ 475 URL: config.Urls.SmallUrl, 476 ConnectToAddr: config.ConnectToAddr, 477 InsecureSkipVerify: *insecureSkipVerify, 478 CongestionControl: congestionControlChosen, 479 } 480 } 481 482 downloadDirection.DirectionLabel = "Download" 483 uploadDirection.DirectionLabel = "Upload" 484 485 directions := []*direction.Direction{&downloadDirection, &uploadDirection} 486 487 // print the banner 488 dt := time.Now().UTC() 489 fmt.Printf( 490 "%s UTC Go Responsiveness to %s...\n", 491 dt.Format("01-02-2006 15:04:05"), 492 configHostPort, 493 ) 494 495 if len(*profile) != 0 { 496 f, err := os.Create(*profile) 497 if err != nil { 498 fmt.Fprintf( 499 os.Stderr, 500 "Error: Profiling requested but could not open the log file ( %s ) for writing: %v\n", 501 *profile, 502 err, 503 ) 504 os.Exit(1) 505 } 506 pprof.StartCPUProfile(f) 507 defer pprof.StopCPUProfile() 508 } 509 510 globalNumericBucketGenerator := series.NewNumericBucketGenerator[uint64](0) 511 512 var baselineRpm *rpm.Rpm[float64] = nil 513 514 if *calculateRelativeRpm { 515 baselineForeignDownloadRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 516 baselineFauxSelfDownloadRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 517 baselineStableResponsiveness := false 518 baselineProbeDebugging := debug.NewDebugWithPrefix(debugLevel, "Baseline RPM Calculation Probe") 519 520 timeoutDuration := specParameters.TestTimeout 521 timeoutAbsoluteTime := time.Now().Add(timeoutDuration) 522 523 timeoutChannel := timeoutat.TimeoutAt( 524 operatingCtx, 525 timeoutAbsoluteTime, 526 debugLevel, 527 ) 528 if debug.IsDebug(debugLevel) { 529 fmt.Printf("Baseline RPM calculation will end no later than %v\n", timeoutAbsoluteTime) 530 } 531 532 baselineProberOperatorCtx, baselineProberOperatorCtxCancel := context.WithCancel(operatingCtx) 533 534 // This context is used to control the network activity (i.e., it controls all 535 // the connections that are open to do load generation and probing). 536 baselineNetworkActivityCtx, baselineNetworkActivityCtxCancel := context.WithCancel(operatingCtx) 537 538 baselineResponsivenessStabilizerDebugConfig := 539 debug.NewDebugWithPrefix(debug.Debug, "Baseline Responsiveness Stabilizer") 540 baselineResponsivenessStabilizerDebugLevel := debug.Error 541 if *debugCliFlag { 542 baselineResponsivenessStabilizerDebugLevel = debug.Debug 543 } 544 baselineResponsivenessStabilizer := stabilizer.NewStabilizer[int64, uint64]( 545 specParameters.MovingAvgDist, specParameters.StdDevTolerance, 546 specParameters.TrimmedMeanPct, "milliseconds", 547 baselineResponsivenessStabilizerDebugLevel, 548 baselineResponsivenessStabilizerDebugConfig) 549 550 baselineStabilityCheckTime := time.Now().Add(specParameters.EvalInterval) 551 baselineStabilityCheckTimeChannel := timeoutat.TimeoutAt( 552 operatingCtx, 553 baselineStabilityCheckTime, 554 debugLevel, 555 ) 556 557 responsivenessStabilizationCommunicationChannel := rpm.ResponsivenessProber( 558 baselineProberOperatorCtx, 559 baselineNetworkActivityCtx, 560 generateForeignProbeConfiguration, 561 generateSelfProbeConfiguration, 562 nil, 563 &globalNumericBucketGenerator, 564 lgc.LGC_DOWN, 565 specParameters.ProbeInterval, 566 sslKeyFileConcurrentWriter, 567 *calculateExtendedStats, 568 baselineProbeDebugging, 569 ) 570 571 lowerBucketBound, upperBucketBound := uint64(0), uint64(0) 572 baseline_responsiveness_timeout: 573 for !baselineStableResponsiveness { 574 select { 575 case probeMeasurement := <-responsivenessStabilizationCommunicationChannel: 576 { 577 switch probeMeasurement.Type { 578 case series.SeriesMessageReserve: 579 { 580 bucket := probeMeasurement.Bucket 581 if *debugCliFlag { 582 fmt.Printf("baseline: Reserving a responsiveness bucket with id %v.\n", bucket) 583 } 584 baselineResponsivenessStabilizer.Reserve(bucket) 585 baselineForeignDownloadRtts.Reserve(bucket) 586 baselineFauxSelfDownloadRtts.Reserve(bucket) 587 } 588 case series.SeriesMessageMeasure: 589 { 590 bucket := probeMeasurement.Bucket 591 measurement := utilities.GetSome(probeMeasurement.Measure) 592 foreignDataPoint := measurement.Foreign 593 594 if *debugCliFlag { 595 fmt.Printf( 596 "baseline: Filling a responsiveness bucket with id %v with value %v.\n", bucket, measurement) 597 } 598 baselineResponsivenessStabilizer.AddMeasurement( 599 bucket, foreignDataPoint.Duration.Milliseconds()) 600 601 if err := baselineForeignDownloadRtts.Fill( 602 bucket, foreignDataPoint.Duration.Seconds()); err != nil { 603 fmt.Printf("Attempting to fill a bucket (id: %d) that does not exist (baselineForeignDownloadRtts)\n", bucket) 604 } 605 if err := baselineFauxSelfDownloadRtts.Fill( 606 bucket, foreignDataPoint.Duration.Seconds()/3.0); err != nil { 607 fmt.Printf("Attempting to fill a bucket (id: %d) that does not exist (baselineFauxSelfDownloadRtts)\n", bucket) 608 } 609 } 610 } 611 } 612 case <-timeoutChannel: 613 { 614 break baseline_responsiveness_timeout 615 } 616 case <-baselineStabilityCheckTimeChannel: 617 { 618 if *debugCliFlag { 619 fmt.Printf("baseline responsiveness stability interval is complete.\n") 620 } 621 622 baselineStabilityCheckTime = time.Now().Add(specParameters.EvalInterval) 623 baselineStabilityCheckTimeChannel = timeoutat.TimeoutAt( 624 operatingCtx, 625 baselineStabilityCheckTime, 626 debugLevel, 627 ) 628 629 // Check stabilization immediately -- this could change if we wait. Not sure if the immediacy 630 // is *actually* important, but it can't hurt? 631 baselineStableResponsiveness = baselineResponsivenessStabilizer.IsStable() 632 633 if *debugCliFlag { 634 fmt.Printf( 635 "baseline responsiveness is instantaneously %s.\n", 636 utilities.Conditional(baselineStableResponsiveness, "stable", "unstable")) 637 } 638 639 // Do not tick an interval if we are stable. Doing so would expel one of the 640 // intervals that we need for our RPM calculations! 641 if !baselineStableResponsiveness { 642 baselineResponsivenessStabilizer.Interval() 643 } 644 } 645 } 646 } 647 baselineNetworkActivityCtxCancel() 648 baselineProberOperatorCtxCancel() 649 650 lowerBucketBound, upperBucketBound = baselineResponsivenessStabilizer.GetBounds() 651 652 if *debugCliFlag { 653 fmt.Printf("Baseline responsiveness stablizer bucket bounds: (%v, %v)\n", lowerBucketBound, upperBucketBound) 654 } 655 656 for _, label := range []string{"Unbounded ", ""} { 657 baselineRpm = rpm.CalculateRpm(baselineFauxSelfDownloadRtts, 658 baselineForeignDownloadRtts, specParameters.TrimmedMeanPct, specParameters.Percentile) 659 660 fmt.Printf("%vBaseline RPM: %5.0f (P%d)\n", label, baselineRpm.PNRpm, specParameters.Percentile) 661 fmt.Printf("%vBaseline RPM: %5.0f (Single-Sided %v%% Trimmed Mean)\n", 662 label, baselineRpm.MeanRpm, specParameters.TrimmedMeanPct) 663 664 baselineFauxSelfDownloadRtts.SetTrimmingBucketBounds(lowerBucketBound, upperBucketBound) 665 baselineForeignDownloadRtts.SetTrimmingBucketBounds(lowerBucketBound, upperBucketBound) 666 } 667 } 668 669 var selfRttsQualityAttenuation *qualityattenuation.SimpleQualityAttenuation = nil 670 if *printQualityAttenuation { 671 selfRttsQualityAttenuation = qualityattenuation.NewSimpleQualityAttenuation() 672 } 673 674 directionExecutionUnits := make([]executor.ExecutionUnit, 0) 675 676 for _, direction := range directions { 677 // Make a copy here to make sure that we do not get go-wierdness in our closure (see https://github.com/golang/go/discussions/56010). 678 direction := direction 679 680 directionExecutionUnit := func() { 681 timeoutDuration := specParameters.TestTimeout 682 timeoutAbsoluteTime := time.Now().Add(timeoutDuration) 683 684 timeoutChannel := timeoutat.TimeoutAt( 685 operatingCtx, 686 timeoutAbsoluteTime, 687 debugLevel, 688 ) 689 if debug.IsDebug(debugLevel) { 690 fmt.Printf("%s Test will end no later than %v\n", 691 direction.DirectionLabel, timeoutAbsoluteTime) 692 } 693 694 throughputOperatorCtx, throughputOperatorCtxCancel := context.WithCancel(operatingCtx) 695 proberOperatorCtx, proberOperatorCtxCancel := context.WithCancel(operatingCtx) 696 697 // This context is used to control the network activity (i.e., it controls all 698 // the connections that are open to do load generation and probing). Cancelling this context will close 699 // all the network connections that are responsible for generating the load. 700 probeNetworkActivityCtx, probeNetworkActivityCtxCancel := context.WithCancel(operatingCtx) 701 throughputCtx, throughputCtxCancel := context.WithCancel(operatingCtx) 702 direction.ThroughputActivityCtx, direction.ThroughputActivityCtxCancel = &throughputCtx, &throughputCtxCancel 703 704 lgStabilizationCommunicationChannel := rpm.LoadGenerator( 705 throughputOperatorCtx, 706 *direction.ThroughputActivityCtx, 707 specParameters.EvalInterval, 708 direction.CreateLgdc, 709 &direction.Lgcc, 710 &globalNumericBucketGenerator, 711 specParameters.MaxParallelConns, 712 specParameters.EvalInterval, 713 *calculateExtendedStats, 714 direction.DirectionDebugging, 715 ) 716 717 throughputStabilizerDebugConfig := debug.NewDebugWithPrefix(debug.Debug, 718 fmt.Sprintf("%v Throughput Stabilizer", direction.DirectionLabel)) 719 downloadThroughputStabilizerDebugLevel := debug.Error 720 if *debugCliFlag { 721 downloadThroughputStabilizerDebugLevel = debug.Debug 722 } 723 throughputStabilizer := stabilizer.NewStabilizer[float64, uint64]( 724 specParameters.MovingAvgDist, specParameters.StdDevTolerance, 0, "bytes", 725 downloadThroughputStabilizerDebugLevel, throughputStabilizerDebugConfig) 726 727 responsivenessStabilizerDebugConfig := debug.NewDebugWithPrefix(debug.Debug, 728 fmt.Sprintf("%v Responsiveness Stabilizer", direction.DirectionLabel)) 729 responsivenessStabilizerDebugLevel := debug.Error 730 if *debugCliFlag { 731 responsivenessStabilizerDebugLevel = debug.Debug 732 } 733 responsivenessStabilizer := stabilizer.NewStabilizer[int64, uint64]( 734 specParameters.MovingAvgDist, specParameters.StdDevTolerance, 735 specParameters.TrimmedMeanPct, "milliseconds", 736 responsivenessStabilizerDebugLevel, responsivenessStabilizerDebugConfig) 737 738 // For later debugging output, record the last throughputs on load-generating connectings 739 // and the number of open connections. 740 lastThroughputRate := float64(0) 741 lastThroughputOpenConnectionCount := int(0) 742 743 stabilityCheckTime := time.Now().Add(specParameters.EvalInterval) 744 stabilityCheckTimeChannel := timeoutat.TimeoutAt( 745 operatingCtx, 746 stabilityCheckTime, 747 debugLevel, 748 ) 749 750 lg_timeout: 751 for !direction.StableThroughput { 752 select { 753 case throughputMeasurement := <-lgStabilizationCommunicationChannel: 754 { 755 switch throughputMeasurement.Type { 756 case series.SeriesMessageReserve: 757 { 758 throughputStabilizer.Reserve(throughputMeasurement.Bucket) 759 if *debugCliFlag { 760 fmt.Printf( 761 "%s: Reserving a throughput bucket with id %v.\n", 762 direction.DirectionLabel, throughputMeasurement.Bucket) 763 } 764 } 765 case series.SeriesMessageMeasure: 766 { 767 bucket := throughputMeasurement.Bucket 768 measurement := utilities.GetSome(throughputMeasurement.Measure) 769 770 throughputStabilizer.AddMeasurement(bucket, measurement.Throughput) 771 772 direction.ThroughputDataLogger.LogRecord(measurement) 773 for _, v := range measurement.GranularThroughputDataPoints { 774 v.Direction = "Download" 775 direction.GranularThroughputDataLogger.LogRecord(v) 776 } 777 778 lastThroughputRate = measurement.Throughput 779 lastThroughputOpenConnectionCount = measurement.Connections 780 } 781 } 782 } 783 case <-stabilityCheckTimeChannel: 784 { 785 if *debugCliFlag { 786 fmt.Printf( 787 "%v throughput stability interval is complete.\n", direction.DirectionLabel) 788 } 789 stabilityCheckTime = time.Now().Add(specParameters.EvalInterval) 790 stabilityCheckTimeChannel = timeoutat.TimeoutAt( 791 operatingCtx, 792 stabilityCheckTime, 793 debugLevel, 794 ) 795 796 direction.StableThroughput = throughputStabilizer.IsStable() 797 if *debugCliFlag { 798 fmt.Printf( 799 "%v is instantaneously %s.\n", direction.DirectionLabel, 800 utilities.Conditional(direction.StableThroughput, "stable", "unstable")) 801 } 802 803 throughputStabilizer.Interval() 804 } 805 case <-timeoutChannel: 806 { 807 break lg_timeout 808 } 809 } 810 } 811 812 if direction.StableThroughput { 813 if *debugCliFlag { 814 fmt.Printf("Throughput is stable; beginning responsiveness testing.\n") 815 } 816 } else { 817 fmt.Fprintf(os.Stderr, "Warning: Throughput stability could not be reached. Making the test 15 seconds longer to calculate speculative RPM results.\n") 818 speculativeTimeoutDuration := time.Second * 15 819 speculativeAbsoluteTimeoutTime := time.Now().Add(speculativeTimeoutDuration) 820 timeoutChannel = timeoutat.TimeoutAt( 821 operatingCtx, 822 speculativeAbsoluteTimeoutTime, 823 debugLevel, 824 ) 825 } 826 827 direction.SelfRtts = series.NewWindowSeries[float64, uint64](series.Forever, 0) 828 direction.ForeignRtts = series.NewWindowSeries[float64, uint64](series.Forever, 0) 829 830 responsivenessStabilizationCommunicationChannel := rpm.ResponsivenessProber( 831 proberOperatorCtx, 832 probeNetworkActivityCtx, 833 generateForeignProbeConfiguration, 834 generateSelfProbeConfiguration, 835 &direction.Lgcc, 836 &globalNumericBucketGenerator, 837 direction.CreateLgdc().Direction(), // TODO: This could be better! 838 specParameters.ProbeInterval, 839 sslKeyFileConcurrentWriter, 840 *calculateExtendedStats, 841 direction.ProbeDebugging, 842 ) 843 844 responsiveness_timeout: 845 for !direction.StableResponsiveness { 846 select { 847 case probeMeasurement := <-responsivenessStabilizationCommunicationChannel: 848 { 849 switch probeMeasurement.Type { 850 case series.SeriesMessageReserve: 851 { 852 bucket := probeMeasurement.Bucket 853 if *debugCliFlag { 854 fmt.Printf( 855 "%s: Reserving a responsiveness bucket with id %v.\n", direction.DirectionLabel, bucket) 856 } 857 responsivenessStabilizer.Reserve(bucket) 858 direction.ForeignRtts.Reserve(bucket) 859 direction.SelfRtts.Reserve(bucket) 860 } 861 case series.SeriesMessageMeasure: 862 { 863 bucket := probeMeasurement.Bucket 864 measurement := utilities.GetSome(probeMeasurement.Measure) 865 foreignDataPoint := measurement.Foreign 866 selfDataPoint := measurement.Self 867 868 if *debugCliFlag { 869 fmt.Printf( 870 "%s: Filling a responsiveness bucket with id %v with value %v.\n", 871 direction.DirectionLabel, bucket, measurement) 872 } 873 responsivenessStabilizer.AddMeasurement(bucket, 874 (foreignDataPoint.Duration + selfDataPoint.Duration).Milliseconds()) 875 876 if err := direction.SelfRtts.Fill(bucket, 877 selfDataPoint.Duration.Seconds()); err != nil { 878 fmt.Printf("Attempting to fill a bucket (id: %d) that does not exist (perDirectionSelfRtts)\n", bucket) 879 } 880 881 if err := direction.ForeignRtts.Fill(bucket, 882 foreignDataPoint.Duration.Seconds()); err != nil { 883 fmt.Printf("Attempting to fill a bucket (id: %d) that does not exist (perDirectionForeignRtts)\n", bucket) 884 } 885 886 if selfRttsQualityAttenuation != nil { 887 selfRttsQualityAttenuation.AddSample(selfDataPoint.Duration.Seconds()) 888 } 889 890 direction.ForeignProbeDataLogger.LogRecord(*foreignDataPoint) 891 direction.SelfProbeDataLogger.LogRecord(*selfDataPoint) 892 893 } 894 } 895 } 896 case throughputMeasurement := <-lgStabilizationCommunicationChannel: 897 { 898 switch throughputMeasurement.Type { 899 case series.SeriesMessageReserve: 900 { 901 // We are no longer tracking stability, so reservation messages are useless! 902 if *debugCliFlag { 903 fmt.Printf( 904 "%s: Discarding a throughput bucket with id %v when ascertaining responsiveness.\n", 905 direction.DirectionLabel, throughputMeasurement.Bucket) 906 } 907 } 908 case series.SeriesMessageMeasure: 909 { 910 measurement := utilities.GetSome(throughputMeasurement.Measure) 911 912 if *debugCliFlag { 913 fmt.Printf("Adding a throughput measurement (while ascertaining responsiveness).\n") 914 } 915 // There may be more than one round trip accumulated together. If that is the case, 916 direction.ThroughputDataLogger.LogRecord(measurement) 917 for _, v := range measurement.GranularThroughputDataPoints { 918 v.Direction = direction.DirectionLabel 919 direction.GranularThroughputDataLogger.LogRecord(v) 920 } 921 922 lastThroughputRate = measurement.Throughput 923 lastThroughputOpenConnectionCount = measurement.Connections 924 } 925 } 926 } 927 case <-timeoutChannel: 928 { 929 if *debugCliFlag { 930 fmt.Printf("%v responsiveness seeking interval has expired.\n", direction.DirectionLabel) 931 } 932 break responsiveness_timeout 933 } 934 case <-stabilityCheckTimeChannel: 935 { 936 if *debugCliFlag { 937 fmt.Printf( 938 "%v responsiveness stability interval is complete.\n", direction.DirectionLabel) 939 } 940 941 stabilityCheckTime = time.Now().Add(specParameters.EvalInterval) 942 stabilityCheckTimeChannel = timeoutat.TimeoutAt( 943 operatingCtx, 944 stabilityCheckTime, 945 debugLevel, 946 ) 947 948 // Check stabilization immediately -- this could change if we wait. Not sure if the immediacy 949 // is *actually* important, but it can't hurt? 950 direction.StableResponsiveness = responsivenessStabilizer.IsStable() 951 952 if *debugCliFlag { 953 fmt.Printf( 954 "%v responsiveness is instantaneously %s.\n", direction.DirectionLabel, 955 utilities.Conditional(direction.StableResponsiveness, "stable", "unstable")) 956 } 957 958 // Do not tick an interval if we are stable. Doing so would expel one of the 959 // intervals that we need for our RPM calculations! 960 if !direction.StableResponsiveness { 961 responsivenessStabilizer.Interval() 962 } 963 } 964 } 965 } 966 967 // Did the test run to stability? 968 testRanToStability := direction.StableThroughput && direction.StableResponsiveness 969 970 if *debugCliFlag { 971 fmt.Printf("Stopping all the load generating data generators (stability: %s).\n", 972 utilities.Conditional(testRanToStability, "success", "failure")) 973 } 974 975 /* At this point there are 976 1. Load generators running 977 -- uploadLoadGeneratorOperatorCtx 978 -- downloadLoadGeneratorOperatorCtx 979 2. Network connections opened by those load generators: 980 -- lgNetworkActivityCtx 981 3. Probes 982 -- proberCtx 983 */ 984 985 // First, stop the load generator and the probe operators (but *not* the network activity) 986 proberOperatorCtxCancel() 987 throughputOperatorCtxCancel() 988 989 // Second, calculate the extended stats (if the user requested and they are available for the direction) 990 extendedStats := extendedstats.AggregateExtendedStats{} 991 if *calculateExtendedStats && direction.ExtendedStatsEligible { 992 if extendedstats.ExtendedStatsAvailable() { 993 func() { 994 // Put inside an IIFE so that we can use a defer! 995 direction.Lgcc.Lock.Lock() 996 defer direction.Lgcc.Lock.Unlock() 997 998 lgcCount, err := direction.Lgcc.Len() 999 if err != nil { 1000 fmt.Fprintf( 1001 os.Stderr, 1002 "Warning: Could not calculate the number of %v load-generating connections; aborting extended stats preparation.\n", direction.DirectionLabel, 1003 ) 1004 return 1005 } 1006 1007 for i := 0; i < lgcCount; i++ { 1008 // Assume that extended statistics are available -- the check was done explicitly at 1009 // program startup if the calculateExtendedStats flag was set by the user on the command line. 1010 currentLgc, _ := direction.Lgcc.Get(i) 1011 1012 if currentLgc == nil || (*currentLgc).Stats() == nil { 1013 fmt.Fprintf( 1014 os.Stderr, 1015 "Warning: Could not add extended stats for the connection: The LGC was nil or there were no stats available.\n", 1016 ) 1017 continue 1018 } 1019 if err := extendedStats.IncorporateConnectionStats( 1020 (*currentLgc).Stats().ConnInfo.Conn); err != nil { 1021 fmt.Fprintf( 1022 os.Stderr, 1023 "Warning: Could not add extended stats for the connection: %v.\n", 1024 err, 1025 ) 1026 } 1027 } 1028 }() 1029 } else { 1030 // TODO: Should we just log here? 1031 panic("Extended stats are not available but the user requested their calculation.") 1032 } 1033 } 1034 1035 // *Always* stop the probers! But, conditionally stop the througput. 1036 probeNetworkActivityCtxCancel() 1037 if parallelTestExecutionPolicy != executor.Parallel { 1038 if direction.ThroughputActivityCtxCancel == nil { 1039 panic(fmt.Sprintf("The cancellation function for the %v direction's throughput is nil!", direction.DirectionLabel)) 1040 } 1041 (*direction.ThroughputActivityCtxCancel)() 1042 } 1043 1044 direction.LowerBucketBound, direction.UpperBucketBound = responsivenessStabilizer.GetBounds() 1045 1046 // Add a header to the results 1047 direction.FormattedResults += fmt.Sprintf("%v:\n", direction.DirectionLabel) 1048 1049 if !testRanToStability { 1050 why := "" 1051 if !direction.StableThroughput { 1052 why += "throughput" 1053 } 1054 if !direction.StableResponsiveness { 1055 if len(why) != 0 { 1056 why += ", " 1057 } 1058 why += "responsiveness" 1059 } 1060 direction.FormattedResults += utilities.IndentOutput( 1061 fmt.Sprintf("Note: Test did not run to stability (%v), these results are estimates.\n", why), 1, "\t") 1062 } 1063 1064 direction.FormattedResults += utilities.IndentOutput(fmt.Sprintf( 1065 "Throughput: %.3f Mbps (%.3f MBps), using %d parallel connections.\n", 1066 utilities.ToMbps(lastThroughputRate), 1067 utilities.ToMBps(lastThroughputRate), 1068 lastThroughputOpenConnectionCount, 1069 ), 1, "\t") 1070 1071 if *calculateExtendedStats { 1072 direction.FormattedResults += utilities.IndentOutput( 1073 fmt.Sprintf("%v", extendedStats.Repr()), 1, "\t") 1074 } 1075 1076 var directionResult *rpm.Rpm[float64] = nil 1077 for _, label := range []string{"Unbounded ", ""} { 1078 directionResult = rpm.CalculateRpm(direction.SelfRtts, direction.ForeignRtts, 1079 specParameters.TrimmedMeanPct, specParameters.Percentile) 1080 if *debugCliFlag { 1081 direction.FormattedResults += utilities.IndentOutput( 1082 fmt.Sprintf("%vRPM Calculation Statistics:\n", label), 1, "\t") 1083 direction.FormattedResults += utilities.IndentOutput(directionResult.ToString(), 2, "\t") 1084 } 1085 1086 direction.SelfRtts.SetTrimmingBucketBounds( 1087 direction.LowerBucketBound, direction.UpperBucketBound) 1088 direction.ForeignRtts.SetTrimmingBucketBounds( 1089 direction.LowerBucketBound, direction.UpperBucketBound) 1090 } 1091 1092 if *debugCliFlag { 1093 direction.FormattedResults += utilities.IndentOutput( 1094 fmt.Sprintf("Bucket bounds: (%v, %v)\n", 1095 direction.LowerBucketBound, direction.UpperBucketBound), 1, "\t") 1096 } 1097 1098 if *printQualityAttenuation { 1099 direction.FormattedResults += utilities.IndentOutput( 1100 "Quality Attenuation Statistics:\n", 1, "\t") 1101 direction.FormattedResults += utilities.IndentOutput(fmt.Sprintf( 1102 ` Number of losses: %d 1103 Number of samples: %d 1104 Min: %.6fs 1105 Max: %.6fs 1106 Mean: %.6fs 1107 Variance: %.6fs 1108 Standard Deviation: %.6fs 1109 PDV(90): %.6fs 1110 PDV(99): %.6fs 1111 P(90): %.6fs 1112 P(99): %.6fs 1113 RPM: %.0f 1114 Gaming QoO: %.0f 1115 `, selfRttsQualityAttenuation.GetNumberOfLosses(), 1116 selfRttsQualityAttenuation.GetNumberOfSamples(), 1117 selfRttsQualityAttenuation.GetMinimum(), 1118 selfRttsQualityAttenuation.GetMaximum(), 1119 selfRttsQualityAttenuation.GetAverage(), 1120 selfRttsQualityAttenuation.GetVariance(), 1121 selfRttsQualityAttenuation.GetStandardDeviation(), 1122 selfRttsQualityAttenuation.GetPDV(90), 1123 selfRttsQualityAttenuation.GetPDV(99), 1124 selfRttsQualityAttenuation.GetPercentile(90), 1125 selfRttsQualityAttenuation.GetPercentile(99), 1126 selfRttsQualityAttenuation.GetRPM(), 1127 selfRttsQualityAttenuation.GetGamingQoO()), 1, "\t") 1128 } 1129 1130 direction.FormattedResults += utilities.IndentOutput(fmt.Sprintf( 1131 "RPM: %.0f (P%d)\n", directionResult.PNRpm, specParameters.Percentile), 1, "\t") 1132 direction.FormattedResults += utilities.IndentOutput(fmt.Sprintf( 1133 "RPM: %.0f (Single-Sided %v%% Trimmed Mean)\n", directionResult.MeanRpm, 1134 specParameters.TrimmedMeanPct), 1, "\t") 1135 1136 if len(*prometheusStatsFilename) > 0 { 1137 var testStable int 1138 if testRanToStability { 1139 testStable = 1 1140 } 1141 var buffer bytes.Buffer 1142 buffer.WriteString(fmt.Sprintf("networkquality_%v_test_stable %d\n", 1143 strings.ToLower(direction.DirectionLabel), testStable)) 1144 buffer.WriteString(fmt.Sprintf("networkquality_%v_p90_rpm_value %d\n", 1145 strings.ToLower(direction.DirectionLabel), int64(directionResult.PNRpm))) 1146 buffer.WriteString(fmt.Sprintf("networkquality_%v_trimmed_rpm_value %d\n", 1147 strings.ToLower(direction.DirectionLabel), 1148 int64(directionResult.MeanRpm))) 1149 1150 buffer.WriteString(fmt.Sprintf("networkquality_%v_bits_per_second %d\n", 1151 strings.ToLower(direction.DirectionLabel), int64(lastThroughputRate))) 1152 buffer.WriteString(fmt.Sprintf("networkquality_%v_connections %d\n", 1153 strings.ToLower(direction.DirectionLabel), 1154 int64(lastThroughputOpenConnectionCount))) 1155 1156 if err := os.WriteFile(*prometheusStatsFilename, buffer.Bytes(), 0o644); err != nil { 1157 fmt.Printf("could not write %s: %s", *prometheusStatsFilename, err) 1158 os.Exit(1) 1159 } 1160 } 1161 1162 direction.ThroughputDataLogger.Export() 1163 if *debugCliFlag { 1164 fmt.Printf("Closing the %v throughput data logger.\n", direction.DirectionLabel) 1165 } 1166 direction.ThroughputDataLogger.Close() 1167 1168 direction.GranularThroughputDataLogger.Export() 1169 if *debugCliFlag { 1170 fmt.Printf("Closing the %v granular throughput data logger.\n", direction.DirectionLabel) 1171 } 1172 direction.GranularThroughputDataLogger.Close() 1173 1174 if *debugCliFlag { 1175 fmt.Printf("In debugging mode, we will cool down after tests.\n") 1176 time.Sleep(constants.CooldownPeriod) 1177 fmt.Printf("Done cooling down.\n") 1178 } 1179 } 1180 directionExecutionUnits = append(directionExecutionUnits, directionExecutionUnit) 1181 } // End of direction testing. 1182 1183 waiter := executor.Execute(parallelTestExecutionPolicy, directionExecutionUnits) 1184 waiter.Wait() 1185 1186 // If we were testing in parallel mode, then the throughputs for each direction are still 1187 // running. We left them running in case one of the directions reached stability before the 1188 // other! 1189 if parallelTestExecutionPolicy == executor.Parallel { 1190 for _, direction := range directions { 1191 if *debugCliFlag { 1192 fmt.Printf("Stopping the throughput connections for the %v test.\n", direction.DirectionLabel) 1193 } 1194 if direction.ThroughputActivityCtxCancel == nil { 1195 panic(fmt.Sprintf("The cancellation function for the %v direction's throughput is nil!", direction.DirectionLabel)) 1196 } 1197 if (*direction.ThroughputActivityCtx).Err() != nil { 1198 fmt.Fprintf(os.Stderr, "Warning: The throughput for the %v direction was already cancelled but should have been ongoing.\n", direction.DirectionLabel) 1199 continue 1200 } 1201 (*direction.ThroughputActivityCtxCancel)() 1202 } 1203 } else { 1204 for _, direction := range directions { 1205 if direction.ThroughputActivityCtxCancel == nil { 1206 panic(fmt.Sprintf("The cancellation function for the %v direction's throughput is nil!", direction.DirectionLabel)) 1207 } 1208 if (*direction.ThroughputActivityCtx).Err() == nil { 1209 fmt.Fprintf(os.Stderr, "Warning: The throughput for the %v direction should have already been stopped but it was not.\n", direction.DirectionLabel) 1210 } 1211 } 1212 } 1213 1214 fmt.Printf("Results:\n") 1215 fmt.Printf("========\n") 1216 // Print out the formatted results from each of the directions. 1217 for _, direction := range directions { 1218 fmt.Print(direction.FormattedResults) 1219 fmt.Printf("========\n") 1220 } 1221 1222 if *debugCliFlag { 1223 unboundedAllSelfRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 1224 unboundedAllForeignRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 1225 1226 unboundedAllSelfRtts.Append(&downloadDirection.SelfRtts) 1227 unboundedAllSelfRtts.Append(&uploadDirection.SelfRtts) 1228 unboundedAllForeignRtts.Append(&downloadDirection.ForeignRtts) 1229 unboundedAllForeignRtts.Append(&uploadDirection.ForeignRtts) 1230 1231 result := rpm.CalculateRpm(unboundedAllSelfRtts, unboundedAllForeignRtts, 1232 specParameters.TrimmedMeanPct, specParameters.Percentile) 1233 1234 fmt.Printf("Unbounded Final RPM Calculation stats:\n%v\n", result.ToString()) 1235 1236 fmt.Printf("Unbounded Final RPM: %.0f (P%d)\n", result.PNRpm, specParameters.Percentile) 1237 fmt.Printf("Unbounded Final RPM: %.0f (Single-Sided %v%% Trimmed Mean)\n", 1238 result.MeanRpm, specParameters.TrimmedMeanPct) 1239 fmt.Printf("\n") 1240 } 1241 1242 boundedAllSelfRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 1243 boundedAllForeignRtts := series.NewWindowSeries[float64, uint64](series.Forever, 0) 1244 1245 // Now, if the test had a stable responsiveness measurement, then only consider the 1246 // probe measurements that are in the MAD intervals. On the other hand, if the test 1247 // did not stabilize, use all measurements to calculate the RPM. 1248 if downloadDirection.StableResponsiveness { 1249 boundedAllSelfRtts.BoundedAppend(&downloadDirection.SelfRtts) 1250 boundedAllForeignRtts.BoundedAppend(&downloadDirection.ForeignRtts) 1251 } else { 1252 boundedAllSelfRtts.Append(&downloadDirection.SelfRtts) 1253 boundedAllForeignRtts.Append(&downloadDirection.ForeignRtts) 1254 } 1255 if uploadDirection.StableResponsiveness { 1256 boundedAllSelfRtts.BoundedAppend(&uploadDirection.SelfRtts) 1257 boundedAllForeignRtts.BoundedAppend(&uploadDirection.ForeignRtts) 1258 } else { 1259 boundedAllSelfRtts.Append(&uploadDirection.SelfRtts) 1260 boundedAllForeignRtts.Append(&uploadDirection.ForeignRtts) 1261 } 1262 1263 result := rpm.CalculateRpm(boundedAllSelfRtts, boundedAllForeignRtts, 1264 specParameters.TrimmedMeanPct, specParameters.Percentile) 1265 1266 if *debugCliFlag { 1267 fmt.Printf("Final RPM Calculation stats:\n%v\n", result.ToString()) 1268 } 1269 1270 fmt.Printf("Final RPM: %.0f (P%d)\n", result.PNRpm, specParameters.Percentile) 1271 fmt.Printf("Final RPM: %.0f (Single-Sided %v%% Trimmed Mean)\n", 1272 result.MeanRpm, specParameters.TrimmedMeanPct) 1273 1274 if *calculateRelativeRpm { 1275 if baselineRpm == nil { 1276 fmt.Printf("User requested relative RPM calculation but an unloaded RPM was not calculated.") 1277 } else { 1278 relativeRpmFactorP := (result.PNRpm / baselineRpm.PNRpm) * 100.0 1279 relativeRpmFactorTM := (result.MeanRpm / baselineRpm.MeanRpm) * 100.0 1280 fmt.Printf("Working-Conditions Effect: Final RPM is %5.0f%% of baseline RPM (P%d)\n", 1281 relativeRpmFactorP, specParameters.Percentile) 1282 fmt.Printf("Working-Conditions Effect: Final RPM is %5.0f%% of baseline RPM (Single-Sided %v%% Trimmed Mean)\n", 1283 relativeRpmFactorTM, specParameters.TrimmedMeanPct) 1284 } 1285 } 1286 1287 // Stop the world. 1288 operatingCtxCancel() 1289 1290 // Note: We do *not* have to export/close the upload *and* download 1291 // sides of the self/foreign probe data loggers because they both 1292 // refer to the same logger. Closing/exporting one will close/export 1293 // the other. 1294 uploadDirection.SelfProbeDataLogger.Export() 1295 if *debugCliFlag { 1296 fmt.Printf("Closing the self data loggers.\n") 1297 } 1298 uploadDirection.SelfProbeDataLogger.Close() 1299 1300 uploadDirection.ForeignProbeDataLogger.Export() 1301 if *debugCliFlag { 1302 fmt.Printf("Closing the foreign data loggers.\n") 1303 } 1304 uploadDirection.ForeignProbeDataLogger.Close() 1305 }