perf: Lazy load files from directory (#335)

Changes context to use `once_cell` to lazily evaluate directory listing on first use.
This commit is contained in:
Nick Young 2019-09-15 00:23:53 +10:00 committed by Kevin Song
parent 8f03c14582
commit 7e891f17c1
12 changed files with 46 additions and 54 deletions

View File

@ -20,7 +20,7 @@ pub struct Context<'a> {
pub current_dir: PathBuf,
/// A vector containing the full paths of all the files in `current_dir`.
pub dir_files: Vec<PathBuf>,
dir_files: OnceCell<Vec<PathBuf>>,
/// The map of arguments that were passed when starship was called.
pub arguments: ArgMatches<'a>,
@ -52,22 +52,11 @@ impl<'a> Context<'a> {
// TODO: Currently gets the physical directory. Get the logical directory.
let current_dir = Context::expand_tilde(dir.into());
let dir_files = fs::read_dir(&current_dir)
.unwrap_or_else(|_| {
panic!(
"Unable to read current directory: {}",
current_dir.to_string_lossy()
)
})
.filter_map(Result::ok)
.map(|entry| entry.path())
.collect::<Vec<PathBuf>>();
Context {
config,
arguments,
current_dir,
dir_files,
dir_files: OnceCell::new(),
repo: OnceCell::new(),
}
}
@ -100,19 +89,18 @@ impl<'a> Context<'a> {
// returns a new ScanDir struct with reference to current dir_files of context
// see ScanDir for methods
pub fn new_scan_dir(&'a self) -> ScanDir<'a> {
ScanDir {
dir_files: self.dir_files.as_ref(),
pub fn try_begin_scan(&'a self) -> Option<ScanDir<'a>> {
Some(ScanDir {
dir_files: self.get_dir_files().ok()?,
files: &[],
folders: &[],
extensions: &[],
}
})
}
/// Will lazily get repo root and branch when a module requests it.
pub fn get_repo(&self) -> Result<&Repo, std::io::Error> {
let repo = self
.repo
self.repo
.get_or_try_init(|| -> Result<Repo, std::io::Error> {
let repository = Repository::discover(&self.current_dir).ok();
let branch = repository
@ -128,9 +116,19 @@ impl<'a> Context<'a> {
root,
state,
})
})?;
})
}
Ok(repo)
pub fn get_dir_files(&self) -> Result<&Vec<PathBuf>, std::io::Error> {
self.dir_files
.get_or_try_init(|| -> Result<Vec<PathBuf>, std::io::Error> {
let dir_files = fs::read_dir(&self.current_dir)?
.filter_map(Result::ok)
.map(|entry| entry.path())
.collect::<Vec<PathBuf>>();
Ok(dir_files)
})
}
}
@ -150,7 +148,7 @@ pub struct Repo {
// A struct of Criteria which will be used to verify current PathBuf is
// of X language, criteria can be set via the builder pattern
pub struct ScanDir<'a> {
dir_files: &'a Vec<PathBuf>, // Replace with reference
dir_files: &'a Vec<PathBuf>,
files: &'a [&'a str],
folders: &'a [&'a str],
extensions: &'a [&'a str],
@ -174,7 +172,7 @@ impl<'a> ScanDir<'a> {
/// based on the current Pathbuf check to see
/// if any of this criteria match or exist and returning a boolean
pub fn scan(&mut self) -> bool {
pub fn is_match(&self) -> bool {
self.dir_files.iter().any(|path| {
if path.is_dir() {
path_has_name(path, self.folders)
@ -261,7 +259,7 @@ mod tests {
#[test]
fn test_criteria_scan_fails() {
let mut failing_criteria = ScanDir {
let failing_criteria = ScanDir {
dir_files: &vec![PathBuf::new()],
files: &["package.json"],
extensions: &["js"],
@ -269,9 +267,9 @@ mod tests {
};
// fails if buffer does not match any criteria
assert_eq!(failing_criteria.scan(), false);
assert_eq!(failing_criteria.is_match(), false);
let mut failing_dir_criteria = ScanDir {
let failing_dir_criteria = ScanDir {
dir_files: &vec![PathBuf::from("/package.js/dog.go")],
files: &["package.json"],
extensions: &["js"],
@ -279,18 +277,18 @@ mod tests {
};
// fails when passed a pathbuf dir matches extension path
assert_eq!(failing_dir_criteria.scan(), false);
assert_eq!(failing_dir_criteria.is_match(), false);
}
#[test]
fn test_criteria_scan_passes() {
let mut passing_criteria = ScanDir {
let passing_criteria = ScanDir {
dir_files: &vec![PathBuf::from("package.json")],
files: &["package.json"],
extensions: &["js"],
folders: &["node_modules"],
};
assert_eq!(passing_criteria.scan(), true);
assert_eq!(passing_criteria.is_match(), true);
}
}

View File

@ -35,7 +35,7 @@ pub struct Module<'a> {
config: Option<&'a toml::value::Table>,
/// The module's name, to be used in configuration and logging.
name: String,
_name: String,
/// The styling to be inherited by all segments contained within this module.
style: Style,
@ -55,7 +55,7 @@ impl<'a> Module<'a> {
pub fn new(name: &str, config: Option<&'a toml::value::Table>) -> Module<'a> {
Module {
config,
name: name.to_string(),
_name: name.to_string(),
style: Style::default(),
prefix: Affix::default_prefix(name),
segments: Vec::new(),
@ -204,7 +204,7 @@ fn ansi_strings_modified(ansi_strings: Vec<ANSIString>, shell: String) -> Vec<AN
/// Module affixes are to be used for the prefix or suffix of a module.
pub struct Affix {
/// The affix's name, to be used in configuration and logging.
name: String,
_name: String,
/// The affix's style.
style: Style,
@ -216,7 +216,7 @@ pub struct Affix {
impl Affix {
pub fn default_prefix(name: &str) -> Self {
Self {
name: format!("{}_prefix", name),
_name: format!("{}_prefix", name),
style: Style::default(),
value: "via ".to_string(),
}
@ -224,7 +224,7 @@ impl Affix {
pub fn default_suffix(name: &str) -> Self {
Self {
name: format!("{}_suffix", name),
_name: format!("{}_suffix", name),
style: Style::default(),
value: " ".to_string(),
}

View File

@ -15,11 +15,11 @@ use super::{Context, Module};
/// - Current directory contains a file with the `.go` extension
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_go_project = context
.new_scan_dir()
.try_begin_scan()?
.set_files(&["go.mod", "go.sum", "glide.yaml", "Gopkg.yml", "Gopkg.lock"])
.set_extensions(&["go"])
.set_folders(&["Godeps"])
.scan();
.is_match();
if !is_go_project {
return None;

View File

@ -11,11 +11,11 @@ use super::{Context, Module};
/// - Current directory contains a `node_modules` directory
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_js_project = context
.new_scan_dir()
.try_begin_scan()?
.set_files(&["package.json"])
.set_extensions(&["js"])
.set_folders(&["node_modules"])
.scan();
.is_match();
if !is_js_project {
return None;

View File

@ -16,7 +16,7 @@ use super::{Context, Module};
/// - Current directory contains a `Pipfile` file
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_py_project = context
.new_scan_dir()
.try_begin_scan()?
.set_files(&[
"requirements.txt",
".python-version",
@ -24,7 +24,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
"Pipfile",
])
.set_extensions(&["py"])
.scan();
.is_match();
if !is_py_project {
return None;

View File

@ -10,10 +10,10 @@ use super::{Context, Module};
/// - Current directory contains a `Gemfile` file
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_rb_project = context
.new_scan_dir()
.try_begin_scan()?
.set_files(&["Gemfile"])
.set_extensions(&["rb"])
.scan();
.is_match();
if !is_rb_project {
return None;

View File

@ -10,10 +10,10 @@ use super::{Context, Module};
/// - Current directory contains a `Cargo.toml` file
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let is_rs_project = context
.new_scan_dir()
.try_begin_scan()?
.set_files(&["Cargo.toml"])
.set_extensions(&["rs"])
.scan();
.is_match();
if !is_rs_project {
return None;

View File

@ -41,8 +41,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
/// Format a given time into the given string. This function should be referentially
/// transparent, which makes it easy to test (unlike anything involving the actual time)
fn format_time(time_format: &str, localtime: DateTime<Local>) -> String {
localtime.format(time_format).to_string()
fn format_time(time_format: &str, local_time: DateTime<Local>) -> String {
local_time.format(time_format).to_string()
}
/* Because we cannot make acceptance tests for the time module, these unit

View File

@ -6,7 +6,7 @@ use std::fmt;
/// (e.g. The version that software is running).
pub struct Segment {
/// The segment's name, to be used in configuration and logging.
name: String,
_name: String,
/// The segment's style. If None, will inherit the style of the module containing it.
style: Option<Style>,
@ -19,7 +19,7 @@ impl Segment {
/// Creates a new segment with default fields.
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
_name: name.to_string(),
style: None,
value: "".to_string(),
}

View File

@ -1,5 +1,4 @@
use ansi_term::Color;
use git2::Repository;
use std::io;
use std::process::Command;

View File

@ -1,5 +1,4 @@
use ansi_term::Color;
use git2::Repository;
use std::fs::{self, File};
use std::io;
use std::process::Command;

View File

@ -1,8 +1,4 @@
use ansi_term::Color;
use std::fs;
use std::io;
use std::path::Path;
use tempfile::TempDir;
use crate::common::{self, TestCommand};