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 }