on master help sign in
Load themes lazily for improved startup time (#1969)

This is for #951. Syntax lazy-loading will come later and have a much bigger impact, but lazy-loading of themes does not have negligible impact.

commit7fceb4878fd3da27cea9562f20342e5fb64c37eb authorMartin Nordholts <enselic@gmail.com> dateMon 6 Dec 2021, 09:14 (5 years ago) parent29711c1
5 files changed+134−17
CHANGELOG.md+1 assets/themes.bin src/assets.rs+21−11 src/assets/build_assets.rs+8−6 src/assets/lazy_theme_set.rs+104
CHANGELOG.md+1
@@ -17,6 +17,7 @@
1717
1818- Load cached assets as fast as integrated assets, see #1753 (@Enselic)
1919- 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)
20+- Load themes lazily to make bat start 25% faster when disregarding syntax load time. See #1969 (@Enselic)
2021
2122## Other
2223
assets/themes.bin

Binary file not shown.

src/assets.rs+21−11
@@ -4,7 +4,7 @@ use std::path::Path;
44
55use once_cell::unsync::OnceCell;
66
7−use syntect::highlighting::{Theme, ThemeSet};
7+use syntect::highlighting::Theme;
88use syntect::parsing::{SyntaxReference, SyntaxSet};
99
1010use path_abs::PathAbs;
@@ -15,6 +15,8 @@ use crate::syntax_mapping::ignored_suffixes::IgnoredSuffixes;
1515use crate::syntax_mapping::MappingTarget;
1616use crate::{bat_warning, SyntaxMapping};
1717
18+use lazy_theme_set::LazyThemeSet;
19+
1820use serialized_syntax_set::*;
1921
2022#[cfg(feature = "build-assets")]
@@ -23,6 +25,7 @@ pub use crate::assets::build_assets::*;
2325pub(crate) mod assets_metadata;
2426#[cfg(feature = "build-assets")]
2527mod build_assets;
28+mod lazy_theme_set;
2629mod serialized_syntax_set;
2730
2831#[derive(Debug)]
@@ -30,7 +33,7 @@ pub struct HighlightingAssets {
3033 syntax_set_cell: OnceCell<SyntaxSet>,
3134 serialized_syntax_set: SerializedSyntaxSet,
3235
33− theme_set: ThemeSet,
36+ theme_set: LazyThemeSet,
3437 fallback_theme: Option<&'static str>,
3538}
3639
@@ -43,11 +46,17 @@ pub struct SyntaxReferenceInSet<'a> {
4346/// Compress for size of ~700 kB instead of ~4600 kB at the cost of ~30% longer deserialization time
4447pub(crate) const COMPRESS_SYNTAXES: bool = true;
4548
46−/// Compress for size of ~20 kB instead of ~200 kB at the cost of ~30% longer deserialization time
47−pub(crate) const COMPRESS_THEMES: bool = true;
49+/// We don't want to compress our [LazyThemeSet] since the lazy-loaded themes
50+/// within it are already compressed, and compressing another time just makes
51+/// performance suffer
52+pub(crate) const COMPRESS_THEMES: bool = false;
53+
54+/// Compress for size of ~40 kB instead of ~200 kB without much difference in
55+/// performance due to lazy-loading
56+pub(crate) const COMPRESS_LAZY_THEMES: bool = true;
4857
4958impl HighlightingAssets {
50− fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: ThemeSet) -> Self {
59+ fn new(serialized_syntax_set: SerializedSyntaxSet, theme_set: LazyThemeSet) -> Self {
5160 HighlightingAssets {
5261 syntax_set_cell: OnceCell::new(),
5362 serialized_syntax_set,
@@ -95,12 +104,12 @@ impl HighlightingAssets {
95104 Ok(self.get_syntax_set()?.syntaxes())
96105 }
97106
98− fn get_theme_set(&self) -> &ThemeSet {
107+ fn get_theme_set(&self) -> &LazyThemeSet {
99108 &self.theme_set
100109 }
101110
102111 pub fn themes(&self) -> impl Iterator<Item = &str> {
103− self.get_theme_set().themes.keys().map(|s| s.as_ref())
112+ self.get_theme_set().themes()
104113 }
105114
106115 /// Use [Self::get_syntax_for_path] instead
@@ -175,7 +184,7 @@ impl HighlightingAssets {
175184 }
176185
177186 pub(crate) fn get_theme(&self, theme: &str) -> &Theme {
178− match self.get_theme_set().themes.get(theme) {
187+ match self.get_theme_set().get(theme) {
179188 Some(theme) => theme,
180189 None => {
181190 if theme == "ansi-light" || theme == "ansi-dark" {
@@ -185,8 +194,9 @@ impl HighlightingAssets {
185194 if !theme.is_empty() {
186195 bat_warning!("Unknown theme '{}', using default.", theme)
187196 }
188− &self.get_theme_set().themes
189− [self.fallback_theme.unwrap_or_else(|| Self::default_theme())]
197+ self.get_theme_set()
198+ .get(self.fallback_theme.unwrap_or_else(|| Self::default_theme()))
199+ .expect("something is very wrong if the default theme is missing")
190200 }
191201 }
192202 }
@@ -291,7 +301,7 @@ pub(crate) fn get_serialized_integrated_syntaxset() -> &'static [u8] {
291301 include_bytes!("../assets/syntaxes.bin")
292302}
293303
294−pub(crate) fn get_integrated_themeset() -> ThemeSet {
304+pub(crate) fn get_integrated_themeset() -> LazyThemeSet {
295305 from_binary(include_bytes!("../assets/themes.bin"), COMPRESS_THEMES)
296306}
297307
src/assets/build_assets.rs+8−6
@@ -1,4 +1,6 @@
1+use std::convert::TryInto;
12use std::path::Path;
3+
24use syntect::highlighting::ThemeSet;
35use syntect::parsing::{SyntaxSet, SyntaxSetBuilder};
46
@@ -10,7 +12,7 @@ pub fn build(
1012 target_dir: &Path,
1113 current_version: &str,
1214) -> Result<()> {
13− let theme_set = build_theme_set(source_dir, include_integrated_assets);
15+ let theme_set = build_theme_set(source_dir, include_integrated_assets)?;
1416
1517 let syntax_set_builder = build_syntax_set_builder(source_dir, include_integrated_assets)?;
1618
@@ -21,9 +23,9 @@ pub fn build(
2123 write_assets(&theme_set, &syntax_set, target_dir, current_version)
2224}
2325
24−fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> ThemeSet {
26+fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> Result<LazyThemeSet> {
2527 let mut theme_set = if include_integrated_assets {
26− crate::assets::get_integrated_themeset()
28+ crate::assets::get_integrated_themeset().try_into()?
2729 } else {
2830 ThemeSet::new()
2931 };
@@ -45,7 +47,7 @@ fn build_theme_set(source_dir: &Path, include_integrated_assets: bool) -> ThemeS
4547 );
4648 }
4749
48− theme_set
50+ theme_set.try_into()
4951}
5052
5153fn build_syntax_set_builder(
@@ -85,7 +87,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
8587}
8688
8789fn write_assets(
88− theme_set: &ThemeSet,
90+ theme_set: &LazyThemeSet,
8991 syntax_set: &SyntaxSet,
9092 target_dir: &Path,
9193 current_version: &str,
@@ -114,7 +116,7 @@ fn write_assets(
114116 Ok(())
115117}
116118
117−fn asset_to_contents<T: serde::Serialize>(
119+pub(crate) fn asset_to_contents<T: serde::Serialize>(
118120 asset: &T,
119121 description: &str,
120122 compressed: bool,
src/assets/lazy_theme_set.rsnew file+104
@@ -0,0 +1,104 @@
1+use super::*;
2+
3+use std::collections::BTreeMap;
4+use std::convert::TryFrom;
5+
6+use serde::Deserialize;
7+use serde::Serialize;
8+
9+use once_cell::unsync::OnceCell;
10+
11+use syntect::highlighting::{Theme, ThemeSet};
12+
13+/// Same structure as a [`syntect::highlighting::ThemeSet`] but with themes
14+/// stored in raw serialized form, and deserialized on demand.
15+#[derive(Debug, Default, Serialize, Deserialize)]
16+pub struct LazyThemeSet {
17+ /// This is a [`BTreeMap`] because that's what [`syntect::highlighting::ThemeSet`] uses
18+ themes: BTreeMap<String, LazyTheme>,
19+}
20+
21+/// Stores raw serialized data for a theme with methods to lazily deserialize
22+/// (load) the theme.
23+#[derive(Debug, Serialize, Deserialize)]
24+struct LazyTheme {
25+ serialized: Vec<u8>,
26+
27+ #[serde(skip, default = "OnceCell::new")]
28+ deserialized: OnceCell<syntect::highlighting::Theme>,
29+}
30+
31+impl LazyThemeSet {
32+ /// Lazily load the given theme
33+ pub fn get(&self, name: &str) -> Option<&Theme> {
34+ self.themes.get(name).and_then(|lazy_theme| {
35+ lazy_theme
36+ .deserialized
37+ .get_or_try_init(|| lazy_theme.deserialize())
38+ .ok()
39+ })
40+ }
41+
42+ /// Returns the name of all themes.
43+ pub fn themes(&self) -> impl Iterator<Item = &str> {
44+ self.themes.keys().map(|name| name.as_ref())
45+ }
46+}
47+
48+impl LazyTheme {
49+ fn deserialize(&self) -> Result<Theme> {
50+ asset_from_contents(
51+ &self.serialized[..],
52+ "lazy-loaded theme",
53+ COMPRESS_LAZY_THEMES,
54+ )
55+ }
56+}
57+
58+impl TryFrom<LazyThemeSet> for ThemeSet {
59+ type Error = Error;
60+
61+ /// Since the user might want to add custom themes to bat, we need a way to
62+ /// convert from a `LazyThemeSet` to a regular [`ThemeSet`] so that more
63+ /// themes can be added. This function does that pretty straight-forward
64+ /// conversion.
65+ fn try_from(lazy_theme_set: LazyThemeSet) -> Result<Self> {
66+ let mut theme_set = ThemeSet::default();
67+
68+ for (name, lazy_theme) in lazy_theme_set.themes {
69+ theme_set.themes.insert(name, lazy_theme.deserialize()?);
70+ }
71+
72+ Ok(theme_set)
73+ }
74+}
75+
76+#[cfg(feature = "build-assets")]
77+impl TryFrom<ThemeSet> for LazyThemeSet {
78+ type Error = Error;
79+
80+ /// To collect themes, a [`ThemeSet`] is needed. Once all desired themes
81+ /// have been added, we need a way to convert that into [`LazyThemeSet`] so
82+ /// that themes can be lazy-loaded later. This function does that
83+ /// conversion.
84+ fn try_from(theme_set: ThemeSet) -> Result<Self> {
85+ let mut lazy_theme_set = LazyThemeSet::default();
86+
87+ for (name, theme) in theme_set.themes {
88+ // All we have to do is to serialize the theme
89+ let lazy_theme = LazyTheme {
90+ serialized: crate::assets::build_assets::asset_to_contents(
91+ &theme,
92+ &format!("theme {}", name),
93+ COMPRESS_LAZY_THEMES,
94+ )?,
95+ deserialized: OnceCell::new(),
96+ };
97+
98+ // Ok done, now we can add it
99+ lazy_theme_set.themes.insert(name, lazy_theme);
100+ }
101+
102+ Ok(lazy_theme_set)
103+ }
104+}