Unverified Commit 1875448d authored by Harry Barber's avatar Harry Barber Committed by GitHub
Browse files

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`.
parent f1a47826
Loading
Loading
Loading
Loading
+10 −4
Original line number Diff line number Diff line
@@ -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));
+193 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}