Skip to content

Commit af30805

Browse files
committed
offload CPU intensive rendering into rayon threadpool
for now only: * source pages * rustdoc pages
1 parent ce770c2 commit af30805

File tree

8 files changed

+85
-42
lines changed

8 files changed

+85
-42
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ exclude = [
1616
]
1717

1818
[features]
19-
consistency_check = ["crates-index", "rayon", "itertools"]
19+
consistency_check = ["crates-index", "itertools"]
2020

2121
[dependencies]
2222
sentry = "0.30.0"
@@ -31,7 +31,8 @@ tracing-log = "0.1.3"
3131
regex = "1"
3232
clap = { version = "4.0.22", features = [ "derive" ] }
3333
crates-index = { version = "0.18.5", optional = true }
34-
rayon = { version = "1", optional = true }
34+
rayon = "1.6.1"
35+
num_cpus = "1.15.0"
3536
crates-index-diff = { version = "16.0.1", features = [ "max-performance" ]}
3637
reqwest = { version = "0.11", features = ["blocking", "json"] } # TODO: Remove blocking when async is ready
3738
semver = { version = "1.0.4", features = ["serde"] }

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ pub struct Config {
4949
// Time between 'git gc --auto' calls in seconds
5050
pub(crate) registry_gc_interval: u64,
5151

52+
/// amout of threads for CPU intensive rendering
53+
pub(crate) render_threads: usize,
54+
5255
// random crate search generates a number of random IDs to
5356
// efficiently find a random crate with > 100 GH stars.
5457
// The amount depends on the ratio of crates with >100 stars
@@ -153,6 +156,7 @@ impl Config {
153156
// https://github.com/rust-lang/docs.rs/pull/930#issuecomment-667729380
154157
max_parse_memory: env("DOCSRS_MAX_PARSE_MEMORY", 5 * 1024 * 1024)?,
155158
registry_gc_interval: env("DOCSRS_REGISTRY_GC_INTERVAL", 60 * 60)?,
159+
render_threads: env("DOCSRS_RENDER_THREADS", num_cpus::get())?,
156160

157161
random_crate_search_view_size: env("DOCSRS_RANDOM_CRATE_SEARCH_VIEW_SIZE", 500)?,
158162

src/test/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,13 @@ impl TestFrontend {
548548
}
549549

550550
debug!("loading template data");
551-
let template_data =
552-
Arc::new(TemplateData::new(&mut context.pool().unwrap().get().unwrap()).unwrap());
551+
let template_data = Arc::new(
552+
TemplateData::new(
553+
&mut context.pool().unwrap().get().unwrap(),
554+
context.config().unwrap().render_threads,
555+
)
556+
.unwrap(),
557+
);
553558

554559
debug!("binding local TCP port for axum");
555560
let axum_listener =

src/web/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,10 @@ pub(crate) fn build_axum_app(
272272

273273
#[instrument(skip_all)]
274274
pub fn start_web_server(addr: Option<&str>, context: &dyn Context) -> Result<(), Error> {
275-
let template_data = Arc::new(TemplateData::new(&mut *context.pool()?.get()?)?);
275+
let template_data = Arc::new(TemplateData::new(
276+
&mut *context.pool()?.get()?,
277+
context.config()?.render_threads,
278+
)?);
276279

277280
let axum_addr: SocketAddr = addr.unwrap_or(DEFAULT_BIND).parse()?;
278281

src/web/page/templates.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
77
use path_slash::PathExt;
88
use postgres::Client;
99
use serde_json::Value;
10-
use std::{collections::HashMap, fmt, path::PathBuf};
10+
use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
1111
use tera::{Result as TeraResult, Tera};
1212
use tracing::{error, trace};
1313
use walkdir::WalkDir;
@@ -18,20 +18,49 @@ const TEMPLATES_DIRECTORY: &str = "templates";
1818
#[derive(Debug)]
1919
pub(crate) struct TemplateData {
2020
pub templates: Tera,
21+
rendering_threadpool: rayon::ThreadPool,
2122
}
2223

2324
impl TemplateData {
24-
pub(crate) fn new(conn: &mut Client) -> Result<Self> {
25+
pub(crate) fn new(conn: &mut Client, num_threads: usize) -> Result<Self> {
2526
trace!("Loading templates");
2627

2728
let data = Self {
2829
templates: load_templates(conn)?,
30+
rendering_threadpool: rayon::ThreadPoolBuilder::new()
31+
.num_threads(num_threads)
32+
.thread_name(move |idx| format!("docsrs-render {idx}"))
33+
.build()?,
2934
};
3035

3136
trace!("Finished loading templates");
3237

3338
Ok(data)
3439
}
40+
41+
/// offload CPU intensive rendering into a rayon threadpool.
42+
///
43+
/// This is a thin wrapper around `rayon::spawn` which waits
44+
/// sync task to finish.
45+
///
46+
/// Use this instead of `spawn_blocking` so we don't block tokio.
47+
pub(crate) async fn render_in_threadpool<F, R>(self: &Arc<Self>, render_fn: F) -> Result<R>
48+
where
49+
F: FnOnce(&TemplateData) -> Result<R> + Send + 'static,
50+
R: Send + 'static,
51+
{
52+
let (send, recv) = tokio::sync::oneshot::channel();
53+
self.rendering_threadpool.spawn({
54+
let templates = self.clone();
55+
move || {
56+
// `.send` only fails when the receiver is dropped,
57+
// at which point we don't need the result any more.
58+
let _ = send.send(render_fn(&templates));
59+
}
60+
});
61+
62+
recv.await.context("sender was dropped")?
63+
}
3564
}
3665

3766
fn load_rustc_resource_suffix(conn: &mut Client) -> Result<String> {

src/web/page/web_page.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
use super::TemplateData;
2-
use crate::{
3-
utils::spawn_blocking,
4-
web::{csp::Csp, error::AxumNope},
5-
};
2+
use crate::web::{csp::Csp, error::AxumNope};
63
use anyhow::Error;
74
use axum::{
85
body::{boxed, Body},
@@ -141,11 +138,14 @@ fn render_response(
141138
context.insert("csp_nonce", &csp_nonce);
142139

143140
let rendered = if cpu_intensive_rendering {
144-
spawn_blocking({
145-
let templates = templates.clone();
146-
move || Ok(templates.templates.render(&template, &context)?)
147-
})
148-
.await
141+
templates
142+
.render_in_threadpool(move |templates| {
143+
templates
144+
.templates
145+
.render(&template, &context)
146+
.map_err(Into::into)
147+
})
148+
.await
149149
} else {
150150
templates
151151
.templates

src/web/rustdoc.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -649,33 +649,33 @@ pub(crate) async fn rustdoc_html_server_handler(
649649
rendering_time.step("rewrite html");
650650

651651
// Build the page of documentation,
652-
// inside `spawn_blocking` since it's CPU intensive.
653-
spawn_blocking({
654-
let metrics = metrics.clone();
655-
move || {
656-
Ok(RustdocPage {
657-
latest_path,
658-
permalink_path,
659-
latest_version,
660-
target,
661-
inner_path,
662-
is_latest_version,
663-
is_latest_url,
664-
is_prerelease,
665-
metadata: krate.metadata.clone(),
666-
krate: krate.clone(),
652+
templates
653+
.render_in_threadpool({
654+
let metrics = metrics.clone();
655+
move |templates| {
656+
Ok(RustdocPage {
657+
latest_path,
658+
permalink_path,
659+
latest_version,
660+
target,
661+
inner_path,
662+
is_latest_version,
663+
is_latest_url,
664+
is_prerelease,
665+
metadata: krate.metadata.clone(),
666+
krate: krate.clone(),
667+
}
668+
.into_response(
669+
&blob.content,
670+
config.max_parse_memory,
671+
templates,
672+
&metrics,
673+
&config,
674+
&storage_path,
675+
))
667676
}
668-
.into_response(
669-
&blob.content,
670-
config.max_parse_memory,
671-
&templates,
672-
&metrics,
673-
&config,
674-
&storage_path,
675-
))
676-
}
677-
})
678-
.await?
677+
})
678+
.await?
679679
}
680680

681681
/// Checks whether the given path exists.

0 commit comments

Comments
 (0)