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  }