App-GHGen
view release on metacpan or search on metacpan
lib/App/GHGen/CostEstimator.pm view on Meta::CPAN
=head3 FORMAL SPECIFICATION
estimate_workflow_cost : Workflow à â¤* â CostRecord
runs â estimate_runs_per_month(w)
dur â estimate_duration(w)
result â {
name ⦠w.name ?? f,
file ⦠f,
runs_per_month ⦠runs,
minutes_per_run ⦠dur,
minutes_per_month ⦠runs à dur,
}
invariant: result.minutes_per_month = result.runs_per_month à result.minutes_per_run
=cut
sub estimate_workflow_cost($workflow, $filename) {
my $name = $workflow->{name} // $filename;
# Estimate triggers per month
my $runs_per_month = estimate_runs_per_month($workflow);
# Estimate duration per run
my $minutes_per_run = estimate_duration($workflow);
# Calculate total
my $minutes_per_month = $runs_per_month * $minutes_per_run;
return {
name => $name,
file => $filename,
runs_per_month => $runs_per_month,
minutes_per_run => $minutes_per_run,
minutes_per_month => $minutes_per_month,
};
}
=head2 estimate_savings($issues, $workflows)
Estimate potential CI-minute and cost savings from resolving a set of issues.
=head3 Purpose
For each issue in C<$issues>, compute how many CI minutes per month would be
saved by fixing it. Optionally uses C<$workflows> to proportion savings
against actual current usage.
=head3 Arguments
=over 4
=item C<$issues> (ArrayRef[HashRef], required)
Array reference of issue hashes, each with at least C<type> and C<message>.
=item C<$workflows> (ArrayRef[Path::Tiny], optional, default C<[]>)
Workflow files used to compute current usage for percentage calculations.
=back
=head3 Returns
A hash reference:
{
minutes => Int, # total minutes saved per month
percentage => Int, # 0â100; 0 when no current usage available
cost => Str, # formatted as "NN.NN" (USD)
details => ArrayRef[{ description => Str, minutes => Int, issue_type => Str }],
}
=head3 Side Effects
May read workflow files from disk when C<$workflows> is non-empty.
=head3 Usage Example
my $savings = estimate_savings(\@issues, \@workflow_paths);
printf "Save %d min/month (\$%s)\n",
$savings->{minutes}, $savings->{cost};
=head3 API SPECIFICATION
=head4 Input
{
issues => { type => 'arrayref', required => 1 },
workflows => { type => 'arrayref', default => [] },
}
=head4 Output
{
type => 'hashref',
keys => {
minutes => { type => 'scalar' },
percentage => { type => 'scalar' },
cost => { type => 'scalar' },
details => { type => 'arrayref' },
},
}
=head3 FORMAL SPECIFICATION
estimate_savings : seq Issue à seq Path â SavingsSummary
savings(i) â
i.type = performance â§ i.message =~ /caching/ â 75
i.type = cost â§ i.message =~ /concurrency/ â 50 | usageÃ0.15
i.type = cost â§ i.message =~ /triggers/ â 100 | usageÃ0.25
otherwise â 0
total â â { savings(i) ⣠i â issues }
result â {
minutes ⦠floor(total),
percentage ⦠floor(total / usage à 100) | 30 (if total > 0, no usage),
cost ⦠sprintf("%.2f", total à 0.008),
details ⦠[ { description, minutes, issue_type } ⣠savings(i) > 0 ],
}
=cut
sub estimate_savings($issues, $workflows = []) {
my %savings = (
minutes => 0,
percentage => 0,
cost => 0,
details => [],
);
# Get current usage if workflows provided
my $current_usage = @$workflows ? estimate_current_usage($workflows) : undef;
for my $issue (@$issues) {
my $saving = 0;
my $description = '';
if ($issue->{type} eq 'performance') {
if ($issue->{message} =~ /caching/) {
# Caching typically saves 30-60 seconds per run
# Estimate 100 runs/month affected
$saving = 100 * 0.75; # 75 minutes
$description = 'Adding dependency caching';
}
}
elsif ($issue->{type} eq 'cost') {
if ($issue->{message} =~ /concurrency/) {
# Concurrency saves by canceling superseded runs
# Estimate 10-20% of runs are canceled
if ($current_usage) {
$saving = $current_usage->{total_minutes} * 0.15;
} else {
$saving = 50; # Conservative estimate
}
$description = 'Adding concurrency controls';
}
elsif ($issue->{message} =~ /triggers/) {
# Trigger filters reduce unnecessary runs
# Estimate 20-30% of runs avoided
if ($current_usage) {
$saving = $current_usage->{total_minutes} * 0.25;
} else {
$saving = 100; # Conservative estimate
}
$description = 'Optimizing workflow triggers';
}
}
if ($saving > 0) {
$savings{minutes} += $saving;
push @{$savings{details}}, {
description => $description,
minutes => int($saving),
issue_type => $issue->{type},
};
}
}
# Calculate percentage and cost
if ($current_usage && $current_usage->{total_minutes} > 0) {
$savings{percentage} = int(($savings{minutes} / $current_usage->{total_minutes}) * 100);
} elsif ($savings{minutes} > 0) {
$savings{percentage} = 30; # Estimate 30% savings
}
$savings{cost} = sprintf('%.2f', $savings{minutes} * 0.008);
$savings{minutes} = int($savings{minutes});
return \%savings;
}
sub estimate_runs_per_month($workflow) {
my $on = $workflow->{on} or return 50; # Default estimate
my $runs = 0;
# Parse different trigger formats
if (ref $on eq 'ARRAY') {
for my $trigger (@$on) {
$runs += estimate_trigger_frequency($trigger);
}
}
elsif (ref $on eq 'HASH') {
for my $trigger (keys %$on) {
$runs += estimate_trigger_frequency($trigger, $on->{$trigger});
}
} else {
$runs += estimate_trigger_frequency($on);
}
return $runs || 50; # Minimum estimate
}
sub estimate_trigger_frequency($trigger, $config = undef) {
# Estimates based on typical project activity
my %frequencies = (
push => 100, # ~5 pushes/day for active projects
pull_request => 60, # ~2-3 PRs/day
schedule => 30, # Depends on cron, assume daily
workflow_dispatch => 10, # Manual runs
release => 4, # ~1 per week
issues => 20, # Issue activity
);
my $base = $frequencies{$trigger} // 20;
# Adjust based on configuration
if ($config && ref $config eq 'HASH') {
# If it has branches filter, likely fewer runs
if ($config->{branches}) {
$base *= 0.6; # 40% reduction
}
# If it has paths filter, significantly fewer runs
if ($config->{paths}) {
$base *= 0.3; # 70% reduction
}
}
return int($base);
}
sub estimate_duration($workflow) {
( run in 1.805 second using v1.01-cache-2.11-cpan-84e82930d8c )