Skip to content

Commit

Permalink
Fixed estimated duration to correctly account for parallelism
Browse files Browse the repository at this point in the history
  • Loading branch information
ericzbeard committed Sep 14, 2023
1 parent b677cf9 commit 5d0ea56
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 42 deletions.
4 changes: 4 additions & 0 deletions internal/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ YAML:
panic(err)
}

// Figure out how long we thing the stack will take to execute
//totalSeconds := forecast.PredictTotalEstimate(template, stackExists)
// TODO - Wait until the forecast command is GA and add this to output

// Create change set
spinner.Push("Creating change set")
changeSetName, createErr := cfn.CreateChangeSet(template, dc.Params, dc.Tags, stackName, roleArn)
Expand Down
64 changes: 33 additions & 31 deletions internal/cmd/forecast/estimate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import (
// Estimates is a map of resource type name to ResourceEstimates, which are based on historical averages
var Estimates map[string]ResourceEstimate

// EstimatesById is a map of logical ids in the stack to the estimated seconds to complete the action
var EstimatesById map[string]int

// ResourceEstimate stores the estimated time, in seconds, to create,
// update, or delete a specific resource type.
// The Name is something like "AWS::S3::Bucket"
Expand Down Expand Up @@ -115,22 +112,24 @@ func addDurations(
t cft.Template,
action StackAction,
dependencies []graph.Node,
duration *int,
parentDuration *int,
indent string,
v *Visited) {

if len(dependencies) == 0 {
return
}

maxDuration := 0

for _, d := range dependencies {
if d.Type != "Resources" {
continue
}
if v.AlreadySaw(d) {
config.Debugf("%v- already saw %v", indent, d.Name)
continue
}
//if v.AlreadySaw(d) {
// config.Debugf("%v- already saw %v", indent, d.Name)
// continue
//}
drt := getResourceType(t, d.Name)
if drt == "" {
panic(fmt.Sprintf("unexpected: no Type for %v", d.Name))
Expand All @@ -140,12 +139,17 @@ func addDurations(
config.Debugf("no estimate for %v", drt)
continue
}
config.Debugf("%v- depends on %v (%vs)", indent, d, dd)
*duration += dd
config.Debugf("%v- depends on %v (%vs)", indent, d.Name, dd)

v.AddVisited(d)
addDurations(g, t, action, g.Get(d), duration, indent+" ", v)
//v.AddVisited(d)
childDuration := dd
addDurations(g, t, action, g.Get(d), &childDuration, indent+" ", v)
if maxDuration < childDuration {
maxDuration = childDuration
}
}

*parentDuration += maxDuration
}

// PredictTotalEstimate returns the total number of seconds expected to deploy the stack.
Expand Down Expand Up @@ -219,17 +223,15 @@ func PredictTotalEstimate(t cft.Template, stackExists bool) int {
}

// FormatEstimate returns a string in human readable format to represent the number of seconds.
// For example, 61 would return "1 minute, 1 second"
// For example, 61 would return "0h, 1m, 1s"
func FormatEstimate(total int) string {
// TODO
return fmt.Sprintf("%v seconds", total)
return fmt.Sprintf("%vh, %vm, %vs", total/360, total/60, total%60)
}

// init initializes the Estimates map for all AWS resource types
func InitEstimates() {

Estimates = make(map[string]ResourceEstimate, 0)
EstimatesById = make(map[string]int, 0)

// TODO - Fill in the values with historically average create, update, delete times

Expand Down Expand Up @@ -543,7 +545,7 @@ func InitEstimates() {
Estimates["AWS::EC2::CustomerGateway"] = NewResourceEstimate("AWS::EC2::CustomerGateway", 1, 1, 1)
Estimates["AWS::EC2::DHCPOptions"] = NewResourceEstimate("AWS::EC2::DHCPOptions", 1, 1, 1)
Estimates["AWS::EC2::EC2Fleet"] = NewResourceEstimate("AWS::EC2::EC2Fleet", 1, 1, 1)
Estimates["AWS::EC2::EIP"] = NewResourceEstimate("AWS::EC2::EIP", 1, 1, 1)
Estimates["AWS::EC2::EIP"] = NewResourceEstimate("AWS::EC2::EIP", 17, 17, 17)
Estimates["AWS::EC2::EIPAssociation"] = NewResourceEstimate("AWS::EC2::EIPAssociation", 1, 1, 1)
Estimates["AWS::EC2::EgressOnlyInternetGateway"] = NewResourceEstimate("AWS::EC2::EgressOnlyInternetGateway", 1, 1, 1)
Estimates["AWS::EC2::EnclaveCertificateIamRoleAssociation"] = NewResourceEstimate("AWS::EC2::EnclaveCertificateIamRoleAssociation", 1, 1, 1)
Expand All @@ -559,14 +561,14 @@ func InitEstimates() {
Estimates["AWS::EC2::IPAMScope"] = NewResourceEstimate("AWS::EC2::IPAMScope", 1, 1, 1)
Estimates["AWS::EC2::Instance"] = NewResourceEstimate("AWS::EC2::Instance", 30, 15, 10)
Estimates["AWS::EC2::InstanceConnectEndpoint"] = NewResourceEstimate("AWS::EC2::InstanceConnectEndpoint", 1, 1, 1)
Estimates["AWS::EC2::InternetGateway"] = NewResourceEstimate("AWS::EC2::InternetGateway", 1, 1, 1)
Estimates["AWS::EC2::InternetGateway"] = NewResourceEstimate("AWS::EC2::InternetGateway", 16, 16, 16)
Estimates["AWS::EC2::KeyPair"] = NewResourceEstimate("AWS::EC2::KeyPair", 1, 1, 1)
Estimates["AWS::EC2::LaunchTemplate"] = NewResourceEstimate("AWS::EC2::LaunchTemplate", 7, 6, 5)
Estimates["AWS::EC2::LocalGatewayRoute"] = NewResourceEstimate("AWS::EC2::LocalGatewayRoute", 1, 1, 1)
Estimates["AWS::EC2::LocalGatewayRouteTable"] = NewResourceEstimate("AWS::EC2::LocalGatewayRouteTable", 1, 1, 1)
Estimates["AWS::EC2::LocalGatewayRouteTableVPCAssociation"] = NewResourceEstimate("AWS::EC2::LocalGatewayRouteTableVPCAssociation", 1, 1, 1)
Estimates["AWS::EC2::LocalGatewayRouteTableVirtualInterfaceGroupAssociation"] = NewResourceEstimate("AWS::EC2::LocalGatewayRouteTableVirtualInterfaceGroupAssociation", 1, 1, 1)
Estimates["AWS::EC2::NatGateway"] = NewResourceEstimate("AWS::EC2::NatGateway", 1, 1, 1)
Estimates["AWS::EC2::NatGateway"] = NewResourceEstimate("AWS::EC2::NatGateway", 107, 107, 107)
Estimates["AWS::EC2::NetworkAcl"] = NewResourceEstimate("AWS::EC2::NetworkAcl", 1, 1, 1)
Estimates["AWS::EC2::NetworkAclEntry"] = NewResourceEstimate("AWS::EC2::NetworkAclEntry", 1, 1, 1)
Estimates["AWS::EC2::NetworkInsightsAccessScope"] = NewResourceEstimate("AWS::EC2::NetworkInsightsAccessScope", 1, 1, 1)
Expand All @@ -580,15 +582,15 @@ func InitEstimates() {
Estimates["AWS::EC2::PlacementGroup"] = NewResourceEstimate("AWS::EC2::PlacementGroup", 1, 1, 1)
Estimates["AWS::EC2::PrefixList"] = NewResourceEstimate("AWS::EC2::PrefixList", 1, 1, 1)
Estimates["AWS::EC2::Route"] = NewResourceEstimate("AWS::EC2::Route", 1, 1, 1)
Estimates["AWS::EC2::RouteTable"] = NewResourceEstimate("AWS::EC2::RouteTable", 1, 1, 1)
Estimates["AWS::EC2::SecurityGroup"] = NewResourceEstimate("AWS::EC2::SecurityGroup", 1, 1, 1)
Estimates["AWS::EC2::RouteTable"] = NewResourceEstimate("AWS::EC2::RouteTable", 14, 14, 14)
Estimates["AWS::EC2::SecurityGroup"] = NewResourceEstimate("AWS::EC2::SecurityGroup", 6, 6, 6)
Estimates["AWS::EC2::SecurityGroupEgress"] = NewResourceEstimate("AWS::EC2::SecurityGroupEgress", 1, 1, 1)
Estimates["AWS::EC2::SecurityGroupIngress"] = NewResourceEstimate("AWS::EC2::SecurityGroupIngress", 1, 1, 1)
Estimates["AWS::EC2::SpotFleet"] = NewResourceEstimate("AWS::EC2::SpotFleet", 1, 1, 1)
Estimates["AWS::EC2::Subnet"] = NewResourceEstimate("AWS::EC2::Subnet", 1, 1, 1)
Estimates["AWS::EC2::Subnet"] = NewResourceEstimate("AWS::EC2::Subnet", 5, 5, 5)
Estimates["AWS::EC2::SubnetCidrBlock"] = NewResourceEstimate("AWS::EC2::SubnetCidrBlock", 1, 1, 1)
Estimates["AWS::EC2::SubnetNetworkAclAssociation"] = NewResourceEstimate("AWS::EC2::SubnetNetworkAclAssociation", 1, 1, 1)
Estimates["AWS::EC2::SubnetRouteTableAssociation"] = NewResourceEstimate("AWS::EC2::SubnetRouteTableAssociation", 1, 1, 1)
Estimates["AWS::EC2::SubnetRouteTableAssociation"] = NewResourceEstimate("AWS::EC2::SubnetRouteTableAssociation", 2, 2, 2)
Estimates["AWS::EC2::TrafficMirrorFilter"] = NewResourceEstimate("AWS::EC2::TrafficMirrorFilter", 1, 1, 1)
Estimates["AWS::EC2::TrafficMirrorFilterRule"] = NewResourceEstimate("AWS::EC2::TrafficMirrorFilterRule", 1, 1, 1)
Estimates["AWS::EC2::TrafficMirrorSession"] = NewResourceEstimate("AWS::EC2::TrafficMirrorSession", 1, 1, 1)
Expand All @@ -606,7 +608,7 @@ func InitEstimates() {
Estimates["AWS::EC2::TransitGatewayRouteTableAssociation"] = NewResourceEstimate("AWS::EC2::TransitGatewayRouteTableAssociation", 1, 1, 1)
Estimates["AWS::EC2::TransitGatewayRouteTablePropagation"] = NewResourceEstimate("AWS::EC2::TransitGatewayRouteTablePropagation", 1, 1, 1)
Estimates["AWS::EC2::TransitGatewayVpcAttachment"] = NewResourceEstimate("AWS::EC2::TransitGatewayVpcAttachment", 1, 1, 1)
Estimates["AWS::EC2::VPC"] = NewResourceEstimate("AWS::EC2::VPC", 1, 1, 1)
Estimates["AWS::EC2::VPC"] = NewResourceEstimate("AWS::EC2::VPC", 12, 12, 12)
Estimates["AWS::EC2::VPCCidrBlock"] = NewResourceEstimate("AWS::EC2::VPCCidrBlock", 1, 1, 1)
Estimates["AWS::EC2::VPCDHCPOptionsAssociation"] = NewResourceEstimate("AWS::EC2::VPCDHCPOptionsAssociation", 1, 1, 1)
Estimates["AWS::EC2::VPCEndpoint"] = NewResourceEstimate("AWS::EC2::VPCEndpoint", 1, 1, 1)
Expand All @@ -631,11 +633,11 @@ func InitEstimates() {
Estimates["AWS::ECR::ReplicationConfiguration"] = NewResourceEstimate("AWS::ECR::ReplicationConfiguration", 1, 1, 1)
Estimates["AWS::ECR::Repository"] = NewResourceEstimate("AWS::ECR::Repository", 1, 1, 1)
Estimates["AWS::ECS::CapacityProvider"] = NewResourceEstimate("AWS::ECS::CapacityProvider", 1, 1, 1)
Estimates["AWS::ECS::Cluster"] = NewResourceEstimate("AWS::ECS::Cluster", 1, 1, 1)
Estimates["AWS::ECS::Cluster"] = NewResourceEstimate("AWS::ECS::Cluster", 4, 4, 4)
Estimates["AWS::ECS::ClusterCapacityProviderAssociations"] = NewResourceEstimate("AWS::ECS::ClusterCapacityProviderAssociations", 1, 1, 1)
Estimates["AWS::ECS::PrimaryTaskSet"] = NewResourceEstimate("AWS::ECS::PrimaryTaskSet", 1, 1, 1)
Estimates["AWS::ECS::Service"] = NewResourceEstimate("AWS::ECS::Service", 1, 1, 1)
Estimates["AWS::ECS::TaskDefinition"] = NewResourceEstimate("AWS::ECS::TaskDefinition", 1, 1, 1)
Estimates["AWS::ECS::Service"] = NewResourceEstimate("AWS::ECS::Service", 92, 92, 92)
Estimates["AWS::ECS::TaskDefinition"] = NewResourceEstimate("AWS::ECS::TaskDefinition", 2, 2, 2)
Estimates["AWS::ECS::TaskSet"] = NewResourceEstimate("AWS::ECS::TaskSet", 1, 1, 1)
Estimates["AWS::EFS::AccessPoint"] = NewResourceEstimate("AWS::EFS::AccessPoint", 1, 1, 1)
Estimates["AWS::EFS::FileSystem"] = NewResourceEstimate("AWS::EFS::FileSystem", 1, 1, 1)
Expand Down Expand Up @@ -668,12 +670,12 @@ func InitEstimates() {
Estimates["AWS::ElasticBeanstalk::ApplicationVersion"] = NewResourceEstimate("AWS::ElasticBeanstalk::ApplicationVersion", 1, 1, 1)
Estimates["AWS::ElasticBeanstalk::ConfigurationTemplate"] = NewResourceEstimate("AWS::ElasticBeanstalk::ConfigurationTemplate", 1, 1, 1)
Estimates["AWS::ElasticBeanstalk::Environment"] = NewResourceEstimate("AWS::ElasticBeanstalk::Environment", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancing::LoadBalancer"] = NewResourceEstimate("AWS::ElasticLoadBalancing::LoadBalancer", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancingV2::Listener"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::Listener", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancing::LoadBalancer"] = NewResourceEstimate("AWS::ElasticLoadBalancing::LoadBalancer", 122, 122, 122)
Estimates["AWS::ElasticLoadBalancingV2::Listener"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::Listener", 2, 2, 2)
Estimates["AWS::ElasticLoadBalancingV2::ListenerCertificate"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::ListenerCertificate", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancingV2::ListenerRule"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::ListenerRule", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancingV2::LoadBalancer"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::LoadBalancer", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancingV2::TargetGroup"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::TargetGroup", 1, 1, 1)
Estimates["AWS::ElasticLoadBalancingV2::LoadBalancer"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::LoadBalancer", 122, 122, 122)
Estimates["AWS::ElasticLoadBalancingV2::TargetGroup"] = NewResourceEstimate("AWS::ElasticLoadBalancingV2::TargetGroup", 17, 17, 17)
Estimates["AWS::Elasticsearch::Domain"] = NewResourceEstimate("AWS::Elasticsearch::Domain", 1, 1, 1)
Estimates["AWS::EntityResolution::MatchingWorkflow"] = NewResourceEstimate("AWS::EntityResolution::MatchingWorkflow", 1, 1, 1)
Estimates["AWS::EntityResolution::SchemaMapping"] = NewResourceEstimate("AWS::EntityResolution::SchemaMapping", 1, 1, 1)
Expand Down
29 changes: 25 additions & 4 deletions internal/cmd/forecast/estimate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"testing"

"github.com/aws-cloudformation/rain/cft/parse"
"github.com/aws-cloudformation/rain/internal/config"
)

func TestResourceEstimate(t *testing.T) {
Expand All @@ -30,37 +29,59 @@ Parameters:
Resources:
# 10s
A:
Type: AWS::S3::Bucket
DependsOn: B
Properties:
BucketName: !Ref N
# 5s
B:
Type: AWS::S3::BucketPolicy
DependsOn: E
# 30s
C:
Type: AWS::EC2::Instance
DependsOn: [B, D]
DependsOn: [B, D, F, G]
# 7s
D:
Type: AWS::EC2::LaunchTemplate
DependsOn: E
# 10s
E:
Type: AWS::S3::Bucket
# 10s
F:
Type: AWS::S3::Bucket
# 10s
G:
Type: AWS::S3::Bucket
`
/*
A C
\ / \ \ \
B D F G
\ /
E
Longest is C-D-E = 47
*/
// Parse the template
tt, err := parse.String(string(template))
if err != nil {
t.Error(err)
return
}
config.Debug = true
// config.Debug = true
total := PredictTotalEstimate(tt, false)
expected := 52 // will need to adjust this when we modify the database of estimates
expected := 47 // will need to adjust this when we modify the database of estimates
if total != expected {
t.Errorf("expected total to be %v, got %v", expected, total)
}
Expand Down
3 changes: 1 addition & 2 deletions internal/cmd/forecast/forecast.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func forecastForType(input PredictionInput) Forecast {
}

// Estimate how long the action will take
// TODO - We need to figure out which resources will be created in parallel
// (This is only for spinner output, we calculate total time separately)
var action StackAction
if input.stackExists {
action = Update
Expand All @@ -150,7 +150,6 @@ func forecastForType(input PredictionInput) Forecast {
}
config.Debugf("Got resource estimate for %v: %v", input.logicalId, est)
spin(input.typeName, input.logicalId, fmt.Sprintf("estimate: %v seconds", est))
EstimatesById[input.logicalId] = est

// Call generic prediction functions that we can run against
// all resources, even if there is not a predictor.
Expand Down
23 changes: 18 additions & 5 deletions internal/cmd/logs/chart-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
border: 2px solid rgb(200,200,200);
letter-spacing: 1px;
font-size: 0.8rem;
width:90%;
width:95%;
}

td, th {
Expand All @@ -24,15 +24,19 @@
}

thead th:nth-child(1) {
width: 16%;
width: 15%;
}

thead th:nth-child(2) {
width: 7%;
width: 5%;
}

thead th:nth-child(3) {
width: 77%;
thead th:nth-child(2) {
width: 5%;
}

thead th:nth-child(4) {
width: 75%;
}

tr:nth-child(even) td {
Expand Down Expand Up @@ -69,6 +73,11 @@
width: 8%;
text-align: right;
}

.resource-type {
font-weight: normal;
font-size: small;
}
</style>
</head>
<body>
Expand All @@ -82,6 +91,7 @@ <h1><span id="stack-name-header"></span></h1>
<thead>
<tr>
<th scope="col">Resource</th>
<th scope="col">Type</th>
<th scope="col">Elapsed</th>
<th scope="col">Time</th>
</tr>
Expand All @@ -93,6 +103,7 @@ <h1><span id="stack-name-header"></span></h1>
<tfoot>
<tr>
<th scope="row">Total</th>
<th>&nbsp;</th>
<td><span id="total-elapsed-time"></span></td>
<td>
<div class="histo">
Expand Down Expand Up @@ -184,6 +195,7 @@ <h1><span id="stack-name-header"></span></h1>

const template = `
<th scope="row">RESOURCE</td>
<td><span class="resource-type">RESOURCE_TYPE</span></td>
<td>ELAPSED</td>
<td>
<div class="histo">
Expand All @@ -200,6 +212,7 @@ <h1><span id="stack-name-header"></span></h1>
r.post = ((latest - r.endts)/total)*100

let rendered = template.replace("RESOURCE", r.id)
rendered = rendered.replace("RESOURCE_TYPE", r.type)
rendered = rendered.replace("PRE", r.pre)
rendered = rendered.replace("ACTIVE", r.active)
rendered = rendered.replace("POST", r.post)
Expand Down

0 comments on commit 5d0ea56

Please sign in to comment.