Skip to content

Commit 19a3a66

Browse files
authored
merge dev in staging perf (#2989)
2 parents d50aed1 + 177e837 commit 19a3a66

25 files changed

+513
-278
lines changed

.changesets/fix_geal_context_mutex.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### use a parking-lot mutex in Context ([Issue #2751](https://github.com/apollographql/router/issues/2751))
2+
3+
The context requires synchronized access to the busy timer, and precedently we used a futures aware mutex for that, but those are susceptible to contention. This replaces that mutex with a parking-lot synchronous mutex that is much faster.
4+
5+
By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2885
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### prevent span attributes from being formatted to write logs
2+
3+
we do not show span attributes in our logs, but the log formatter still spends some time formatting them to a string, even when there will be no logs written for the trace. This adds the `NullFieldFormatter` that entirely avoids formatting the attributes
4+
5+
By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2890

.changesets/maint_garypen_jemalloc.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
### use jemalloc on linux
2+
3+
Detailed memory investigations of the router in use have revealed that there is a significant amount of memory fragmentation when using the default allocator, glibc, on linux. Performance testing and flamegraph analysis suggests that jemalloc on linux can yield significant performance improvements. In our tests, this figure shows performance to be about 35% faster than the default allocator. The improvement in performance being due to less time spent managing memory fragmentation.
4+
5+
Not everyone will see a 35% performance improvement in this release of the router. Depending on your usage pattern, you may see more or less than this. If you see a regression, please file an issue with details.
6+
7+
We have no reason to believe that there are allocation problems on other platforms, so this change is confined to linux.
8+
9+
By [@garypen](https://github.com/garypen) in https://github.com/apollographql/router/pull/2882
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
### lighter path manipulation in response formatting
2+
3+
Response formatting generates a lot of temporary allocations to create response paths that end up unused. By making a reference based type to hold these paths, we can prevent those allocations and improve performance.
4+
5+
By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2854
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Add a private part to the Context structure ([Issue #2800](https://github.com/apollographql/router/issues/2800))
2+
3+
There's a cost in using the `Context` structure throughout a request's lifecycle, due to the JSON serialization and deserialization, so it should be reserved from inter plugin communication between rhai, coprocessor and Rust. But for internal router usage, we can have a more efficient structure that avoids serialization costs, and does not expose data that should not be modified by plugins.
4+
5+
That structure is based on a map indexed by type id, which means that if some part of the code can see that type, then it can access it in the map.
6+
7+
By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2802

Cargo.lock

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ dependencies = [
344344
"opentelemetry-semantic-conventions",
345345
"opentelemetry-zipkin",
346346
"p256 0.12.0",
347+
"parking_lot 0.12.1",
347348
"paste",
348349
"pin-project-lite",
349350
"prometheus",
@@ -377,6 +378,7 @@ dependencies = [
377378
"test-log",
378379
"test-span",
379380
"thiserror",
381+
"tikv-jemallocator",
380382
"tokio",
381383
"tokio-rustls",
382384
"tokio-stream",
@@ -5880,6 +5882,26 @@ dependencies = [
58805882
"tower",
58815883
]
58825884

5885+
[[package]]
5886+
name = "tikv-jemalloc-sys"
5887+
version = "0.5.3+5.3.0-patched"
5888+
source = "registry+https://github.com/rust-lang/crates.io-index"
5889+
checksum = "a678df20055b43e57ef8cddde41cdfda9a3c1a060b67f4c5836dfb1d78543ba8"
5890+
dependencies = [
5891+
"cc",
5892+
"libc",
5893+
]
5894+
5895+
[[package]]
5896+
name = "tikv-jemallocator"
5897+
version = "0.5.0"
5898+
source = "registry+https://github.com/rust-lang/crates.io-index"
5899+
checksum = "20612db8a13a6c06d57ec83953694185a367e16945f66565e8028d2c0bd76979"
5900+
dependencies = [
5901+
"libc",
5902+
"tikv-jemalloc-sys",
5903+
]
5904+
58835905
[[package]]
58845906
name = "time"
58855907
version = "0.3.20"

apollo-router/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,14 @@ yaml-rust = "0.4.5"
203203
wsl = "0.1.0"
204204
tokio-rustls = "0.23.4"
205205
http-serde = "1.1.2"
206+
parking_lot = "0.12.1"
206207

207208
[target.'cfg(macos)'.dependencies]
208209
uname = "0.1.1"
209210

210211
[target.'cfg(unix)'.dependencies]
211212
uname = "0.1.1"
213+
tikv-jemallocator = "0.5"
212214

213215
[dev-dependencies]
214216
ecdsa = { version = "0.15.1", features = ["signing", "pem", "pkcs8"] }
@@ -256,3 +258,4 @@ tonic-build = "0.8.4"
256258
[[test]]
257259
name = "integration_tests"
258260
path = "tests/integration_tests.rs"
261+

apollo-router/src/axum_factory/axum_http_server_factory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ async fn handle_graphql(
436436
let context = request.context.clone();
437437

438438
let res = service.oneshot(request).await;
439-
let dur = context.busy_time().await;
439+
let dur = context.busy_time();
440440
let processing_seconds = dur.as_secs_f64();
441441

442442
tracing::info!(histogram.apollo_router_processing_time = processing_seconds,);
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// NOTE: this module is taken from tokio's tracing span's extensions
2+
// which is taken from https://github.com/hyperium/http/blob/master/src/extensions.rs
3+
4+
use std::any::Any;
5+
use std::any::TypeId;
6+
use std::collections::HashMap;
7+
use std::fmt;
8+
use std::hash::BuildHasherDefault;
9+
use std::hash::Hasher;
10+
11+
type AnyMap = HashMap<TypeId, Box<dyn Any + Send + Sync>, BuildHasherDefault<IdHasher>>;
12+
13+
// With TypeIds as keys, there's no need to hash them. They are already hashes
14+
// themselves, coming from the compiler. The IdHasher just holds the u64 of
15+
// the TypeId, and then returns it, instead of doing any bit fiddling.
16+
#[derive(Default)]
17+
struct IdHasher(u64);
18+
19+
impl Hasher for IdHasher {
20+
fn write(&mut self, _: &[u8]) {
21+
unreachable!("TypeId calls write_u64");
22+
}
23+
24+
#[inline]
25+
fn write_u64(&mut self, id: u64) {
26+
self.0 = id;
27+
}
28+
29+
#[inline]
30+
fn finish(&self) -> u64 {
31+
self.0
32+
}
33+
}
34+
35+
/// A type map of protocol extensions.
36+
///
37+
/// `Extensions` can be used by `Request` and `Response` to store
38+
/// extra data derived from the underlying protocol.
39+
#[derive(Default)]
40+
pub(crate) struct Extensions {
41+
// If extensions are never used, no need to carry around an empty HashMap.
42+
// That's 3 words. Instead, this is only 1 word.
43+
map: Option<Box<AnyMap>>,
44+
}
45+
46+
#[allow(unused)]
47+
impl Extensions {
48+
/// Create an empty `Extensions`.
49+
#[inline]
50+
pub(crate) fn new() -> Extensions {
51+
Extensions { map: None }
52+
}
53+
54+
/// Insert a type into this `Extensions`.
55+
///
56+
/// If a extension of this type already existed, it will
57+
/// be returned.
58+
pub(crate) fn insert<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
59+
self.map
60+
.get_or_insert_with(Box::default)
61+
.insert(TypeId::of::<T>(), Box::new(val))
62+
.and_then(|boxed| {
63+
(boxed as Box<dyn Any + 'static>)
64+
.downcast()
65+
.ok()
66+
.map(|boxed| *boxed)
67+
})
68+
}
69+
70+
/// Get a reference to a type previously inserted on this `Extensions`.
71+
pub(crate) fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
72+
self.map
73+
.as_ref()
74+
.and_then(|map| map.get(&TypeId::of::<T>()))
75+
.and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref())
76+
}
77+
78+
/// Get a mutable reference to a type previously inserted on this `Extensions`.
79+
pub(crate) fn get_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
80+
self.map
81+
.as_mut()
82+
.and_then(|map| map.get_mut(&TypeId::of::<T>()))
83+
.and_then(|boxed| (&mut **boxed as &mut (dyn Any + 'static)).downcast_mut())
84+
}
85+
86+
pub(crate) fn contains_key<T: Send + Sync + 'static>(&self) -> bool {
87+
self.map
88+
.as_ref()
89+
.map(|map| map.contains_key(&TypeId::of::<T>()))
90+
.unwrap_or_default()
91+
}
92+
93+
/// Remove a type from this `Extensions`.
94+
///
95+
/// If a extension of this type existed, it will be returned.
96+
pub(crate) fn remove<T: Send + Sync + 'static>(&mut self) -> Option<T> {
97+
self.map
98+
.as_mut()
99+
.and_then(|map| map.remove(&TypeId::of::<T>()))
100+
.and_then(|boxed| {
101+
(boxed as Box<dyn Any + 'static>)
102+
.downcast()
103+
.ok()
104+
.map(|boxed| *boxed)
105+
})
106+
}
107+
108+
/// Clear the `Extensions` of all inserted extensions.
109+
#[inline]
110+
pub(crate) fn clear(&mut self) {
111+
if let Some(ref mut map) = self.map {
112+
map.clear();
113+
}
114+
}
115+
116+
/// Check whether the extension set is empty or not.
117+
#[inline]
118+
pub(crate) fn is_empty(&self) -> bool {
119+
self.map.as_ref().map_or(true, |map| map.is_empty())
120+
}
121+
122+
/// Get the numer of extensions available.
123+
#[inline]
124+
pub(crate) fn len(&self) -> usize {
125+
self.map.as_ref().map_or(0, |map| map.len())
126+
}
127+
}
128+
129+
impl fmt::Debug for Extensions {
130+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131+
f.debug_struct("Extensions").finish()
132+
}
133+
}
134+
135+
#[test]
136+
fn test_extensions() {
137+
#[derive(Debug, PartialEq)]
138+
struct MyType(i32);
139+
140+
let mut extensions = Extensions::new();
141+
142+
extensions.insert(5i32);
143+
extensions.insert(MyType(10));
144+
145+
assert_eq!(extensions.get(), Some(&5i32));
146+
assert_eq!(extensions.get_mut(), Some(&mut 5i32));
147+
148+
assert_eq!(extensions.remove::<i32>(), Some(5i32));
149+
assert!(extensions.get::<i32>().is_none());
150+
151+
assert_eq!(extensions.get::<bool>(), None);
152+
assert_eq!(extensions.get(), Some(&MyType(10)));
153+
}

apollo-router/src/context.rs renamed to apollo-router/src/context/mod.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ use std::time::Instant;
1010
use dashmap::mapref::multiple::RefMulti;
1111
use dashmap::mapref::multiple::RefMutMulti;
1212
use dashmap::DashMap;
13-
use futures::lock::Mutex;
13+
use derivative::Derivative;
14+
use parking_lot::Mutex;
1415
use serde::Deserialize;
1516
use serde::Serialize;
1617
use tower::BoxError;
1718

19+
use self::extensions::Extensions;
1820
use crate::json_ext::Value;
1921

22+
pub(crate) mod extensions;
23+
2024
/// Holds [`Context`] entries.
2125
pub(crate) type Entries = Arc<DashMap<String, Value>>;
2226

@@ -31,17 +35,22 @@ pub(crate) type Entries = Arc<DashMap<String, Value>>;
3135
/// [`crate::services::SubgraphResponse`] processing. At such times,
3236
/// plugins should restrict themselves to the [`Context::get`] and [`Context::upsert`]
3337
/// functions to minimise the possibility of mis-sequenced updates.
34-
#[derive(Clone, Debug, Deserialize, Serialize)]
38+
#[derive(Clone, Deserialize, Serialize, Derivative)]
39+
#[derivative(Debug)]
3540
pub struct Context {
3641
// Allows adding custom entries to the context.
3742
entries: Entries,
3843

44+
#[serde(skip, default)]
45+
pub(crate) private_entries: Arc<parking_lot::Mutex<Extensions>>,
46+
3947
/// Creation time
4048
#[serde(skip)]
4149
#[serde(default = "Instant::now")]
4250
pub(crate) created_at: Instant,
4351

4452
#[serde(skip)]
53+
#[derivative(Debug = "ignore")]
4554
busy_timer: Arc<Mutex<BusyTimer>>,
4655
}
4756

@@ -50,6 +59,7 @@ impl Context {
5059
pub fn new() -> Self {
5160
Context {
5261
entries: Default::default(),
62+
private_entries: Arc::new(parking_lot::Mutex::new(Extensions::default())),
5363
created_at: Instant::now(),
5464
busy_timer: Arc::new(Mutex::new(BusyTimer::new())),
5565
}
@@ -186,18 +196,18 @@ impl Context {
186196
}
187197

188198
/// Notify the busy timer that we're waiting on a network request
189-
pub(crate) async fn enter_active_request(&self) {
190-
self.busy_timer.lock().await.increment_active_requests()
199+
pub(crate) fn enter_active_request(&self) {
200+
self.busy_timer.lock().increment_active_requests()
191201
}
192202

193203
/// Notify the busy timer that we stopped waiting on a network request
194-
pub(crate) async fn leave_active_request(&self) {
195-
self.busy_timer.lock().await.decrement_active_requests()
204+
pub(crate) fn leave_active_request(&self) {
205+
self.busy_timer.lock().decrement_active_requests()
196206
}
197207

198208
/// How much time was spent working on the request
199-
pub(crate) async fn busy_time(&self) -> Duration {
200-
self.busy_timer.lock().await.current()
209+
pub(crate) fn busy_time(&self) -> Duration {
210+
self.busy_timer.lock().current()
201211
}
202212
}
203213

0 commit comments

Comments
 (0)