diff --git a/.github/actions/artifact-size-metrics/calculate-and-upload/action.yml b/.github/actions/artifact-size-metrics/calculate-and-upload/action.yml new file mode 100644 index 00000000..c64e68c6 --- /dev/null +++ b/.github/actions/artifact-size-metrics/calculate-and-upload/action.yml @@ -0,0 +1,24 @@ +name: Calculate Artifact Size +description: Calculates size for JVM artifacts + +inputs: + upload: + description: Whether the metrics should be uploaded to S3/Cloudwatch + type: boolean + release_metrics: + description: Whether the metrics are coming from a release build + type: boolean + +runs: + using: composite + steps: + - name: Calculate and upload artifact sizes + shell: bash + env: + GITHUB_REPOSITORY: ${{ github.repository }} + IDENTIFIER: ${{ github.ref_name }} + UPLOAD: ${{ inputs.upload }} + RELEASE_METRICS: ${{ inputs.release_metrics }} + run: | + chmod +x $GITHUB_ACTION_PATH/../utils/calculate-and-upload/main.sh + $GITHUB_ACTION_PATH/../utils/calculate-and-upload/main.sh diff --git a/.github/actions/artifact-size-metrics/download-and-process/action.yml b/.github/actions/artifact-size-metrics/download-and-process/action.yml new file mode 100644 index 00000000..17ae5c6f --- /dev/null +++ b/.github/actions/artifact-size-metrics/download-and-process/action.yml @@ -0,0 +1,31 @@ +name: Process artifact size metrics +description: Compares artifact size metrics, leaves a comment, and fails if a size increase of ≥5% is detected. + +inputs: + download: + description: Whether the artifact size metrics should be downloaded from S3 + type: boolean + +runs: + using: composite + steps: + # Compares artifact size metrics and sets LARGE_DIFF to true if a size increase of ≥5% is detected. + - name: Download and process artifact size metrics + shell: bash + env: + DOWNLOAD: ${{ inputs.download }} + GITHUB_REPOSITORY: ${{ github.repository }} + IDENTIFIER: ${{ github.ref_name }} + run: | + chmod +x $GITHUB_ACTION_PATH/../utils/download-and-process/main.sh + $GITHUB_ACTION_PATH/../utils/download-and-process/main.sh + + - name: Large size increase? + if: ${{ !contains(github.event.pull_request.labels.*.name, 'acknowledge-artifact-size-increase') }} + shell: bash + run: | + if [ "$LARGE_DIFF" == "true" ]; then + echo "An artifact has increased in size by more than 5%. + If this is expected, please add the acknowledge-artifact-size-increase label to this pull request." + exit 1 + fi diff --git a/.github/actions/artifact-size-metrics/show-results/action.yml b/.github/actions/artifact-size-metrics/show-results/action.yml deleted file mode 100644 index 45642f41..00000000 --- a/.github/actions/artifact-size-metrics/show-results/action.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Artifact Size Metrics - Show Results -description: Posts a new artifact analysis comment on the PR -inputs: - working-directory: - required: false - description: The working directory to use for reading the previously-generated report. - default: '' -runs: - using: "composite" - steps: - - name: Post Artifact Analysis Comment - uses: actions/github-script@v7 - with: - script: | - const workingDirectory = '${{ inputs.working-directory }}' - if (workingDirectory) { - process.chdir(workingDirectory) - } - - const fs = require('node:fs') - const prNumber = context.issue.number ?? process.env.SDK_PR - - const prInfo = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber - }) - - const hasAcknowledgeLabel = prInfo.data.labels.some(label => label.name === 'acknowledge-artifact-size-increase') - - const getComments = ` - query { - repository(owner:"${context.repo.owner}", name:"${context.repo.repo}"){ - pullRequest(number: ${prNumber}) { - id - comments(last:100) { - nodes { - id - body - author { - login - } - isMinimized - } - } - } - } - }` - - const response = await github.graphql(getComments) - const comments = response.repository.pullRequest.comments.nodes - - // Minimize outdated artifact-size comments - const mutations = comments - .filter(comment => comment.author.login == 'github-actions' && !comment.isMinimized && comment.body.startsWith('Affected Artifacts')) - .map(comment => - github.graphql(`mutation { - minimizeComment(input:{subjectId:"${comment.id}", classifier:OUTDATED}){ - clientMutationId - } - }`) - ) - await Promise.all(mutations) - - const comment = fs.readFileSync('build/reports/metrics/artifact-analysis.md', 'utf8') - - // Create a new comment with the latest artifact analysis - const writeComment = `mutation { - addComment(input:{body:"""${comment}""", subjectId:"${response.repository.pullRequest.id}"}){ - commentEdge { - node { - id - } - } - }}` - const addCommentResponse = await github.graphql(writeComment) - const newCommentId = addCommentResponse.addComment.commentEdge.node.id - - // Minimize the newly-created comment if artifact size increase is acknowledged - if (hasAcknowledgeLabel) { - await github.graphql(`mutation { - minimizeComment(input:{subjectId:"${newCommentId}", classifier:RESOLVED}){ - clientMutationId - } - }`) - } \ No newline at end of file diff --git a/.github/actions/artifact-size-metrics/utils/calculate-and-upload/calculate_metrics.sh b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/calculate_metrics.sh new file mode 100644 index 00000000..24b8973e --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/calculate_metrics.sh @@ -0,0 +1,29 @@ +# Gets artifact size metrics from staging dir +calculateArtifactSizes() { + # Artifact staging dir + input_dir="build/m2" + + # Create output_file + output_file="$1" + mkdir -p "$(dirname "$output_file")" + touch "$output_file" + + # Write CSV header + echo "Artifact, Size (Bytes)" > "$output_file" + + # Find all JARs (exclude sources and javadoc) + # TODO: Calculate KN artifacts sizes + find "$input_dir" -type f -name "*.jar" ! -name "*-sources.jar" ! -name "*-javadoc.jar" | while read -r jar; do + size=$(stat -c%s "$jar") + + # remove dir path, version, optional timestamp, and .jar + artifact=$(basename "$jar") + artifact=$(echo "$artifact" | sed -E 's/-[0-9].*\.jar$//') + + # Add artifact size to CSV + echo "$artifact, $size" >> "$output_file" + done + + # Print results for debugging + cat "$output_file" +} diff --git a/.github/actions/artifact-size-metrics/utils/calculate-and-upload/cloudwatch.sh b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/cloudwatch.sh new file mode 100644 index 00000000..f9bc5238 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/cloudwatch.sh @@ -0,0 +1,42 @@ +# Upload the artifact size metrics to cloudwatch +uploadToCloudwatch() { + metrics_file="$1" + metrics=() + + # Read CSV + while IFS=',' read -r artifactName artifactSize; do + # Skip header + [[ "$artifactName" == "Artifact" ]] && continue + + # Trim spaces + artifactName=$(echo "$artifactName" | xargs) + artifactSize=$(echo "$artifactSize" | xargs) + + # Build metric JSON + metrics+=$(jq -n \ + --arg name "$GITHUB_REPOSITORY-$artifactName" \ + --arg value "$artifactSize" \ + --arg project "$GITHUB_REPOSITORY" \ + '{ + MetricName: $name, + Timestamp: (now | todate), + Unit: "Bytes", + Value: ($value | tonumber), + Dimensions: [ + { Name: "Project", Value: $project } + ] + }' + ) + done < "$metrics_file" + + namespace="Artifact Size Metrics" + chunk_size=1000 + + # Send metrics in chunks + for ((i=0; i<${#metrics[@]}; i+=chunk_size)); do + chunk=("${metrics[@]:i:chunk_size}") + aws cloudwatch put-metric-data \ + --namespace "$namespace" \ + --metric-data "$(printf '%s\n' "${chunk[@]}" | jq -s '.')" + done +} diff --git a/.github/actions/artifact-size-metrics/utils/calculate-and-upload/main.sh b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/main.sh new file mode 100755 index 00000000..76007b91 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/calculate-and-upload/main.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Bash script to calculate, and upload artifact size metrics + +source "$(dirname "$0")/calculate_metrics.sh" +source "$(dirname "$0")/cloudwatch.sh" +source "$(dirname "$0")/../s3.sh" +source "$(dirname "$0")/../constants.sh" +source "$(dirname "$0")/../setup.sh" + +setup + +# Calculate size for artifacts in staging dir (build/m2) and save them to metrics_file +calculateArtifactSizes "$metrics_file" # see: constants.sh + +# Upload metrics to S3/cloudwatch if required +if [ "$UPLOAD" == "true" ]; then + if [ "$RELEASE_METRICS" == "true" ]; then + # For record-keeping + uploadToMetricsBucket "$metrics_file" "$GITHUB_REPOSITORY"-v"$IDENTIFIER".csv + uploadToMetricsBucket "$metrics_file" "$GITHUB_REPOSITORY"-latest.csv + + # For display in our OPS dashboard + uploadToCloudwatch "$metrics_file" "$GITHUB_REPOSITORY" + else + # For downstream consumption in pull requests + uploadToMetricsBucket "$metrics_file" [TEMP]"$GITHUB_REPOSITORY"-pull-request-"$IDENTIFIER".csv + fi +fi diff --git a/.github/actions/artifact-size-metrics/utils/constants.sh b/.github/actions/artifact-size-metrics/utils/constants.sh new file mode 100644 index 00000000..183d66d4 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/constants.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Where current metrics are stored +metrics_file="build/reports/metrics/artifact-size-metrics.csv" + +# Where metrics from latest release are stored +latest_release_metrics_file="build/reports/metrics/latest-release-artifact-size-metrics.csv" + +# Where the metrics comparison results are stored +metrics_comparison_file="build/reports/metrics/comparison.md" diff --git a/.github/actions/artifact-size-metrics/utils/download-and-process/compare.sh b/.github/actions/artifact-size-metrics/utils/download-and-process/compare.sh new file mode 100644 index 00000000..81d953ba --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/download-and-process/compare.sh @@ -0,0 +1,50 @@ +# Compares artifact size metrics to ones from the latest available release, +# stores comparison as a markdown table, +# and returns "true" if a large diff was found (over 5%) +compareMetrics() { + local metrics_file="$1" + local latest_release_metrics_file="$2" + local metrics_comparison_file="$3" + + # Title and table headers + { + echo "Affected Artifacts" + echo "=" + echo "| Artifact | Pull Request (bytes) | Latest Release (bytes) | Delta (bytes) | Delta (percentage) |" + echo "|----------|----------------------|------------------------|---------------|--------------------|" + } > "$metrics_comparison_file" + + large_diff=false + + # Read CSV + while IFS=',' read -r artifact size; do + # Skip header + [ "$artifact" = "Artifact" ] && continue + + # Trim spaces + artifact=$(echo "$artifact" | xargs) + size=$(echo "$size" | xargs) + + # Find corresponding artifact size in release file or skip + latest_release_size=$(awk -F',' -v art="$artifact" 'NR>1 && $1==art {gsub(/ /,"",$2); print $2}' "$latest_release_metrics_file") + [ -z "$latest_release_size" ] && continue + + # Find delta + delta=$((size - latest_release_size)) + abs_delta=${delta#-} + percent=$((100 * abs_delta / latest_release_size)) + + # Add to file + echo "| $artifact | $size | $latest_release_size | $delta | ${percent}% |" >> "$metrics_comparison_file" + + # Check for large diff + if [ "$percent" -gt 5 ]; then + large_diff=true + fi + done < "$metrics_file" + + # Print results for debugging + cat "$metrics_comparison_file" + + $large_diff && echo "true" +} diff --git a/.github/actions/artifact-size-metrics/utils/download-and-process/main.sh b/.github/actions/artifact-size-metrics/utils/download-and-process/main.sh new file mode 100755 index 00000000..acd56754 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/download-and-process/main.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Bash script to download, and compare artifact size metrics + +source "$(dirname "$0")/compare.sh" +source "$(dirname "$0")/../s3.sh" +source "$(dirname "$0")/../constants.sh" +source "$(dirname "$0")/../setup.sh" + +setup + +if [ "$DOWNLOAD" == "true" ]; then + # Get metrics calculated in codebuild - otherwise metrics will already be here + downloadFromMetricsBucket [TEMP]"$GITHUB_REPOSITORY"-pull-request-"$IDENTIFIER".csv "$metrics_file" # see: constants.sh +fi + +# Metrics from the latest release are never calculated here so we need to download them +downloadFromMetricsBucket "$GITHUB_REPOSITORY"-latest.csv "$latest_release_metrics_file" # see: constants.sh + +# Compare metrics +export LARGE_DIFF=$(compareMetrics "$metrics_file" "$latest_release_metrics_file" "$metrics_comparison_file") # see: constants.sh + +if [ "$LARGE_DIFF" == "true" ]; then + echo "Large diff found!" +fi diff --git a/.github/actions/artifact-size-metrics/utils/s3.sh b/.github/actions/artifact-size-metrics/utils/s3.sh new file mode 100644 index 00000000..49fdc512 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/s3.sh @@ -0,0 +1,12 @@ +# Owned by: aws-kotlin-sdk+ci +S3_ARTIFACT_SIZE_METRICS_BUCKET="artifact-size-metrics" + +# Uploads metrics to the metrics bucket under the specified file name +uploadToMetricsBucket() { + aws s3 cp "$1" s3://"$S3_ARTIFACT_SIZE_METRICS_BUCKET"/"$2" +} + +# Downloads metrics from the metrics bucket to the specified local file +downloadFromMetricsBucket() { + aws s3 cp s3://"$S3_ARTIFACT_SIZE_METRICS_BUCKET"/"$1" "$2" +} \ No newline at end of file diff --git a/.github/actions/artifact-size-metrics/utils/setup.sh b/.github/actions/artifact-size-metrics/utils/setup.sh new file mode 100644 index 00000000..27edc2c8 --- /dev/null +++ b/.github/actions/artifact-size-metrics/utils/setup.sh @@ -0,0 +1,6 @@ +# Exit if non zero exit code or if env var is missing, and enable command tracing +setup() { + set -u + set -e + set -x +} diff --git a/build-plugins/build-support/build.gradle.kts b/build-plugins/build-support/build.gradle.kts index a161f362..7ec4736b 100644 --- a/build-plugins/build-support/build.gradle.kts +++ b/build-plugins/build-support/build.gradle.kts @@ -31,15 +31,6 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) } -gradlePlugin { - plugins { - create("artifact-size-metrics") { - id = "aws.sdk.kotlin.gradle.artifactsizemetrics" - implementationClass = "aws.sdk.kotlin.gradle.plugins.artifactsizemetrics.ArtifactSizeMetricsPlugin" - } - } -} - val generateKtlintVersion by tasks.registering { // generate the version of the runtime to use as a resource. // this keeps us from having to manually change version numbers in multiple places diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/AnalyzeArtifactSizeMetrics.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/AnalyzeArtifactSizeMetrics.kt deleted file mode 100644 index 2f764764..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/AnalyzeArtifactSizeMetrics.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import aws.sdk.kotlin.services.s3.S3Client -import aws.sdk.kotlin.services.s3.model.GetObjectRequest -import aws.smithy.kotlin.runtime.content.decodeToString -import aws.smithy.kotlin.runtime.io.use -import kotlinx.coroutines.runBlocking -import org.gradle.api.DefaultTask -import org.gradle.api.GradleException -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import java.io.File -import kotlin.math.abs - -/** - * Gradle task that analyzes/compares a project's local artifact size metrics to - * ones from a project's latest GitHub release. Outputs the results into various files. - */ -internal abstract class AnalyzeArtifactSizeMetrics : DefaultTask() { - /** - * File containing the project's current computed artifact size metrics. - */ - @get:InputFile - abstract val metricsFile: RegularFileProperty - - /** - * File containing the results of analyzing the artifact size metrics. - */ - @get:OutputFile - abstract val analysisFile: RegularFileProperty - - /** - * File containing either "true" or "false". - */ - @get:OutputFile - abstract val hasSignificantChangeFile: RegularFileProperty - - init { - metricsFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv")) - analysisFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-analysis.md")) - hasSignificantChangeFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "has-significant-change.txt")) - } - - private val pluginConfig = project.rootProject.extensions.getByType(ArtifactSizeMetricsPluginConfig::class.java) - - @TaskAction - fun analyze() { - val latestReleaseMetricsFile = - project.layout.buildDirectory.file(OUTPUT_PATH + "latest-release-artifact-size-metrics.csv").get().asFile - writeLatestReleaseMetrics(latestReleaseMetricsFile) - - val latestReleaseMetrics = latestReleaseMetricsFile.toMap() - val currentMetrics = metricsFile.get().asFile.toMap() - val analysis = analyzeArtifactSizeMetrics(latestReleaseMetrics, currentMetrics) - - hasSignificantChangeFile.get().asFile.writeText(analysis.significantChange.toString()) - val diffTables = createDiffTables(analysis) - val output = if (analysis.hasDelta) diffTables else noDiffMessage - - this.analysisFile.get().asFile.writeText(output) - } - - private fun writeLatestReleaseMetrics(file: File) = runBlocking { - S3Client.fromEnvironment().use { s3 -> - s3.getObject( - GetObjectRequest { - bucket = S3_ARTIFACT_SIZE_METRICS_BUCKET - key = "${pluginConfig.projectRepositoryName}-latest-release.csv" - }, - ) { latestReleaseMetrics -> - file.writeText( - latestReleaseMetrics.body?.decodeToString() ?: throw GradleException("Metrics from latest release are empty"), - ) - } - } - } - - private fun analyzeArtifactSizeMetrics( - releaseMetrics: Map, - currentMetrics: Map, - ): ArtifactSizeMetricsAnalysis { - val artifactNames = releaseMetrics.keys + currentMetrics.keys - val artifactSizeMetrics = artifactNames.associateWith { artifact -> - val current = currentMetrics[artifact] ?: 0 - val release = releaseMetrics[artifact] ?: 0 - - val delta = current - release - val percentage = if (release == 0L) Double.NaN else delta.toDouble() / release.toDouble() * 100 - - ArtifactSizeMetric( - current, - release, - delta, - percentage, - ) - } - - val changeHappened = artifactSizeMetrics.values.any { it.delta.isNotaFluctuation() } - val significantChange = artifactSizeMetrics.values.any { - // Increase in size above threshold or new artifact - (it.percentage > pluginConfig.significantChangeThresholdPercentage) || (it.latestReleaseSize == 0L) - } - - return ArtifactSizeMetricsAnalysis(artifactSizeMetrics, significantChange, changeHappened) - } - - // There are small fluctuations in artifact size that are not real delta - private fun Long.isNotaFluctuation() = abs(this) > 5L - - private data class ArtifactSizeMetricsAnalysis( - val metrics: Map, - val significantChange: Boolean, - val hasDelta: Boolean, - ) - - private fun createDiffTables(analysis: ArtifactSizeMetricsAnalysis): String = buildString { - appendLine("Affected Artifacts\n=") - - val requiresAttention = StringBuilder() - .appendLine("Significantly increased in size") - .appendLine() - .append(tableHeader) - var artifactRequiresAttention = false - - val ordinary = StringBuilder() - .appendLine("
") // See: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections - .appendLine("Changed in size") - .appendLine() - .append(tableHeader) - var artifactOrdinaryChange = false - - analysis.metrics - .toList() - .sortedByDescending { it.second.percentage } - .toMap() - .forEach { metric -> - if (metric.value.delta.isNotaFluctuation()) { - val row = buildString { - append("|") - append(metric.key) - append("|") - if (metric.value.currentSize == 0L) append("(does not exist)") else append("%,d".format(metric.value.currentSize)) - append("|") - if (metric.value.latestReleaseSize == 0L) append("(does not exist)") else append("%,d".format(metric.value.latestReleaseSize)) - append("|") - append("%,d".format(metric.value.delta)) - append("|") - append("%.2f".format(metric.value.percentage)) - append("%") - appendLine("|") - } - - if (metric.value.requiresAttention()) { - requiresAttention.append(row) - artifactRequiresAttention = true - } else { - ordinary.append(row) - artifactOrdinaryChange = true - } - } - } - - ordinary - .appendLine() - .append("
") - - if (artifactRequiresAttention) appendLine(requiresAttention) - if (artifactOrdinaryChange) appendLine(ordinary) - } - - private fun ArtifactSizeMetric.requiresAttention() = this.percentage > pluginConfig.significantChangeThresholdPercentage || this.percentage.isNaN() - - private data class ArtifactSizeMetric( - val currentSize: Long, - val latestReleaseSize: Long, - val delta: Long, - val percentage: Double, - ) - - private fun File.toMap(): Map { - val metrics = this - .readLines() - .drop(1) // Ignoring header - .map { metricLine -> - metricLine.split(",").map { it.trim() } // e.g. ["S3-jvm.jar", "103948"] - } - - return metrics.associate { metric -> - metric[0] to metric[1].toLong() - } - } - - private val noDiffMessage = """ - Affected Artifacts - = - No artifacts changed size - """.trimIndent() - - private val tableHeader = buildString { - appendLine("| Artifact |Pull Request (bytes) | Latest Release (bytes) | Delta (bytes) | Delta (percentage) |") - appendLine("| -------- | ------------------: | ---------------------: | ------------: | -----------------: |") - } -} diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/ArtifactSizeMetricsPlugin.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/ArtifactSizeMetricsPlugin.kt deleted file mode 100644 index b4ea5bf9..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/ArtifactSizeMetricsPlugin.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import aws.sdk.kotlin.gradle.util.verifyRootProject -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.tasks.TaskProvider -import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.register -import org.gradle.kotlin.dsl.withType - -private const val TASK_GROUP = "Verification" -internal const val OUTPUT_PATH = "reports/metrics/" -internal const val S3_ARTIFACT_SIZE_METRICS_BUCKET = "artifact-size-metrics" // Owned by: aws-kotlin-sdk+ci - -/** - * Facilitates the collection and analysis of artifact size metrics via the `artifactSizeMetrics` and `analyzeArtifactSizeMetrics` gradle tasks. - * Includes additional tasks for CI to run. - */ -class ArtifactSizeMetricsPlugin : Plugin { - override fun apply(target: Project) { - target.verifyRootProject { "${this::class.java} can only be applied to the root project" } - - target.extensions.create("artifactSizeMetrics", ArtifactSizeMetricsPluginConfig::class.java) - - val tasks = mutableListOf>() - target.subprojects { tasks.add(subprojectArtifactSizeMetricsTask()) } - - target.registerRootProjectArtifactSizeMetricsTask(tasks) - - target.tasks.register("collectDelegatedArtifactSizeMetrics") { group = TASK_GROUP } - target.tasks.register("analyzeArtifactSizeMetrics") { group = TASK_GROUP } - target.tasks.register("putArtifactSizeMetricsInCloudWatch") { group = TASK_GROUP } - target.tasks.register("saveArtifactSizeMetrics") { group = TASK_GROUP } - } -} - -private fun Project.subprojectArtifactSizeMetricsTask(): TaskProvider = tasks.register("artifactSizeMetrics") { - group = TASK_GROUP - onlyIf { tasks.findByName("jvmJar") != null } - dependsOn(tasks.withType()) -} - -private fun Project.registerRootProjectArtifactSizeMetricsTask( - subProjects: List>, -) { - tasks.register("artifactSizeMetrics") { - group = TASK_GROUP - dependsOn(subProjects) - val artifactSizeMetricsFile = layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv") - outputs.file(artifactSizeMetricsFile) - - doLast { - val subProjectArtifactSizeMetrics = mutableListOf() - - subProjects - .map { it.get().metricsFile.asFile.get() } - .filter { it.exists() && it.length() > 0 } - .forEach { metricsFile -> - metricsFile - .readLines() - .drop(1) // Remove header - .forEach { metric -> - subProjectArtifactSizeMetrics.add(metric) - } - } - - val projectArtifactSizeMetrics = buildString { - val header = "Artifact, Size" - appendLine(header) - - subProjectArtifactSizeMetrics.forEach { entry -> - appendLine(entry) - } - } - - artifactSizeMetricsFile.get().asFile.writeText(projectArtifactSizeMetrics) - } - } -} - -open class ArtifactSizeMetricsPluginConfig { - /** - * Changes the prefix used to get artifact size metrics in the - * "collectDelegatedArtifactSizeMetrics" task. - */ - var bucketPrefixOverride: String? = null - - /** - * The gradle project name prefixes to collect metrics on. Check projects using "./gradlew project" - */ - var artifactPrefixes: Set = emptySet() - - /** - * The gradle project name prefixes to collect metrics on. This will consider the whole closure. - * Check projects using "./gradlew project" - */ - var closurePrefixes: Set = emptySet() - - /** - * The threshold for an acceptable artifact size increase (percentage) - */ - var significantChangeThresholdPercentage: Double = 5.0 - - /** - * The GitHub repository name for the project - */ - var projectRepositoryName: String? = null - get() { - check(!field.isNullOrEmpty()) { - missingProjectRepositoryNameMessage - } - return field - } -} - -internal val missingProjectRepositoryNameMessage = """ - Please specify a repository name in the plugin DSL for this project. - In build.gradle.kts: - - artifactSizeMetrics { - ${ArtifactSizeMetricsPluginConfig::projectRepositoryName.name} = "YOUR_REPOSITORY_NAME" - } -""".trimIndent() diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectArtifactSizeMetrics.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectArtifactSizeMetrics.kt deleted file mode 100644 index 1e4a4a5d..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectArtifactSizeMetrics.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction -import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.getByName - -/** - * Gradle task that collects artifact size metrics for configured projects and closures. - * Outputs the results to a CSV file. - */ -internal abstract class CollectArtifactSizeMetrics : DefaultTask() { - /** - * The file where the artifact size metrics will be stored, defaults to /build/reports/metrics/artifact-size-metrics.csv - */ - @get:OutputFile - abstract val metricsFile: RegularFileProperty - - init { - metricsFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv")) - } - - @TaskAction - fun collect() { - val pluginConfig = this.project.rootProject.extensions.getByType(ArtifactSizeMetricsPluginConfig::class.java) - if (pluginConfig.artifactPrefixes.none { project.path.startsWith(it) }) return - - // TODO: Start collecting metrics for KMP artifacts - val jvmJarTask = project.tasks.getByName("jvmJar") - val jarSize = jvmJarTask.archiveFile.get().asFile.length() - val artifact = buildString { - append(jvmJarTask.archiveBaseName.get()) - append("-") - append(jvmJarTask.archiveAppendix.orNull ?: "unknown") - append(".") - append(jvmJarTask.archiveExtension.get()) - } - - var closureSize: Long? = null - if (pluginConfig.closurePrefixes.any { project.path.startsWith(it) }) { - closureSize = jarSize + project.configurations.getByName("jvmRuntimeClasspath").sumOf { it.length() } - } - - val metrics = buildString { - val header = "Artifact, Size" - appendLine(header) - - append(artifact) - append(",") - appendLine(jarSize) - - if (closureSize != null) { - append("$artifact closure") - append(",") - appendLine(closureSize) - } - } - - metricsFile.asFile.get().writeText(metrics) - } -} diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectDelegatedArtifactSizeMetrics.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectDelegatedArtifactSizeMetrics.kt deleted file mode 100644 index ae8971db..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/CollectDelegatedArtifactSizeMetrics.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import aws.sdk.kotlin.gradle.util.AwsSdkGradleException -import aws.sdk.kotlin.services.s3.S3Client -import aws.sdk.kotlin.services.s3.listObjects -import aws.sdk.kotlin.services.s3.model.GetObjectRequest -import aws.smithy.kotlin.runtime.content.decodeToString -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction - -/** - * Collects the delegated artifact size metrics for a projects subprojects from S3, combines them and outputs them to a file. - * This task should typically be run after codebuild gathers metrics and puts them in S3 during CI but can also be used to - * query the metrics bucket if you modify the file key filter. - */ -internal abstract class CollectDelegatedArtifactSizeMetrics : DefaultTask() { - /** - * The file where the artifact size metrics will be stored, defaults to build/reports/metrics/artifact-size-metrics.csv - */ - @get:OutputFile - abstract val metricsFile: RegularFileProperty - - init { - metricsFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv")) - } - - private val pluginConfig = project.rootProject.extensions.getByType(ArtifactSizeMetricsPluginConfig::class.java) - - @TaskAction - fun collect() { - val pullRequestNumber = project.findProperty("pullRequest")?.toString()?.takeIf { it.isNotEmpty() } - val releaseTag = project.findProperty("release")?.toString()?.takeIf { it.isNotEmpty() } - val identifier = pullRequestNumber ?: releaseTag ?: throw AwsSdkGradleException("Please specify a pull request or release number") - - val artifactSizeMetricsFileKeys = getFileKeys(identifier) ?: throw AwsSdkGradleException("Unable to list objects from artifact size metrics bucket") - val artifactSizeMetricsFiles = getFiles(artifactSizeMetricsFileKeys) - val combined = combine(artifactSizeMetricsFiles) - - metricsFile.asFile.get().writeText(combined) - } - - private fun getFileKeys(identifier: String): List? = runBlocking { - val prefixes = pluginConfig.bucketPrefixOverride?.let { listOf(it) } ?: listOf( - "[TEMP]${pluginConfig.projectRepositoryName}-$identifier-", - "[TEMP]private-${pluginConfig.projectRepositoryName}-staging-$identifier-", // private repo metrics files have different prefix - ) - - return@runBlocking prefixes.firstNotNullOfOrNull { prefix -> - S3Client.fromEnvironment().use { s3 -> - s3.listObjects { - bucket = S3_ARTIFACT_SIZE_METRICS_BUCKET - this.prefix = prefix - }.contents?.map { - it.key ?: throw AwsSdkGradleException("A file from the artifact size metrics bucket is missing a key") - } - } - } - } - - private fun getFiles(keys: List): List = runBlocking { - S3Client.fromEnvironment().use { s3 -> - keys.map { key -> - async { - s3.getObject( - GetObjectRequest { - bucket = S3_ARTIFACT_SIZE_METRICS_BUCKET - this.key = key - }, - ) { it.body?.decodeToString() ?: throw AwsSdkGradleException("Metrics file $key is missing a body") } - } - }.awaitAll() - } - } - - private fun combine(metricsFiles: List) = buildString { - appendLine("Artifact, Size") - metricsFiles.forEach { metricsFile -> - metricsFile - .split("\n") - .drop(1) // Remove header - .forEach { metric -> - if (metric.isNotEmpty()) { - appendLine(metric) - } - } - } - } -} diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/PutArtifactSizeMetricsInCloudWatch.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/PutArtifactSizeMetricsInCloudWatch.kt deleted file mode 100644 index 73ec0628..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/PutArtifactSizeMetricsInCloudWatch.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import aws.sdk.kotlin.services.cloudwatch.CloudWatchClient -import aws.sdk.kotlin.services.cloudwatch.model.Dimension -import aws.sdk.kotlin.services.cloudwatch.model.MetricDatum -import aws.sdk.kotlin.services.cloudwatch.model.StandardUnit -import aws.sdk.kotlin.services.cloudwatch.putMetricData -import aws.smithy.kotlin.runtime.time.Instant -import kotlinx.coroutines.runBlocking -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.TaskAction - -/** - * Puts a projects local artifact size metrics in CloudWatch. - * This task should typically be run after gathering metrics for a release in our CI - */ -internal abstract class PutArtifactSizeMetricsInCloudWatch : DefaultTask() { - /** - * File containing the project's current computed artifact size metrics. - */ - @get:InputFile - abstract val metricsFile: RegularFileProperty - - init { - metricsFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv")) - } - - @TaskAction - fun put() { - val currentTime = Instant.now() - val pluginConfig = project.rootProject.extensions.getByType(ArtifactSizeMetricsPluginConfig::class.java) - - val metrics = metricsFile - .get() - .asFile - .readLines() - .drop(1) // Ignoring header - .map { metric -> - val split = metric.split(",").map { it.trim() } - val artifactName = split[0] - val artifactSize = split[1].toDouble() // CloudWatch's requires metric values to be double - - MetricDatum { - metricName = "${pluginConfig.projectRepositoryName}-$artifactName" - timestamp = currentTime - unit = StandardUnit.Bytes - value = artifactSize - dimensions = listOf( - Dimension { - name = "Project" - value = pluginConfig.projectRepositoryName - }, - ) - } - } - - runBlocking { - CloudWatchClient.fromEnvironment().use { cloudWatch -> - metrics - .chunked(1000) // CloudWatch allows up to 1000 metrics at a time - .forEach { chunk -> - cloudWatch.putMetricData { - namespace = "Artifact Size Metrics" - metricData = chunk - } - } - } - } - } -} diff --git a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/SaveArtifactSizeMetrics.kt b/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/SaveArtifactSizeMetrics.kt deleted file mode 100644 index 299fb8ca..00000000 --- a/build-plugins/build-support/src/main/kotlin/aws/sdk/kotlin/gradle/plugins/artifactsizemetrics/SaveArtifactSizeMetrics.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package aws.sdk.kotlin.gradle.plugins.artifactsizemetrics - -import aws.sdk.kotlin.gradle.util.stringPropertyNotNull -import aws.sdk.kotlin.services.s3.S3Client -import aws.sdk.kotlin.services.s3.putObject -import aws.smithy.kotlin.runtime.content.ByteStream -import kotlinx.coroutines.runBlocking -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.TaskAction - -/** - * Puts artifact size metrics in S3 to save them. - * We put them in CloudWatch also (as metrics) but CloudWatch only keeps metrics temporarily. - */ -internal abstract class SaveArtifactSizeMetrics : DefaultTask() { - /** - * File containing the project's artifact size metrics. - */ - @get:InputFile - abstract val metricsFile: RegularFileProperty - - init { - metricsFile.convention(project.layout.buildDirectory.file(OUTPUT_PATH + "artifact-size-metrics.csv")) - } - - private val pluginConfig = project.rootProject.extensions.getByType(ArtifactSizeMetricsPluginConfig::class.java) - - @TaskAction - fun save() { - val releaseTag = project.stringPropertyNotNull("release") - val artifactSizeMetrics = ByteStream.fromString(metricsFile.get().asFile.readText()) - - runBlocking { - S3Client.fromEnvironment().use { s3 -> - s3.putObject { - bucket = S3_ARTIFACT_SIZE_METRICS_BUCKET - key = "${pluginConfig.projectRepositoryName}-latest-release.csv" - body = artifactSizeMetrics - } - - s3.putObject { - bucket = S3_ARTIFACT_SIZE_METRICS_BUCKET - key = "${pluginConfig.projectRepositoryName}-$releaseTag-release.csv" - body = artifactSizeMetrics - } - } - } - } -}