on master help sign in
Load independent and minimal syntax sets when using --language (#1787)

This significantly speeds up the startup time of bat, since only a single linked SyntaxDefinition is loaded for each file. The size increase of the binary is just ~400 kB. In order for startup time to be improved, the --language arg must be used, and it must match one of the following names: "Plain Text", "ActionScript", "AppleScript", "Batch File", "NAnt Build File", "C#", "C", "CSS", "D", "Diff", "Erlang", "Go", "Haskell", "JSON", "Java Properties", "BibTeX", "LaTeX Log", "TeX", "Lisp", "Lua", "MATLAB", "Pascal", "R", "Regular Expression", "Rust", "SQL", "Scala", "Tcl", "XML", "YAML", "Apache Conf", "ARM Assembly", "Assembly (x86_64)", "CMakeCache", "Comma Separated Values", "Cabal", "CoffeeScript", "CpuInfo", "Dart Analysis Output", "Dart", "Dockerfile", "DotENV", "F#", "Friendly Interactive Shell (fish)", "Fortran (Fixed Form)", "Fortran (Modern)", "Fortran Namelist", "fstab", "GLSL", "GraphQL", "Groff/troff", "group", "hosts", "INI", "Jinja2", "jsonnet", "Kotlin", "Less", "LLVM", "Lean", "MemInfo", "Nim", "Ninja", "Nix", "passwd", "PowerShell", "Protocol Buffer (TEXT)", "Puppet", "Rego", "resolv", "Robot Framework", "SML", "Strace", "Stylus", "Solidity", "Vyper", "Swift", "SystemVerilog", "TOML", "Terraform", "TypeScript", "TypeScriptReact", "Verilog", "VimL", "Zig", "gnuplot", "log", "requirements.txt", "Highlight non-printables", "Private Key", "varlink" Later commits will improve startup time for more code paths. * fix some typos and misspellings * CHANGELOG.md: Add Performance section (preliminary) * Add a CHANGELOG.md entry for this PR

commit9124271eaf237519f9381b78681d71113e308a58 authorMartin Nordholts <enselic@gmail.com> dateThu 9 Sep 2021, 20:52 (5 years ago) parent156dec2
5 files changed+181−27
CHANGELOG.md+5−2 assets/minimal_syntaxes.bin src/assets.rs+96−2 src/bin/bat/assets.rs+1 src/build_assets.rs+79−23
CHANGELOG.md+5−2
@@ -9,12 +9,15 @@
99
1010- Python syntax highlighting no longer suffers from abysmal performance in specific scenarios. See #1688 (@keith-hall)
1111
12−## Other
12+## Performance
1313
14−- Add PowerShell completion, see #1826 (@rashil2000)
1514- Load cached assets as fast as integrated assets, see #1753 (@Enselic)
1615- Greatly reduce startup time in loop-through mode, e.g. when redirecting output. Instead of *50 ms* - *100 ms*, startup takes *5 ms* - *10 ms*. See #1747 (@Enselic)
16+- Reduce startup time by approximately 80% for 91 out of 168 syntaxes when using `--language`. See #1787 (@Enselic)
1717
18+## Other
19+
20+- Add PowerShell completion, see #1826 (@rashil2000)
1821
1922## Syntaxes
2023
assets/minimal_syntaxes.binnew file

Binary file not shown.

src/assets.rs+96−2
@@ -1,3 +1,4 @@
1+use std::collections::HashMap;
12use std::ffi::OsStr;
23use std::fs;
34use std::path::{Path, PathBuf};
@@ -18,6 +19,14 @@ use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
1819pub struct HighlightingAssets {
1920 syntax_set_cell: LazyCell<SyntaxSet>,
2021 serialized_syntax_set: SerializedSyntaxSet,
22+
23+ minimal_syntaxes: MinimalSyntaxes,
24+
25+ /// Lazily load serialized [SyntaxSet]s from [Self.minimal_syntaxes]. The
26+ /// index in this vec matches the index in
27+ /// [Self.minimal_syntaxes.serialized_syntax_sets]
28+ deserialized_minimal_syntaxes: Vec<LazyCell<SyntaxSet>>,
29+
2130 theme_set: ThemeSet,
2231 fallback_theme: Option<&'static str>,
2332}
@@ -28,12 +37,39 @@ pub struct SyntaxReferenceInSet<'a> {
2837 pub syntax_set: &'a SyntaxSet,
2938}
3039
40+/// Stores and allows lookup of minimal [SyntaxSet]s. The [SyntaxSet]s are
41+/// stored in serialized form, and are deserialized on-demand. This gives good
42+/// startup performance since only the necessary [SyntaxReference]s needs to be
43+/// deserialized.
44+#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
45+pub(crate) struct MinimalSyntaxes {
46+ /// Lookup the index into `serialized_syntax_sets` of a [SyntaxSet] by the
47+ /// name of any [SyntaxReference] inside the [SyntaxSet]
48+ /// (We will later add `by_extension`, `by_first_line`, etc.)
49+ pub(crate) by_name: HashMap<String, usize>,
50+
51+ /// Serialized [SyntaxSet]s. Whether or not this data is compressed is
52+ /// decided by [COMPRESS_SERIALIZED_MINIMAL_SYNTAXES]
53+ pub(crate) serialized_syntax_sets: Vec<Vec<u8>>,
54+}
55+
3156// Compress for size of ~700 kB instead of ~4600 kB at the cost of ~30% longer deserialization time
3257pub(crate) const COMPRESS_SYNTAXES: bool = true;
3358
3459// Compress for size of ~20 kB instead of ~200 kB at the cost of ~30% longer deserialization time
3560pub(crate) const COMPRESS_THEMES: bool = true;
3661
62+// Compress for size of ~400 kB instead of ~2100 kB at the cost of ~30% longer deserialization time
63+pub(crate) const COMPRESS_SERIALIZED_MINIMAL_SYNTAXES: bool = true;
64+
65+// Whether or not to compress the serialized form of [MinimalSyntaxes]. Shall
66+// always be `false`, because the data in
67+// [MinimalSyntaxes.serialized_syntax_sets] has already been compressed
68+// (assuming [COMPRESS_SERIALIZED_MINIMAL_SYNTAXES] is `true`). The "outer" data
69+// structures like `by_name` are tiny. If we compress, deserialization can't do
70+// efficient byte-by-byte copy of `serialized_syntax_sets`.
71+pub(crate) const COMPRESS_MINIMAL_SYNTAXES: bool = false;
72+
3773const IGNORED_SUFFIXES: [&str; 13] = [
3874 // Editor etc backups
3975 "~",
@@ -55,10 +91,20 @@ const IGNORED_SUFFIXES: [&str; 13] = [
5591];
5692
5793impl HighlightingAssets {
58− fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: ThemeSet) -> Self {
94+ fn new(
95+ serialized_syntax_set: SerializedSyntaxSet,
96+ minimal_syntaxes: MinimalSyntaxes,
97+ theme_set: ThemeSet,
98+ ) -> Self {
99+ // Prepare so we can lazily load minimal syntaxes without a mut reference
100+ let deserialized_minimal_syntaxes =
101+ vec![LazyCell::new(); minimal_syntaxes.serialized_syntax_sets.len()];
102+
59103 HighlightingAssets {
60104 syntax_set_cell: LazyCell::new(),
61105 serialized_syntax_set,
106+ deserialized_minimal_syntaxes,
107+ minimal_syntaxes,
62108 theme_set,
63109 fallback_theme: None,
64110 }
@@ -71,6 +117,11 @@ impl HighlightingAssets {
71117 pub fn from_cache(cache_path: &Path) -> Result<Self> {
72118 Ok(HighlightingAssets::new(
73119 SerializedSyntaxSet::FromFile(cache_path.join("syntaxes.bin")),
120+ asset_from_cache(
121+ &cache_path.join("minimal_syntaxes.bin"),
122+ "minimal syntax sets",
123+ COMPRESS_MINIMAL_SYNTAXES,
124+ )?,
74125 asset_from_cache(&cache_path.join("themes.bin"), "theme set", COMPRESS_THEMES)?,
75126 ))
76127 }
@@ -78,6 +129,7 @@ impl HighlightingAssets {
78129 pub fn from_binary() -> Self {
79130 HighlightingAssets::new(
80131 SerializedSyntaxSet::FromBinary(get_serialized_integrated_syntaxset()),
132+ get_integrated_minimal_syntaxes(),
81133 get_integrated_themeset(),
82134 )
83135 }
@@ -111,6 +163,41 @@ impl HighlightingAssets {
111163 self.get_theme_set().themes.keys().map(|s| s.as_ref())
112164 }
113165
166+ /// Finds a [SyntaxSet] that contains a [SyntaxReference] by its name. First
167+ /// tries to find a minimal [SyntaxSet]. If none is found, returns the
168+ /// [SyntaxSet] that contains all syntaxes.
169+ fn get_syntax_set_by_name(&self, name: &str) -> Result<&SyntaxSet> {
170+ let minimal_syntax_set = self
171+ .minimal_syntaxes
172+ .by_name
173+ .get(&name.to_ascii_lowercase())
174+ .and_then(|index| self.get_minimal_syntax_set_with_index(*index));
175+
176+ match minimal_syntax_set {
177+ Some(syntax_set) => Ok(syntax_set),
178+ None => self.get_syntax_set(),
179+ }
180+ }
181+
182+ fn load_minimal_syntax_set_with_index(&self, index: usize) -> Result<SyntaxSet> {
183+ let serialized_syntax_set = &self.minimal_syntaxes.serialized_syntax_sets[index];
184+ asset_from_contents(
185+ &serialized_syntax_set[..],
186+ &format!("minimal syntax set {}", index),
187+ COMPRESS_SERIALIZED_MINIMAL_SYNTAXES,
188+ )
189+ .map_err(|_| format!("Could not parse minimal syntax set {}", index).into())
190+ }
191+
192+ fn get_minimal_syntax_set_with_index(&self, index: usize) -> Option<&SyntaxSet> {
193+ self.deserialized_minimal_syntaxes
194+ .get(index)
195+ .and_then(|cell| {
196+ cell.try_borrow_with(|| self.load_minimal_syntax_set_with_index(index))
197+ .ok()
198+ })
199+ }
200+
114201 /// Use [Self::get_syntax_for_file_name] instead
115202 #[deprecated]
116203 pub fn syntax_for_file_name(
@@ -167,7 +254,7 @@ impl HighlightingAssets {
167254 mapping: &SyntaxMapping,
168255 ) -> Result<SyntaxReferenceInSet> {
169256 if let Some(language) = language {
170− let syntax_set = self.get_syntax_set()?;
257+ let syntax_set = self.get_syntax_set_by_name(language)?;
171258 syntax_set
172259 .find_syntax_by_token(language)
173260 .map(|syntax| SyntaxReferenceInSet { syntax, syntax_set })
@@ -320,6 +407,13 @@ pub(crate) fn get_integrated_themeset() -> ThemeSet {
320407 from_binary(include_bytes!("../assets/themes.bin"), COMPRESS_THEMES)
321408}
322409
410+fn get_integrated_minimal_syntaxes() -> MinimalSyntaxes {
411+ from_binary(
412+ include_bytes!("../assets/minimal_syntaxes.bin"),
413+ COMPRESS_MINIMAL_SYNTAXES,
414+ )
415+}
416+
323417pub(crate) fn from_binary<T: serde::de::DeserializeOwned>(v: &[u8], compressed: bool) -> T {
324418 asset_from_contents(v, "n/a", compressed)
325419 .expect("data integrated in binary is never faulty, but make sure `compressed` is in sync!")
src/bin/bat/assets.rs+1
@@ -20,6 +20,7 @@ pub fn cache_dir() -> Cow<'static, str> {
2020pub fn clear_assets() {
2121 clear_asset("themes.bin", "theme set cache");
2222 clear_asset("syntaxes.bin", "syntax set cache");
23+ clear_asset("minimal_syntaxes.bin", "minimal syntax sets cache");
2324 clear_asset("metadata.yaml", "metadata file");
2425}
2526
src/build_assets.rs+79−23
@@ -37,17 +37,19 @@ pub fn build_assets(
3737
3838 let syntax_set_builder = build_syntax_set_builder(source_dir, include_integrated_assets)?;
3939
40− if std::env::var("BAT_PRINT_SYNTAX_DEPENDENCIES").is_ok() {
41− // To trigger this code, run:
42− // BAT_PRINT_SYNTAX_DEPENDENCIES=1 cargo run -- cache --build --source assets --blank --target /tmp
43− print_syntax_dependencies(&syntax_set_builder);
44− }
40+ let minimal_syntaxes = build_minimal_syntaxes(&syntax_set_builder, include_integrated_assets)?;
4541
4642 let syntax_set = syntax_set_builder.build();
4743
4844 print_unlinked_contexts(&syntax_set);
4945
50− write_assets(&theme_set, &syntax_set, target_dir, current_version)
46+ write_assets(
47+ &theme_set,
48+ &syntax_set,
49+ &minimal_syntaxes,
50+ target_dir,
51+ current_version,
52+ )
5153}
5254
5355fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> ThemeSet {
@@ -116,6 +118,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
116118fn write_assets(
117119 theme_set: &ThemeSet,
118120 syntax_set: &SyntaxSet,
121+ minimal_syntaxes: &MinimalSyntaxes,
119122 target_dir: &Path,
120123 current_version: &str,
121124) -> Result<()> {
@@ -132,6 +135,12 @@ fn write_assets(
132135 "syntax set",
133136 COMPRESS_SYNTAXES,
134137 )?;
138+ asset_to_cache(
139+ minimal_syntaxes,
140+ &target_dir.join("minimal_syntaxes.bin"),
141+ "minimal syntax sets",
142+ COMPRESS_MINIMAL_SYNTAXES,
143+ )?;
135144
136145 print!(
137146 "Writing metadata to folder {} ... ",
@@ -143,26 +152,65 @@ fn write_assets(
143152 Ok(())
144153}
145154
146−/// Generates independent [SyntaxSet]s after analyzing dependencies between syntaxes
147−/// in a [SyntaxSetBuilder], and then prints the reults.
148−fn print_syntax_dependencies(syntax_set_builder: &SyntaxSetBuilder) {
149− println!("Constructing independent SyntaxSets...");
150− let independent_syntax_sets = build_independent_syntax_sets(syntax_set_builder);
155+fn print_syntax_set_names(syntax_set: &SyntaxSet) {
156+ let names = syntax_set
157+ .syntaxes()
158+ .iter()
159+ .map(|syntax| &syntax.name)
160+ .collect::<Vec<_>>();
161+ println!("{:?}", names);
162+}
151163
152− println!("Independent SyntaxSets:");
153− for syntax_set in independent_syntax_sets {
154− let names = syntax_set
155− .syntaxes()
156− .iter()
157− .map(|syntax| &syntax.name)
158− .collect::<Vec<_>>();
159− println!("{:?}", names);
164+fn build_minimal_syntaxes(
165+ syntax_set_builder: &'_ SyntaxSetBuilder,
166+ include_integrated_assets: bool,
167+) -> Result<MinimalSyntaxes> {
168+ let mut minimal_syntaxes = MinimalSyntaxes {
169+ by_name: HashMap::new(),
170+ serialized_syntax_sets: vec![],
171+ };
172+
173+ if include_integrated_assets {
174+ // Dependency info is not present in integrated assets, so we can't
175+ // calculate minimal syntax sets. Return early without any data filled
176+ // in. This means that no minimal syntax sets will be available to use, and
177+ // the full, slow-to-deserialize, fallback syntax set will be used instead.
178+ return Ok(minimal_syntaxes);
160179 }
180+
181+ let minimal_syntax_sets_to_serialize = build_minimal_syntax_sets(syntax_set_builder)
182+ // For now, only store syntax sets with one syntax, otherwise
183+ // the binary grows by several megs
184+ .filter(|syntax_set| syntax_set.syntaxes().len() == 1);
185+
186+ for minimal_syntax_set in minimal_syntax_sets_to_serialize {
187+ // Remember what index it is found at
188+ let current_index = minimal_syntaxes.serialized_syntax_sets.len();
189+
190+ for syntax in minimal_syntax_set.syntaxes() {
191+ minimal_syntaxes
192+ .by_name
193+ .insert(syntax.name.to_ascii_lowercase().clone(), current_index);
194+ }
195+
196+ let serialized_syntax_set = asset_to_contents(
197+ &minimal_syntax_set,
198+ &format!("failed to serialize minimal syntax set {}", current_index),
199+ COMPRESS_SERIALIZED_MINIMAL_SYNTAXES,
200+ )?;
201+
202+ // Add last so that it ends up at `current_index`
203+ minimal_syntaxes
204+ .serialized_syntax_sets
205+ .push(serialized_syntax_set);
206+ }
207+
208+ Ok(minimal_syntaxes)
161209}
162210
163211/// Analyzes dependencies between syntaxes in a [SyntaxSetBuilder].
164−/// From that, it builds independent [SyntaxSet]s.
165−fn build_independent_syntax_sets(
212+/// From that, it builds minimal [SyntaxSet]s.
213+fn build_minimal_syntax_sets(
166214 syntax_set_builder: &'_ SyntaxSetBuilder,
167215) -> impl Iterator<Item = SyntaxSet> + '_ {
168216 let syntaxes = syntax_set_builder.syntaxes();
@@ -170,7 +218,7 @@ fn build_independent_syntax_sets(
170218 // Build the data structures we need for dependency resolution
171219 let (syntax_to_dependencies, dependency_to_syntax) = generate_maps(syntaxes);
172220
173− // Create one independent SyntaxSet from each (non-hidden) SyntaxDefinition
221+ // Create one minimal SyntaxSet from each (non-hidden) SyntaxDefinition
174222 syntaxes.iter().filter_map(move |syntax| {
175223 if syntax.hidden {
176224 return None;
@@ -178,7 +226,15 @@ fn build_independent_syntax_sets(
178226
179227 let mut builder = SyntaxSetDependencyBuilder::new();
180228 builder.add_with_dependencies(syntax, &syntax_to_dependencies, &dependency_to_syntax);
181− Some(builder.build())
229+ let syntax_set = builder.build();
230+
231+ if std::env::var("BAT_PRINT_SYNTAX_DEPENDENCIES").is_ok() {
232+ // To trigger this code, run:
233+ // BAT_PRINT_SYNTAX_DEPENDENCIES=1 cargo run -- cache --build --source assets --blank --target /tmp
234+ print_syntax_set_names(&syntax_set);
235+ }
236+
237+ Some(syntax_set)
182238 })
183239}
184240