github.com/eth-easl/loader@v0.0.0-20230908084258-8a37e1d94279/pkg/driver/trace_driver.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 "encoding/csv" 29 "encoding/json" 30 "fmt" 31 "math" 32 "os" 33 "sync" 34 "sync/atomic" 35 "time" 36 37 "strconv" 38 39 "github.com/eth-easl/loader/pkg/common" 40 "github.com/eth-easl/loader/pkg/config" 41 "github.com/eth-easl/loader/pkg/generator" 42 mc "github.com/eth-easl/loader/pkg/metric" 43 "github.com/eth-easl/loader/pkg/trace" 44 "github.com/gocarina/gocsv" 45 log "github.com/sirupsen/logrus" 46 ) 47 48 type DriverConfiguration struct { 49 LoaderConfiguration *config.LoaderConfiguration 50 IATDistribution common.IatDistribution 51 ShiftIAT bool // shift the invocations inside minute 52 TraceGranularity common.TraceGranularity 53 TraceDuration int // in minutes 54 55 YAMLPath string 56 TestMode bool 57 58 Functions []*common.Function 59 } 60 61 type Driver struct { 62 Configuration *DriverConfiguration 63 SpecificationGenerator *generator.SpecificationGenerator 64 } 65 66 func NewDriver(driverConfig *DriverConfiguration) *Driver { 67 return &Driver{ 68 Configuration: driverConfig, 69 SpecificationGenerator: generator.NewSpecificationGenerator(driverConfig.LoaderConfiguration.Seed), 70 } 71 } 72 73 func (c *DriverConfiguration) WithWarmup() bool { 74 if c.LoaderConfiguration.WarmupDuration > 0 { 75 return true 76 } else { 77 return false 78 } 79 } 80 81 // /////////////////////////////////////// 82 // HELPER METHODS 83 // /////////////////////////////////////// 84 func (d *Driver) outputFilename(name string) string { 85 return fmt.Sprintf("%s_%s_%d.csv", d.Configuration.LoaderConfiguration.OutputPathPrefix, name, d.Configuration.TraceDuration) 86 } 87 88 func (d *Driver) runCSVWriter(records chan interface{}, filename string, writerDone *sync.WaitGroup) { 89 log.Debugf("Starting writer for %s", filename) 90 91 file, err := os.Create(filename) 92 common.Check(err) 93 defer file.Close() 94 95 writer := gocsv.NewSafeCSVWriter(csv.NewWriter(file)) 96 if err := gocsv.MarshalChan(records, writer); err != nil { 97 log.Fatal(err) 98 } 99 100 writerDone.Done() 101 } 102 103 ///////////////////////////////////////// 104 // METRICS SCRAPPERS 105 ///////////////////////////////////////// 106 107 func (d *Driver) CreateMetricsScrapper(interval time.Duration, 108 signalReady *sync.WaitGroup, finishCh chan int, allRecordsWritten *sync.WaitGroup) func() { 109 timer := time.NewTicker(interval) 110 111 return func() { 112 signalReady.Done() 113 knStatRecords := make(chan interface{}, 100) 114 scaleRecords := make(chan interface{}, 100) 115 writerDone := sync.WaitGroup{} 116 117 clusterUsageFile, err := os.Create(d.outputFilename("cluster_usage")) 118 common.Check(err) 119 defer clusterUsageFile.Close() 120 121 writerDone.Add(1) 122 go d.runCSVWriter(knStatRecords, d.outputFilename("kn_stats"), &writerDone) 123 124 writerDone.Add(1) 125 go d.runCSVWriter(scaleRecords, d.outputFilename("deployment_scale"), &writerDone) 126 127 for { 128 select { 129 case <-timer.C: 130 recCluster := mc.ScrapeClusterUsage() 131 recCluster.Timestamp = time.Now().UnixMicro() 132 133 byteArr, err := json.Marshal(recCluster) 134 common.Check(err) 135 136 _, err = clusterUsageFile.Write(byteArr) 137 common.Check(err) 138 139 _, err = clusterUsageFile.WriteString("\n") 140 common.Check(err) 141 142 recScale := mc.ScrapeDeploymentScales() 143 timestamp := time.Now().UnixMicro() 144 for _, rec := range recScale { 145 rec.Timestamp = timestamp 146 scaleRecords <- rec 147 } 148 149 recKnative := mc.ScrapeKnStats() 150 recKnative.Timestamp = time.Now().UnixMicro() 151 knStatRecords <- recKnative 152 case <-finishCh: 153 close(knStatRecords) 154 close(scaleRecords) 155 156 writerDone.Wait() 157 allRecordsWritten.Done() 158 159 return 160 } 161 } 162 } 163 } 164 165 ///////////////////////////////////////// 166 // DRIVER LOGIC 167 ///////////////////////////////////////// 168 169 type InvocationMetadata struct { 170 Function *common.Function 171 RuntimeSpecifications *common.RuntimeSpecification 172 Phase common.ExperimentPhase 173 174 MinuteIndex int 175 InvocationIndex int 176 177 SuccessCount *int64 178 FailedCount *int64 179 FailedCountByMinute []int64 180 181 RecordOutputChannel chan interface{} 182 AnnounceDoneWG *sync.WaitGroup 183 AnnouceDoneExe *sync.WaitGroup 184 ReadOpenWhiskMetadata *sync.Mutex 185 } 186 187 func composeInvocationID(timeGranularity common.TraceGranularity, minuteIndex int, invocationIndex int) string { 188 var timePrefix string 189 190 switch timeGranularity { 191 case common.MinuteGranularity: 192 timePrefix = "min" 193 case common.SecondGranularity: 194 timePrefix = "sec" 195 default: 196 log.Fatal("Invalid trace granularity parameter.") 197 } 198 199 return fmt.Sprintf("%s%d.inv%d", timePrefix, minuteIndex, invocationIndex) 200 } 201 202 func (d *Driver) invokeFunction(metadata *InvocationMetadata) { 203 defer metadata.AnnounceDoneWG.Done() 204 205 var success bool 206 207 if d.Configuration.LoaderConfiguration.Platform == "Knative" { 208 var record *mc.ExecutionRecord 209 success, record = Invoke(metadata.Function, metadata.RuntimeSpecifications, d.Configuration.LoaderConfiguration) 210 211 record.Phase = int(metadata.Phase) 212 record.InvocationID = composeInvocationID(d.Configuration.TraceGranularity, metadata.MinuteIndex, metadata.InvocationIndex) 213 214 metadata.RecordOutputChannel <- record 215 } else if d.Configuration.LoaderConfiguration.Platform == "OpenWhisk" { 216 var record *mc.ExecutionRecordOpenWhisk 217 success, record = InvokeOpenWhisk(metadata.Function, metadata.RuntimeSpecifications, d.Configuration.LoaderConfiguration, metadata.AnnouceDoneExe, metadata.ReadOpenWhiskMetadata) 218 219 record.Phase = int(metadata.Phase) 220 record.InvocationID = composeInvocationID(d.Configuration.TraceGranularity, metadata.MinuteIndex, metadata.InvocationIndex) 221 222 metadata.RecordOutputChannel <- record 223 } 224 225 if success { 226 atomic.AddInt64(metadata.SuccessCount, 1) 227 } else { 228 atomic.AddInt64(metadata.FailedCount, 1) 229 atomic.AddInt64(&metadata.FailedCountByMinute[metadata.MinuteIndex], 1) 230 } 231 } 232 233 func (d *Driver) individualFunctionDriver(function *common.Function, announceFunctionDone *sync.WaitGroup, 234 addInvocationsToGroup *sync.WaitGroup, readOpenWhiskMetadata *sync.Mutex, totalSuccessful *int64, 235 totalFailed *int64, totalIssued *int64, recordOutputChannel chan interface{}) { 236 237 numberOfInvocations := 0 238 for i := 0; i < len(function.InvocationStats.Invocations); i++ { 239 numberOfInvocations += function.InvocationStats.Invocations[i] 240 } 241 addInvocationsToGroup.Add(numberOfInvocations) 242 243 totalTraceDuration := d.Configuration.TraceDuration 244 minuteIndex, invocationIndex := 0, 0 245 246 IAT, runtimeSpecification := function.Specification.IAT, function.Specification.RuntimeSpecification 247 248 var successfulInvocations int64 249 var failedInvocations int64 250 var failedInvocationByMinute = make([]int64, totalTraceDuration) 251 var numberOfIssuedInvocations int64 252 var currentPhase = common.ExecutionPhase 253 254 waitForInvocations := sync.WaitGroup{} 255 256 if d.Configuration.WithWarmup() { 257 currentPhase = common.WarmupPhase 258 // skip the first minute because of profiling 259 minuteIndex = 1 260 261 log.Infof("Warmup phase has started.") 262 } 263 264 startOfMinute := time.Now() 265 var previousIATSum int64 266 267 for { 268 if minuteIndex >= totalTraceDuration { 269 // Check whether the end of trace has been reached 270 break 271 } else if function.InvocationStats.Invocations[minuteIndex] == 0 { 272 // Sleep for a minute if there are no invocations 273 if d.proceedToNextMinute(function, &minuteIndex, &invocationIndex, 274 &startOfMinute, true, ¤tPhase, failedInvocationByMinute, &previousIATSum) { 275 break 276 } 277 278 switch d.Configuration.TraceGranularity { 279 case common.MinuteGranularity: 280 time.Sleep(time.Minute) 281 case common.SecondGranularity: 282 time.Sleep(time.Second) 283 default: 284 log.Fatal("Unsupported trace granularity.") 285 } 286 287 continue 288 } 289 290 iat := time.Duration(IAT[minuteIndex][invocationIndex]) * time.Microsecond 291 292 currentTime := time.Now() 293 schedulingDelay := currentTime.Sub(startOfMinute).Microseconds() - previousIATSum 294 sleepFor := iat.Microseconds() - schedulingDelay 295 time.Sleep(time.Duration(sleepFor) * time.Microsecond) 296 297 previousIATSum += iat.Microseconds() 298 299 if function.InvocationStats.Invocations[minuteIndex] == invocationIndex || hasMinuteExpired(startOfMinute) { 300 readyToBreak := d.proceedToNextMinute(function, &minuteIndex, &invocationIndex, &startOfMinute, 301 false, ¤tPhase, failedInvocationByMinute, &previousIATSum) 302 303 if readyToBreak { 304 break 305 } 306 } else { 307 if !d.Configuration.TestMode { 308 waitForInvocations.Add(1) 309 310 go d.invokeFunction(&InvocationMetadata{ 311 Function: function, 312 RuntimeSpecifications: &runtimeSpecification[minuteIndex][invocationIndex], 313 Phase: currentPhase, 314 MinuteIndex: minuteIndex, 315 InvocationIndex: invocationIndex, 316 SuccessCount: &successfulInvocations, 317 FailedCount: &failedInvocations, 318 FailedCountByMinute: failedInvocationByMinute, 319 RecordOutputChannel: recordOutputChannel, 320 AnnounceDoneWG: &waitForInvocations, 321 AnnouceDoneExe: addInvocationsToGroup, 322 ReadOpenWhiskMetadata: readOpenWhiskMetadata, 323 }) 324 } else { 325 // To be used from within the Golang testing framework 326 log.Debugf("Test mode invocation fired.\n") 327 328 recordOutputChannel <- &mc.ExecutionRecordBase{ 329 Phase: int(currentPhase), 330 InvocationID: composeInvocationID(d.Configuration.TraceGranularity, minuteIndex, invocationIndex), 331 StartTime: time.Now().UnixNano(), 332 } 333 334 successfulInvocations++ 335 } 336 numberOfIssuedInvocations++ 337 invocationIndex++ 338 } 339 } 340 341 waitForInvocations.Wait() 342 343 log.Debugf("All the invocations for function %s have been completed.\n", function.Name) 344 announceFunctionDone.Done() 345 346 atomic.AddInt64(totalSuccessful, successfulInvocations) 347 atomic.AddInt64(totalFailed, failedInvocations) 348 atomic.AddInt64(totalIssued, numberOfIssuedInvocations) 349 } 350 351 func (d *Driver) proceedToNextMinute(function *common.Function, minuteIndex *int, invocationIndex *int, startOfMinute *time.Time, 352 skipMinute bool, currentPhase *common.ExperimentPhase, failedInvocationByMinute []int64, previousIATSum *int64) bool { 353 354 if d.Configuration.TraceGranularity == common.MinuteGranularity { 355 if !isRequestTargetAchieved(function.InvocationStats.Invocations[*minuteIndex], *invocationIndex, common.RequestedVsIssued) { 356 // Not fatal because we want to keep the measurements to be written to the output file 357 log.Warnf("Relative difference between requested and issued number of invocations is greater than %.2f%%. Terminating function driver for %s!\n", common.RequestedVsIssuedTerminateThreshold*100, function.Name) 358 359 return true 360 } 361 362 for i := 0; i <= *minuteIndex; i++ { 363 notFailedCount := function.InvocationStats.Invocations[i] - int(atomic.LoadInt64(&failedInvocationByMinute[i])) 364 if !isRequestTargetAchieved(function.InvocationStats.Invocations[i], notFailedCount, common.IssuedVsFailed) { 365 // Not fatal because we want to keep the measurements to be written to the output file 366 log.Warnf("Percentage of failed request is greater than %.2f%%. Terminating function driver for %s!\n", common.FailedTerminateThreshold*100, function.Name) 367 368 return true 369 } 370 } 371 } 372 373 *minuteIndex++ 374 *invocationIndex = 0 375 *previousIATSum = 0 376 377 if d.Configuration.WithWarmup() && *minuteIndex == (d.Configuration.LoaderConfiguration.WarmupDuration+1) { 378 *currentPhase = common.ExecutionPhase 379 log.Infof("Warmup phase has finished. Starting the execution phase.") 380 } 381 382 if !skipMinute { 383 *startOfMinute = time.Now() 384 } else { 385 switch d.Configuration.TraceGranularity { 386 case common.MinuteGranularity: 387 *startOfMinute = time.Now().Add(time.Minute) 388 case common.SecondGranularity: 389 *startOfMinute = time.Now().Add(time.Second) 390 default: 391 log.Fatal("Unsupported trace granularity.") 392 } 393 } 394 395 return false 396 } 397 398 func isRequestTargetAchieved(ideal int, real int, assertType common.RuntimeAssertType) bool { 399 if ideal == 0 { 400 return true 401 } 402 403 ratio := float64(ideal-real) / float64(ideal) 404 405 var warnBound float64 406 var terminationBound float64 407 var warnMessage string 408 409 switch assertType { 410 case common.RequestedVsIssued: 411 warnBound = common.RequestedVsIssuedWarnThreshold 412 terminationBound = common.RequestedVsIssuedTerminateThreshold 413 warnMessage = fmt.Sprintf("Relative difference between requested and issued number of invocations has reached %.2f.", ratio) 414 case common.IssuedVsFailed: 415 warnBound = common.FailedWarnThreshold 416 terminationBound = common.FailedTerminateThreshold 417 warnMessage = fmt.Sprintf("Percentage of failed invocations within a minute has reached %.2f.", ratio) 418 default: 419 log.Fatal("Invalid type of assertion at runtime.") 420 } 421 422 if ratio < 0 || ratio > 1 { 423 log.Fatalf("Invalid arguments provided to runtime assertion.\n") 424 } else if ratio >= terminationBound { 425 return false 426 } 427 428 if ratio >= warnBound && ratio < terminationBound { 429 log.Warn(warnMessage) 430 } 431 432 return true 433 } 434 435 func hasMinuteExpired(t1 time.Time) bool { 436 return time.Since(t1) > time.Minute 437 } 438 439 func (d *Driver) globalTimekeeper(totalTraceDuration int, signalReady *sync.WaitGroup) { 440 ticker := time.NewTicker(time.Minute) 441 globalTimeCounter := 0 442 443 signalReady.Done() 444 445 for { 446 <-ticker.C 447 448 log.Debugf("End of minute %d\n", globalTimeCounter) 449 globalTimeCounter++ 450 if globalTimeCounter >= totalTraceDuration { 451 break 452 } 453 454 log.Debugf("Start of minute %d\n", globalTimeCounter) 455 } 456 457 ticker.Stop() 458 } 459 460 func (d *Driver) createGlobalMetricsCollector(filename string, collector chan interface{}, 461 signalReady *sync.WaitGroup, signalEverythingWritten *sync.WaitGroup, totalIssuedChannel chan int64) { 462 463 // NOTE: totalNumberOfInvocations is initialized to MaxInt64 not to allow collector to complete before 464 // the end signal is received on totalIssuedChannel, which deliver the total number of issued invocations. 465 // This number is known once all the individual function drivers finish issuing invocations and 466 // when all the invocations return 467 var totalNumberOfInvocations int64 = math.MaxInt64 468 var currentlyWritten int64 469 470 file, err := os.Create(filename) 471 common.Check(err) 472 defer file.Close() 473 474 signalReady.Done() 475 476 records := make(chan interface{}, 100) 477 writerDone := sync.WaitGroup{} 478 writerDone.Add(1) 479 go d.runCSVWriter(records, filename, &writerDone) 480 481 for { 482 select { 483 case record := <-collector: 484 records <- record 485 486 currentlyWritten++ 487 case record := <-totalIssuedChannel: 488 totalNumberOfInvocations = record 489 } 490 491 if currentlyWritten == totalNumberOfInvocations { 492 close(records) 493 writerDone.Wait() 494 (*signalEverythingWritten).Done() 495 496 return 497 } 498 } 499 } 500 501 func (d *Driver) startBackgroundProcesses(allRecordsWritten *sync.WaitGroup) (*sync.WaitGroup, chan interface{}, chan int64, chan int) { 502 auxiliaryProcessBarrier := &sync.WaitGroup{} 503 504 finishCh := make(chan int, 1) 505 506 if d.Configuration.LoaderConfiguration.EnableMetricsScrapping { 507 auxiliaryProcessBarrier.Add(1) 508 509 allRecordsWritten.Add(1) 510 metricsScrapper := d.CreateMetricsScrapper(time.Second*time.Duration(d.Configuration.LoaderConfiguration.MetricScrapingPeriodSeconds), auxiliaryProcessBarrier, finishCh, allRecordsWritten) 511 go metricsScrapper() 512 } 513 514 auxiliaryProcessBarrier.Add(2) 515 516 globalMetricsCollector := make(chan interface{}) 517 totalIssuedChannel := make(chan int64) 518 go d.createGlobalMetricsCollector(d.outputFilename("duration"), globalMetricsCollector, auxiliaryProcessBarrier, allRecordsWritten, totalIssuedChannel) 519 520 traceDurationInMinutes := d.Configuration.TraceDuration 521 go d.globalTimekeeper(traceDurationInMinutes, auxiliaryProcessBarrier) 522 523 return auxiliaryProcessBarrier, globalMetricsCollector, totalIssuedChannel, finishCh 524 } 525 526 func (d *Driver) internalRun(iatOnly bool, generated bool) { 527 var successfulInvocations int64 528 var failedInvocations int64 529 var invocationsIssued int64 530 531 readOpenWhiskMetadata := sync.Mutex{} 532 allFunctionsInvoked := sync.WaitGroup{} 533 allIndividualDriversCompleted := sync.WaitGroup{} 534 allRecordsWritten := sync.WaitGroup{} 535 allRecordsWritten.Add(1) 536 537 backgroundProcessesInitializationBarrier, globalMetricsCollector, totalIssuedChannel, scraperFinishCh := d.startBackgroundProcesses(&allRecordsWritten) 538 539 if !iatOnly { 540 log.Info("Generating IAT and runtime specifications for all the functions") 541 for i, function := range d.Configuration.Functions { 542 spec := d.SpecificationGenerator.GenerateInvocationData( 543 function, 544 d.Configuration.IATDistribution, 545 d.Configuration.ShiftIAT, 546 d.Configuration.TraceGranularity, 547 ) 548 549 d.Configuration.Functions[i].Specification = spec 550 } 551 } 552 553 backgroundProcessesInitializationBarrier.Wait() 554 555 if generated { 556 for i := range d.Configuration.Functions { 557 var spec common.FunctionSpecification 558 559 iatFile, _ := os.ReadFile("iat" + strconv.Itoa(i) + ".json") 560 err := json.Unmarshal(iatFile, &spec) 561 if err != nil { 562 log.Fatalf("Failed tu unmarshal iat file: %s", err) 563 } 564 565 d.Configuration.Functions[i].Specification = &spec 566 } 567 } 568 569 log.Infof("Starting function invocation driver\n") 570 for _, function := range d.Configuration.Functions { 571 allIndividualDriversCompleted.Add(1) 572 573 go d.individualFunctionDriver( 574 function, 575 &allIndividualDriversCompleted, 576 &allFunctionsInvoked, 577 &readOpenWhiskMetadata, 578 &successfulInvocations, 579 &failedInvocations, 580 &invocationsIssued, 581 globalMetricsCollector, 582 ) 583 } 584 585 allIndividualDriversCompleted.Wait() 586 if atomic.LoadInt64(&successfulInvocations)+atomic.LoadInt64(&failedInvocations) != 0 { 587 log.Debugf("Waiting for all the invocations record to be written.\n") 588 589 totalIssuedChannel <- atomic.LoadInt64(&invocationsIssued) 590 scraperFinishCh <- 0 // Ask the scraper to finish metrics collection 591 592 allRecordsWritten.Wait() 593 } 594 595 log.Infof("Trace has finished executing function invocation driver\n") 596 log.Infof("Number of successful invocations: \t%d\n", atomic.LoadInt64(&successfulInvocations)) 597 log.Infof("Number of failed invocations: \t%d\n", atomic.LoadInt64(&failedInvocations)) 598 } 599 600 func (d *Driver) RunExperiment(iatOnly bool, generated bool) { 601 if iatOnly { 602 log.Info("Generating IAT and runtime specifications for all the functions") 603 for i, function := range d.Configuration.Functions { 604 spec := d.SpecificationGenerator.GenerateInvocationData( 605 function, 606 d.Configuration.IATDistribution, 607 d.Configuration.ShiftIAT, 608 d.Configuration.TraceGranularity, 609 ) 610 d.Configuration.Functions[i].Specification = spec 611 612 file, _ := json.MarshalIndent(spec, "", " ") 613 err := os.WriteFile("iat"+strconv.Itoa(i)+".json", file, 0644) 614 if err != nil { 615 log.Fatalf("Writing the loader config file failed: %s", err) 616 } 617 } 618 619 return 620 } 621 622 if d.Configuration.WithWarmup() { 623 trace.DoStaticTraceProfiling(d.Configuration.Functions) 624 } 625 626 trace.ApplyResourceLimits(d.Configuration.Functions) 627 628 if d.Configuration.LoaderConfiguration.Platform == "Knative" { 629 DeployFunctionsKnative(d.Configuration.Functions, 630 d.Configuration.YAMLPath, 631 d.Configuration.LoaderConfiguration.IsPartiallyPanic, 632 d.Configuration.LoaderConfiguration.EndpointPort, 633 d.Configuration.LoaderConfiguration.AutoscalingMetric) 634 } else if d.Configuration.LoaderConfiguration.Platform == "OpenWhisk" { 635 DeployFunctionsOpenWhisk(d.Configuration.Functions) 636 } 637 638 d.internalRun(iatOnly, generated) 639 640 if d.Configuration.LoaderConfiguration.Platform == "Knative" { 641 CleanKnative() 642 } else if d.Configuration.LoaderConfiguration.Platform == "OpenWhisk" { 643 CleanOpenWhisk(d.Configuration.Functions) 644 } 645 }