use chromiumoxide::Page;
use tracing::{info, warn};
use super::assembly::{compute_viewport_overall, update_audit_quality};
use super::{set_viewport, PipelineConfig};
use crate::audit::module::Viewport;
use crate::audit::report::{AuditReport, PerformanceResults};
use crate::browser::{throttle, BrowserManager, ThrottleProfile};
use crate::interaction::stability::settle;
use crate::performance::prepare_vitals_collection;
pub(super) fn attach_throttled_profile_subchecks(
report: &mut AuditReport,
results: &[crate::audit::report::ThrottledPerfResult],
) {
let subchecks: Vec<crate::audit::SubcheckRun> = ThrottleProfile::AUTO_PROFILES
.iter()
.map(|profile| {
let completed = results.iter().any(|result| result.profile == *profile);
crate::audit::SubcheckRun {
subcheck: format!("throttled_profile:{}", profile.label()),
status: if completed {
crate::audit::ExecutionStatus::Completed
} else {
crate::audit::ExecutionStatus::Failed
},
reason_code: (!completed).then(|| "profile_measurement_failed".to_string()),
}
})
.collect();
if let Some(performance) = report
.accessibility
.execution
.module_runs
.iter_mut()
.find(|run| run.module == "performance")
{
if subchecks
.iter()
.any(|check| check.status == crate::audit::ExecutionStatus::Failed)
&& performance.status == crate::audit::ExecutionStatus::Completed
{
performance.status = crate::audit::ExecutionStatus::Partial;
performance.reason_code = Some("throttled_profile_incomplete".to_string());
}
performance.subchecks.extend(subchecks);
}
update_audit_quality(report);
}
async fn recover_after_throttled_failure(page: &Page) -> bool {
use chromiumoxide::cdp::browser_protocol::page::StopLoadingParams;
let step = std::time::Duration::from_secs(5);
let cpu = tokio::time::timeout(step, throttle::disable_cpu_throttling(page)).await;
let _ = tokio::time::timeout(step, page.execute(StopLoadingParams::default())).await;
let network = tokio::time::timeout(step, throttle::disable_throttling(page)).await;
let cache = tokio::time::timeout(step, throttle::enable_cache(page)).await;
matches!(cpu, Ok(Ok(()))) && matches!(network, Ok(Ok(()))) && matches!(cache, Ok(Ok(())))
}
pub(super) async fn collect_throttled_performance(
page: &Page,
url: &str,
browser: &BrowserManager,
_config: &PipelineConfig,
content_weight: Option<&crate::performance::ContentWeight>,
) -> (
Vec<crate::audit::report::ThrottledPerfResult>,
Option<(
crate::performance::WebVitals,
crate::performance::PerformanceScore,
)>,
) {
use crate::audit::report::ThrottledPerfResult;
use crate::performance::calculate_performance_score;
let mut results = Vec::new();
let mut canonical: Option<(
crate::performance::WebVitals,
crate::performance::PerformanceScore,
)> = None;
for &profile in ThrottleProfile::AUTO_PROFILES {
info!("Throttled perf pass: {:?}", profile);
if let Err(e) = throttle::apply_throttling(page, profile).await {
warn!("Throttle apply failed for {:?}: {}", profile, e);
continue;
}
if let Err(e) = throttle::apply_cpu_throttling(page, profile).await {
warn!("CPU throttle apply failed for {:?}: {}", profile, e);
}
if let Err(e) = throttle::disable_cache(page).await {
warn!("Cache disable failed for {:?}: {}", profile, e);
}
let failure = match prepare_vitals_collection(page).await {
Err(e) => Some(format!("Vitals injection failed for {profile:?}: {e}")),
Ok(()) => browser
.navigate(page, url)
.await
.err()
.map(|e| format!("Navigation failed for {profile:?}: {e}")),
};
if let Some(failure) = failure {
warn!("{failure}");
if !recover_after_throttled_failure(page).await {
warn!(
"Tab did not recover after the failed {:?} pass; skipping the remaining throttled profiles",
profile
);
break;
}
continue;
}
match crate::performance::extract_web_vitals(page).await {
Ok(vitals) => {
let score = calculate_performance_score(&vitals, content_weight);
let final_score = if vitals.lcp.is_none() {
score.overall.min(50)
} else {
score.overall
};
results.push(ThrottledPerfResult {
profile,
lcp_ms: vitals.lcp.as_ref().map(|v| v.value),
tbt_ms: vitals.tbt.as_ref().map(|v| v.value),
cls: vitals.cls.as_ref().map(|v| v.value),
score: final_score,
});
if profile == ThrottleProfile::LhMobile && vitals.lcp.is_some() {
let mut adopted_score = score.clone();
adopted_score.overall = final_score;
adopted_score.grade =
crate::performance::PerformanceGrade::from_score(final_score);
let mut throttled_vitals = vitals.clone();
crate::performance::mark_throttled_mobile(&mut throttled_vitals);
canonical = Some((throttled_vitals, adopted_score));
}
let _ = throttle::enable_cache(page).await;
}
Err(e) => {
warn!("Vitals collection failed for {:?}: {}", profile, e);
let _ = throttle::enable_cache(page).await;
}
}
if let Err(e) = throttle::disable_throttling(page).await {
warn!("Throttle disable failed for {:?}: {}", profile, e);
}
if let Err(e) = throttle::disable_cpu_throttling(page).await {
warn!("CPU throttle disable failed for {:?}: {}", profile, e);
}
if let Err(e) = settle(page).await {
warn!("Browser settle failed after {:?}: {}", profile, e);
}
}
if let Err(e) = set_viewport(page, Viewport::Mobile).await {
warn!(
"Failed to restore mobile viewport after throttled pass: {}",
e
);
}
if !results.is_empty() {
let _ = throttle::disable_throttling(page).await;
let _ = throttle::disable_cpu_throttling(page).await;
}
(results, canonical)
}
pub(super) fn apply_canonical_perf(
report: &mut AuditReport,
vitals: crate::performance::WebVitals,
score: crate::performance::PerformanceScore,
) {
let measurement_warnings = crate::performance::validate_metrics(&vitals);
if let Some(ref mut perf) = report.performance {
perf.measurement_warnings = measurement_warnings;
perf.vitals = vitals;
perf.score = score;
} else {
report.performance = Some(PerformanceResults {
vitals,
score,
render_blocking: None,
content_weight: None,
third_party: None,
critical_chain: None,
minification: None,
animations: None,
coverage: None,
measurement_warnings,
});
}
let new_perf = report.performance.as_ref().map(|p| p.score.overall);
let mobile_seo = report.discoverability.seo.as_ref().map(|s| s.score);
let mobile_mf = report.experience.mobile.as_ref().map(|m| m.score);
if let Some(ref mut vps) = report.viewport_scores {
vps.mobile.performance = new_perf;
let mobile_overall = compute_viewport_overall(
vps.mobile.accessibility as f32,
vps.mobile.performance,
mobile_seo,
mobile_mf,
);
let desktop_overall = vps.desktop.overall;
vps.mobile.overall = mobile_overall;
vps.weighted_overall =
(mobile_overall as f64 * 0.7 + desktop_overall as f64 * 0.3).round() as u32;
}
}