Skip to content

Commit c7ff312

Browse files
authored
Add parallel processing to forc-doc using rayon to improve performance (#7296)
## Description Key changes: - Parallelize document rendering and link generation - Add type aliases for complex nested types (`DocLinkMap`, `ModuleMap`, `RenderResult`) - Remove unnecessary wrapper functions and clones The parallel processing maintains insertion order through sequential merging, ensuring identical documentation output. Not sure why the codspeed report isn't showing this below but when I run `cargo bench` locally i'm seeing these results. | Metric | Before | After | Improvement | |--------|--------|-------|-------------| | build_std_lib_docs | 70.069 ms | 43.227 ms | **43.2% faster** | ## Checklist - [ ] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [x] I have requested a review from the relevant team or maintainers.
1 parent 32dcbaa commit c7ff312

File tree

7 files changed

+116
-58
lines changed

7 files changed

+116
-58
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.

forc-plugins/forc-doc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ horrorshow.workspace = true
1919
include_dir.workspace = true
2020
minifier.workspace = true
2121
opener.workspace = true
22+
rayon.workspace = true
2223
serde.workspace = true
2324
serde_json.workspace = true
2425
sway-ast.workspace = true

forc-plugins/forc-doc/src/doc/descriptor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ trait RequiredMethods {
2727
impl RequiredMethods for Vec<DeclRefTraitFn> {
2828
fn to_methods(&self, decl_engine: &DeclEngine) -> Vec<TyTraitFn> {
2929
self.iter()
30-
.map(|decl_ref| (*decl_engine.get_trait_fn(decl_ref)).clone())
30+
.map(|decl_ref| decl_engine.get_trait_fn(decl_ref).as_ref().clone())
3131
.collect()
3232
}
3333
}
@@ -371,7 +371,7 @@ impl Descriptor {
371371
module_info,
372372
ty: DocumentableType::Primitive(type_info.clone()),
373373
item_name: item_name.clone(),
374-
code_str: item_name.clone().to_string(),
374+
code_str: item_name.to_string(),
375375
attrs_opt: attrs_opt.clone(),
376376
item_context: Default::default(),
377377
},

forc-plugins/forc-doc/src/doc/mod.rs

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::{
1414
},
1515
};
1616
use anyhow::Result;
17+
use rayon::prelude::*;
1718
use std::{
1819
collections::HashMap,
1920
ops::{Deref, DerefMut},
@@ -80,14 +81,24 @@ impl Documentation {
8081
.collect::<HashMap<BaseIdent, ModuleInfo>>();
8182

8283
// Add one documentation page for each primitive type that has an implementation.
83-
for (impl_trait, module_info) in impl_traits.iter() {
84-
let impl_for_type = engines.te().get(impl_trait.implementing_for.type_id());
85-
if let Ok(Descriptor::Documentable(doc)) =
86-
Descriptor::from_type_info(impl_for_type.as_ref(), engines, module_info.clone())
87-
{
88-
if !docs.contains(&doc) {
89-
docs.push(doc);
84+
let primitive_docs: Vec<_> = impl_traits
85+
.par_iter()
86+
.filter_map(|(impl_trait, module_info)| {
87+
let impl_for_type = engines.te().get(impl_trait.implementing_for.type_id());
88+
if let Ok(Descriptor::Documentable(doc)) =
89+
Descriptor::from_type_info(impl_for_type.as_ref(), engines, module_info.clone())
90+
{
91+
Some(doc)
92+
} else {
93+
None
9094
}
95+
})
96+
.collect();
97+
98+
// Add unique primitive docs
99+
for doc in primitive_docs {
100+
if !docs.contains(&doc) {
101+
docs.push(doc);
91102
}
92103
}
93104

@@ -103,7 +114,7 @@ impl Documentation {
103114
DocumentableType::Declared(TyDecl::StructDecl(_))
104115
| DocumentableType::Declared(TyDecl::EnumDecl(_))
105116
| DocumentableType::Primitive(_) => {
106-
let item_name = doc.item_header.item_name.clone();
117+
let item_name = &doc.item_header.item_name;
107118
for (impl_trait, _) in impl_traits.iter_mut() {
108119
// Check if this implementation is for this struct/enum.
109120
if item_name.as_str()
@@ -157,13 +168,23 @@ impl Documentation {
157168
document_private_items: bool,
158169
experimental: ExperimentalFeatures,
159170
) -> Result<()> {
160-
for ast_node in &ty_module.all_nodes {
161-
if let TyAstNodeContent::Declaration(ref decl) = ast_node.content {
171+
let results: Result<Vec<_>, anyhow::Error> = ty_module
172+
.all_nodes
173+
.par_iter()
174+
.filter_map(|ast_node| {
175+
if let TyAstNodeContent::Declaration(ref decl) = ast_node.content {
176+
Some(decl)
177+
} else {
178+
None
179+
}
180+
})
181+
.map(|decl| {
162182
if let TyDecl::ImplSelfOrTrait(impl_trait) = decl {
163-
impl_traits.push((
183+
let impl_data = (
164184
(*decl_engine.get_impl_self_or_trait(&impl_trait.decl_id)).clone(),
165185
module_info.clone(),
166-
));
186+
);
187+
Ok((Some(impl_data), None))
167188
} else {
168189
let desc = Descriptor::from_typed_decl(
169190
decl_engine,
@@ -173,10 +194,21 @@ impl Documentation {
173194
experimental,
174195
)?;
175196

176-
if let Descriptor::Documentable(doc) = desc {
177-
docs.push(doc);
178-
}
197+
let doc = match desc {
198+
Descriptor::Documentable(doc) => Some(doc),
199+
Descriptor::NonDocumentable => None,
200+
};
201+
Ok((None, doc))
179202
}
203+
})
204+
.collect();
205+
206+
for (impl_trait_opt, doc_opt) in results? {
207+
if let Some(impl_trait) = impl_trait_opt {
208+
impl_traits.push(impl_trait);
209+
}
210+
if let Some(doc) = doc_opt {
211+
docs.push(doc);
180212
}
181213
}
182214

forc-plugins/forc-doc/src/render/item/components.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ use sway_types::BaseIdent;
1616

1717
use super::documentable_type::DocumentableType;
1818

19+
// Asset file names to avoid repeated string formatting
20+
const SWAY_LOGO_FILE: &str = "sway-logo.svg";
21+
const NORMALIZE_CSS_FILE: &str = "normalize.css";
22+
const SWAYDOC_CSS_FILE: &str = "swaydoc.css";
23+
const AYU_CSS_FILE: &str = "ayu.css";
24+
const AYU_MIN_CSS_FILE: &str = "ayu.min.css";
25+
1926
/// All necessary components to render the header portion of
2027
/// the item html doc.
2128
#[derive(Clone, Debug)]
@@ -33,15 +40,16 @@ impl Renderable for ItemHeader {
3340
item_name,
3441
} = self;
3542

36-
let favicon =
37-
module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/sway-logo.svg"));
38-
let normalize =
39-
module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/normalize.css"));
40-
let swaydoc =
41-
module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/swaydoc.css"));
42-
let ayu = module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/ayu.css"));
43-
let ayu_hjs =
44-
module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/ayu.min.css"));
43+
let favicon = module_info
44+
.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/{SWAY_LOGO_FILE}"));
45+
let normalize = module_info
46+
.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/{NORMALIZE_CSS_FILE}"));
47+
let swaydoc = module_info
48+
.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/{SWAYDOC_CSS_FILE}"));
49+
let ayu =
50+
module_info.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/{AYU_CSS_FILE}"));
51+
let ayu_hjs = module_info
52+
.to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/{AYU_MIN_CSS_FILE}"));
4553

4654
Ok(box_html! {
4755
head {

forc-plugins/forc-doc/src/render/item/type_anchor.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,16 @@ pub(crate) fn render_type_anchor(
6767
let enum_decl = render_plan.engines.de().get_enum(&decl_id);
6868
if !render_plan.document_private_items && enum_decl.visibility.is_private() {
6969
Ok(box_html! {
70-
: enum_decl.name().clone().as_str();
70+
: enum_decl.name().as_str();
7171
})
7272
} else {
7373
let module_info = ModuleInfo::from_call_path(&enum_decl.call_path);
74-
let file_name = format!("enum.{}.html", enum_decl.name().clone().as_str());
74+
let file_name = format!("enum.{}.html", enum_decl.name().as_str());
7575
let href =
7676
module_info.file_path_from_location(&file_name, current_module_info, false)?;
7777
Ok(box_html! {
7878
a(class="enum", href=href) {
79-
: enum_decl.name().clone().as_str();
79+
: enum_decl.name().as_str();
8080
}
8181
})
8282
}
@@ -85,16 +85,16 @@ pub(crate) fn render_type_anchor(
8585
let struct_decl = render_plan.engines.de().get_struct(&decl_id);
8686
if !render_plan.document_private_items && struct_decl.visibility.is_private() {
8787
Ok(box_html! {
88-
: struct_decl.name().clone().as_str();
88+
: struct_decl.name().as_str();
8989
})
9090
} else {
9191
let module_info = ModuleInfo::from_call_path(&struct_decl.call_path);
92-
let file_name = format!("struct.{}.html", struct_decl.name().clone().as_str());
92+
let file_name = format!("struct.{}.html", struct_decl.name().as_str());
9393
let href =
9494
module_info.file_path_from_location(&file_name, current_module_info, false)?;
9595
Ok(box_html! {
9696
a(class="struct", href=href) {
97-
: struct_decl.name().clone().as_str();
97+
: struct_decl.name().as_str();
9898
}
9999
})
100100
}

forc-plugins/forc-doc/src/render/mod.rs

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::{
1414
};
1515
use anyhow::Result;
1616
use horrorshow::{box_html, helper::doctype, html, prelude::*};
17+
use rayon::prelude::*;
1718
use std::{
1819
collections::BTreeMap,
1920
ops::{Deref, DerefMut},
@@ -33,6 +34,10 @@ pub const ALL_DOC_FILENAME: &str = "all.html";
3334
pub const INDEX_FILENAME: &str = "index.html";
3435
pub const IDENTITY: &str = "#";
3536

37+
type DocLinkMap = BTreeMap<BlockTitle, Vec<DocLink>>;
38+
type ModuleMap = BTreeMap<ModulePrefixes, DocLinkMap>;
39+
type RenderResult = (RenderedDocument, ModuleMap, DocLinks);
40+
3641
/// Something that can be rendered to HTML.
3742
pub(crate) trait Renderable {
3843
fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>>;
@@ -90,19 +95,36 @@ impl RenderedDocumentation {
9095
style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
9196
links: BTreeMap::default(),
9297
};
93-
let mut module_map: BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>> =
94-
BTreeMap::new();
95-
for doc in raw_docs.0 {
96-
rendered_docs
97-
.0
98-
.push(RenderedDocument::from_doc(&doc, render_plan.clone())?);
98+
// Parallel document rendering
99+
let rendered_results: Result<Vec<RenderResult>, anyhow::Error> = raw_docs
100+
.0
101+
.par_iter()
102+
.map(|doc| {
103+
let rendered_doc = RenderedDocument::from_doc(doc, render_plan.clone())?;
104+
let mut local_module_map = ModuleMap::new();
105+
let mut local_all_docs = DocLinks {
106+
style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
107+
links: BTreeMap::default(),
108+
};
109+
110+
populate_decls(doc, &mut local_module_map);
111+
populate_modules(doc, &mut local_module_map);
112+
populate_doc_links(doc, &mut local_all_docs.links);
113+
114+
Ok((rendered_doc, local_module_map, local_all_docs))
115+
})
116+
.collect();
99117

100-
// Here we gather all of the `doc_links` based on which module they belong to.
101-
populate_decls(&doc, &mut module_map);
102-
// Create links to child modules.
103-
populate_modules(&doc, &mut module_map);
104-
// Above we check for the module a link belongs to, here we want _all_ links so the check is much more shallow.
105-
populate_all_doc(&doc, &mut all_docs);
118+
// Merge results sequentially
119+
let mut module_map = ModuleMap::new();
120+
for (rendered_doc, local_module_map, local_all_docs) in rendered_results? {
121+
rendered_docs.0.push(rendered_doc);
122+
123+
for (key, value) in local_module_map {
124+
module_map.entry(key).or_default().extend(value);
125+
}
126+
127+
all_docs.links.extend(local_all_docs.links);
106128
}
107129

108130
// ProjectIndex
@@ -199,7 +221,8 @@ impl DerefMut for RenderedDocumentation {
199221
}
200222
}
201223

202-
fn populate_doc_links(doc: &Document, doc_links: &mut BTreeMap<BlockTitle, Vec<DocLink>>) {
224+
/// Adds a document's link to the appropriate category in the doc links map.
225+
fn populate_doc_links(doc: &Document, doc_links: &mut DocLinkMap) {
203226
let key = doc.item_body.ty.as_block_title();
204227
match doc_links.get_mut(&key) {
205228
Some(links) => links.push(doc.link()),
@@ -208,23 +231,19 @@ fn populate_doc_links(doc: &Document, doc_links: &mut BTreeMap<BlockTitle, Vec<D
208231
}
209232
}
210233
}
211-
fn populate_decls(
212-
doc: &Document,
213-
module_map: &mut BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>>,
214-
) {
234+
/// Organizes document links by module prefix for navigation.
235+
fn populate_decls(doc: &Document, module_map: &mut ModuleMap) {
215236
let module_prefixes = &doc.module_info.module_prefixes;
216237
if let Some(doc_links) = module_map.get_mut(module_prefixes) {
217238
populate_doc_links(doc, doc_links)
218239
} else {
219-
let mut doc_links: BTreeMap<BlockTitle, Vec<DocLink>> = BTreeMap::new();
240+
let mut doc_links = DocLinkMap::new();
220241
populate_doc_links(doc, &mut doc_links);
221242
module_map.insert(module_prefixes.clone(), doc_links);
222243
}
223244
}
224-
fn populate_modules(
225-
doc: &Document,
226-
module_map: &mut BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>>,
227-
) {
245+
/// Creates links to parent modules for hierarchical navigation.
246+
fn populate_modules(doc: &Document, module_map: &mut ModuleMap) {
228247
let mut module_clone = doc.module_info.clone();
229248
while module_clone.parent().is_some() {
230249
let html_filename = if module_clone.depth() > 2 {
@@ -257,16 +276,13 @@ fn populate_modules(
257276
}
258277
}
259278
} else {
260-
let mut doc_links: BTreeMap<BlockTitle, Vec<DocLink>> = BTreeMap::new();
279+
let mut doc_links = DocLinkMap::new();
261280
doc_links.insert(BlockTitle::Modules, vec![module_link]);
262281
module_map.insert(module_prefixes.clone(), doc_links);
263282
}
264283
module_clone.module_prefixes.pop();
265284
}
266285
}
267-
fn populate_all_doc(doc: &Document, all_docs: &mut DocLinks) {
268-
populate_doc_links(doc, &mut all_docs.links);
269-
}
270286

271287
/// The finalized HTML file contents.
272288
#[derive(Debug)]

0 commit comments

Comments
 (0)