Use Vec for route resolution when number of routes <15 (#1429)

* Add `TinyMap` collection, which is a map backed by either `Vec` or `HashMap` depending on the number of entries.

* Replace `HashMap` with  `TinyMap` for `Routes::AwsJson10` and `Routes::AwsJson11`.
This commit is contained in:
Harry Barber 2022-06-07 10:58:19 +01:00 committed by GitHub
parent f1a4782614
commit 1875448dbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 203 additions and 4 deletions

View File

@ -9,13 +9,13 @@
use self::future::RouterFuture;
use self::request_spec::RequestSpec;
use self::tiny_map::TinyMap;
use crate::body::{boxed, Body, BoxBody, HttpBody};
use crate::error::BoxError;
use crate::protocols::Protocol;
use crate::response::IntoResponse;
use crate::runtime_error::{RuntimeError, RuntimeErrorKind};
use http::{Request, Response, StatusCode};
use std::collections::HashMap;
use std::{
convert::Infallible,
task::{Context, Poll},
@ -32,6 +32,7 @@ mod into_make_service;
pub mod request_spec;
mod route;
mod tiny_map;
pub use self::{into_make_service::IntoMakeService, route::Route};
@ -60,6 +61,11 @@ pub struct Router<B = Body> {
routes: Routes<B>,
}
// This constant determines when the `TinyMap` implementation switches from being a `Vec` to a
// `HashMap`. This is chosen to be 15 as a result of the discussion around
// https://github.com/awslabs/smithy-rs/pull/1429#issuecomment-1147516546
const ROUTE_CUTOFF: usize = 15;
/// Protocol-aware routes types.
///
/// RestJson1 and RestXml routes are stored in a `Vec` because there can be multiple matches on the
@ -71,8 +77,8 @@ pub struct Router<B = Body> {
enum Routes<B = Body> {
RestXml(Vec<(Route<B>, RequestSpec)>),
RestJson1(Vec<(Route<B>, RequestSpec)>),
AwsJson10(HashMap<String, Route<B>>),
AwsJson11(HashMap<String, Route<B>>),
AwsJson10(TinyMap<String, Route<B>, ROUTE_CUTOFF>),
AwsJson11(TinyMap<String, Route<B>, ROUTE_CUTOFF>),
}
impl<B> Clone for Router<B> {
@ -343,7 +349,7 @@ where
// Find the `x-amz-target` header.
if let Some(target) = req.headers().get("x-amz-target") {
if let Ok(target) = target.to_str() {
// Lookup in the `HashMap` for a route for the target.
// Lookup in the `TinyMap` for a route for the target.
let route = routes.get(target);
if let Some(route) = route {
return RouterFuture::from_oneshot(route.clone().oneshot(req));

View File

@ -0,0 +1,193 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
use std::{borrow::Borrow, collections::HashMap, hash::Hash};
/// A map implementation with fast iteration which switches backing storage from [`Vec`] to
/// [`HashMap`] when the number of entries exceeds `CUTOFF`.
#[derive(Clone, Debug)]
pub struct TinyMap<K, V, const CUTOFF: usize> {
inner: TinyMapInner<K, V, CUTOFF>,
}
#[derive(Clone, Debug)]
enum TinyMapInner<K, V, const CUTOFF: usize> {
Vec(Vec<(K, V)>),
HashMap(HashMap<K, V>),
}
enum OrIterator<Left, Right> {
Left(Left),
Right(Right),
}
impl<Left, Right> Iterator for OrIterator<Left, Right>
where
Left: Iterator,
Right: Iterator<Item = Left::Item>,
{
type Item = Left::Item;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Left(left) => left.next(),
Self::Right(right) => right.next(),
}
}
}
/// An owning iterator over the entries of a `TinyMap`.
///
/// This struct is created by the [`into_iter`](IntoIterator::into_iter) method on [`TinyMap`] (
/// provided by the [`IntoIterator`] trait). See its documentation for more.
pub struct IntoIter<K, V> {
inner: OrIterator<std::vec::IntoIter<(K, V)>, std::collections::hash_map::IntoIter<K, V>>,
}
impl<K, V> Iterator for IntoIter<K, V> {
type Item = (K, V);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
impl<K, V, const CUTOFF: usize> IntoIterator for TinyMap<K, V, CUTOFF> {
type Item = (K, V);
type IntoIter = IntoIter<K, V>;
fn into_iter(self) -> Self::IntoIter {
let inner = match self.inner {
TinyMapInner::Vec(vec) => OrIterator::Left(vec.into_iter()),
TinyMapInner::HashMap(hash_map) => OrIterator::Right(hash_map.into_iter()),
};
IntoIter { inner }
}
}
impl<K, V, const CUTOFF: usize> FromIterator<(K, V)> for TinyMap<K, V, CUTOFF>
where
K: Hash + Eq,
{
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
let mut vec = Vec::with_capacity(CUTOFF);
let mut iter = iter.into_iter().enumerate();
// Populate the `Vec`
while let Some((index, pair)) = iter.next() {
// If overflow `CUTOFF` then return a `HashMap` instead
if index == CUTOFF {
let inner = TinyMapInner::HashMap(vec.into_iter().chain(iter.map(|(_, pair)| pair)).collect());
return TinyMap { inner };
}
vec.push(pair);
}
TinyMap {
inner: TinyMapInner::Vec(vec),
}
}
}
impl<K, V, const CUTOFF: usize> TinyMap<K, V, CUTOFF>
where
K: Eq + Hash,
{
/// Returns a reference to the value corresponding to the key.
///
/// The key may be borrowed form of map's key type, but [`Hash`] and [`Eq`] on the borrowed
/// form _must_ match those for the key type.
pub fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq,
{
match &self.inner {
TinyMapInner::Vec(vec) => vec
.iter()
.find(|(key_inner, _)| key_inner.borrow() == key)
.map(|(_, value)| value),
TinyMapInner::HashMap(hash_map) => hash_map.get(key),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const CUTOFF: usize = 5;
const SMALL_VALUES: [(&'static str, usize); 3] = [("a", 0), ("b", 1), ("c", 2)];
const MEDIUM_VALUES: [(&'static str, usize); 5] = [("a", 0), ("b", 1), ("c", 2), ("d", 3), ("e", 4)];
const LARGE_VALUES: [(&'static str, usize); 10] = [
("a", 0),
("b", 1),
("c", 2),
("d", 3),
("e", 4),
("f", 5),
("g", 6),
("h", 7),
("i", 8),
("j", 9),
];
#[test]
fn collect_small() {
let tiny_map: TinyMap<_, _, CUTOFF> = SMALL_VALUES.into_iter().collect();
assert!(matches!(tiny_map.inner, TinyMapInner::Vec(_)))
}
#[test]
fn collect_medium() {
let tiny_map: TinyMap<_, _, CUTOFF> = MEDIUM_VALUES.into_iter().collect();
assert!(matches!(tiny_map.inner, TinyMapInner::Vec(_)))
}
#[test]
fn collect_large() {
let tiny_map: TinyMap<_, _, CUTOFF> = LARGE_VALUES.into_iter().collect();
assert!(matches!(tiny_map.inner, TinyMapInner::HashMap(_)))
}
#[test]
fn get_small_success() {
let tiny_map: TinyMap<_, _, CUTOFF> = SMALL_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("a"), Some(&0))
}
#[test]
fn get_medium_success() {
let tiny_map: TinyMap<_, _, CUTOFF> = MEDIUM_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("d"), Some(&3))
}
#[test]
fn get_large_success() {
let tiny_map: TinyMap<_, _, CUTOFF> = LARGE_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("h"), Some(&7))
}
#[test]
fn get_small_fail() {
let tiny_map: TinyMap<_, _, CUTOFF> = SMALL_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("x"), None)
}
#[test]
fn get_medium_fail() {
let tiny_map: TinyMap<_, _, CUTOFF> = MEDIUM_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("y"), None)
}
#[test]
fn get_large_fail() {
let tiny_map: TinyMap<_, _, CUTOFF> = LARGE_VALUES.into_iter().collect();
assert_eq!(tiny_map.get("z"), None)
}
}