diff --git a/agent/api/task_test.go b/agent/api/task_test.go index d75e7bcc2f5..219eff3bca5 100644 --- a/agent/api/task_test.go +++ b/agent/api/task_test.go @@ -1277,7 +1277,7 @@ func TestSetConfigHostconfigBasedOnAPIVersion(t *testing.T) { Containers: []*Container{ { Name: "c1", - CPU: uint(100), + CPU: uint(10), Memory: uint(50), }, }, @@ -1290,7 +1290,7 @@ func TestSetConfigHostconfigBasedOnAPIVersion(t *testing.T) { assert.Nil(t, cerr) assert.Equal(t, int64(50*1024*1024), config.Memory) - assert.Equal(t, int64(100), config.CPUShares) + assert.Equal(t, int64(10), config.CPUShares) assert.Empty(t, hostconfig.CPUShares) assert.Empty(t, hostconfig.Memory) @@ -1300,7 +1300,7 @@ func TestSetConfigHostconfigBasedOnAPIVersion(t *testing.T) { config, cerr = testTask.DockerConfig(testTask.Containers[0], dockerclient.Version_1_18) assert.Nil(t, err) assert.Equal(t, int64(50*1024*1024), hostconfig.Memory) - assert.Equal(t, int64(100), hostconfig.CPUShares) + assert.Equal(t, int64(10), hostconfig.CPUShares) assert.Empty(t, config.CPUShares) assert.Empty(t, config.Memory) } diff --git a/agent/api/task_windows.go b/agent/api/task_windows.go index dd273020546..1b4480f4a5b 100644 --- a/agent/api/task_windows.go +++ b/agent/api/task_windows.go @@ -27,9 +27,12 @@ import ( const ( //memorySwappinessDefault is the expected default value for this platform memorySwappinessDefault = -1 + // cpuSharesPerCore represents the cpu shares of a cpu core in docker + cpuSharesPerCore = 1024 + percentageFactor = 100 ) -var cpuShareScaleFactor = runtime.NumCPU() * 1024 +var cpuShareScaleFactor = runtime.NumCPU() * cpuSharesPerCore // adjustForPlatform makes Windows-specific changes to the task after unmarshal func (task *Task) adjustForPlatform(cfg *config.Config) { @@ -62,7 +65,12 @@ func getCanonicalPath(path string) string { // passed to Docker API. func (task *Task) platformHostConfigOverride(hostConfig *docker.HostConfig) error { task.overrideDefaultMemorySwappiness(hostConfig) - hostConfig.CPUPercent = hostConfig.CPUShares / int64(cpuShareScaleFactor) + // Convert the CPUShares to CPUPercent + hostConfig.CPUPercent = hostConfig.CPUShares * percentageFactor / int64(cpuShareScaleFactor) + if hostConfig.CPUPercent != 0 { + // Only unset the CPUShares if the CPUPercent has valid value + hostConfig.CPUShares = 0 + } return nil } diff --git a/agent/api/task_windows_test.go b/agent/api/task_windows_test.go index ea382606a82..a8a14bbc66c 100644 --- a/agent/api/task_windows_test.go +++ b/agent/api/task_windows_test.go @@ -112,11 +112,17 @@ func TestWindowsPlatformHostConfigOverride(t *testing.T) { task := &Task{} - hostConfig := &docker.HostConfig{CPUShares: 1024} + hostConfig := &docker.HostConfig{CPUShares: int64(1 * cpuSharesPerCore)} task.platformHostConfigOverride(hostConfig) - assert.Equal(t, int64(1024)/int64(cpuShareScaleFactor), hostConfig.CPUPercent) + assert.Equal(t, int64(1*cpuSharesPerCore*percentageFactor)/int64(cpuShareScaleFactor), hostConfig.CPUPercent) + assert.Equal(t, int64(0), hostConfig.CPUShares) assert.EqualValues(t, expectedMemorySwappinessDefault, hostConfig.MemorySwappiness) + + hostConfig = &docker.HostConfig{CPUShares: 10} + task.platformHostConfigOverride(hostConfig) + assert.Equal(t, int64(0), hostConfig.CPUPercent) + assert.Equal(t, int64(10), hostConfig.CPUShares) } func TestWindowsMemorySwappinessOption(t *testing.T) { diff --git a/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json b/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json index ca21837602c..b50de930cc9 100644 --- a/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json +++ b/agent/functional_tests/testdata/taskdefinitions/telemetry-windows/task-definition.json @@ -1,10 +1,9 @@ { - "family": "ecsftest-telemetry-windows", + "family": "ecsftest-windows-telemetry", "containerDefinitions": [{ - "image": "microsoft/iis:latest", - "name": "http_server", - "cpu": 100, - "memory": 500, - "command": ["powershell", "-c", "New-Item -Path C:\\inetpub\\wwwroot\\index.html -Type file -Value '
Your application is now running on a container in Amazon ECS.
'; C:\\ServiceMonitor.exe w3svc"] + "image": "amazon/amazon-ecs-windows-cpupercent-test:make", + "name": "windows-cpu-percent", + "cpu": $$$$CPUSHARE$$$$, + "memory": 500 }] } diff --git a/agent/functional_tests/tests/functionaltests_unix_test.go b/agent/functional_tests/tests/functionaltests_unix_test.go index 302a517f462..d43acf9879c 100644 --- a/agent/functional_tests/tests/functionaltests_unix_test.go +++ b/agent/functional_tests/tests/functionaltests_unix_test.go @@ -383,11 +383,11 @@ func TestTelemetry(t *testing.T) { time.Sleep(waitMetricsInCloudwatchDuration) cwclient := cloudwatch.New(session.New(), aws.NewConfig().WithRegion(*ECS.Config.Region)) - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for memory utilization failed") testTask, err := agent.StartTask(t, "telemetry") @@ -400,11 +400,11 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, false) + _, err = VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, false) + _, err = VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for memory utilization failed") err = testTask.Stop() @@ -417,11 +417,11 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped: verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped, verify metrics for memory utilization failed") } diff --git a/agent/functional_tests/tests/functionaltests_windows_test.go b/agent/functional_tests/tests/functionaltests_windows_test.go index c36f34d3a2d..b2a791c8b6f 100644 --- a/agent/functional_tests/tests/functionaltests_windows_test.go +++ b/agent/functional_tests/tests/functionaltests_windows_test.go @@ -18,6 +18,8 @@ package functional_tests import ( "fmt" "os" + "runtime" + "strconv" "strings" "testing" "time" @@ -41,6 +43,7 @@ const ( logDriverTaskDefinition = "logdriver-jsonfile-windows" cleanupTaskDefinition = "cleanup-windows" networkModeTaskDefinition = "network-mode-windows" + cpuSharesPerCore = 1024 ) // TestAWSLogsDriver verifies that container logs are sent to Amazon CloudWatch Logs with awslogs as the log driver @@ -266,14 +269,20 @@ func TestTelemetry(t *testing.T) { time.Sleep(waitMetricsInCloudwatchDuration) cwclient := cloudwatch.New(session.New(), aws.NewConfig().WithRegion(*ECS.Config.Region)) - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Before task running, verify metrics for memory utilization failed") - testTask, err := agent.StartTask(t, "telemetry-windows") + cpuNum := runtime.NumCPU() + + tdOverrides := make(map[string]string) + // Set the container cpu percentage 25% + tdOverrides["$$$$CPUSHARE$$$$"] = strconv.Itoa(int(float64(cpuNum*cpuSharesPerCore) * 0.25)) + + testTask, err := agent.StartTaskWithTaskDefinitionOverrides(t, "telemetry-windows", tdOverrides) require.NoError(t, err, "Failed to start telemetry task") // Wait for the task to run and the agent to send back metrics err = testTask.WaitRunning(waitTaskStateChangeDuration) @@ -283,11 +292,13 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, false) + metrics, err := VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for CPU utilization failed") + // Also verify the cpu usage is around 25% + assert.InDelta(t, 0.25, *metrics.Average, 0.05) params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, false) + _, err = VerifyMetrics(cwclient, params, false) assert.NoError(t, err, "Task is running, verify metrics for memory utilization failed") err = testTask.Stop() @@ -300,10 +311,10 @@ func TestTelemetry(t *testing.T) { params.EndTime = aws.Time(RoundTimeUp(time.Now(), time.Minute).UTC()) params.StartTime = aws.Time((*params.EndTime).Add(-waitMetricsInCloudwatchDuration).UTC()) params.MetricName = aws.String("CPUUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped: verify metrics for CPU utilization failed") params.MetricName = aws.String("MemoryUtilization") - err = VerifyMetrics(cwclient, params, true) + _, err = VerifyMetrics(cwclient, params, true) assert.NoError(t, err, "Task stopped, verify metrics for memory utilization failed") } diff --git a/agent/functional_tests/util/utils.go b/agent/functional_tests/util/utils.go index 90867537da8..8eb1e0f50ab 100644 --- a/agent/functional_tests/util/utils.go +++ b/agent/functional_tests/util/utils.go @@ -266,36 +266,36 @@ func DeleteCluster(t *testing.T, clusterName string) { // VerifyMetrics whether the response is as expected // the expected value can be 0 or positive -func VerifyMetrics(cwclient *cloudwatch.CloudWatch, params *cloudwatch.GetMetricStatisticsInput, idleCluster bool) error { +func VerifyMetrics(cwclient *cloudwatch.CloudWatch, params *cloudwatch.GetMetricStatisticsInput, idleCluster bool) (*cloudwatch.Datapoint, error) { resp, err := cwclient.GetMetricStatistics(params) if err != nil { - return fmt.Errorf("Error getting metrics of cluster: %v", err) + return nil, fmt.Errorf("Error getting metrics of cluster: %v", err) } if resp == nil || resp.Datapoints == nil { - return fmt.Errorf("Cloudwatch get metrics failed, returned null") + return nil, fmt.Errorf("Cloudwatch get metrics failed, returned null") } metricsCount := len(resp.Datapoints) if metricsCount == 0 { - return fmt.Errorf("No datapoints returned") + return nil, fmt.Errorf("No datapoints returned") } datapoint := resp.Datapoints[metricsCount-1] // Samplecount is always expected to be "1" for cluster metrics if *datapoint.SampleCount != 1.0 { - return fmt.Errorf("Incorrect SampleCount %f, expected 1", *datapoint.SampleCount) + return nil, fmt.Errorf("Incorrect SampleCount %f, expected 1", *datapoint.SampleCount) } if idleCluster { if *datapoint.Average != 0.0 { - return fmt.Errorf("non-zero utilization for idle cluster") + return nil, fmt.Errorf("non-zero utilization for idle cluster") } } else { if *datapoint.Average == 0.0 { - return fmt.Errorf("utilization is zero for non-idle cluster") + return nil, fmt.Errorf("utilization is zero for non-idle cluster") } } - return nil + return datapoint, nil } // ResolveTaskDockerID determines the Docker ID for a container within a given diff --git a/misc/windows-cpupercent/build.ps1 b/misc/windows-cpupercent/build.ps1 new file mode 100644 index 00000000000..529f3dcef95 --- /dev/null +++ b/misc/windows-cpupercent/build.ps1 @@ -0,0 +1,14 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +docker build -t "amazon/amazon-ecs-windows-cpupercent-test:make" -f "${PSScriptRoot}/windows.dockerfile" ${PSScriptRoot} diff --git a/misc/windows-cpupercent/main.go b/misc/windows-cpupercent/main.go new file mode 100644 index 00000000000..3b04483f2d2 --- /dev/null +++ b/misc/windows-cpupercent/main.go @@ -0,0 +1,37 @@ +// Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package main + +import ( + "crypto/md5" + "flag" + "fmt" +) + +func main() { + concurrency := flag.Int("concurrency", 1, "amount of concurrency") + flag.Parse() + neverdie := make(chan struct{}) + + fmt.Printf("Hogging CPU with concurrency %d\n", *concurrency) + for i := 0; i < *concurrency; i++ { + go func() { + md5hash := md5.New() + for { + md5hash.Write([]byte{0}) + } + }() + } + <-neverdie +} diff --git a/misc/windows-cpupercent/windows.dockerfile b/misc/windows-cpupercent/windows.dockerfile new file mode 100644 index 00000000000..482f47763eb --- /dev/null +++ b/misc/windows-cpupercent/windows.dockerfile @@ -0,0 +1,20 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You may +# not use this file except in compliance with the License. A copy of the +# License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +FROM golang:nanoserver + +WORKDIR /gopath +COPY main.go . + +RUN go build -o cpuhog main.go +ENTRYPOINT ["./cpuhog"] +CMD [ "-concurrency", "1000" ] diff --git a/scripts/run-functional-tests.ps1 b/scripts/run-functional-tests.ps1 index 7b78e046d09..20aea6af795 100644 --- a/scripts/run-functional-tests.ps1 +++ b/scripts/run-functional-tests.ps1 @@ -13,6 +13,7 @@ Invoke-Expression "${PSScriptRoot}\..\misc\windows-iam\Setup_Iam.ps1" Invoke-Expression "${PSScriptRoot}\..\misc\windows-listen80\Setup_Listen80.ps1" +Invoke-Expression "${PSScriptRoot}\..\misc\windows-cpupercent\build.ps1" # Run the tests $cwd = (pwd).Path