283 lines
7.8 KiB
Rust
283 lines
7.8 KiB
Rust
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
|
// This file is part of Parity.
|
|
|
|
// Parity is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Parity is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//! Request load timer and distribution manager.
|
|
//!
|
|
//! This uses empirical samples of the length of time taken to respond
|
|
//! to requests in order to inform request credit costs.
|
|
//!
|
|
//! The average request time is determined by an exponential moving average
|
|
//! of the mean request times during the last `MOVING_SAMPLE_SIZE` time periods of
|
|
//! length `TIME_PERIOD_MS`, with the exception that time periods where no data is
|
|
//! gathered are excluded.
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::fs::File;
|
|
use std::path::PathBuf;
|
|
|
|
use request::{CompleteRequest, Kind};
|
|
|
|
use bincode;
|
|
use time;
|
|
use parking_lot::{RwLock, Mutex};
|
|
|
|
/// Number of time periods samples should be kept for.
|
|
pub const MOVING_SAMPLE_SIZE: usize = 256;
|
|
|
|
/// Stores rolling load timer samples.
|
|
// TODO: switch to bigint if possible (FP casts aren't available)
|
|
pub trait SampleStore: Send + Sync {
|
|
/// Load samples.
|
|
fn load(&self) -> HashMap<Kind, VecDeque<u64>>;
|
|
|
|
/// Store all samples.
|
|
fn store(&self, samples: &HashMap<Kind, VecDeque<u64>>);
|
|
}
|
|
|
|
// get a hardcoded, arbitrarily determined (but intended overestimate)
|
|
// of the time in nanoseconds to serve a request of the given kind.
|
|
//
|
|
// TODO: seed this with empirical data.
|
|
fn hardcoded_serve_time(kind: Kind) -> u64 {
|
|
match kind {
|
|
Kind::Headers => 500_000,
|
|
Kind::HeaderProof => 500_000,
|
|
Kind::TransactionIndex => 500_000,
|
|
Kind::Receipts => 1_000_000,
|
|
Kind::Body => 1_000_000,
|
|
Kind::Account => 1_500_000,
|
|
Kind::Storage => 2_000_000,
|
|
Kind::Code => 1_500_000,
|
|
Kind::Execution => 250, // per gas.
|
|
Kind::Signal => 500_000,
|
|
}
|
|
}
|
|
|
|
/// A no-op store.
|
|
pub struct NullStore;
|
|
|
|
impl SampleStore for NullStore {
|
|
fn load(&self) -> HashMap<Kind, VecDeque<u64>> { HashMap::new() }
|
|
fn store(&self, _samples: &HashMap<Kind, VecDeque<u64>>) { }
|
|
}
|
|
|
|
/// Request load distributions.
|
|
pub struct LoadDistribution {
|
|
active_period: RwLock<HashMap<Kind, Mutex<(u64, u64)>>>,
|
|
samples: RwLock<HashMap<Kind, VecDeque<u64>>>,
|
|
}
|
|
|
|
impl LoadDistribution {
|
|
/// Load rolling samples from the given store.
|
|
pub fn load(store: &SampleStore) -> Self {
|
|
let mut samples = store.load();
|
|
|
|
for kind_samples in samples.values_mut() {
|
|
while kind_samples.len() > MOVING_SAMPLE_SIZE {
|
|
kind_samples.pop_front();
|
|
}
|
|
}
|
|
|
|
LoadDistribution {
|
|
active_period: RwLock::new(HashMap::new()),
|
|
samples: RwLock::new(samples),
|
|
}
|
|
}
|
|
|
|
/// Begin a timer.
|
|
pub fn begin_timer<'a>(&'a self, req: &CompleteRequest) -> LoadTimer<'a> {
|
|
let kind = req.kind();
|
|
let n = match *req {
|
|
CompleteRequest::Headers(ref req) => req.max,
|
|
CompleteRequest::Execution(ref req) => req.gas.low_u64(),
|
|
_ => 1,
|
|
};
|
|
|
|
LoadTimer {
|
|
start: time::precise_time_ns(),
|
|
n: n,
|
|
dist: self,
|
|
kind: kind,
|
|
}
|
|
}
|
|
|
|
/// Calculate EMA of load in nanoseconds for a specific request kind.
|
|
/// If there is no data for the given request kind, no EMA will be calculated,
|
|
/// but a hardcoded time will be returned.
|
|
pub fn expected_time_ns(&self, kind: Kind) -> u64 {
|
|
let samples = self.samples.read();
|
|
samples.get(&kind).and_then(|s| {
|
|
if s.len() == 0 { return None }
|
|
|
|
let alpha: f64 = 1f64 / s.len() as f64;
|
|
let start = s.front().expect("length known to be non-zero; qed").clone();
|
|
let ema = s.iter().skip(1).fold(start as f64, |a, &c| {
|
|
(alpha * c as f64) + ((1.0 - alpha) * a)
|
|
});
|
|
|
|
Some(ema as u64)
|
|
}).unwrap_or_else(move || hardcoded_serve_time(kind))
|
|
}
|
|
|
|
/// End the current time period. Provide a store to
|
|
pub fn end_period(&self, store: &SampleStore) {
|
|
let active_period = self.active_period.read();
|
|
let mut samples = self.samples.write();
|
|
|
|
for (&kind, set) in active_period.iter() {
|
|
let (elapsed, n) = ::std::mem::replace(&mut *set.lock(), (0, 0));
|
|
if n == 0 { continue }
|
|
|
|
let kind_samples = samples.entry(kind)
|
|
.or_insert_with(|| VecDeque::with_capacity(MOVING_SAMPLE_SIZE));
|
|
|
|
if kind_samples.len() == MOVING_SAMPLE_SIZE { kind_samples.pop_front(); }
|
|
kind_samples.push_back(elapsed / n);
|
|
}
|
|
|
|
store.store(&*samples);
|
|
}
|
|
|
|
fn update(&self, kind: Kind, elapsed: u64, n: u64) {
|
|
macro_rules! update_counters {
|
|
($counters: expr) => {
|
|
$counters.0 = $counters.0.saturating_add(elapsed);
|
|
$counters.1 = $counters.1.saturating_add(n);
|
|
}
|
|
};
|
|
|
|
{
|
|
let set = self.active_period.read();
|
|
if let Some(counters) = set.get(&kind) {
|
|
let mut counters = counters.lock();
|
|
update_counters!(counters);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let mut set = self.active_period.write();
|
|
let counters = set
|
|
.entry(kind)
|
|
.or_insert_with(|| Mutex::new((0, 0)));
|
|
|
|
update_counters!(counters.get_mut());
|
|
}
|
|
}
|
|
|
|
/// A timer for a single request.
|
|
/// On drop, this will update the distribution.
|
|
pub struct LoadTimer<'a> {
|
|
start: u64,
|
|
n: u64,
|
|
dist: &'a LoadDistribution,
|
|
kind: Kind,
|
|
}
|
|
|
|
impl<'a> Drop for LoadTimer<'a> {
|
|
fn drop(&mut self) {
|
|
let elapsed = time::precise_time_ns() - self.start;
|
|
self.dist.update(self.kind, elapsed, self.n);
|
|
}
|
|
}
|
|
|
|
/// A store which writes directly to a file.
|
|
pub struct FileStore(pub PathBuf);
|
|
|
|
impl SampleStore for FileStore {
|
|
fn load(&self) -> HashMap<Kind, VecDeque<u64>> {
|
|
File::open(&self.0)
|
|
.map_err(|e| Box::new(bincode::ErrorKind::IoError(e)))
|
|
.and_then(|mut file| bincode::deserialize_from(&mut file, bincode::Infinite))
|
|
.unwrap_or_else(|_| HashMap::new())
|
|
}
|
|
|
|
fn store(&self, samples: &HashMap<Kind, VecDeque<u64>>) {
|
|
let res = File::create(&self.0)
|
|
.map_err(|e| Box::new(bincode::ErrorKind::IoError(e)))
|
|
.and_then(|mut file| bincode::serialize_into(&mut file, samples, bincode::Infinite));
|
|
|
|
if let Err(e) = res {
|
|
warn!(target: "pip", "Error writing light request timing samples to file: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use request::Kind;
|
|
|
|
#[test]
|
|
fn hardcoded_before_data() {
|
|
let dist = LoadDistribution::load(&NullStore);
|
|
assert_eq!(dist.expected_time_ns(Kind::Headers), hardcoded_serve_time(Kind::Headers));
|
|
|
|
dist.update(Kind::Headers, 100_000, 100);
|
|
dist.end_period(&NullStore);
|
|
|
|
assert_eq!(dist.expected_time_ns(Kind::Headers), 1000);
|
|
}
|
|
|
|
#[test]
|
|
fn moving_average() {
|
|
let dist = LoadDistribution::load(&NullStore);
|
|
|
|
let mut sum = 0;
|
|
|
|
for (i, x) in (0..10).map(|x| x * 10_000).enumerate() {
|
|
dist.update(Kind::Headers, x, 1);
|
|
dist.end_period(&NullStore);
|
|
|
|
sum += x;
|
|
if i == 0 { continue }
|
|
|
|
let moving_average = dist.expected_time_ns(Kind::Headers);
|
|
|
|
// should be weighted below the maximum entry.
|
|
let arith_average = (sum as f64 / (i + 1) as f64) as u64;
|
|
assert!(moving_average < x);
|
|
|
|
// when there are only 2 entries, they should be equal due to choice of
|
|
// ALPHA = 1/N.
|
|
// otherwise, the weight should be below the arithmetic mean because the much
|
|
// smaller previous values are discounted less.
|
|
if i == 1 {
|
|
assert_eq!(moving_average, arith_average);
|
|
} else {
|
|
assert!(moving_average < arith_average)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn file_store() {
|
|
let tempdir = ::tempdir::TempDir::new("").unwrap();
|
|
let path = tempdir.path().join("file");
|
|
let store = FileStore(path);
|
|
|
|
let mut samples = store.load();
|
|
assert!(samples.is_empty());
|
|
samples.insert(Kind::Headers, vec![5, 2, 7, 2, 2, 4].into());
|
|
samples.insert(Kind::Execution, vec![1, 1, 100, 250].into());
|
|
|
|
store.store(&samples);
|
|
|
|
let dup = store.load();
|
|
|
|
assert_eq!(samples, dup);
|
|
}
|
|
}
|