Skip to content

Commit 513cf86

Browse files
committed
rustdoc: use a more compact encoding for implementors/trait.*.js
The exact amount that this reduces the size of an implementors file depends on whether most of the impls are synthetic or not. For `Send`, it reduces the file from 128K to 116K, while for `Clone` it went from 64K to 52K.
1 parent 3830eca commit 513cf86

File tree

2 files changed

+83
-21
lines changed

2 files changed

+83
-21
lines changed

Diff for: src/librustdoc/html/render/write_shared.rs

+73-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::ffi::OsStr;
2-
use std::fmt::Write;
32
use std::fs::{self, File};
43
use std::io::prelude::*;
54
use std::io::{self, BufReader};
@@ -10,7 +9,6 @@ use std::sync::LazyLock as Lazy;
109
use itertools::Itertools;
1110
use rustc_data_structures::flock;
1211
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
13-
use serde::Serialize;
1412

1513
use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
1614
use crate::clean::Crate;
@@ -284,25 +282,43 @@ pub(super) fn write_shared(
284282
cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
285283
}
286284

287-
fn collect(path: &Path, krate: &str, key: &str) -> io::Result<(Vec<String>, Vec<String>)> {
285+
/// Read a file and return all lines that match the `"{crate}":{data},` format,
286+
/// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
287+
///
288+
/// This forms the payload of files that look like this:
289+
///
290+
/// ```javascript
291+
/// var data = {
292+
/// "{crate1}":{data},
293+
/// "{crate2}":{data}
294+
/// };
295+
/// use_data(data);
296+
/// ```
297+
///
298+
/// The file needs to be formatted so that *only crate data lines start with `"`*.
299+
fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
288300
let mut ret = Vec::new();
289301
let mut krates = Vec::new();
290302

291303
if path.exists() {
292-
let prefix = format!(r#"{}["{}"]"#, key, krate);
304+
let prefix = format!("\"{}\"", krate);
293305
for line in BufReader::new(File::open(path)?).lines() {
294306
let line = line?;
295-
if !line.starts_with(key) {
307+
if !line.starts_with('"') {
296308
continue;
297309
}
298310
if line.starts_with(&prefix) {
299311
continue;
300312
}
301-
ret.push(line.to_string());
313+
if line.ends_with(",") {
314+
ret.push(line[..line.len() - 1].to_string());
315+
} else {
316+
// No comma (it's the case for the last added crate line)
317+
ret.push(line.to_string());
318+
}
302319
krates.push(
303-
line[key.len() + 2..]
304-
.split('"')
305-
.next()
320+
line.split('"')
321+
.find(|s| !s.is_empty())
306322
.map(|s| s.to_owned())
307323
.unwrap_or_else(String::new),
308324
);
@@ -311,6 +327,20 @@ pub(super) fn write_shared(
311327
Ok((ret, krates))
312328
}
313329

330+
/// Read a file and return all lines that match the <code>"{crate}":{data},\</code> format,
331+
/// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
332+
///
333+
/// This forms the payload of files that look like this:
334+
///
335+
/// ```javascript
336+
/// var data = JSON.parse('{\
337+
/// "{crate1}":{data},\
338+
/// "{crate2}":{data}\
339+
/// }');
340+
/// use_data(data);
341+
/// ```
342+
///
343+
/// The file needs to be formatted so that *only crate data lines start with `"`*.
314344
fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
315345
let mut ret = Vec::new();
316346
let mut krates = Vec::new();
@@ -526,13 +556,40 @@ if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
526556
},
527557
};
528558

529-
#[derive(Serialize)]
530559
struct Implementor {
531560
text: String,
532561
synthetic: bool,
533562
types: Vec<String>,
534563
}
535564

565+
impl Implementor {
566+
fn to_js_string(&self) -> String {
567+
fn single_quote_string(s: &str) -> String {
568+
let mut result = String::with_capacity(s.len() + 2);
569+
result.push_str("'");
570+
for c in s.chars() {
571+
if c == '"' {
572+
result.push_str("\"");
573+
} else {
574+
result.extend(c.escape_default());
575+
}
576+
}
577+
result.push_str("'");
578+
result
579+
}
580+
let text_esc = single_quote_string(&self.text);
581+
if self.synthetic {
582+
let types = self.types.iter().map(|type_| single_quote_string(type_)).join(",");
583+
// use `1` to represent a synthetic, because it's fewer bytes than `true`
584+
format!("[{text_esc},1,[{types}]]")
585+
} else {
586+
// The types list is only used for synthetic impls.
587+
// If this changes, `main.js` and `write_shared.rs` both need changed.
588+
format!("[{text_esc}]")
589+
}
590+
}
591+
}
592+
536593
let implementors = imps
537594
.iter()
538595
.filter_map(|imp| {
@@ -563,9 +620,9 @@ if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
563620
}
564621

565622
let implementors = format!(
566-
r#"implementors["{}"] = {};"#,
623+
r#""{}":[{}]"#,
567624
krate.name(cx.tcx()),
568-
serde_json::to_string(&implementors).unwrap()
625+
implementors.iter().map(Implementor::to_js_string).join(",")
569626
);
570627

571628
let mut mydst = dst.clone();
@@ -576,16 +633,15 @@ if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
576633
mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
577634

578635
let (mut all_implementors, _) =
579-
try_err!(collect(&mydst, krate.name(cx.tcx()).as_str(), "implementors"), &mydst);
636+
try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
580637
all_implementors.push(implementors);
581638
// Sort the implementors by crate so the file will be generated
582639
// identically even with rustdoc running in parallel.
583640
all_implementors.sort();
584641

585-
let mut v = String::from("(function() {var implementors = {};\n");
586-
for implementor in &all_implementors {
587-
writeln!(v, "{}", *implementor).unwrap();
588-
}
642+
let mut v = String::from("(function() {var implementors = {\n");
643+
v.push_str(&all_implementors.join(",\n"));
644+
v.push_str("\n};");
589645
v.push_str(
590646
"if (window.register_implementors) {\
591647
window.register_implementors(implementors);\

Diff for: src/librustdoc/html/static/js/main.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,10 @@ function loadCss(cssFileName) {
501501
const synthetic_implementors = document.getElementById("synthetic-implementors-list");
502502
const inlined_types = new Set();
503503

504+
const TEXT_IDX = 0;
505+
const SYNTHETIC_IDX = 1;
506+
const TYPES_IDX = 2;
507+
504508
if (synthetic_implementors) {
505509
// This `inlined_types` variable is used to avoid having the same implementation
506510
// showing up twice. For example "String" in the "Sync" doc page.
@@ -536,10 +540,12 @@ function loadCss(cssFileName) {
536540

537541
struct_loop:
538542
for (const struct of structs) {
539-
const list = struct.synthetic ? synthetic_implementors : implementors;
543+
const list = struct[SYNTHETIC_IDX] ? synthetic_implementors : implementors;
540544

541-
if (struct.synthetic) {
542-
for (const struct_type of struct.types) {
545+
// The types list is only used for synthetic impls.
546+
// If this changes, `main.js` and `write_shared.rs` both need changed.
547+
if (struct[SYNTHETIC_IDX]) {
548+
for (const struct_type of struct[TYPES_IDX]) {
543549
if (inlined_types.has(struct_type)) {
544550
continue struct_loop;
545551
}
@@ -548,7 +554,7 @@ function loadCss(cssFileName) {
548554
}
549555

550556
const code = document.createElement("h3");
551-
code.innerHTML = struct.text;
557+
code.innerHTML = struct[TEXT_IDX];
552558
addClass(code, "code-header");
553559
addClass(code, "in-band");
554560

0 commit comments

Comments
 (0)