github.com/eth-easl/loader@v0.0.0-20230908084258-8a37e1d94279/pkg/driver/trace_driver_test.go (about)

     1  /*
     2   * MIT License
     3   *
     4   * Copyright (c) 2023 EASL and the vHive community
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining a copy
     7   * of this software and associated documentation files (the "Software"), to deal
     8   * in the Software without restriction, including without limitation the rights
     9   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    10   * copies of the Software, and to permit persons to whom the Software is
    11   * furnished to do so, subject to the following conditions:
    12   *
    13   * The above copyright notice and this permission notice shall be included in all
    14   * copies or substantial portions of the Software.
    15   *
    16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    22   * SOFTWARE.
    23   */
    24  
    25  package driver
    26  
    27  import (
    28  	"fmt"
    29  	"log"
    30  	"os"
    31  	"sync"
    32  	"testing"
    33  	"time"
    34  
    35  	"github.com/eth-easl/loader/pkg/common"
    36  	"github.com/eth-easl/loader/pkg/metric"
    37  	"github.com/eth-easl/loader/pkg/workload/standard"
    38  	"github.com/gocarina/gocsv"
    39  	"github.com/sirupsen/logrus"
    40  )
    41  
    42  func createTestDriver() *Driver {
    43  	cfg := createFakeLoaderConfiguration()
    44  
    45  	driver := NewDriver(&DriverConfiguration{
    46  		LoaderConfiguration: cfg,
    47  		IATDistribution:     common.Equidistant,
    48  		TraceDuration:       1,
    49  
    50  		Functions: []*common.Function{
    51  			{
    52  				Name: "test-function",
    53  				InvocationStats: &common.FunctionInvocationStats{
    54  					Invocations: []int{
    55  						5, 5, 5, 5, 5,
    56  						5, 5, 5, 5, 5,
    57  						5, 5, 5, 5, 5,
    58  						5, 5, 5, 5, 5,
    59  					},
    60  				},
    61  				RuntimeStats: &common.FunctionRuntimeStats{
    62  					Average:       50,
    63  					Count:         100,
    64  					Minimum:       0,
    65  					Maximum:       100,
    66  					Percentile0:   0,
    67  					Percentile1:   1,
    68  					Percentile25:  25,
    69  					Percentile50:  50,
    70  					Percentile75:  75,
    71  					Percentile99:  99,
    72  					Percentile100: 100,
    73  				},
    74  				MemoryStats: &common.FunctionMemoryStats{
    75  					Average:       5000,
    76  					Count:         100,
    77  					Percentile1:   100,
    78  					Percentile5:   500,
    79  					Percentile25:  2500,
    80  					Percentile50:  5000,
    81  					Percentile75:  7500,
    82  					Percentile95:  9500,
    83  					Percentile99:  9900,
    84  					Percentile100: 10000,
    85  				},
    86  			},
    87  		},
    88  		TestMode: true,
    89  	})
    90  
    91  	return driver
    92  }
    93  
    94  func TestInvokeFunctionFromDriver(t *testing.T) {
    95  	tests := []struct {
    96  		testName  string
    97  		port      int
    98  		forceFail bool
    99  	}{
   100  		{
   101  			testName:  "invoke_failure",
   102  			port:      8082,
   103  			forceFail: true,
   104  		},
   105  		{
   106  			testName:  "invoke_success",
   107  			port:      8083,
   108  			forceFail: false,
   109  		},
   110  	}
   111  
   112  	for _, test := range tests {
   113  		t.Run(test.testName, func(t *testing.T) {
   114  			var successCount int64 = 0
   115  			var failureCount int64 = 0
   116  
   117  			invocationRecordOutputChannel := make(chan interface{}, 1)
   118  			announceDone := &sync.WaitGroup{}
   119  
   120  			testDriver := createTestDriver()
   121  			var failureCountByMinute = make([]int64, testDriver.Configuration.TraceDuration)
   122  
   123  			if !test.forceFail {
   124  				address, port := "localhost", test.port
   125  				testDriver.Configuration.Functions[0].Endpoint = fmt.Sprintf("%s:%d", address, port)
   126  
   127  				go standard.StartGRPCServer(address, port, standard.TraceFunction, "")
   128  
   129  				// make sure that the gRPC server is running
   130  				time.Sleep(2 * time.Second)
   131  			}
   132  
   133  			metadata := &InvocationMetadata{
   134  				Function: testDriver.Configuration.Functions[0],
   135  				RuntimeSpecifications: &common.RuntimeSpecification{
   136  					Runtime: 1000,
   137  					Memory:  128,
   138  				},
   139  				Phase:               common.ExecutionPhase,
   140  				MinuteIndex:         0,
   141  				InvocationIndex:     2,
   142  				SuccessCount:        &successCount,
   143  				FailedCount:         &failureCount,
   144  				FailedCountByMinute: failureCountByMinute,
   145  				RecordOutputChannel: invocationRecordOutputChannel,
   146  				AnnounceDoneWG:      announceDone,
   147  			}
   148  
   149  			announceDone.Add(1)
   150  			testDriver.invokeFunction(metadata)
   151  
   152  			switch test.forceFail {
   153  			case true:
   154  				if !(successCount == 0 && failureCount == 1) {
   155  					t.Error("The function somehow managed to execute.")
   156  				}
   157  			case false:
   158  				if !(successCount == 1 && failureCount == 0) {
   159  					t.Error("The function should not have failed.")
   160  				}
   161  			}
   162  
   163  			record := (<-invocationRecordOutputChannel).(*metric.ExecutionRecord)
   164  			announceDone.Wait()
   165  
   166  			if record.Phase != int(metadata.Phase) ||
   167  				record.InvocationID != composeInvocationID(common.MinuteGranularity, metadata.MinuteIndex, metadata.InvocationIndex) {
   168  
   169  				t.Error("Invalid invocation record received.")
   170  			}
   171  		})
   172  	}
   173  }
   174  
   175  func TestGlobalMetricsCollector(t *testing.T) {
   176  	driver := createTestDriver()
   177  
   178  	inputChannel := make(chan interface{})
   179  	totalIssuedChannel := make(chan int64)
   180  	collectorReady, collectorFinished := &sync.WaitGroup{}, &sync.WaitGroup{}
   181  
   182  	collectorReady.Add(1)
   183  	collectorFinished.Add(1)
   184  
   185  	go driver.createGlobalMetricsCollector(driver.outputFilename("duration"), inputChannel, collectorReady, collectorFinished, totalIssuedChannel)
   186  	collectorReady.Wait()
   187  
   188  	bogusRecord := &metric.ExecutionRecord{
   189  		ExecutionRecordBase: metric.ExecutionRecordBase{
   190  			Phase:        int(common.ExecutionPhase),
   191  			Instance:     "",
   192  			InvocationID: "min1.inv1",
   193  			StartTime:    123456789,
   194  
   195  			RequestedDuration: 1,
   196  			ResponseTime:      2,
   197  			ActualDuration:    3,
   198  
   199  			ConnectionTimeout: false,
   200  			FunctionTimeout:   true,
   201  		},
   202  		ActualMemoryUsage: 4,
   203  	}
   204  
   205  	for i := 0; i < driver.Configuration.Functions[0].InvocationStats.Invocations[0]; i++ {
   206  		inputChannel <- bogusRecord
   207  	}
   208  
   209  	totalIssuedChannel <- int64(driver.Configuration.Functions[0].InvocationStats.Invocations[0])
   210  	collectorFinished.Wait()
   211  
   212  	f, err := os.Open(driver.outputFilename("duration"))
   213  	if err != nil {
   214  		t.Error(err)
   215  	}
   216  
   217  	var record []metric.ExecutionRecord
   218  	err = gocsv.UnmarshalFile(f, &record)
   219  	if err != nil {
   220  		log.Fatalf(err.Error())
   221  	}
   222  
   223  	for i := 0; i < driver.Configuration.Functions[0].InvocationStats.Invocations[0]; i++ {
   224  		if record[i] != *bogusRecord {
   225  			t.Error("Failed due to unexpected data received.")
   226  		}
   227  	}
   228  }
   229  
   230  func TestDriverBackgroundProcesses(t *testing.T) {
   231  	tests := []struct {
   232  		testName                 string
   233  		metricsCollectionEnabled bool
   234  	}{
   235  		{
   236  			testName:                 "without_metrics",
   237  			metricsCollectionEnabled: false,
   238  		},
   239  		{
   240  			testName:                 "with_metrics",
   241  			metricsCollectionEnabled: true,
   242  		},
   243  	}
   244  
   245  	for _, test := range tests {
   246  		t.Run(test.testName, func(t *testing.T) {
   247  			if test.metricsCollectionEnabled {
   248  				// TODO: implement testing once metrics collection feature is ready
   249  				t.Skip("Not yet implemented")
   250  			}
   251  
   252  			driver := createTestDriver()
   253  			globalCollectorAnnounceDone := &sync.WaitGroup{}
   254  
   255  			completed, _, _, _ := driver.startBackgroundProcesses(globalCollectorAnnounceDone)
   256  
   257  			completed.Wait()
   258  		})
   259  	}
   260  }
   261  
   262  func TestDriverCompletely(t *testing.T) {
   263  	tests := []struct {
   264  		testName          string
   265  		withWarmup        bool
   266  		secondGranularity bool
   267  	}{
   268  		{
   269  			testName:   "without_warmup",
   270  			withWarmup: false,
   271  		},
   272  		{
   273  			testName:   "with_warmup",
   274  			withWarmup: true,
   275  		},
   276  		{
   277  			testName:          "without_warmup_second_granularity",
   278  			withWarmup:        false,
   279  			secondGranularity: true,
   280  		},
   281  		{
   282  			testName:          "with_warmup_second_granularity",
   283  			withWarmup:        true,
   284  			secondGranularity: true,
   285  		},
   286  	}
   287  
   288  	for _, test := range tests {
   289  		t.Run(test.testName, func(t *testing.T) {
   290  			logrus.SetLevel(logrus.DebugLevel)
   291  
   292  			driver := createTestDriver()
   293  			if test.withWarmup {
   294  				driver.Configuration.LoaderConfiguration.WarmupDuration = 1
   295  				driver.Configuration.TraceDuration = 3 // 1 profiling - 1 withWarmup - 1 execution
   296  			}
   297  			if test.secondGranularity {
   298  				driver.Configuration.TraceGranularity = common.SecondGranularity
   299  			}
   300  
   301  			driver.RunExperiment(false, false)
   302  
   303  			f, err := os.Open(driver.outputFilename("duration"))
   304  			if err != nil {
   305  				t.Error(err)
   306  			}
   307  
   308  			var records []metric.ExecutionRecordBase
   309  			err = gocsv.UnmarshalFile(f, &records)
   310  			if err != nil {
   311  				log.Fatalf(err.Error())
   312  			}
   313  
   314  			successfulInvocation, failedInvocations := 0, 0
   315  			clockTolerance := int64(20_000) // ms
   316  
   317  			for i := 0; i < len(records); i++ {
   318  				record := records[i]
   319  
   320  				if test.withWarmup {
   321  					if i < 5 && record.Phase != int(common.WarmupPhase) {
   322  						t.Error("Invalid record phase in warmup.")
   323  					} else if i > 5 && record.Phase != int(common.ExecutionPhase) {
   324  						t.Error("Invalid record phase in execution phase.")
   325  					}
   326  				}
   327  
   328  				if !record.ConnectionTimeout && !record.FunctionTimeout {
   329  					successfulInvocation++
   330  				} else {
   331  					failedInvocations++
   332  				}
   333  
   334  				if i < len(records)-1 {
   335  					diff := (records[i+1].StartTime - records[i].StartTime) / 1_000_000 // ms
   336  
   337  					if diff > clockTolerance {
   338  						t.Error("Too big clock drift for the test to pass.")
   339  					}
   340  				}
   341  			}
   342  
   343  			expectedInvocations := 5
   344  			if test.withWarmup {
   345  				expectedInvocations = 10
   346  			}
   347  
   348  			if !(successfulInvocation == expectedInvocations && failedInvocations == 0) {
   349  				t.Error("Number of successful and failed invocations do not match.")
   350  			}
   351  		})
   352  	}
   353  }
   354  
   355  func TestHasMinuteExpired(t *testing.T) {
   356  	if !hasMinuteExpired(time.Now().Add(-2 * time.Minute)) {
   357  		t.Error("Time should have expired.")
   358  	}
   359  
   360  	if hasMinuteExpired(time.Now().Add(-30 * time.Second)) {
   361  		t.Error("Time shouldn't have expired.")
   362  	}
   363  }
   364  
   365  func TestRequestedVsIssued(t *testing.T) {
   366  	if !isRequestTargetAchieved(100, 100*(1-common.RequestedVsIssuedWarnThreshold+0.05), common.RequestedVsIssued) {
   367  		t.Error("Unexpected value received.")
   368  	}
   369  
   370  	if !isRequestTargetAchieved(100, 100*(1-common.RequestedVsIssuedWarnThreshold-0.05), common.RequestedVsIssued) {
   371  		t.Error("Unexpected value received.")
   372  	}
   373  
   374  	if isRequestTargetAchieved(100, 100*(1-common.RequestedVsIssuedWarnThreshold-0.15), common.RequestedVsIssued) {
   375  		t.Error("Unexpected value received.")
   376  	}
   377  
   378  	if isRequestTargetAchieved(100, 100*(common.FailedWarnThreshold-0.1), common.IssuedVsFailed) {
   379  		t.Error("Unexpected value received.")
   380  	}
   381  
   382  	if isRequestTargetAchieved(100, 100*(common.FailedWarnThreshold+0.05), common.IssuedVsFailed) {
   383  		t.Error("Unexpected value received.")
   384  	}
   385  
   386  	if isRequestTargetAchieved(100, 100*(common.FailedTerminateThreshold-0.1), common.IssuedVsFailed) {
   387  		t.Error("Unexpected value received.")
   388  	}
   389  }
   390  
   391  func TestProceedToNextMinute(t *testing.T) {
   392  	function := &common.Function{
   393  		Name: "test-function",
   394  		InvocationStats: &common.FunctionInvocationStats{
   395  			Invocations: []int{100, 100, 100, 100, 100},
   396  		},
   397  	}
   398  
   399  	tests := []struct {
   400  		testName        string
   401  		minuteIndex     int
   402  		invocationIndex int
   403  		failedCount     int64
   404  		skipMinute      bool
   405  		toBreak         bool
   406  	}{
   407  		{
   408  			testName:        "proceed_to_next_minute_no_break_no_fail",
   409  			minuteIndex:     0,
   410  			invocationIndex: 95,
   411  			failedCount:     0,
   412  			skipMinute:      false,
   413  			toBreak:         false,
   414  		},
   415  		{
   416  			testName:        "proceed_to_next_minute_break_no_fail",
   417  			minuteIndex:     0,
   418  			invocationIndex: 75,
   419  			failedCount:     0,
   420  			skipMinute:      false,
   421  			toBreak:         true,
   422  		},
   423  		{
   424  			testName:        "proceed_to_next_minute_break_with_fail",
   425  			minuteIndex:     0,
   426  			invocationIndex: 90,
   427  			failedCount:     55,
   428  			skipMinute:      false,
   429  			toBreak:         true,
   430  		},
   431  	}
   432  
   433  	for _, test := range tests {
   434  		t.Run(test.testName, func(t *testing.T) {
   435  			driver := createTestDriver()
   436  
   437  			minuteIndex := test.minuteIndex
   438  			invocationIndex := test.invocationIndex
   439  			startOfMinute := time.Now()
   440  			phase := common.ExecutionPhase
   441  			var failedCountByMinute = make([]int64, driver.Configuration.TraceDuration)
   442  			failedCountByMinute[minuteIndex] = test.failedCount
   443  			var iatSum int64 = 2500
   444  
   445  			toBreak := driver.proceedToNextMinute(function, &minuteIndex, &invocationIndex, &startOfMinute,
   446  				test.skipMinute, &phase, failedCountByMinute, &iatSum)
   447  
   448  			if toBreak != test.toBreak {
   449  				t.Error("Invalid response from minute cleanup procedure.")
   450  			}
   451  
   452  			if !toBreak && ((minuteIndex != test.minuteIndex+1) || (invocationIndex != 0) || (failedCountByMinute[test.minuteIndex] != 0) || (iatSum != 0)) {
   453  				t.Error("Invalid response from minute cleanup procedure.")
   454  			}
   455  		})
   456  	}
   457  }