Skip to content

Commit 627d356

Browse files
committed
perf: speed up import sorting
1 parent a8c550e commit 627d356

5 files changed

Lines changed: 465 additions & 161 deletions

File tree

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ name = "specs"
2929
path = "tests/spec_test.rs"
3030
harness = false
3131

32+
[[bench]]
33+
name = "formatting"
34+
path = "benches/formatting.rs"
35+
harness = false
36+
3237
[dependencies]
3338
anyhow = "1.0.64"
3439
capacity_builder = "0.5.0"

benches/formatting.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::hint::black_box;
2+
use std::path::Path;
3+
use std::sync::Arc;
4+
use std::time::Duration;
5+
use std::time::Instant;
6+
7+
use deno_ast::MediaType;
8+
use deno_ast::ModuleSpecifier;
9+
use deno_ast::ParsedSource;
10+
use dprint_plugin_typescript::configuration::BuiltinCategory;
11+
use dprint_plugin_typescript::configuration::Configuration;
12+
use dprint_plugin_typescript::configuration::ConfigurationBuilder;
13+
use dprint_plugin_typescript::configuration::ImportGroup;
14+
use dprint_plugin_typescript::configuration::ImportGroupMatch;
15+
use dprint_plugin_typescript::configuration::ImportMatcher;
16+
use dprint_plugin_typescript::configuration::SortOrder;
17+
use dprint_plugin_typescript::format_parsed_source;
18+
use dprint_plugin_typescript::format_text;
19+
use dprint_plugin_typescript::FormatTextOptions;
20+
21+
fn main() {
22+
let iterations = env_usize("DPRINT_BENCH_ITERS").unwrap_or(10);
23+
let import_count = env_usize("DPRINT_BENCH_IMPORTS").unwrap_or(2_000);
24+
let named_count = env_usize("DPRINT_BENCH_NAMED_IMPORTS").unwrap_or(2_000);
25+
let parent_segments = env_usize("DPRINT_BENCH_PARENT_SEGMENTS").unwrap_or(6).max(1);
26+
let config = ConfigurationBuilder::new().build();
27+
let maintain_order_config = ConfigurationBuilder::new()
28+
.module_sort_import_declarations(SortOrder::Maintain)
29+
.module_sort_export_declarations(SortOrder::Maintain)
30+
.import_declaration_sort_named_imports(SortOrder::Maintain)
31+
.export_declaration_sort_named_exports(SortOrder::Maintain)
32+
.build();
33+
let grouped_imports_config = ConfigurationBuilder::new()
34+
.module_import_groups(vec![
35+
import_group(BuiltinCategory::Builtin),
36+
import_group(BuiltinCategory::External),
37+
import_group(BuiltinCategory::Parent),
38+
ImportGroup {
39+
matchers: ImportGroupMatch::Multiple(vec![
40+
ImportMatcher::Category(BuiltinCategory::Sibling),
41+
ImportMatcher::Category(BuiltinCategory::Index),
42+
]),
43+
},
44+
])
45+
.build();
46+
47+
let sorted_imports_text = sorted_import_declarations(import_count, parent_segments);
48+
let sorted_exports_text = sorted_export_declarations(import_count, parent_segments);
49+
let grouped_imports_text = grouped_import_declarations(import_count, parent_segments);
50+
let sorted_named_imports_text = sorted_named_imports(named_count);
51+
let sorted_named_exports_text = sorted_named_exports(named_count);
52+
let sorted_imports_parsed = parse_source(&sorted_imports_text);
53+
let sorted_exports_parsed = parse_source(&sorted_exports_text);
54+
let grouped_imports_parsed = parse_source(&grouped_imports_text);
55+
let sorted_named_imports_parsed = parse_source(&sorted_named_imports_text);
56+
let sorted_named_exports_parsed = parse_source(&sorted_named_exports_text);
57+
58+
println!("iterations: {iterations}");
59+
println!("import declarations: {import_count}");
60+
println!("named imports: {named_count}");
61+
println!("max parent segments: {parent_segments}");
62+
63+
bench("sorted_import_declarations/full", iterations, || {
64+
run_format_text(&sorted_imports_text, &config)
65+
});
66+
bench("sorted_import_declarations/parsed", iterations, || {
67+
run_format_parsed_source(&sorted_imports_parsed, &config)
68+
});
69+
bench("maintain_import_declarations/parsed", iterations, || {
70+
run_format_parsed_source(&sorted_imports_parsed, &maintain_order_config)
71+
});
72+
bench("sorted_export_declarations/full", iterations, || {
73+
run_format_text(&sorted_exports_text, &config)
74+
});
75+
bench("sorted_export_declarations/parsed", iterations, || {
76+
run_format_parsed_source(&sorted_exports_parsed, &config)
77+
});
78+
bench("maintain_export_declarations/parsed", iterations, || {
79+
run_format_parsed_source(&sorted_exports_parsed, &maintain_order_config)
80+
});
81+
bench("grouped_import_declarations/full", iterations, || {
82+
run_format_text(&grouped_imports_text, &grouped_imports_config)
83+
});
84+
bench("grouped_import_declarations/parsed", iterations, || {
85+
run_format_parsed_source(&grouped_imports_parsed, &grouped_imports_config)
86+
});
87+
bench("sorted_named_imports/full", iterations, || {
88+
run_format_text(&sorted_named_imports_text, &config)
89+
});
90+
bench("sorted_named_imports/parsed", iterations, || {
91+
run_format_parsed_source(&sorted_named_imports_parsed, &config)
92+
});
93+
bench("maintain_named_imports/parsed", iterations, || {
94+
run_format_parsed_source(&sorted_named_imports_parsed, &maintain_order_config)
95+
});
96+
bench("sorted_named_exports/full", iterations, || {
97+
run_format_text(&sorted_named_exports_text, &config)
98+
});
99+
bench("sorted_named_exports/parsed", iterations, || {
100+
run_format_parsed_source(&sorted_named_exports_parsed, &config)
101+
});
102+
bench("maintain_named_exports/parsed", iterations, || {
103+
run_format_parsed_source(&sorted_named_exports_parsed, &maintain_order_config)
104+
});
105+
}
106+
107+
fn bench(name: &str, iterations: usize, mut run: impl FnMut() -> usize) {
108+
for _ in 0..2 {
109+
black_box(run());
110+
}
111+
112+
let mut timings = Vec::with_capacity(iterations);
113+
for _ in 0..iterations {
114+
let start = Instant::now();
115+
black_box(run());
116+
timings.push(start.elapsed());
117+
}
118+
119+
timings.sort();
120+
let total = timings.iter().copied().fold(Duration::ZERO, |acc, timing| acc + timing);
121+
let min = timings[0];
122+
let median = timings[timings.len() / 2];
123+
let avg = total / timings.len() as u32;
124+
125+
println!(
126+
"{name}: min={:.2}ms median={:.2}ms avg={:.2}ms",
127+
ms(min),
128+
ms(median),
129+
ms(avg),
130+
);
131+
}
132+
133+
fn run_format_text(file_text: &str, config: &Configuration) -> usize {
134+
let result = format_text(FormatTextOptions {
135+
path: Path::new("bench.ts"),
136+
extension: None,
137+
text: file_text.to_string(),
138+
config,
139+
external_formatter: None,
140+
})
141+
.unwrap();
142+
result.as_deref().unwrap_or(file_text).len()
143+
}
144+
145+
fn run_format_parsed_source(parsed_source: &ParsedSource, config: &Configuration) -> usize {
146+
let result = format_parsed_source(parsed_source, config, None).unwrap();
147+
result.as_deref().unwrap_or(parsed_source.text()).len()
148+
}
149+
150+
fn parse_source(file_text: &str) -> ParsedSource {
151+
let media_type = MediaType::TypeScript;
152+
deno_ast::parse_program(deno_ast::ParseParams {
153+
specifier: ModuleSpecifier::parse("file:///bench.ts").unwrap(),
154+
capture_tokens: true,
155+
maybe_syntax: Some(deno_ast::get_syntax(media_type)),
156+
media_type,
157+
scope_analysis: false,
158+
text: Arc::from(file_text),
159+
})
160+
.unwrap()
161+
}
162+
163+
fn sorted_import_declarations(import_count: usize, max_parent_segments: usize) -> String {
164+
let mut text = String::new();
165+
for i in (0..import_count).rev() {
166+
let parent_count = i % max_parent_segments;
167+
let prefix = if parent_count == 0 {
168+
".".to_string()
169+
} else {
170+
"../".repeat(parent_count)
171+
};
172+
text.push_str(&format!(
173+
"import {{ Z{i}, a{i}, type T{i}, b{i} as B{i} }} from \"{prefix}pkg{}/mod{i}\";\n",
174+
i % 97,
175+
));
176+
}
177+
text.push_str("\nexport const value = 1;\n");
178+
text
179+
}
180+
181+
fn grouped_import_declarations(import_count: usize, max_parent_segments: usize) -> String {
182+
let mut text = String::new();
183+
for i in (0..import_count).rev() {
184+
let source = match i % 4 {
185+
0 => format!("node:bench{i}/mod"),
186+
1 => format!("pkg{}/mod{i}", i % 97),
187+
2 => format!("{}pkg{}/mod{i}", "../".repeat((i % max_parent_segments) + 1), i % 97),
188+
_ => format!("./pkg{}/mod{i}", i % 97),
189+
};
190+
text.push_str(&format!(
191+
"import {{ Z{i}, a{i}, type T{i}, b{i} as B{i} }} from \"{source}\";\n",
192+
));
193+
}
194+
text.push_str("\nexport const value = 1;\n");
195+
text
196+
}
197+
198+
fn sorted_export_declarations(export_count: usize, max_parent_segments: usize) -> String {
199+
let mut text = String::new();
200+
for i in (0..export_count).rev() {
201+
let parent_count = i % max_parent_segments;
202+
let prefix = if parent_count == 0 {
203+
".".to_string()
204+
} else {
205+
"../".repeat(parent_count)
206+
};
207+
text.push_str(&format!(
208+
"export {{ Z{i}, a{i}, type T{i}, b{i} as B{i} }} from \"{prefix}pkg{}/mod{i}\";\n",
209+
i % 97,
210+
));
211+
}
212+
text.push_str("\nexport const value = 1;\n");
213+
text
214+
}
215+
216+
fn sorted_named_imports(named_count: usize) -> String {
217+
let mut text = String::from("import {\n");
218+
for i in (0..named_count).rev() {
219+
if i % 5 == 0 {
220+
text.push_str(&format!(" type T{i},\n"));
221+
} else {
222+
text.push_str(&format!(" Z{i} as z{i},\n"));
223+
}
224+
}
225+
text.push_str("} from \"pkg\";\n\nexport const value = 1;\n");
226+
text
227+
}
228+
229+
fn sorted_named_exports(named_count: usize) -> String {
230+
let mut text = String::from("export {\n");
231+
for i in (0..named_count).rev() {
232+
if i % 5 == 0 {
233+
text.push_str(&format!(" type T{i},\n"));
234+
} else {
235+
text.push_str(&format!(" Z{i} as z{i},\n"));
236+
}
237+
}
238+
text.push_str("} from \"pkg\";\n\nexport const value = 1;\n");
239+
text
240+
}
241+
242+
fn env_usize(name: &str) -> Option<usize> {
243+
std::env::var(name).ok().and_then(|value| value.parse().ok())
244+
}
245+
246+
fn ms(duration: Duration) -> f64 {
247+
duration.as_secs_f64() * 1_000.0
248+
}
249+
250+
fn import_group(category: BuiltinCategory) -> ImportGroup {
251+
ImportGroup {
252+
matchers: ImportGroupMatch::Single(ImportMatcher::Category(category)),
253+
}
254+
}

0 commit comments

Comments
 (0)