mirror of
https://github.com/rust-lang/rust.git
synced 2025-06-04 19:29:07 +00:00
Rollup merge of #139481 - Kobzol:post-merge-links, r=marcoieni
Add job summary links to post-merge report This should make it much easier to investigate the individual job test/duration changes. The GitHub API handling is a bit crude, but I didn't want to include octocrab, because it more than doubles the current number of dependencies of `citool`... Can be tested with: ```bash $ cargo run --manifest-path src/ci/citool/Cargo.toml post-merge-reportbad13a970a
1e008dd5d8
``` r? ```@marcoieni```
This commit is contained in:
commit
704d301b43
@ -7,6 +7,7 @@ use build_helper::metrics::{
|
|||||||
format_build_steps,
|
format_build_steps,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::github::JobInfoResolver;
|
||||||
use crate::metrics;
|
use crate::metrics;
|
||||||
use crate::metrics::{JobMetrics, JobName, get_test_suites};
|
use crate::metrics::{JobMetrics, JobName, get_test_suites};
|
||||||
use crate::utils::{output_details, pluralize};
|
use crate::utils::{output_details, pluralize};
|
||||||
@ -185,13 +186,19 @@ fn render_table(suites: BTreeMap<String, TestSuiteRecord>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Outputs a report of test differences between the `parent` and `current` commits.
|
/// Outputs a report of test differences between the `parent` and `current` commits.
|
||||||
pub fn output_test_diffs(job_metrics: &HashMap<JobName, JobMetrics>) {
|
pub fn output_test_diffs(
|
||||||
|
job_metrics: &HashMap<JobName, JobMetrics>,
|
||||||
|
job_info_resolver: &mut JobInfoResolver,
|
||||||
|
) {
|
||||||
let aggregated_test_diffs = aggregate_test_diffs(&job_metrics);
|
let aggregated_test_diffs = aggregate_test_diffs(&job_metrics);
|
||||||
report_test_diffs(aggregated_test_diffs);
|
report_test_diffs(aggregated_test_diffs, job_metrics, job_info_resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints the ten largest differences in bootstrap durations.
|
/// Prints the ten largest differences in bootstrap durations.
|
||||||
pub fn output_largest_duration_changes(job_metrics: &HashMap<JobName, JobMetrics>) {
|
pub fn output_largest_duration_changes(
|
||||||
|
job_metrics: &HashMap<JobName, JobMetrics>,
|
||||||
|
job_info_resolver: &mut JobInfoResolver,
|
||||||
|
) {
|
||||||
struct Entry<'a> {
|
struct Entry<'a> {
|
||||||
job: &'a JobName,
|
job: &'a JobName,
|
||||||
before: Duration,
|
before: Duration,
|
||||||
@ -225,14 +232,14 @@ pub fn output_largest_duration_changes(job_metrics: &HashMap<JobName, JobMetrics
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
changes.sort_by(|e1, e2| e1.change.partial_cmp(&e2.change).unwrap().reverse());
|
changes.sort_by(|e1, e2| e1.change.abs().partial_cmp(&e2.change.abs()).unwrap().reverse());
|
||||||
|
|
||||||
println!("# Job duration changes");
|
println!("# Job duration changes");
|
||||||
for (index, entry) in changes.into_iter().take(10).enumerate() {
|
for (index, entry) in changes.into_iter().take(10).enumerate() {
|
||||||
println!(
|
println!(
|
||||||
"{}. `{}`: {:.1}s -> {:.1}s ({:.1}%)",
|
"{}. {}: {:.1}s -> {:.1}s ({:.1}%)",
|
||||||
index + 1,
|
index + 1,
|
||||||
entry.job,
|
format_job_link(job_info_resolver, job_metrics, entry.job),
|
||||||
entry.before.as_secs_f64(),
|
entry.before.as_secs_f64(),
|
||||||
entry.after.as_secs_f64(),
|
entry.after.as_secs_f64(),
|
||||||
entry.change
|
entry.change
|
||||||
@ -400,7 +407,11 @@ fn generate_test_name(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Prints test changes in Markdown format to stdout.
|
/// Prints test changes in Markdown format to stdout.
|
||||||
fn report_test_diffs(diff: AggregatedTestDiffs) {
|
fn report_test_diffs(
|
||||||
|
diff: AggregatedTestDiffs,
|
||||||
|
job_metrics: &HashMap<JobName, JobMetrics>,
|
||||||
|
job_info_resolver: &mut JobInfoResolver,
|
||||||
|
) {
|
||||||
println!("# Test differences");
|
println!("# Test differences");
|
||||||
if diff.diffs.is_empty() {
|
if diff.diffs.is_empty() {
|
||||||
println!("No test diffs found");
|
println!("No test diffs found");
|
||||||
@ -521,9 +532,26 @@ fn report_test_diffs(diff: AggregatedTestDiffs) {
|
|||||||
println!(
|
println!(
|
||||||
"- {}: {}",
|
"- {}: {}",
|
||||||
format_job_group(group as u64),
|
format_job_group(group as u64),
|
||||||
jobs.iter().map(|j| format!("`{j}`")).collect::<Vec<_>>().join(", ")
|
jobs.iter()
|
||||||
|
.map(|j| format_job_link(job_info_resolver, job_metrics, j))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tries to get a GitHub Actions job summary URL from the resolver.
|
||||||
|
/// If it is not available, just wraps the job name in backticks.
|
||||||
|
fn format_job_link(
|
||||||
|
job_info_resolver: &mut JobInfoResolver,
|
||||||
|
job_metrics: &HashMap<JobName, JobMetrics>,
|
||||||
|
job_name: &str,
|
||||||
|
) -> String {
|
||||||
|
job_metrics
|
||||||
|
.get(job_name)
|
||||||
|
.and_then(|metrics| job_info_resolver.get_job_summary_link(job_name, &metrics.current))
|
||||||
|
.map(|summary_url| format!("[{job_name}]({summary_url})"))
|
||||||
|
.unwrap_or_else(|| format!("`{job_name}`"))
|
||||||
|
}
|
||||||
|
109
src/ci/citool/src/github.rs
Normal file
109
src/ci/citool/src/github.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use build_helper::metrics::{CiMetadata, JsonRoot};
|
||||||
|
|
||||||
|
pub struct GitHubClient;
|
||||||
|
|
||||||
|
impl GitHubClient {
|
||||||
|
fn get_workflow_run_jobs(
|
||||||
|
&self,
|
||||||
|
repo: &str,
|
||||||
|
workflow_run_id: u64,
|
||||||
|
) -> anyhow::Result<Vec<GitHubJob>> {
|
||||||
|
let req = ureq::get(format!(
|
||||||
|
"https://api.github.com/repos/{repo}/actions/runs/{workflow_run_id}/jobs?per_page=100"
|
||||||
|
))
|
||||||
|
.header("User-Agent", "rust-lang/rust/citool")
|
||||||
|
.header("Accept", "application/vnd.github+json")
|
||||||
|
.header("X-GitHub-Api-Version", "2022-11-28")
|
||||||
|
.call()
|
||||||
|
.context("cannot get workflow job list")?;
|
||||||
|
|
||||||
|
let status = req.status();
|
||||||
|
let mut body = req.into_body();
|
||||||
|
if status.is_success() {
|
||||||
|
// This API response is actually paged, but we assume for now that there are at
|
||||||
|
// most 100 jobs per workflow.
|
||||||
|
let response = body
|
||||||
|
.read_json::<WorkflowRunJobsResponse>()
|
||||||
|
.context("cannot deserialize workflow run jobs response")?;
|
||||||
|
// The CI job names have a prefix, e.g. `auto - foo`. We remove the prefix here to
|
||||||
|
// normalize the job name.
|
||||||
|
Ok(response
|
||||||
|
.jobs
|
||||||
|
.into_iter()
|
||||||
|
.map(|mut job| {
|
||||||
|
job.name = job
|
||||||
|
.name
|
||||||
|
.split_once(" - ")
|
||||||
|
.map(|res| res.1.to_string())
|
||||||
|
.unwrap_or_else(|| job.name);
|
||||||
|
job
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
} else {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"Cannot get jobs of workflow run {workflow_run_id}: {status}\n{}",
|
||||||
|
body.read_to_string()?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct WorkflowRunJobsResponse {
|
||||||
|
jobs: Vec<GitHubJob>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct GitHubJob {
|
||||||
|
name: String,
|
||||||
|
id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can be used to resolve information about GitHub Actions jobs.
|
||||||
|
/// Caches results internally to avoid too unnecessary GitHub API calls.
|
||||||
|
pub struct JobInfoResolver {
|
||||||
|
client: GitHubClient,
|
||||||
|
// Workflow run ID -> jobs
|
||||||
|
workflow_job_cache: HashMap<u64, Vec<GitHubJob>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobInfoResolver {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { client: GitHubClient, workflow_job_cache: Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a link to a job summary for the given job name and bootstrap execution.
|
||||||
|
pub fn get_job_summary_link(&mut self, job_name: &str, metrics: &JsonRoot) -> Option<String> {
|
||||||
|
metrics.ci_metadata.as_ref().and_then(|metadata| {
|
||||||
|
self.get_job_id(metadata, job_name).map(|job_id| {
|
||||||
|
format!(
|
||||||
|
"https://github.com/{}/actions/runs/{}#summary-{job_id}",
|
||||||
|
metadata.repository, metadata.workflow_run_id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_job_id(&mut self, ci_metadata: &CiMetadata, job_name: &str) -> Option<u64> {
|
||||||
|
if let Some(job) = self
|
||||||
|
.workflow_job_cache
|
||||||
|
.get(&ci_metadata.workflow_run_id)
|
||||||
|
.and_then(|jobs| jobs.iter().find(|j| j.name == job_name))
|
||||||
|
{
|
||||||
|
return Some(job.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let jobs = self
|
||||||
|
.client
|
||||||
|
.get_workflow_run_jobs(&ci_metadata.repository, ci_metadata.workflow_run_id)
|
||||||
|
.inspect_err(|e| eprintln!("Cannot download workflow jobs: {e:?}"))
|
||||||
|
.ok()?;
|
||||||
|
let job_id = jobs.iter().find(|j| j.name == job_name).map(|j| j.id);
|
||||||
|
// Save the cache even if the job name was not found, it could be useful for further lookups
|
||||||
|
self.workflow_job_cache.insert(ci_metadata.workflow_run_id, jobs);
|
||||||
|
job_id
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
mod analysis;
|
mod analysis;
|
||||||
mod cpu_usage;
|
mod cpu_usage;
|
||||||
mod datadog;
|
mod datadog;
|
||||||
|
mod github;
|
||||||
mod jobs;
|
mod jobs;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod utils;
|
mod utils;
|
||||||
@ -18,6 +19,7 @@ use serde_yaml::Value;
|
|||||||
use crate::analysis::{output_largest_duration_changes, output_test_diffs};
|
use crate::analysis::{output_largest_duration_changes, output_test_diffs};
|
||||||
use crate::cpu_usage::load_cpu_usage;
|
use crate::cpu_usage::load_cpu_usage;
|
||||||
use crate::datadog::upload_datadog_metric;
|
use crate::datadog::upload_datadog_metric;
|
||||||
|
use crate::github::JobInfoResolver;
|
||||||
use crate::jobs::RunType;
|
use crate::jobs::RunType;
|
||||||
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
|
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
|
||||||
use crate::utils::load_env_var;
|
use crate::utils::load_env_var;
|
||||||
@ -145,6 +147,7 @@ fn postprocess_metrics(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let metrics = load_metrics(&metrics_path)?;
|
let metrics = load_metrics(&metrics_path)?;
|
||||||
|
|
||||||
|
let mut job_info_resolver = JobInfoResolver::new();
|
||||||
if let (Some(parent), Some(job_name)) = (parent, job_name) {
|
if let (Some(parent), Some(job_name)) = (parent, job_name) {
|
||||||
// This command is executed also on PR builds, which might not have parent metrics
|
// This command is executed also on PR builds, which might not have parent metrics
|
||||||
// available, because some PR jobs don't run on auto builds, and PR jobs do not upload metrics
|
// available, because some PR jobs don't run on auto builds, and PR jobs do not upload metrics
|
||||||
@ -160,7 +163,7 @@ fn postprocess_metrics(
|
|||||||
job_name,
|
job_name,
|
||||||
JobMetrics { parent: Some(parent_metrics), current: metrics },
|
JobMetrics { parent: Some(parent_metrics), current: metrics },
|
||||||
)]);
|
)]);
|
||||||
output_test_diffs(&job_metrics);
|
output_test_diffs(&job_metrics, &mut job_info_resolver);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@ -180,8 +183,10 @@ fn post_merge_report(db: JobDatabase, current: String, parent: String) -> anyhow
|
|||||||
let metrics = download_auto_job_metrics(&db, &parent, ¤t)?;
|
let metrics = download_auto_job_metrics(&db, &parent, ¤t)?;
|
||||||
|
|
||||||
println!("\nComparing {parent} (parent) -> {current} (this PR)\n");
|
println!("\nComparing {parent} (parent) -> {current} (this PR)\n");
|
||||||
output_test_diffs(&metrics);
|
|
||||||
output_largest_duration_changes(&metrics);
|
let mut job_info_resolver = JobInfoResolver::new();
|
||||||
|
output_test_diffs(&metrics, &mut job_info_resolver);
|
||||||
|
output_largest_duration_changes(&metrics, &mut job_info_resolver);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user