Skip to content

Commit 5ac9252

Browse files
authored
Merge pull request #902 from rylev/noise-and-variance
Noise and variance
2 parents a478ade + c0b654c commit 5ac9252

File tree

5 files changed

+243
-82
lines changed

5 files changed

+243
-82
lines changed

database/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ version = "0.1.0"
44
authors = ["Mark Rousskov <[email protected]>"]
55
edition = "2018"
66

7-
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8-
97
[dependencies]
108
hashbrown = { version = "0.11", features = ["serde"] }
119
serde = { version = "1", features = ["derive"] }

site/src/api.rs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,6 @@ pub struct CommitResponse {
7070
pub commit: Option<String>,
7171
}
7272

73-
pub mod data {
74-
use crate::comparison::ArtifactData;
75-
use collector::Bound;
76-
use serde::{Deserialize, Serialize};
77-
78-
#[derive(Debug, Clone, Serialize, Deserialize)]
79-
pub struct Request {
80-
pub start: Bound,
81-
pub end: Bound,
82-
83-
/// Which statistic to return data for
84-
pub stat: String,
85-
}
86-
87-
/// List of DateData's from oldest to newest
88-
#[derive(Debug, Clone, Serialize)]
89-
pub struct Response(pub Vec<ArtifactData>);
90-
}
91-
9273
pub mod graph {
9374
use collector::Bound;
9475
use serde::{Deserialize, Serialize};
@@ -157,6 +138,7 @@ pub mod bootstrap {
157138
}
158139

159140
pub mod comparison {
141+
use crate::comparison;
160142
use collector::Bound;
161143
use database::Date;
162144
use serde::{Deserialize, Serialize};
@@ -171,6 +153,8 @@ pub mod comparison {
171153

172154
#[derive(Debug, Clone, Serialize)]
173155
pub struct Response {
156+
/// The variance data for each benchmark, if any.
157+
pub variance: Option<HashMap<String, comparison::BenchmarkVariance>>,
174158
/// The names for the previous artifact before `a`, if any.
175159
pub prev: Option<String>,
176160

site/src/comparison.rs

Lines changed: 226 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ use crate::load::SiteCtxt;
88
use crate::selector::{self, Tag};
99

1010
use collector::Bound;
11+
use log::debug;
1112
use serde::Serialize;
1213

1314
use std::collections::HashMap;
1415
use std::error::Error;
16+
use std::hash::Hash;
1517
use std::sync::Arc;
1618

1719
type BoxedError = Box<dyn Error + Send + Sync>;
@@ -99,6 +101,7 @@ pub async fn handle_compare(
99101
let is_contiguous = comparison.is_contiguous(&*conn, &master_commits).await;
100102

101103
Ok(api::comparison::Response {
104+
variance: comparison.benchmark_variances.map(|b| b.data),
102105
prev,
103106
a: comparison.a.into(),
104107
b: comparison.b.into(),
@@ -222,7 +225,7 @@ pub async fn compare(
222225
}
223226

224227
/// Compare two bounds on a given stat
225-
pub async fn compare_given_commits(
228+
async fn compare_given_commits(
226229
start: Bound,
227230
end: Bound,
228231
stat: String,
@@ -247,18 +250,41 @@ pub async fn compare_given_commits(
247250

248251
// `responses` contains series iterators. The first element in the iterator is the data
249252
// for `a` and the second is the data for `b`
250-
let mut responses = ctxt.query::<Option<f64>>(query, aids).await?;
253+
let mut responses = ctxt.query::<Option<f64>>(query.clone(), aids).await?;
251254

252255
let conn = ctxt.conn().await;
253-
254256
Ok(Some(Comparison {
257+
benchmark_variances: BenchmarkVariances::calculate(ctxt, a.clone(), master_commits, stat)
258+
.await?,
255259
a: ArtifactData::consume_one(&*conn, a.clone(), &mut responses, master_commits).await,
256260
b: ArtifactData::consume_one(&*conn, b.clone(), &mut responses, master_commits).await,
257261
}))
258262
}
259263

264+
fn previous_commits(
265+
mut from: ArtifactId,
266+
n: usize,
267+
master_commits: &[collector::MasterCommit],
268+
) -> Vec<ArtifactId> {
269+
let mut prevs = Vec::with_capacity(n);
270+
while prevs.len() < n {
271+
match prev_commit(&from, master_commits) {
272+
Some(c) => {
273+
let new = ArtifactId::Commit(database::Commit {
274+
sha: c.sha.clone(),
275+
date: database::Date(c.time),
276+
});
277+
from = new.clone();
278+
prevs.push(new);
279+
}
280+
None => break,
281+
}
282+
}
283+
prevs
284+
}
285+
260286
/// Data associated with a specific artifact
261-
#[derive(Debug, Clone, Serialize)]
287+
#[derive(Debug, Clone)]
262288
pub struct ArtifactData {
263289
/// The artifact in question
264290
pub artifact: ArtifactId,
@@ -288,25 +314,7 @@ impl ArtifactData {
288314
where
289315
T: Iterator<Item = (ArtifactId, Option<f64>)>,
290316
{
291-
let mut data = HashMap::new();
292-
293-
for response in series {
294-
let (id, point) = response.series.next().expect("must have element");
295-
assert_eq!(artifact, id);
296-
297-
let point = if let Some(pt) = point {
298-
pt
299-
} else {
300-
continue;
301-
};
302-
data.entry(format!(
303-
"{}-{}",
304-
response.path.get::<Crate>().unwrap(),
305-
response.path.get::<Profile>().unwrap(),
306-
))
307-
.or_insert_with(Vec::new)
308-
.push((response.path.get::<Cache>().unwrap().to_string(), point));
309-
}
317+
let data = data_from_series(series);
310318

311319
let bootstrap = conn
312320
.get_bootstrap(&[conn.artifact_id(&artifact).await])
@@ -348,6 +356,32 @@ impl ArtifactData {
348356
}
349357
}
350358

359+
fn data_from_series<T>(
360+
series: &mut [selector::SeriesResponse<T>],
361+
) -> HashMap<String, Vec<(String, f64)>>
362+
where
363+
T: Iterator<Item = (ArtifactId, Option<f64>)>,
364+
{
365+
let mut data = HashMap::new();
366+
for response in series {
367+
let (_, point) = response.series.next().expect("must have element");
368+
369+
let point = if let Some(pt) = point {
370+
pt
371+
} else {
372+
continue;
373+
};
374+
data.entry(format!(
375+
"{}-{}",
376+
response.path.get::<Crate>().unwrap(),
377+
response.path.get::<Profile>().unwrap(),
378+
))
379+
.or_insert_with(Vec::new)
380+
.push((response.path.get::<Cache>().unwrap().to_string(), point));
381+
}
382+
data
383+
}
384+
351385
impl From<ArtifactData> for api::comparison::ArtifactData {
352386
fn from(data: ArtifactData) -> Self {
353387
api::comparison::ArtifactData {
@@ -369,20 +403,18 @@ impl From<ArtifactData> for api::comparison::ArtifactData {
369403

370404
// A comparison of two artifacts
371405
pub struct Comparison {
406+
/// Data on how variable benchmarks have historically been
407+
///
408+
/// Is `None` if we cannot determine historical variance
409+
pub benchmark_variances: Option<BenchmarkVariances>,
372410
pub a: ArtifactData,
373411
pub b: ArtifactData,
374412
}
375413

376414
impl Comparison {
377415
/// Gets the previous commit before `a`
378416
pub fn prev(&self, master_commits: &[collector::MasterCommit]) -> Option<String> {
379-
match &self.a.artifact {
380-
ArtifactId::Commit(a) => master_commits
381-
.iter()
382-
.find(|c| c.sha == a.sha)
383-
.map(|c| c.parent_sha.clone()),
384-
ArtifactId::Artifact(_) => None,
385-
}
417+
prev_commit(&self.a.artifact, master_commits).map(|c| c.parent_sha.clone())
386418
}
387419

388420
/// Determines if `a` and `b` are contiguous
@@ -405,13 +437,7 @@ impl Comparison {
405437

406438
/// Gets the sha of the next commit after `b`
407439
pub fn next(&self, master_commits: &[collector::MasterCommit]) -> Option<String> {
408-
match &self.b.artifact {
409-
ArtifactId::Commit(b) => master_commits
410-
.iter()
411-
.find(|c| c.parent_sha == b.sha)
412-
.map(|c| c.sha.clone()),
413-
ArtifactId::Artifact(_) => None,
414-
}
440+
next_commit(&self.a.artifact, master_commits).map(|c| c.parent_sha.clone())
415441
}
416442

417443
fn get_benchmarks<'a>(&'a self) -> Vec<BenchmarkComparison<'a>> {
@@ -437,6 +463,169 @@ impl Comparison {
437463
}
438464
}
439465

466+
/// A description of the amount of variance a certain benchmark is historically
467+
/// experiencing at a given point in time.
468+
pub struct BenchmarkVariances {
469+
/// Variance data on a per benchmark basis
470+
/// Key: $benchmark-$profile-$cache
471+
/// Value: `BenchmarkVariance`
472+
pub data: HashMap<String, BenchmarkVariance>,
473+
}
474+
475+
impl BenchmarkVariances {
476+
async fn calculate(
477+
ctxt: &SiteCtxt,
478+
from: ArtifactId,
479+
master_commits: &[collector::MasterCommit],
480+
stat: String,
481+
) -> Result<Option<Self>, BoxedError> {
482+
// get all crates, cache, and profile combinations for the given stat
483+
let query = selector::Query::new()
484+
.set::<String>(Tag::Crate, selector::Selector::All)
485+
.set::<String>(Tag::Cache, selector::Selector::All)
486+
.set::<String>(Tag::Profile, selector::Selector::All)
487+
.set(Tag::ProcessStatistic, selector::Selector::One(stat));
488+
489+
let num_commits = 100;
490+
let previous_commits = Arc::new(previous_commits(from, num_commits, master_commits));
491+
if previous_commits.len() < num_commits {
492+
return Ok(None);
493+
}
494+
let mut previous_commit_series = ctxt
495+
.query::<Option<f64>>(query, previous_commits.clone())
496+
.await?;
497+
498+
let mut variance_data: HashMap<String, BenchmarkVariance> = HashMap::new();
499+
for _ in previous_commits.iter() {
500+
let series_data = data_from_series(&mut previous_commit_series);
501+
for (bench_and_profile, data) in series_data {
502+
for (cache, val) in data {
503+
variance_data
504+
.entry(format!("{}-{}", bench_and_profile, cache))
505+
.or_default()
506+
.push(val);
507+
}
508+
}
509+
}
510+
511+
for (bench, results) in variance_data.iter_mut() {
512+
debug!("Calculating variance for: {}", bench);
513+
results.calculate_description();
514+
}
515+
Ok(Some(Self {
516+
data: variance_data,
517+
}))
518+
}
519+
}
520+
521+
#[derive(Debug, Default, Clone, Serialize)]
522+
pub struct BenchmarkVariance {
523+
data: Vec<f64>,
524+
description: BenchmarkVarianceDescription,
525+
}
526+
527+
impl BenchmarkVariance {
528+
/// The ratio of change that we consider significant.
529+
const SIGNFICANT_DELTA_THRESHOLD: f64 = 0.01;
530+
/// The percentage of significant changes that we consider too high
531+
const SIGNFICANT_CHANGE_THRESHOLD: f64 = 5.0;
532+
/// The percentage of change that constitutes noisy data
533+
const NOISE_THRESHOLD: f64 = 0.1;
534+
535+
fn push(&mut self, value: f64) {
536+
self.data.push(value);
537+
}
538+
539+
fn mean(&self) -> f64 {
540+
self.data.iter().sum::<f64>() / self.data.len() as f64
541+
}
542+
543+
fn calculate_description(&mut self) {
544+
self.description = BenchmarkVarianceDescription::Normal;
545+
546+
let results_mean = self.mean();
547+
let mut deltas = self
548+
.data
549+
.windows(2)
550+
.map(|window| (window[0] - window[1]).abs())
551+
.collect::<Vec<_>>();
552+
deltas.sort_by(|d1, d2| d1.partial_cmp(d2).unwrap_or(std::cmp::Ordering::Equal));
553+
let non_significant = deltas
554+
.iter()
555+
.zip(self.data.iter())
556+
.take_while(|(&d, &r)| d / r < Self::SIGNFICANT_DELTA_THRESHOLD)
557+
.collect::<Vec<_>>();
558+
559+
let percent_significant_changes =
560+
((deltas.len() - non_significant.len()) as f64 / deltas.len() as f64) * 100.0;
561+
debug!(
562+
"Percent significant changes: {:.1}%",
563+
percent_significant_changes
564+
);
565+
566+
if percent_significant_changes > Self::SIGNFICANT_CHANGE_THRESHOLD {
567+
self.description =
568+
BenchmarkVarianceDescription::HighlyVariable(percent_significant_changes);
569+
return;
570+
}
571+
572+
let delta_mean =
573+
non_significant.iter().map(|(&d, _)| d).sum::<f64>() / (non_significant.len() as f64);
574+
let percent_change = (delta_mean / results_mean) * 100.0;
575+
debug!("Percent change: {:.3}%", percent_change);
576+
if percent_change > Self::NOISE_THRESHOLD {
577+
self.description = BenchmarkVarianceDescription::Noisy(percent_change);
578+
}
579+
}
580+
}
581+
582+
#[derive(Debug, Clone, Copy, Serialize)]
583+
#[serde(tag = "type", content = "percent")]
584+
pub enum BenchmarkVarianceDescription {
585+
Normal,
586+
/// A highly variable benchmark that produces many significant changes.
587+
/// This might indicate a benchmark which is very sensitive to compiler changes.
588+
///
589+
/// Cotains the percentage of significant changes.
590+
HighlyVariable(f64),
591+
/// A noisy benchmark which is likely to see changes in performance simply between
592+
/// compiler runs.
593+
///
594+
/// Contains the percent change that happens on average
595+
Noisy(f64),
596+
}
597+
598+
impl Default for BenchmarkVarianceDescription {
599+
fn default() -> Self {
600+
Self::Normal
601+
}
602+
}
603+
604+
/// Gets the previous commit
605+
pub fn prev_commit<'a>(
606+
artifact: &ArtifactId,
607+
master_commits: &'a [collector::MasterCommit],
608+
) -> Option<&'a collector::MasterCommit> {
609+
match &artifact {
610+
ArtifactId::Commit(a) => {
611+
let current = master_commits.iter().find(|c| c.sha == a.sha)?;
612+
master_commits.iter().find(|c| c.sha == current.parent_sha)
613+
}
614+
ArtifactId::Artifact(_) => None,
615+
}
616+
}
617+
618+
/// Gets the next commit
619+
pub fn next_commit<'a>(
620+
artifact: &ArtifactId,
621+
master_commits: &'a [collector::MasterCommit],
622+
) -> Option<&'a collector::MasterCommit> {
623+
match artifact {
624+
ArtifactId::Commit(b) => master_commits.iter().find(|c| c.parent_sha == b.sha),
625+
ArtifactId::Artifact(_) => None,
626+
}
627+
}
628+
440629
// A single comparison based on benchmark and cache state
441630
#[derive(Debug)]
442631
pub struct BenchmarkComparison<'a> {

0 commit comments

Comments
 (0)