From f6670aac299512b58d314f627f111f1648792a56 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 18 Jan 2024 20:58:54 -0800 Subject: [PATCH 01/29] Multithreading WIP --- src/lib.rs | 1 + src/load.rs | 179 ++++++++++++++++++++++++++++----------------- src/parse.rs | 107 +++++++++++++++++++++++---- src/scanner.rs | 7 +- src/smallmap.rs | 8 ++ src/thread_pool.rs | 47 ++++++++++++ 6 files changed, 262 insertions(+), 87 deletions(-) create mode 100644 src/thread_pool.rs diff --git a/src/lib.rs b/src/lib.rs index c1e8cb0..edb805c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ mod task; mod terminal; mod trace; mod work; +mod thread_pool; #[cfg(not(any(windows, target_arch = "wasm32")))] use jemallocator::Jemalloc; diff --git a/src/load.rs b/src/load.rs index edcf49d..1453dd8 100644 --- a/src/load.rs +++ b/src/load.rs @@ -2,24 +2,24 @@ use crate::{ canon::{canon_path, canon_path_fast}, - eval::{EvalPart, EvalString}, + eval::{EvalPart, EvalString, Vars}, graph::{FileId, RspFile}, parse::Statement, scanner, smallmap::SmallMap, - {db, eval, graph, parse, trace}, + {db, eval, graph, parse, trace}, thread_pool::{self, ThreadPoolExecutor}, scanner::ParseResult, }; use anyhow::{anyhow, bail}; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Mutex, cell::UnsafeCell, thread::Thread}; use std::path::PathBuf; use std::{borrow::Cow, path::Path}; /// A variable lookup environment for magic $in/$out variables. -struct BuildImplicitVars<'a> { - graph: &'a graph::Graph, - build: &'a graph::Build, +struct BuildImplicitVars<'text> { + graph: &'text graph::Graph, + build: &'text graph::Build, } -impl<'a> BuildImplicitVars<'a> { +impl<'text> BuildImplicitVars<'text> { fn file_list(&self, ids: &[FileId], sep: char) -> String { let mut out = String::new(); for &id in ids { @@ -31,7 +31,7 @@ impl<'a> BuildImplicitVars<'a> { out } } -impl<'a> eval::Env for BuildImplicitVars<'a> { +impl<'text> eval::Env for BuildImplicitVars<'text> { fn get_var(&self, var: &str) -> Option>> { let string_to_evalstring = |s: String| Some(EvalString::new(vec![EvalPart::Literal(Cow::Owned(s))])); @@ -45,22 +45,56 @@ impl<'a> eval::Env for BuildImplicitVars<'a> { } } +/// FilePool is a datastucture that is intended to hold onto byte buffers and give out immutable +/// references to them. But it can also accept new byte buffers while old ones are still lent out. +/// This requires interior mutability / unsafe code. Appending to a Vec while references to other +/// elements are held is generally unsafe, because the Vec can reallocate all the prior elements +/// to a new memory location. But if the elements themselves are Vecs that never change, the +/// contents of those inner vecs can be referenced safely. This also requires guarding the outer +/// Vec with a Mutex so that two threads don't append to it at the same time. +struct FilePool { + files: Mutex>>>, +} +impl FilePool { + fn new() -> FilePool { + FilePool { files: Mutex::new(UnsafeCell::new(Vec::new())) } + } + /// Add the file to the file pool, and then return it back to the caller as a slice. + /// Returning the Vec instead of a slice would be unsafe, as the Vecs will be reallocated. + fn add_file(&self, file: Vec) -> &[u8] { + let files = self.files.lock().unwrap().get(); + unsafe { + (*files).push(file); + (*files).last().unwrap().as_slice() + } + } +} + /// Internal state used while loading. -#[derive(Default)] -pub struct Loader { +pub struct Loader<'text> { + file_pool: &'text FilePool, + vars: Vars<'text>, graph: graph::Graph, default: Vec, /// rule name -> list of (key, val) - rules: HashMap>>, + rules: HashMap<&'text str, SmallMap<&'text str, eval::EvalString<&'text str>>>, pools: SmallMap, builddir: Option, } -impl Loader { - pub fn new() -> Self { - let mut loader = Loader::default(); +impl<'text> Loader<'text> { + pub fn new(file_pool: &'text FilePool) -> Self { + let mut loader = Loader { + file_pool, + vars: Vars::default(), + graph: graph::Graph::default(), + default: Vec::default(), + rules: HashMap::default(), + pools: SmallMap::default(), + builddir: None, + }; - loader.rules.insert("phony".to_owned(), SmallMap::default()); + loader.rules.insert("phony", SmallMap::default()); loader } @@ -94,18 +128,21 @@ impl Loader { fn add_build( &mut self, filename: std::rc::Rc, - env: &eval::Vars, b: parse::Build, ) -> anyhow::Result<()> { let ins = graph::BuildIns { - ids: self.evaluate_paths(b.ins, &[&b.vars, env]), + ids: b.ins.iter().map(|x| { + self.path(x.evaluate(&[&b.vars, &self.vars])) + }).collect(), explicit: b.explicit_ins, implicit: b.implicit_ins, order_only: b.order_only_ins, // validation is implied by the other counts }; let outs = graph::BuildOuts { - ids: self.evaluate_paths(b.outs, &[&b.vars, env]), + ids: b.outs.iter().map(|x| { + self.path(x.evaluate(&[&b.vars, &self.vars])) + }).collect(), explicit: b.explicit_outs, }; let mut build = graph::Build::new( @@ -132,8 +169,8 @@ impl Loader { let lookup = |key: &str| -> Option { // Look up `key = ...` binding in build and rule block. Some(match rule.get(key) { - Some(val) => val.evaluate(&[&implicit_vars, build_vars, env]), - None => build_vars.get(key)?.evaluate(&[env]), + Some(val) => val.evaluate(&[&implicit_vars, build_vars, &self.vars]), + None => build_vars.get(key)?.evaluate(&[&self.vars]), }) }; @@ -169,66 +206,63 @@ impl Loader { self.graph.add_build(build) } - fn read_file(&mut self, id: FileId) -> anyhow::Result<()> { + fn read_file(&mut self, id: FileId, executor: &ThreadPoolExecutor<'text>) -> anyhow::Result<()> { let path = self.graph.file(id).path().to_path_buf(); let bytes = match trace::scope("read file", || scanner::read_file_with_nul(&path)) { Ok(b) => b, Err(e) => bail!("read {}: {}", path.display(), e), }; - self.parse(path, &bytes) - } - - fn evaluate_and_read_file( - &mut self, - file: EvalString<&str>, - envs: &[&dyn eval::Env], - ) -> anyhow::Result<()> { - let evaluated = self.evaluate_path(file, envs); - self.read_file(evaluated) + self.parse(path, self.file_pool.add_file(bytes), executor) } - pub fn parse(&mut self, path: PathBuf, bytes: &[u8]) -> anyhow::Result<()> { + pub fn parse(&mut self, path: PathBuf, bytes: &'text [u8], executor: &ThreadPoolExecutor<'text>) -> anyhow::Result<()> { let filename = std::rc::Rc::new(path); - let mut parser = parse::Parser::new(&bytes); + let chunks = parse::split_manifest_into_chunks(bytes, executor.get_num_threads().get()); - loop { - let stmt = match parser - .read() - .map_err(|err| anyhow!(parser.format_parse_error(&filename, err)))? - { - None => break, - Some(s) => s, - }; + let mut receivers = Vec::with_capacity(chunks.len()); + + for chunk in chunks.into_iter() { + let (sender, receiver) = std::sync::mpsc::channel::>>(); + receivers.push(receiver); + executor.execute(move || { + let mut parser = parse::Parser::new(chunk); + parser.read_to_channel(sender); + }) + } + + for stmt in receivers.into_iter().flatten() { match stmt { - Statement::Include(id) => trace::scope("include", || { - self.evaluate_and_read_file(id, &[&parser.vars]) + Ok(Statement::VariableAssignment((name, val))) => { + self.vars.insert(name, val.evaluate(&[&self.vars])); + }, + Ok(Statement::Include(id)) => trace::scope("include", || { + let evaluated = self.path(id.evaluate(&[&self.vars])); + self.read_file(evaluated, executor) })?, // TODO: implement scoping for subninja - Statement::Subninja(id) => trace::scope("subninja", || { - self.evaluate_and_read_file(id, &[&parser.vars]) + Ok(Statement::Subninja(id)) => trace::scope("subninja", || { + let evaluated = self.path(id.evaluate(&[&self.vars])); + self.read_file(evaluated, executor) })?, - Statement::Default(defaults) => { - let evaluated = self.evaluate_paths(defaults, &[&parser.vars]); - self.default.extend(evaluated); + Ok(Statement::Default(defaults)) => { + let it: Vec = defaults.into_iter().map(|x| { + self.path(x.evaluate(&[&self.vars])) + }).collect(); + self.default.extend(it); } - Statement::Rule(rule) => { - let mut vars: SmallMap> = SmallMap::default(); - for (name, val) in rule.vars.into_iter() { - // TODO: We should not need to call .into_owned() here - // if we keep the contents of all included files in - // memory. - vars.insert(name.to_owned(), val.into_owned()); - } - self.rules.insert(rule.name.to_owned(), vars); + Ok(Statement::Rule(rule)) => { + self.rules.insert(rule.name, rule.vars); } - Statement::Build(build) => self.add_build(filename.clone(), &parser.vars, build)?, - Statement::Pool(pool) => { + Ok(Statement::Build(build)) => self.add_build(filename.clone(), build)?, + Ok(Statement::Pool(pool)) => { self.pools.insert(pool.name.to_string(), pool.depth); } + // TODO: Call format_parse_error + Err(e) => bail!(e.msg), }; } - self.builddir = parser.vars.get("builddir").cloned(); + self.builddir = self.vars.get("builddir").cloned(); Ok(()) } } @@ -244,13 +278,17 @@ pub struct State { /// Load build.ninja/.n2_db and return the loaded build graph and state. pub fn read(build_filename: &str) -> anyhow::Result { - let mut loader = Loader::new(); - trace::scope("loader.read_file", || { - let id = loader - .graph - .files - .id_from_canonical(canon_path(build_filename)); - loader.read_file(id) + let file_pool = FilePool::new(); + let mut loader = trace::scope("loader.read_file", || -> anyhow::Result { + thread_pool::scoped_thread_pool(std::thread::available_parallelism()?, |executor| { + let mut loader = Loader::new(&file_pool); + let id = loader + .graph + .files + .id_from_canonical(canon_path(build_filename)); + loader.read_file(id, executor)?; + Ok(loader) + }) })?; let mut hashes = graph::Hashes::default(); let db = trace::scope("db::open", || { @@ -277,9 +315,12 @@ pub fn read(build_filename: &str) -> anyhow::Result { #[cfg(test)] pub fn parse(name: &str, mut content: Vec) -> anyhow::Result { content.push(0); - let mut loader = Loader::new(); + let file_pool = FilePool::new(); + let mut loader = Loader::new(&file_pool); trace::scope("loader.read_file", || { - loader.parse(PathBuf::from(name), &content) + thread_pool::scoped_thread_pool(std::num::NonZeroUsize::new(1).unwrap(), |executor| { + loader.parse(PathBuf::from(name), &content, executor) + }) })?; Ok(loader.graph) } diff --git a/src/parse.rs b/src/parse.rs index 04a8397..4f08fe3 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -16,11 +16,13 @@ use std::path::Path; /// key = $val pub type VarList<'text> = SmallMap<&'text str, EvalString<&'text str>>; +#[derive(Debug, PartialEq)] pub struct Rule<'text> { pub name: &'text str, pub vars: VarList<'text>, } +#[derive(Debug, PartialEq)] pub struct Build<'text> { pub rule: &'text str, pub line: usize, @@ -34,12 +36,13 @@ pub struct Build<'text> { pub vars: VarList<'text>, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Pool<'text> { pub name: &'text str, pub depth: usize, } +#[derive(Debug, PartialEq)] pub enum Statement<'text> { Rule(Rule<'text>), Build(Build<'text>), @@ -47,11 +50,12 @@ pub enum Statement<'text> { Include(EvalString<&'text str>), Subninja(EvalString<&'text str>), Pool(Pool<'text>), + VariableAssignment((&'text str, EvalString<&'text str>)), } pub struct Parser<'text> { scanner: Scanner<'text>, - pub vars: Vars<'text>, + buf_len: usize, /// Reading EvalStrings is very hot when parsing, so we always read into /// this buffer and then clone it afterwards. eval_buf: Vec>, @@ -61,7 +65,7 @@ impl<'text> Parser<'text> { pub fn new(buf: &'text [u8]) -> Parser<'text> { Parser { scanner: Scanner::new(buf), - vars: Vars::default(), + buf_len: buf.len(), eval_buf: Vec::with_capacity(16), } } @@ -70,6 +74,27 @@ impl<'text> Parser<'text> { self.scanner.format_parse_error(filename, err) } + pub fn read_all(&mut self) -> ParseResult>> { + let mut result = Vec::new(); + while let Some(stmt) = self.read()? { + result.push(stmt) + } + Ok(result) + } + + pub fn read_to_channel(&mut self, sender: std::sync::mpsc::Sender>>) { + loop { + match self.read() { + Ok(None) => return, + Ok(Some(stmt)) => sender.send(Ok(stmt)).unwrap(), + Err(e) => { + sender.send(Err(e)).unwrap(); + return; + }, + } + } + } + pub fn read(&mut self) -> ParseResult>> { loop { match self.scanner.peek() { @@ -78,6 +103,18 @@ impl<'text> Parser<'text> { '#' => self.skip_comment()?, ' ' | '\t' => return self.scanner.parse_error("unexpected whitespace"), _ => { + if self.scanner.ofs >= self.buf_len { + // The parsing code expects there to be a null byte at the end of the file, + // to allow the parsing to be more performant and exclude most checks for + // EOF. However, when parsing an individual "chunk" of the manifest, there + // won't be a null byte at the end, the scanner will do an out-of-bounds + // read past the end of the chunk and into the next chunk. When we split + // the file into chunks, we made sure to end all the chunks just before + // identifiers at the start of a new line, so that we can easily detect + // that here. + assert!(self.scanner.ofs == self.buf_len); + return Ok(None) + } let ident = self.read_ident()?; self.skip_spaces(); match ident { @@ -92,12 +129,7 @@ impl<'text> Parser<'text> { } "pool" => return Ok(Some(Statement::Pool(self.read_pool()?))), ident => { - // TODO: The evaluation of global variables should - // be moved out of the parser, so that we can run - // multiple parsers in parallel and then evaluate - // all the variables in series at the end. - let val = self.read_vardef()?.evaluate(&[&self.vars]); - self.vars.insert(ident, val); + return Ok(Some(Statement::VariableAssignment((ident, self.read_vardef()?)))) } } } @@ -440,6 +472,50 @@ impl<'text> Parser<'text> { } } +pub fn split_manifest_into_chunks(buf: &[u8], num_threads: usize) -> Vec<&[u8]> { + let min_chunk_size = 1024 * 1024; + let chunk_count = num_threads * 2; + let chunk_size = std::cmp::max(min_chunk_size, buf.len() / chunk_count + 1); + let mut result = Vec::with_capacity(chunk_count); + let mut start = 0; + while start < buf.len() { + let next = std::cmp::min(start + chunk_size, buf.len()); + let next = find_start_of_next_manifest_chunk(buf, next); + result.push(&buf[start..next]); + start = next; + } + result +} + +fn find_start_of_next_manifest_chunk(buf: &[u8], prospective_start: usize) -> usize { + let mut idx = prospective_start; + loop { + // TODO: Replace the search with something that uses SIMD instructions like the memchr crate + let Some(nl_index) = &buf[idx..].iter().position(|&b| b == b'\n') else { + return buf.len() + }; + idx += nl_index + 1; + + // This newline was escaped, try again. It's possible that this check is too conservative, + // for example, you could have: + // - a comment that ends with a "$": "# $\n" + // - an escaped-dollar: "X=$$\n" + if idx >= 2 && buf[idx-2] == b'$' || + idx >= 3 && buf[idx-2] == b'\r' && buf[idx-3] == b'$' { + continue; + } + + // We want chunk boundaries to be at an easy/predictable place for the scanner to stop + // at. So only stop at an identifier after a newline. + if idx == buf.len() || matches!( + buf[idx], + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.' + ) { + return idx; + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -463,9 +539,13 @@ mod tests { test_for_line_endings(&["var = 3", "default a b$var c", ""], |test_case| { let mut buf = test_case_buffer(test_case); let mut parser = Parser::new(&mut buf); + match parser.read().unwrap().unwrap() { + Statement::VariableAssignment(_) => {}, + stmt => panic!("expected variable assignment, got {:?}", stmt), + }; let default = match parser.read().unwrap().unwrap() { Statement::Default(d) => d, - _ => panic!("expected default"), + stmt => panic!("expected default, got {:?}", stmt), }; assert_eq!( default, @@ -482,9 +562,10 @@ mod tests { fn parse_dot_in_eval() { let mut buf = test_case_buffer("x = $y.z\n"); let mut parser = Parser::new(&mut buf); - parser.read().unwrap(); - let x = parser.vars.get("x").unwrap(); - assert_eq!(x, ".z"); + assert_eq!(parser.read(), Ok(Some(Statement::VariableAssignment(("x", EvalString::new(vec![ + EvalPart::VarRef("y"), + EvalPart::Literal(".z"), + ])))))); } #[test] diff --git a/src/scanner.rs b/src/scanner.rs index 091791d..bfea31b 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -2,9 +2,9 @@ use std::{io::Read, path::Path}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct ParseError { - msg: String, + pub msg: String, ofs: usize, } pub type ParseResult = Result; @@ -17,9 +17,6 @@ pub struct Scanner<'a> { impl<'a> Scanner<'a> { pub fn new(buf: &'a [u8]) -> Self { - if !buf.ends_with(b"\0") { - panic!("Scanner requires nul-terminated buf"); - } Scanner { buf, ofs: 0, diff --git a/src/smallmap.rs b/src/smallmap.rs index da8298c..13185d3 100644 --- a/src/smallmap.rs +++ b/src/smallmap.rs @@ -6,6 +6,7 @@ use std::{borrow::Borrow, fmt::Debug}; /// A map-like object implemented as a list of pairs, for cases where the /// number of entries in the map is small. +#[derive(Debug)] pub struct SmallMap(Vec<(K, V)>); impl Default for SmallMap { @@ -78,3 +79,10 @@ impl PartialEq for SmallMap { return self.0 == other.0; } } + +// TODO: Make this not order-sensitive +impl PartialEq for SmallMap { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} \ No newline at end of file diff --git a/src/thread_pool.rs b/src/thread_pool.rs new file mode 100644 index 0000000..b60edff --- /dev/null +++ b/src/thread_pool.rs @@ -0,0 +1,47 @@ +use std::{sync::{Arc, Mutex}, num::NonZeroUsize}; + + +type Job<'a> = Box; + +pub struct ThreadPoolExecutor<'a> { + sender: std::sync::mpsc::Sender>, + num_threads: NonZeroUsize, +} + +impl<'a> ThreadPoolExecutor<'a> { + pub fn execute () + Send + 'a>(&self, f: F) { + let job = Box::new(f); + self.sender.send(job).unwrap(); + } + + pub fn get_num_threads(&self) -> NonZeroUsize { + self.num_threads + } +} + +pub fn scoped_thread_pool<'env, T, F: FnOnce(&ThreadPoolExecutor<'env>) -> T>(num_threads: NonZeroUsize, scope: F) -> T { + std::thread::scope(|s| { + let (sender, receiver) = std::sync::mpsc::channel::(); + let receiver = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(num_threads.get()); + for _ in 0..num_threads.get() { + let receiver = receiver.clone(); + workers.push(s.spawn(move || { + loop { + let message = receiver.lock().unwrap().recv(); + match message { + Ok(job) => job(), + Err(_) => break, + } + } + })); + } + + let pool: ThreadPoolExecutor<'env> = ThreadPoolExecutor { + sender, + num_threads, + }; + scope(&pool) + }) +} From c6905227336b3d54510801568ea7c6e65758129b Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Sat, 27 Jan 2024 20:14:48 -0800 Subject: [PATCH 02/29] Multithreaded loading --- Cargo.lock | 64 ++- Cargo.toml | 2 + src/concurrent_linked_list.rs | 117 ++++++ src/db.rs | 2 +- src/densemap.rs | 29 ++ src/eval.rs | 68 ++- src/file_pool.rs | 72 ++++ src/graph.rs | 87 ++-- src/lib.rs | 3 +- src/load.rs | 763 +++++++++++++++++++++++----------- src/parse.rs | 193 +++++++-- src/run.rs | 13 +- src/smallmap.rs | 11 +- src/thread_pool.rs | 47 --- src/work.rs | 25 +- tests/e2e/basic.rs | 18 + 16 files changed, 1080 insertions(+), 434 deletions(-) create mode 100644 src/concurrent_linked_list.rs create mode 100644 src/file_pool.rs delete mode 100644 src/thread_pool.rs diff --git a/Cargo.lock b/Cargo.lock index 19c25c1..cf0b4af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "either" version = "1.9.0" @@ -272,6 +285,12 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hermit-abi" version = "0.3.1" @@ -371,6 +390,16 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -390,9 +419,11 @@ dependencies = [ "anyhow", "argh", "criterion", + "dashmap", "filetime", "jemallocator", "libc", + "rayon", "rustc-hash", "tempfile", "windows-sys 0.48.0", @@ -419,6 +450,19 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.0", +] + [[package]] name = "plotters" version = "0.3.5" @@ -467,9 +511,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" dependencies = [ "either", "rayon-core", @@ -477,9 +521,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -580,6 +624,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.164" @@ -611,6 +661,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index b91b120..87d091f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,9 @@ description = "a ninja compatible build system" [dependencies] anyhow = "1.0" argh = "0.1.10" +dashmap = "5.5.3" libc = "0.2" +rayon = "1.8.1" rustc-hash = "1.1.0" [target.'cfg(windows)'.dependencies.windows-sys] diff --git a/src/concurrent_linked_list.rs b/src/concurrent_linked_list.rs new file mode 100644 index 0000000..52bffde --- /dev/null +++ b/src/concurrent_linked_list.rs @@ -0,0 +1,117 @@ +use std::{borrow::Borrow, fmt::Debug, marker::PhantomData, ptr::null_mut, sync::atomic::{AtomicPtr, Ordering}}; + +/// ConcurrentLinkedList is a linked list that can only be prepended to or +/// iterated over. prepend() accepts an &self instead of an &mut self, and can +/// be called from multiple threads at the same time. +pub struct ConcurrentLinkedList { + head: AtomicPtr>, +} + +struct ConcurrentLinkedListNode { + val: T, + next: *mut ConcurrentLinkedListNode, +} + +impl ConcurrentLinkedList { + pub fn new() -> Self { + ConcurrentLinkedList { + head: AtomicPtr::new(null_mut()), + } + } + + pub fn prepend(&self, val: T) { + let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + val, + next: null_mut(), + })); + loop { + let old_head = self.head.load(Ordering::SeqCst); + unsafe { + (*new_head).next = old_head; + if self.head.compare_exchange_weak(old_head, new_head, Ordering::SeqCst, Ordering::SeqCst).is_ok() { + break; + } + } + } + } + + pub fn iter(&self) -> impl Iterator { + ConcurrentLinkedListIterator { + cur: self.head.load(Ordering::Relaxed), + lifetime: PhantomData, + } + } +} + +impl Default for ConcurrentLinkedList { + fn default() -> Self { + Self { head: Default::default() } + } +} + +impl Clone for ConcurrentLinkedList { + fn clone(&self) -> Self { + let mut iter = self.iter(); + match iter.next() { + None => Self { head: AtomicPtr::new(null_mut()) }, + Some(x) => { + let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + val: x.clone(), + next: null_mut(), + })); + let mut new_tail = new_head; + for x in iter { + unsafe { + (*new_tail).next = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + val: x.clone(), + next: null_mut(), + })); + new_tail = (*new_tail).next; + } + } + Self { head: AtomicPtr::new(new_head) } + } + } + } +} + +impl Debug for ConcurrentLinkedList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Slow, but hopefully Debug is only used for actual debugging + f.write_fmt(format_args!("{:?}", self.iter().collect::>())) + } +} + +impl Drop for ConcurrentLinkedList { + fn drop(&mut self) { + let mut cur = self.head.load(Ordering::Relaxed); + while !cur.is_null() { + unsafe { + // Re-box it so that box will call Drop and deallocate the memory + let boxed = Box::from_raw(cur); + cur = boxed.next; + } + } + } +} + +struct ConcurrentLinkedListIterator<'a, T> { + cur: *const ConcurrentLinkedListNode, + lifetime: PhantomData<&'a ()>, +} + +impl<'a, T: 'a> Iterator for ConcurrentLinkedListIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + if self.cur.is_null() { + None + } else { + unsafe { + let result = Some((*self.cur).val.borrow()); + self.cur = (*self.cur).next; + result + } + } + } +} diff --git a/src/db.rs b/src/db.rs index d6481ad..85bfab5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -238,7 +238,7 @@ impl<'a> Reader<'a> { } let len = self.read_u16()?; - let mut deps = Vec::new(); + let mut deps = Vec::with_capacity(len as usize); for _ in 0..len { let id = self.read_id()?; deps.push(self.ids.fileids[id]); diff --git a/src/densemap.rs b/src/densemap.rs index 2d9878e..d4afb65 100644 --- a/src/densemap.rs +++ b/src/densemap.rs @@ -37,6 +37,20 @@ impl std::ops::IndexMut for DenseMap { } impl DenseMap { + pub fn from_vec(v: Vec) -> Self { + Self { + vec: v, + key_type: PhantomData, + } + } + + pub fn with_capacity(c: usize) -> Self { + Self { + vec: Vec::with_capacity(c), + key_type: PhantomData, + } + } + pub fn lookup(&self, k: K) -> Option<&V> { self.vec.get(k.index()) } @@ -50,6 +64,21 @@ impl DenseMap { self.vec.push(val); id } + + pub fn keys(&self) -> impl Iterator { + (0..self.vec.len()).map(|x| K::from(x)) + } + + pub fn iter(&self) -> impl Iterator { + self.vec.iter().enumerate().map(|(i, v)| (K::from(i), v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.vec + .iter_mut() + .enumerate() + .map(|(i, v)| (K::from(i), v)) + } } impl DenseMap { diff --git a/src/eval.rs b/src/eval.rs index d737bfe..a02bf68 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,8 +1,8 @@ //! Represents parsed Ninja strings with embedded variable references, e.g. //! `c++ $in -o $out`, and mechanisms for expanding those into plain strings. -use rustc_hash::FxHashMap; - +use crate::load::Scope; +use crate::load::ScopePosition; use crate::smallmap::SmallMap; use std::borrow::Borrow; use std::borrow::Cow; @@ -32,50 +32,50 @@ impl> EvalString { EvalString(parts) } - fn evaluate_inner(&self, result: &mut String, envs: &[&dyn Env]) { + fn evaluate_inner( + &self, + result: &mut String, + envs: &[&dyn Env], + scope: &Scope, + position: ScopePosition, + ) { for part in &self.0 { match part { EvalPart::Literal(s) => result.push_str(s.as_ref()), EvalPart::VarRef(v) => { + let mut found = false; for (i, env) in envs.iter().enumerate() { if let Some(v) = env.get_var(v.as_ref()) { - v.evaluate_inner(result, &envs[i + 1..]); + v.evaluate_inner(result, &envs[i + 1..], scope, position); + found = true; break; } } + if !found { + scope.evaluate(result, v.as_ref(), position); + } } } } } - fn calc_evaluated_length(&self, envs: &[&dyn Env]) -> usize { - self.0 - .iter() - .map(|part| match part { - EvalPart::Literal(s) => s.as_ref().len(), - EvalPart::VarRef(v) => { - for (i, env) in envs.iter().enumerate() { - if let Some(v) = env.get_var(v.as_ref()) { - return v.calc_evaluated_length(&envs[i + 1..]); - } - } - 0 - } - }) - .sum() - } - /// evalulate turns the EvalString into a regular String, looking up the /// values of variable references in the provided Envs. It will look up /// its variables in the earliest Env that has them, and then those lookups /// will be recursively expanded starting from the env after the one that /// had the first successful lookup. - pub fn evaluate(&self, envs: &[&dyn Env]) -> String { + pub fn evaluate(&self, envs: &[&dyn Env], scope: &Scope, position: ScopePosition) -> String { let mut result = String::new(); - result.reserve(self.calc_evaluated_length(envs)); - self.evaluate_inner(&mut result, envs); + self.evaluate_inner(&mut result, envs, scope, position); result } + + pub fn maybe_literal(&self) -> Option<&T> { + match &self.0[..] { + [EvalPart::Literal(x)] => Some(x), + _ => None, + } + } } impl EvalString<&str> { @@ -120,26 +120,6 @@ impl EvalString<&str> { } } -/// A single scope's worth of variable definitions. -#[derive(Debug, Default)] -pub struct Vars<'text>(FxHashMap<&'text str, String>); - -impl<'text> Vars<'text> { - pub fn insert(&mut self, key: &'text str, val: String) { - self.0.insert(key, val); - } - pub fn get(&self, key: &str) -> Option<&String> { - self.0.get(key) - } -} -impl<'a> Env for Vars<'a> { - fn get_var(&self, var: &str) -> Option>> { - Some(EvalString::new(vec![EvalPart::Literal( - std::borrow::Cow::Borrowed(self.get(var)?), - )])) - } -} - impl + PartialEq> Env for SmallMap> { fn get_var(&self, var: &str) -> Option>> { Some(self.get(var)?.as_cow()) diff --git a/src/file_pool.rs b/src/file_pool.rs new file mode 100644 index 0000000..4f7e4c1 --- /dev/null +++ b/src/file_pool.rs @@ -0,0 +1,72 @@ +use core::slice; +use std::{os::fd::{AsFd, AsRawFd}, path::Path, ptr::null_mut, sync::Mutex}; +use anyhow::bail; +use libc::{c_void, mmap, munmap, strerror, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, PROT_READ, PROT_WRITE, _SC_PAGESIZE}; + +/// FilePool is a datastucture that is intended to hold onto byte buffers and give out immutable +/// references to them. But it can also accept new byte buffers while old ones are still lent out. +/// This requires interior mutability / unsafe code. Appending to a Vec while references to other +/// elements are held is generally unsafe, because the Vec can reallocate all the prior elements +/// to a new memory location. But if the elements themselves are pointers to stable memory, the +/// contents of those pointers can be referenced safely. This also requires guarding the outer +/// Vec with a Mutex so that two threads don't append to it at the same time. +pub struct FilePool { + files: Mutex>, +} +impl FilePool { + pub fn new() -> FilePool { + FilePool { + files: Mutex::new(Vec::new()), + } + } + + pub fn read_file(&self, path: &Path) -> anyhow::Result<&[u8]> { + let page_size = unsafe {sysconf(_SC_PAGESIZE)} as usize; + let file = std::fs::File::open(path)?; + let fd = file.as_fd().as_raw_fd(); + let file_size = file.metadata()?.len() as usize; + let mapping_size = (file_size + page_size).next_multiple_of(page_size); + unsafe { + // size + 1 to add a null terminator. + let addr = mmap(null_mut(), mapping_size, PROT_READ, MAP_PRIVATE, fd, 0); + if addr == MAP_FAILED { + bail!("mmap failed"); + } + + let addr2 = mmap( + addr.add(mapping_size).sub(page_size), + page_size, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, 0); + if addr2 == MAP_FAILED { + bail!("mmap failed"); + } + *(addr.add(mapping_size).sub(page_size) as *mut u8) = 0; + // The manpages say the extra bytes past the end of the file are + // zero-filled, but just to make sure: + assert!(*(addr.add(file_size) as *mut u8) == 0); + + let files = &mut self.files.lock().unwrap(); + files.push((addr, mapping_size)); + + Ok(slice::from_raw_parts(addr as *mut u8, file_size + 1)) + } + } +} + +// SAFETY: Sync isn't implemented automatically because we have a *mut pointer, +// but that pointer isn't used at all aside from the drop implementation, so +// we won't have data races. +unsafe impl Sync for FilePool{} + +impl Drop for FilePool { + fn drop(&mut self) { + let files = self.files.lock().unwrap(); + for &(addr, len) in files.iter() { + unsafe { + munmap(addr, len); + } + } + } +} diff --git a/src/graph.rs b/src/graph.rs index d6b1a38..8bd3483 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,14 +1,11 @@ //! The build graph, a graph between files and commands. -use rustc_hash::FxHashMap; - use crate::{ - densemap::{self, DenseMap}, - hash::BuildHash, + concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, hash::BuildHash, trace }; -use std::collections::{hash_map::Entry, HashMap}; use std::path::{Path, PathBuf}; use std::time::SystemTime; +use std::{collections::HashMap, sync::Arc}; /// Id for File nodes in the Graph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -23,6 +20,11 @@ impl From for FileId { FileId(u as u32) } } +impl From for FileId { + fn from(u: u32) -> FileId { + FileId(u) + } +} /// Id for Build nodes in the Graph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -39,14 +41,14 @@ impl From for BuildId { } /// A single file referenced as part of a build. -#[derive(Debug)] +#[derive(Debug, Default, Clone)] pub struct File { /// Canonical path to the file. pub name: String, /// The Build that generates this file, if any. pub input: Option, /// The Builds that depend on this file as an input. - pub dependents: Vec, + pub dependents: ConcurrentLinkedList, } impl File { @@ -56,9 +58,9 @@ impl File { } /// A textual location within a build.ninja file, used in error messages. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FileLoc { - pub filename: std::rc::Rc, + pub filename: Arc, pub line: usize, } impl std::fmt::Display for FileLoc { @@ -146,6 +148,8 @@ mod tests { /// A single build action, generating File outputs from File inputs with a command. pub struct Build { + pub id: BuildId, + /// Source location this Build was declared. pub location: FileLoc, @@ -176,8 +180,9 @@ pub struct Build { pub outs: BuildOuts, } impl Build { - pub fn new(loc: FileLoc, ins: BuildIns, outs: BuildOuts) -> Self { + pub fn new(id: BuildId, loc: FileLoc, ins: BuildIns, outs: BuildOuts) -> Self { Build { + id, location: loc, desc: None, cmdline: None, @@ -260,24 +265,51 @@ pub struct Graph { #[derive(Default)] pub struct GraphFiles { pub by_id: DenseMap, - by_name: FxHashMap, + by_name: dashmap::DashMap, } impl Graph { + pub fn from_uninitialized_builds_and_files( + builds: Vec, + files: ( + dashmap::DashMap, + dashmap::DashMap, + ), + ) -> anyhow::Result { + let files_by_name = files.0; + let files_by_id_orig = files.1; + let files_by_id = trace::scope("create files_by_id", || { + let mut files_by_id = + DenseMap::new_sized(FileId::from(files_by_id_orig.len()), File::default()); + for (id, file) in files_by_id_orig.into_iter() { + files_by_id[id] = file; + } + files_by_id + }); + let result = Graph { + builds: DenseMap::from_vec(builds), + files: GraphFiles { + by_name: files_by_name, + by_id: files_by_id, + }, + }; + Ok(result) + } + /// Look up a file by its FileId. pub fn file(&self, id: FileId) -> &File { &self.files.by_id[id] } - /// Add a new Build, generating a BuildId for it. - pub fn add_build(&mut self, mut build: Build) -> anyhow::Result<()> { - let new_id = self.builds.next_id(); - for &id in &build.ins.ids { - self.files.by_id[id].dependents.push(new_id); - } + + pub fn initialize_build( + files_by_id: &dashmap::DashMap, + build: &mut Build, + ) -> anyhow::Result<()> { + let new_id = build.id; let mut fixup_dups = false; - for &id in &build.outs.ids { - let f = &mut self.files.by_id[id]; + for id in &build.outs.ids { + let f = &mut files_by_id.get_mut(id).unwrap(); match f.input { Some(prev) if prev == new_id => { fixup_dups = true; @@ -287,11 +319,13 @@ impl Graph { ); } Some(prev) => { + let location = build.location.clone(); anyhow::bail!( - "{}: {:?} is already an output at {}", - build.location, + "{}: {:?} is already an output at ", // {} + location, f.name, - self.builds[prev].location + // TODO + //self.builds[prev].location ); } None => f.input = Some(new_id), @@ -300,7 +334,6 @@ impl Graph { if fixup_dups { build.outs.remove_duplicates(); } - self.builds.push(build); Ok(()) } } @@ -308,7 +341,7 @@ impl Graph { impl GraphFiles { /// Look up a file by its name. Name must have been canonicalized already. pub fn lookup(&self, file: &str) -> Option { - self.by_name.get(file).copied() + self.by_name.get(file).map(|x| *x) } /// Look up a file by its name, adding it if not already present. @@ -321,12 +354,12 @@ impl GraphFiles { pub fn id_from_canonical(&mut self, file: String) -> FileId { // TODO: so many string copies :< match self.by_name.entry(file) { - Entry::Occupied(o) => *o.get(), - Entry::Vacant(v) => { + dashmap::mapref::entry::Entry::Occupied(o) => *o.get(), + dashmap::mapref::entry::Entry::Vacant(v) => { let id = self.by_id.push(File { name: v.key().clone(), input: None, - dependents: Vec::new(), + dependents: ConcurrentLinkedList::new(), }); v.insert(id); id diff --git a/src/lib.rs b/src/lib.rs index edb805c..769a924 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ pub mod canon; +mod concurrent_linked_list; mod db; mod densemap; mod depfile; mod eval; +mod file_pool; mod graph; mod hash; pub mod load; @@ -21,7 +23,6 @@ mod task; mod terminal; mod trace; mod work; -mod thread_pool; #[cfg(not(any(windows, target_arch = "wasm32")))] use jemallocator::Jemalloc; diff --git a/src/load.rs b/src/load.rs index 1453dd8..6c91a12 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,270 +1,542 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::{canon_path, canon_path_fast}, - eval::{EvalPart, EvalString, Vars}, - graph::{FileId, RspFile}, - parse::Statement, + canon::canon_path, + densemap::Index, + eval::{EvalPart, EvalString}, + file_pool::FilePool, + graph::{BuildId, FileId, Graph, RspFile}, + parse::{Build, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner, + scanner::ParseResult, smallmap::SmallMap, - {db, eval, graph, parse, trace}, thread_pool::{self, ThreadPoolExecutor}, scanner::ParseResult, + {db, eval, graph, parse, trace}, }; use anyhow::{anyhow, bail}; -use std::{collections::HashMap, sync::Mutex, cell::UnsafeCell, thread::Thread}; -use std::path::PathBuf; -use std::{borrow::Cow, path::Path}; +use rayon::prelude::*; +use rustc_hash::FxHashMap; +use std::{borrow::Cow, path::Path, sync::{atomic::AtomicUsize, mpsc::TryRecvError}}; +use std::{ + cell::UnsafeCell, + cmp::Ordering, + collections::{hash_map::Entry, HashMap}, + sync::{Arc, Mutex}, + thread::available_parallelism, +}; +use std::{path::PathBuf, sync::atomic::AtomicU32}; /// A variable lookup environment for magic $in/$out variables. -struct BuildImplicitVars<'text> { - graph: &'text graph::Graph, - build: &'text graph::Build, -} -impl<'text> BuildImplicitVars<'text> { - fn file_list(&self, ids: &[FileId], sep: char) -> String { - let mut out = String::new(); - for &id in ids { - if !out.is_empty() { - out.push(sep); - } - out.push_str(&self.graph.file(id).name); - } - out - } +struct BuildImplicitVars<'a> { + explicit_ins: &'a [String], + explicit_outs: &'a [String], } impl<'text> eval::Env for BuildImplicitVars<'text> { fn get_var(&self, var: &str) -> Option>> { let string_to_evalstring = |s: String| Some(EvalString::new(vec![EvalPart::Literal(Cow::Owned(s))])); match var { - "in" => string_to_evalstring(self.file_list(self.build.explicit_ins(), ' ')), - "in_newline" => string_to_evalstring(self.file_list(self.build.explicit_ins(), '\n')), - "out" => string_to_evalstring(self.file_list(self.build.explicit_outs(), ' ')), - "out_newline" => string_to_evalstring(self.file_list(self.build.explicit_outs(), '\n')), + "in" => string_to_evalstring(self.explicit_ins.join(" ")), + "in_newline" => string_to_evalstring(self.explicit_ins.join("\n")), + "out" => string_to_evalstring(self.explicit_outs.join(" ")), + "out_newline" => string_to_evalstring(self.explicit_outs.join("\n")), _ => None, } } } -/// FilePool is a datastucture that is intended to hold onto byte buffers and give out immutable -/// references to them. But it can also accept new byte buffers while old ones are still lent out. -/// This requires interior mutability / unsafe code. Appending to a Vec while references to other -/// elements are held is generally unsafe, because the Vec can reallocate all the prior elements -/// to a new memory location. But if the elements themselves are Vecs that never change, the -/// contents of those inner vecs can be referenced safely. This also requires guarding the outer -/// Vec with a Mutex so that two threads don't append to it at the same time. -struct FilePool { - files: Mutex>>>, +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ScopePosition(pub usize); + +pub struct ParentScopeReference<'text>(pub Arc>, pub ScopePosition); + +pub struct Scope<'text> { + parent: Option>, + rules: HashMap<&'text str, Rule<'text>>, + variables: FxHashMap<&'text str, Vec>>, + next_free_position: ScopePosition, } -impl FilePool { - fn new() -> FilePool { - FilePool { files: Mutex::new(UnsafeCell::new(Vec::new())) } + +impl<'text> Scope<'text> { + pub fn new(parent: Option>) -> Self { + Self { + parent, + rules: HashMap::new(), + variables: FxHashMap::default(), + next_free_position: ScopePosition(0), + } + } + + pub fn get_and_inc_scope_position(&mut self) -> ScopePosition { + let result = self.next_free_position; + self.next_free_position.0 += 1; + result } - /// Add the file to the file pool, and then return it back to the caller as a slice. - /// Returning the Vec instead of a slice would be unsafe, as the Vecs will be reallocated. - fn add_file(&self, file: Vec) -> &[u8] { - let files = self.files.lock().unwrap().get(); - unsafe { - (*files).push(file); - (*files).last().unwrap().as_slice() + + pub fn get_last_scope_position(&self) -> ScopePosition { + self.next_free_position + } + + pub fn get_rule(&self, name: &'text str, position: ScopePosition) -> Option<&Rule> { + match self.rules.get(name) { + Some(rule) if rule.scope_position.0 < position.0 => Some(rule), + Some(_) | None => self + .parent + .as_ref() + .map(|p| p.0.get_rule(name, p.1)) + .flatten(), } } -} -/// Internal state used while loading. -pub struct Loader<'text> { - file_pool: &'text FilePool, - vars: Vars<'text>, - graph: graph::Graph, - default: Vec, - /// rule name -> list of (key, val) - rules: HashMap<&'text str, SmallMap<&'text str, eval::EvalString<&'text str>>>, - pools: SmallMap, - builddir: Option, + pub fn evaluate(&self, result: &mut String, varname: &'text str, position: ScopePosition) { + if let Some(variables) = self.variables.get(varname) { + let i = variables + .binary_search_by(|x| { + if x.scope_position.0 < position.0 { + Ordering::Less + } else if x.scope_position.0 > position.0 { + Ordering::Greater + } else { + // If we're evaluating a variable assignment, we don't want to + // get the same assignment, but instead, we want the one just + // before it. So return Greater instead of Equal. + Ordering::Greater + } + }) + .unwrap_err(); + let i = std::cmp::min(i, variables.len() - 1); + if variables[i].scope_position.0 < position.0 { + variables[i].evaluate(result, &self); + return; + } + // We couldn't find a variable assignment before the input + // position, so check the parent scope if there is one. + } + if let Some(parent) = &self.parent { + parent.0.evaluate(result, varname, position); + } + } } -impl<'text> Loader<'text> { - pub fn new(file_pool: &'text FilePool) -> Self { - let mut loader = Loader { - file_pool, - vars: Vars::default(), - graph: graph::Graph::default(), - default: Vec::default(), - rules: HashMap::default(), - pools: SmallMap::default(), - builddir: None, - }; +fn add_build<'text>( + files: &Files, + filename: Arc, + scope: &Scope, + b: parse::Build, +) -> anyhow::Result> { + let ins: Vec<_> = b + .ins + .iter() + .map(|x| canon_path(x.evaluate(&[&b.vars], scope, b.scope_position))) + .collect(); + let outs: Vec<_> = b + .outs + .iter() + .map(|x| canon_path(x.evaluate(&[&b.vars], scope, b.scope_position))) + .collect(); + + let rule = match scope.get_rule(b.rule, b.scope_position) { + Some(r) => r, + None => bail!("unknown rule {:?}", b.rule), + }; + + let implicit_vars = BuildImplicitVars { + explicit_ins: &ins[..b.explicit_ins], + explicit_outs: &outs[..b.explicit_outs], + }; + + // temp variable in order to not move all of b into the closure + let build_vars = &b.vars; + let lookup = |key: &str| -> Option { + // Look up `key = ...` binding in build and rule block. + Some(match rule.vars.get(key) { + Some(val) => val.evaluate(&[&implicit_vars, build_vars], scope, b.scope_position), + None => build_vars.get(key)?.evaluate(&[], scope, b.scope_position), + }) + }; + + let cmdline = lookup("command"); + let desc = lookup("description"); + let depfile = lookup("depfile"); + let parse_showincludes = match lookup("deps").as_deref() { + None => false, + Some("gcc") => false, + Some("msvc") => true, + Some(other) => bail!("invalid deps attribute {:?}", other), + }; + let pool = lookup("pool"); - loader.rules.insert("phony", SmallMap::default()); + let rspfile_path = lookup("rspfile"); + let rspfile_content = lookup("rspfile_content"); + let rspfile = match (rspfile_path, rspfile_content) { + (None, None) => None, + (Some(path), Some(content)) => Some(RspFile { + path: std::path::PathBuf::from(path), + content, + }), + _ => bail!("rspfile and rspfile_content need to be both specified"), + }; - loader + let build_id = files.create_build_id(); + + let ins = graph::BuildIns { + ids: ins + .into_iter() + .map(|x| files.id_from_canonical_and_add_dependant(x, build_id)) + .collect(), + explicit: b.explicit_ins, + implicit: b.implicit_ins, + order_only: b.order_only_ins, + // validation is implied by the other counts + }; + let outs = graph::BuildOuts { + ids: outs + .into_iter() + .map(|x| files.id_from_canonical(x)) + .collect(), + explicit: b.explicit_outs, + }; + let mut build = graph::Build::new( + build_id, + graph::FileLoc { + filename, + line: b.line, + }, + ins, + outs, + ); + + build.cmdline = cmdline; + build.desc = desc; + build.depfile = depfile; + build.parse_showincludes = parse_showincludes; + build.rspfile = rspfile; + build.pool = pool; + + graph::Graph::initialize_build(&files.by_id, &mut build)?; + + Ok(SubninjaResults { + builds: vec![build], + ..SubninjaResults::default() + }) +} + +struct Files { + by_name: dashmap::DashMap, + by_id: dashmap::DashMap, + next_id: AtomicU32, + next_build_id: AtomicUsize, +} +impl Files { + pub fn new() -> Self { + Self { + by_name: dashmap::DashMap::new(), + by_id: dashmap::DashMap::new(), + next_id: AtomicU32::new(0), + next_build_id: AtomicUsize::new(0), + } } - /// Convert a path string to a FileId. For performance reasons - /// this requires an owned 'path' param. - fn path(&mut self, mut path: String) -> FileId { - // Perf: this is called while parsing build.ninja files. We go to - // some effort to avoid allocating in the common case of a path that - // refers to a file that is already known. - let len = canon_path_fast(&mut path); - path.truncate(len); - self.graph.files.id_from_canonical(path) + pub fn id_from_canonical(&self, file: String) -> FileId { + match self.by_name.entry(file) { + dashmap::mapref::entry::Entry::Occupied(o) => *o.get(), + dashmap::mapref::entry::Entry::Vacant(v) => { + let id = self + .next_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let id = FileId::from(id); + let mut f = graph::File::default(); + f.name = v.key().clone(); + self.by_id.insert(id, f); + v.insert(id); + id + } + } } - fn evaluate_path(&mut self, path: EvalString<&str>, envs: &[&dyn eval::Env]) -> FileId { - self.path(path.evaluate(envs)) + pub fn id_from_canonical_and_add_dependant(&self, file: String, build: BuildId) -> FileId { + match self.by_name.entry(file) { + dashmap::mapref::entry::Entry::Occupied(o) => { + let id = *o.get(); + self.by_id.get(&id).unwrap().dependents.prepend(build); + id + }, + dashmap::mapref::entry::Entry::Vacant(v) => { + let id = self + .next_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let id = FileId::from(id); + let mut f = graph::File::default(); + f.name = v.key().clone(); + f.dependents.prepend(build); + self.by_id.insert(id, f); + v.insert(id); + id + } + } } - fn evaluate_paths( - &mut self, - paths: Vec>, - envs: &[&dyn eval::Env], - ) -> Vec { - paths - .into_iter() - .map(|path| self.evaluate_path(path, envs)) - .collect() + pub fn into_maps( + self, + ) -> ( + dashmap::DashMap, + dashmap::DashMap, + ) { + (self.by_name, self.by_id) } - fn add_build( - &mut self, - filename: std::rc::Rc, - b: parse::Build, - ) -> anyhow::Result<()> { - let ins = graph::BuildIns { - ids: b.ins.iter().map(|x| { - self.path(x.evaluate(&[&b.vars, &self.vars])) - }).collect(), - explicit: b.explicit_ins, - implicit: b.implicit_ins, - order_only: b.order_only_ins, - // validation is implied by the other counts - }; - let outs = graph::BuildOuts { - ids: b.outs.iter().map(|x| { - self.path(x.evaluate(&[&b.vars, &self.vars])) - }).collect(), - explicit: b.explicit_outs, - }; - let mut build = graph::Build::new( - graph::FileLoc { - filename, - line: b.line, + pub fn create_build_id(&self) -> BuildId { + let id = self + .next_build_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + BuildId::from(id) + } +} + +#[derive(Default)] +struct SubninjaResults<'text> { + pub builds: Vec, + defaults: Vec, + builddir: Option, + pools: SmallMap<&'text str, usize>, +} + +fn subninja<'thread, 'text>( + num_threads: usize, + files: &'thread Files, + file_pool: &'text FilePool, + path: String, + parent_scope: Option>, + executor: &rayon::Scope<'thread>, +) -> anyhow::Result> +where + 'text: 'thread, +{ + let path = PathBuf::from(path); + let top_level_scope = parent_scope.is_none(); + let mut scope = Scope::new(parent_scope); + if top_level_scope { + let position = scope.get_and_inc_scope_position(); + scope.rules.insert( + "phony", + Rule { + name: "phony", + vars: SmallMap::default(), + scope_position: position, }, - ins, - outs, ); + } + let parse_results = parse( + num_threads, + file_pool, + file_pool.read_file(&path)?, + &mut scope, + executor, + )?; + let (sender, receiver) = std::sync::mpsc::channel::>>(); + let scope = Arc::new(scope); + for sn in parse_results.subninjas.into_iter() { + let scope = scope.clone(); + let sender = sender.clone(); + executor.spawn(move |executor| { + let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position)); + sender + .send(subninja( + num_threads, + files, + file_pool, + file, + Some(ParentScopeReference(scope, sn.scope_position)), + executor, + )) + .unwrap(); + }); + } + let filename = Arc::new(path); + for build in parse_results.builds.into_iter() { + let filename = filename.clone(); + let scope = scope.clone(); + let sender = sender.clone(); + executor.spawn(move |_| { + sender + .send(add_build(files, filename, &scope, build)) + .unwrap(); + }); + } + let mut results = SubninjaResults::default(); + results.pools = parse_results.pools; + for default in parse_results.defaults.into_iter() { + let scope = scope.clone(); + results.defaults.extend(default.files.iter().map(|x| { + let path = canon_path(x.evaluate(&[], &scope, default.scope_position)); + files.id_from_canonical(path) + })); + } - let rule = match self.rules.get(b.rule) { - Some(r) => r, - None => bail!("unknown rule {:?}", b.rule), - }; + // Only the builddir in the outermost scope is respected + if top_level_scope { + let mut build_dir = String::new(); + scope.evaluate(&mut build_dir, "builddir", scope.get_last_scope_position()); + if !build_dir.is_empty() { + results.builddir = Some(build_dir); + } + } - let implicit_vars = BuildImplicitVars { - graph: &self.graph, - build: &build, - }; + drop(sender); - // temp variable in order to not move all of b into the closure - let build_vars = &b.vars; - let lookup = |key: &str| -> Option { - // Look up `key = ...` binding in build and rule block. - Some(match rule.get(key) { - Some(val) => val.evaluate(&[&implicit_vars, build_vars, &self.vars]), - None => build_vars.get(key)?.evaluate(&[&self.vars]), - }) - }; + let mut err = None; + loop { + match receiver.try_recv() { + Ok(Err(e)) => { + if err.is_none() { + err = Some(Err(e)) + } + }, + Ok(Ok(new_results)) => { + results.builds.extend(new_results.builds); + results.defaults.extend(new_results.defaults); + for (name, depth) in new_results.pools.into_iter() { + add_pool(&mut results.pools, name, depth)?; + } + }, + // We can't risk having any tasks blocked on other tasks, lest + // the thread pool fill up with only blocked tasks. + Err(TryRecvError::Empty) => {rayon::yield_now();}, + Err(TryRecvError::Disconnected) => break, + } + } - let cmdline = lookup("command"); - let desc = lookup("description"); - let depfile = lookup("depfile"); - let parse_showincludes = match lookup("deps").as_deref() { - None => false, - Some("gcc") => false, - Some("msvc") => true, - Some(other) => bail!("invalid deps attribute {:?}", other), - }; - let pool = lookup("pool"); - - let rspfile_path = lookup("rspfile"); - let rspfile_content = lookup("rspfile_content"); - let rspfile = match (rspfile_path, rspfile_content) { - (None, None) => None, - (Some(path), Some(content)) => Some(RspFile { - path: std::path::PathBuf::from(path), - content, - }), - _ => bail!("rspfile and rspfile_content need to be both specified"), - }; + if let Some(e) = err { + e + } else { + Ok(results) + } +} - build.cmdline = cmdline; - build.desc = desc; - build.depfile = depfile; - build.parse_showincludes = parse_showincludes; - build.rspfile = rspfile; - build.pool = pool; +fn include<'thread, 'text>( + num_threads: usize, + file_pool: &'text FilePool, + path: String, + scope: &mut Scope<'text>, + executor: &rayon::Scope<'thread>, +) -> anyhow::Result> +where + 'text: 'thread, +{ + let path = PathBuf::from(path); + parse( + num_threads, + file_pool, + file_pool.read_file(&path)?, + scope, + executor, + ) +} - self.graph.add_build(build) +fn add_pool<'text>( + pools: &mut SmallMap<&'text str, usize>, + name: &'text str, + depth: usize, +) -> anyhow::Result<()> { + if let Some(_) = pools.get(name) { + bail!("duplicate pool {}", name); } + pools.insert(name, depth); + Ok(()) +} - fn read_file(&mut self, id: FileId, executor: &ThreadPoolExecutor<'text>) -> anyhow::Result<()> { - let path = self.graph.file(id).path().to_path_buf(); - let bytes = match trace::scope("read file", || scanner::read_file_with_nul(&path)) { - Ok(b) => b, - Err(e) => bail!("read {}: {}", path.display(), e), - }; - self.parse(path, self.file_pool.add_file(bytes), executor) - } +#[derive(Default)] +struct ParseResults<'text> { + builds: Vec>, + defaults: Vec>, + subninjas: Vec>, + pools: SmallMap<&'text str, usize>, +} - pub fn parse(&mut self, path: PathBuf, bytes: &'text [u8], executor: &ThreadPoolExecutor<'text>) -> anyhow::Result<()> { - let filename = std::rc::Rc::new(path); +impl<'text> ParseResults<'text> { + pub fn merge(&mut self, other: ParseResults<'text>) -> anyhow::Result<()> { + self.builds.extend(other.builds); + self.defaults.extend(other.defaults); + self.subninjas.extend(other.subninjas); + for (name, depth) in other.pools.into_iter() { + add_pool(&mut self.pools, name, depth)?; + } + Ok(()) + } +} - let chunks = parse::split_manifest_into_chunks(bytes, executor.get_num_threads().get()); +fn parse<'thread, 'text>( + num_threads: usize, + file_pool: &'text FilePool, + bytes: &'text [u8], + scope: &mut Scope<'text>, + executor: &rayon::Scope<'thread>, +) -> anyhow::Result> +where + 'text: 'thread, +{ + let chunks = parse::split_manifest_into_chunks(bytes, num_threads); - let mut receivers = Vec::with_capacity(chunks.len()); + let mut receivers = Vec::with_capacity(chunks.len()); - for chunk in chunks.into_iter() { - let (sender, receiver) = std::sync::mpsc::channel::>>(); - receivers.push(receiver); - executor.execute(move || { - let mut parser = parse::Parser::new(chunk); - parser.read_to_channel(sender); - }) - } + for chunk in chunks.into_iter() { + let (sender, receiver) = std::sync::mpsc::channel::>>(); + receivers.push(receiver); + executor.spawn(move |_| { + let mut parser = parse::Parser::new(chunk); + parser.read_to_channel(sender); + }) + } - for stmt in receivers.into_iter().flatten() { - match stmt { - Ok(Statement::VariableAssignment((name, val))) => { - self.vars.insert(name, val.evaluate(&[&self.vars])); - }, - Ok(Statement::Include(id)) => trace::scope("include", || { - let evaluated = self.path(id.evaluate(&[&self.vars])); - self.read_file(evaluated, executor) - })?, - // TODO: implement scoping for subninja - Ok(Statement::Subninja(id)) => trace::scope("subninja", || { - let evaluated = self.path(id.evaluate(&[&self.vars])); - self.read_file(evaluated, executor) - })?, - Ok(Statement::Default(defaults)) => { - let it: Vec = defaults.into_iter().map(|x| { - self.path(x.evaluate(&[&self.vars])) - }).collect(); - self.default.extend(it); - } - Ok(Statement::Rule(rule)) => { - self.rules.insert(rule.name, rule.vars); + let mut i = 0; + let mut results = ParseResults::default(); + while i < receivers.len() { + match receivers[i].try_recv() { + Ok(Ok(Statement::VariableAssignment(mut variable_assignment))) => { + variable_assignment.scope_position = scope.get_and_inc_scope_position(); + match scope.variables.entry(variable_assignment.name) { + Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), + Entry::Vacant(e) => { + e.insert(vec![variable_assignment]); + } } - Ok(Statement::Build(build)) => self.add_build(filename.clone(), build)?, - Ok(Statement::Pool(pool)) => { - self.pools.insert(pool.name.to_string(), pool.depth); - } - // TODO: Call format_parse_error - Err(e) => bail!(e.msg), - }; - } - self.builddir = self.vars.get("builddir").cloned(); - Ok(()) + } + Ok(Ok(Statement::Include(i))) => trace::scope("include", || -> anyhow::Result<()> { + let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); + let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; + results.merge(new_results)?; + Ok(()) + })?, + Ok(Ok(Statement::Subninja(mut subninja))) => trace::scope("subninja", || { + subninja.scope_position = scope.get_and_inc_scope_position(); + results.subninjas.push(subninja); + }), + Ok(Ok(Statement::Default(mut default))) => { + default.scope_position = scope.get_and_inc_scope_position(); + results.defaults.push(default); + } + Ok(Ok(Statement::Rule(mut rule))) => { + rule.scope_position = scope.get_and_inc_scope_position(); + match scope.rules.entry(rule.name) { + Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + Entry::Vacant(e) => e.insert(rule), + }; + } + Ok(Ok(Statement::Build(mut build))) => { + build.scope_position = scope.get_and_inc_scope_position(); + results.builds.push(build); + } + Ok(Ok(Statement::Pool(pool))) => { + add_pool(&mut results.pools, pool.name, pool.depth)?; + } + // TODO: Call format_parse_error + Ok(Err(e)) => bail!(e.msg), + // We can't risk having any tasks blocked on other tasks, lest + // the thread pool fill up with only blocked tasks. + Err(TryRecvError::Empty) => {rayon::yield_now();}, + Err(TryRecvError::Disconnected) => {i += 1;}, + }; } + Ok(results) } /// State loaded by read(). @@ -278,49 +550,58 @@ pub struct State { /// Load build.ninja/.n2_db and return the loaded build graph and state. pub fn read(build_filename: &str) -> anyhow::Result { + let build_filename = canon_path(build_filename); let file_pool = FilePool::new(); - let mut loader = trace::scope("loader.read_file", || -> anyhow::Result { - thread_pool::scoped_thread_pool(std::thread::available_parallelism()?, |executor| { - let mut loader = Loader::new(&file_pool); - let id = loader - .graph - .files - .id_from_canonical(canon_path(build_filename)); - loader.read_file(id, executor)?; - Ok(loader) + let files = Files::new(); + let num_threads = available_parallelism()?.get(); + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(num_threads) + .build()?; + let SubninjaResults { + builds, + defaults, + builddir, + pools, + } = trace::scope("loader.read_file", || -> anyhow::Result { + pool.scope(|executor: &rayon::Scope| { + let mut results = subninja( + num_threads, + &files, + &file_pool, + build_filename, + None, + executor, + )?; + results.builds.par_sort_unstable_by_key(|b| b.id.index()); + Ok(results) }) })?; + let mut graph = trace::scope("loader.from_uninitialized_builds_and_files", || { + Graph::from_uninitialized_builds_and_files(builds, files.into_maps()) + })?; let mut hashes = graph::Hashes::default(); let db = trace::scope("db::open", || { let mut db_path = PathBuf::from(".n2_db"); - if let Some(builddir) = &loader.builddir { + if let Some(builddir) = &builddir { db_path = Path::new(&builddir).join(db_path); if let Some(parent) = db_path.parent() { std::fs::create_dir_all(parent)?; } }; - db::open(&db_path, &mut loader.graph, &mut hashes) + db::open(&db_path, &mut graph, &mut hashes) }) .map_err(|err| anyhow!("load .n2_db: {}", err))?; + + let mut owned_pools = SmallMap::with_capacity(pools.len()); + for pool in pools.iter() { + owned_pools.insert(pool.0.to_owned(), pool.1); + } + Ok(State { - graph: loader.graph, + graph, db, hashes, - default: loader.default, - pools: loader.pools, + default: defaults, + pools: owned_pools, }) } - -/// Parse a single file's content. -#[cfg(test)] -pub fn parse(name: &str, mut content: Vec) -> anyhow::Result { - content.push(0); - let file_pool = FilePool::new(); - let mut loader = Loader::new(&file_pool); - trace::scope("loader.read_file", || { - thread_pool::scoped_thread_pool(std::num::NonZeroUsize::new(1).unwrap(), |executor| { - loader.parse(PathBuf::from(name), &content, executor) - }) - })?; - Ok(loader.graph) -} diff --git a/src/parse.rs b/src/parse.rs index 4f08fe3..7581fa1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,11 +6,16 @@ //! text, marked with the lifetime `'text`. use crate::{ - eval::{EvalPart, EvalString, Vars}, + eval::{EvalPart, EvalString}, + load::{Scope, ScopePosition}, scanner::{ParseError, ParseResult, Scanner}, smallmap::SmallMap, }; -use std::path::Path; +use std::{ + cell::UnsafeCell, + path::Path, + sync::{atomic::AtomicBool, Mutex}, +}; /// A list of variable bindings, as expressed with syntax like: /// key = $val @@ -20,6 +25,7 @@ pub type VarList<'text> = SmallMap<&'text str, EvalString<&'text str>>; pub struct Rule<'text> { pub name: &'text str, pub vars: VarList<'text>, + pub scope_position: ScopePosition, } #[derive(Debug, PartialEq)] @@ -34,6 +40,7 @@ pub struct Build<'text> { pub order_only_ins: usize, pub validation_ins: usize, pub vars: VarList<'text>, + pub scope_position: ScopePosition, } #[derive(Debug, PartialEq)] @@ -42,15 +49,77 @@ pub struct Pool<'text> { pub depth: usize, } +#[derive(Debug)] +pub struct VariableAssignment<'text> { + pub name: &'text str, + pub unevaluated: EvalString<&'text str>, + pub scope_position: ScopePosition, + pub evaluated: UnsafeCell>, + pub is_evaluated: AtomicBool, + pub lock: Mutex<()>, +} + +unsafe impl Sync for VariableAssignment<'_> {} + +impl<'text> VariableAssignment<'text> { + fn new( + name: &'text str, + unevaluated: EvalString<&'text str>, + ) -> Self { + Self { + name, + unevaluated, + scope_position: ScopePosition(0), + evaluated: UnsafeCell::new(None), + is_evaluated: AtomicBool::new(false), + lock: Mutex::new(()), + } + } + + pub fn evaluate(&self, result: &mut String, scope: &Scope) { + unsafe { + if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { + result.push_str((*self.evaluated.get()).as_ref().unwrap()); + return; + } + let guard = self.lock.lock().unwrap(); + if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { + result.push_str((*self.evaluated.get()).as_ref().unwrap()); + return; + } + + let cache = self.unevaluated.evaluate(&[], scope, self.scope_position); + result.push_str(&cache); + *self.evaluated.get() = Some(cache); + self.is_evaluated + .store(true, std::sync::atomic::Ordering::Relaxed); + + drop(guard); + } + } +} + #[derive(Debug, PartialEq)] +pub struct DefaultStmt<'text> { + pub files: Vec>, + pub scope_position: ScopePosition, +} + +#[derive(Debug, PartialEq)] +pub struct IncludeOrSubninja<'text> { + pub file: EvalString<&'text str>, + pub scope_position: ScopePosition, +} + +#[derive(Debug)] pub enum Statement<'text> { Rule(Rule<'text>), Build(Build<'text>), - Default(Vec>), - Include(EvalString<&'text str>), - Subninja(EvalString<&'text str>), + Default(DefaultStmt<'text>), + Include(IncludeOrSubninja<'text>), + Subninja(IncludeOrSubninja<'text>), Pool(Pool<'text>), - VariableAssignment((&'text str, EvalString<&'text str>)), + VariableAssignment(VariableAssignment<'text>), } pub struct Parser<'text> { @@ -82,7 +151,10 @@ impl<'text> Parser<'text> { Ok(result) } - pub fn read_to_channel(&mut self, sender: std::sync::mpsc::Sender>>) { + pub fn read_to_channel( + &mut self, + sender: std::sync::mpsc::Sender>>, + ) { loop { match self.read() { Ok(None) => return, @@ -90,7 +162,7 @@ impl<'text> Parser<'text> { Err(e) => { sender.send(Err(e)).unwrap(); return; - }, + } } } } @@ -113,7 +185,7 @@ impl<'text> Parser<'text> { // identifiers at the start of a new line, so that we can easily detect // that here. assert!(self.scanner.ofs == self.buf_len); - return Ok(None) + return Ok(None); } let ident = self.read_ident()?; self.skip_spaces(); @@ -122,14 +194,26 @@ impl<'text> Parser<'text> { "build" => return Ok(Some(Statement::Build(self.read_build()?))), "default" => return Ok(Some(Statement::Default(self.read_default()?))), "include" => { - return Ok(Some(Statement::Include(self.read_eval(false)?))); + let result = IncludeOrSubninja { + file: self.read_eval(false)?, + scope_position: ScopePosition(0), + }; + return Ok(Some(Statement::Include(result))); } "subninja" => { - return Ok(Some(Statement::Subninja(self.read_eval(false)?))); + let result = IncludeOrSubninja { + file: self.read_eval(false)?, + scope_position: ScopePosition(0), + }; + return Ok(Some(Statement::Subninja(result))); } "pool" => return Ok(Some(Statement::Pool(self.read_pool()?))), ident => { - return Ok(Some(Statement::VariableAssignment((ident, self.read_vardef()?)))) + let result = VariableAssignment::new( + ident, + self.read_vardef()?, + ); + return Ok(Some(Statement::VariableAssignment(result))); } } } @@ -194,7 +278,11 @@ impl<'text> Parser<'text> { | "msvc_deps_prefix" ) })?; - Ok(Rule { name, vars }) + Ok(Rule { + name, + vars, + scope_position: ScopePosition(0), + }) } fn read_pool(&mut self) -> ParseResult> { @@ -204,10 +292,16 @@ impl<'text> Parser<'text> { let vars = self.read_scoped_vars(|var| matches!(var, "depth"))?; let mut depth = 0; if let Some((_, val)) = vars.into_iter().next() { - let val = val.evaluate(&[]); - depth = match val.parse::() { - Ok(d) => d, - Err(err) => return self.scanner.parse_error(format!("pool depth: {}", err)), + match val.maybe_literal() { + Some(x) => match x.parse::() { + Ok(d) => depth = d, + Err(err) => return self.scanner.parse_error(format!("pool depth: {}", err)), + }, + None => { + return self + .scanner + .parse_error(format!("pool depth must be a literal string")) + } } } Ok(Pool { name, depth }) @@ -279,6 +373,7 @@ impl<'text> Parser<'text> { self.scanner.skip('\r'); self.scanner.expect('\n')?; let vars = self.read_scoped_vars(|_| true)?; + Ok(Build { rule, line, @@ -290,18 +385,22 @@ impl<'text> Parser<'text> { order_only_ins, validation_ins, vars, + scope_position: ScopePosition(0), }) } - fn read_default(&mut self) -> ParseResult>> { - let mut defaults = Vec::new(); - self.read_unevaluated_paths_to(&mut defaults)?; - if defaults.is_empty() { + fn read_default(&mut self) -> ParseResult> { + let mut files = Vec::new(); + self.read_unevaluated_paths_to(&mut files)?; + if files.is_empty() { return self.scanner.parse_error("expected path"); } self.scanner.skip('\r'); self.scanner.expect('\n')?; - Ok(defaults) + Ok(DefaultStmt { + files, + scope_position: ScopePosition(0), + }) } fn skip_comment(&mut self) -> ParseResult<()> { @@ -492,7 +591,7 @@ fn find_start_of_next_manifest_chunk(buf: &[u8], prospective_start: usize) -> us loop { // TODO: Replace the search with something that uses SIMD instructions like the memchr crate let Some(nl_index) = &buf[idx..].iter().position(|&b| b == b'\n') else { - return buf.len() + return buf.len(); }; idx += nl_index + 1; @@ -500,17 +599,20 @@ fn find_start_of_next_manifest_chunk(buf: &[u8], prospective_start: usize) -> us // for example, you could have: // - a comment that ends with a "$": "# $\n" // - an escaped-dollar: "X=$$\n" - if idx >= 2 && buf[idx-2] == b'$' || - idx >= 3 && buf[idx-2] == b'\r' && buf[idx-3] == b'$' { + if idx >= 2 && buf[idx - 2] == b'$' + || idx >= 3 && buf[idx - 2] == b'\r' && buf[idx - 3] == b'$' + { continue; } // We want chunk boundaries to be at an easy/predictable place for the scanner to stop // at. So only stop at an identifier after a newline. - if idx == buf.len() || matches!( - buf[idx], - b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.' - ) { + if idx == buf.len() + || matches!( + buf[idx], + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'.' + ) + { return idx; } } @@ -540,11 +642,11 @@ mod tests { let mut buf = test_case_buffer(test_case); let mut parser = Parser::new(&mut buf); match parser.read().unwrap().unwrap() { - Statement::VariableAssignment(_) => {}, + Statement::VariableAssignment(_) => {} stmt => panic!("expected variable assignment, got {:?}", stmt), }; let default = match parser.read().unwrap().unwrap() { - Statement::Default(d) => d, + Statement::Default(d) => d.files, stmt => panic!("expected default, got {:?}", stmt), }; assert_eq!( @@ -562,24 +664,29 @@ mod tests { fn parse_dot_in_eval() { let mut buf = test_case_buffer("x = $y.z\n"); let mut parser = Parser::new(&mut buf); - assert_eq!(parser.read(), Ok(Some(Statement::VariableAssignment(("x", EvalString::new(vec![ - EvalPart::VarRef("y"), - EvalPart::Literal(".z"), - ])))))); + let Ok(Some(Statement::VariableAssignment(x))) = parser.read() else { + panic!("Fail"); + }; + assert_eq!(x.name, "x"); + assert_eq!( + x.unevaluated, + EvalString::new(vec![EvalPart::VarRef("y"), EvalPart::Literal(".z"),]) + ); } #[test] fn parse_dot_in_rule() { let mut buf = test_case_buffer("rule x.y\n command = x\n"); let mut parser = Parser::new(&mut buf); - let stmt = parser.read().unwrap().unwrap(); - assert!(matches!( - stmt, - Statement::Rule(Rule { - name: "x.y", - vars: _ - }) - )); + let Ok(Some(Statement::Rule(stmt))) = parser.read() else { + panic!("Fail"); + }; + assert_eq!(stmt.name, "x.y"); + assert_eq!(stmt.vars.len(), 1); + assert_eq!( + stmt.vars.get("command"), + Some(&EvalString::new(vec![EvalPart::Literal("x"),])) + ); } #[test] diff --git a/src/run.rs b/src/run.rs index e84ec26..efa8e9e 100644 --- a/src/run.rs +++ b/src/run.rs @@ -34,7 +34,7 @@ fn build( let mut tasks_finished = 0; // Attempt to rebuild build.ninja. - let build_file_target = work.lookup(&build_filename); + let mut build_file_target = work.lookup(&build_filename); if let Some(target) = build_file_target { work.want_file(target)?; match trace::scope("work.run", || work.run())? { @@ -57,6 +57,7 @@ fn build( progress, state.pools, ); + build_file_target = work.lookup(&build_filename); } } } @@ -81,6 +82,16 @@ fn build( } let tasks = trace::scope("work.run", || work.run())?; + + // Important! Deallocating all the builds and files stored in the work + // object actually takes a considerable amount of time (>1 second on an + // AOSP build), so instead, leak the memory. This means that none of the + // Drop implementations will be called for work or anything inside of it, + // so we need to be sure to we don't put anything important in the Drop + // implementations. std::mem::forget used to be an unsafe api, and should + // be treated as such. + std::mem::forget(work); + // Include any tasks from initial build in final count of steps. Ok(tasks.map(|n| n + tasks_finished)) } diff --git a/src/smallmap.rs b/src/smallmap.rs index 13185d3..3b358ac 100644 --- a/src/smallmap.rs +++ b/src/smallmap.rs @@ -9,6 +9,15 @@ use std::{borrow::Borrow, fmt::Debug}; #[derive(Debug)] pub struct SmallMap(Vec<(K, V)>); +impl SmallMap { + pub fn with_capacity(cap: usize) -> Self { + Self(Vec::with_capacity(cap)) + } + pub fn len(&self) -> usize { + self.0.len() + } +} + impl Default for SmallMap { fn default() -> Self { SmallMap(Vec::default()) @@ -85,4 +94,4 @@ impl PartialEq for SmallMap { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } -} \ No newline at end of file +} diff --git a/src/thread_pool.rs b/src/thread_pool.rs deleted file mode 100644 index b60edff..0000000 --- a/src/thread_pool.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::{sync::{Arc, Mutex}, num::NonZeroUsize}; - - -type Job<'a> = Box; - -pub struct ThreadPoolExecutor<'a> { - sender: std::sync::mpsc::Sender>, - num_threads: NonZeroUsize, -} - -impl<'a> ThreadPoolExecutor<'a> { - pub fn execute () + Send + 'a>(&self, f: F) { - let job = Box::new(f); - self.sender.send(job).unwrap(); - } - - pub fn get_num_threads(&self) -> NonZeroUsize { - self.num_threads - } -} - -pub fn scoped_thread_pool<'env, T, F: FnOnce(&ThreadPoolExecutor<'env>) -> T>(num_threads: NonZeroUsize, scope: F) -> T { - std::thread::scope(|s| { - let (sender, receiver) = std::sync::mpsc::channel::(); - let receiver = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(num_threads.get()); - for _ in 0..num_threads.get() { - let receiver = receiver.clone(); - workers.push(s.spawn(move || { - loop { - let message = receiver.lock().unwrap().recv(); - match message { - Ok(job) => job(), - Err(_) => break, - } - } - })); - } - - let pool: ThreadPoolExecutor<'env> = ThreadPoolExecutor { - sender, - num_threads, - }; - scope(&pool) - }) -} diff --git a/src/work.rs b/src/work.rs index 6366c72..1ab75c7 100644 --- a/src/work.rs +++ b/src/work.rs @@ -500,7 +500,7 @@ impl<'a> Work<'a> { let mut dependents = HashSet::new(); for &id in build.outs() { - for &id in &self.graph.file(id).dependents { + for &id in self.graph.file(id).dependents.iter() { if self.build_states.get(id) != BuildState::Want { continue; } @@ -775,26 +775,3 @@ impl<'a> Work<'a> { Ok(success.then_some(tasks_done)) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_cycle() -> Result<(), anyhow::Error> { - let file = " -build a: phony b -build b: phony c -build c: phony a -"; - let mut graph = crate::load::parse("build.ninja", file.as_bytes().to_vec())?; - let a_id = graph.files.id_from_canonical("a".to_owned()); - let mut states = BuildStates::new(graph.builds.next_id(), SmallMap::default()); - let mut stack = Vec::new(); - match states.want_file(&graph, &mut stack, a_id) { - Ok(_) => panic!("expected build cycle error"), - Err(err) => assert_eq!(err.to_string(), "dependency cycle: a -> b -> c -> a"), - } - Ok(()) - } -} diff --git a/tests/e2e/basic.rs b/tests/e2e/basic.rs index fca23b8..cdff1b3 100644 --- a/tests/e2e/basic.rs +++ b/tests/e2e/basic.rs @@ -451,3 +451,21 @@ build foo: write_file assert_eq!(space.read("foo")?, b"Hello, world!\n"); Ok(()) } + +#[test] +fn cycle() -> anyhow::Result<()> { + let space = TestSpace::new()?; + space.write( + "build.ninja", + " +build a: phony b +build b: phony c +build c: phony a +", + )?; + space.write("in", "")?; + let out = space.run(&mut n2_command(vec!["a"]))?; + assert_output_contains(&out, "dependency cycle: a -> b -> c -> a"); + + Ok(()) +} From dd80981fb37b729c6eb2b3af17145e7095fb5b81 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Fri, 9 Feb 2024 16:49:57 -0800 Subject: [PATCH 03/29] Remove FileIds and Use Arc instead This removes the need for maintaining 2 maps from ids to files, and strings to ids. --- src/db.rs | 34 ++++++------ src/graph.rs | 152 ++++++++++++++++++++++++++------------------------- src/hash.rs | 44 ++++++--------- src/load.rs | 61 ++++++--------------- src/run.rs | 10 ++-- src/work.rs | 117 ++++++++++++++++++++------------------- 6 files changed, 194 insertions(+), 224 deletions(-) diff --git a/src/db.rs b/src/db.rs index 85bfab5..7c014c0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,6 +1,7 @@ //! The n2 database stores information about previous builds for determining //! which files are up to date. +use crate::graph; use crate::{ densemap, densemap::DenseMap, graph::BuildId, graph::FileId, graph::Graph, graph::Hashes, hash::BuildHash, @@ -12,6 +13,7 @@ use std::io::BufReader; use std::io::Read; use std::io::Write; use std::path::Path; +use std::sync::Arc; const VERSION: u32 = 1; @@ -34,9 +36,9 @@ impl From for Id { #[derive(Default)] pub struct IdMap { /// Maps db::Id to FileId. - fileids: DenseMap, + fileids: DenseMap>, /// Maps FileId to db::Id. - db_ids: HashMap, + db_ids: HashMap<*const graph::File, Id>, } /// RecordWriter buffers writes into a Vec. @@ -110,13 +112,13 @@ impl Writer { w.finish(&mut self.w) } - fn ensure_id(&mut self, graph: &Graph, fileid: FileId) -> std::io::Result { - let id = match self.ids.db_ids.get(&fileid) { + fn ensure_id(&mut self, file: Arc) -> std::io::Result { + let id = match self.ids.db_ids.get(&(file.as_ref() as *const graph::File)) { Some(&id) => id, None => { - let id = self.ids.fileids.push(fileid); - self.ids.db_ids.insert(fileid, id); - self.write_path(&graph.file(fileid).name)?; + let id = self.ids.fileids.push(file.clone()); + self.ids.db_ids.insert(file.as_ref() as *const graph::File, id); + self.write_path(&file.name)?; id } }; @@ -134,15 +136,15 @@ impl Writer { let outs = build.outs(); let mark = (outs.len() as u16) | 0b1000_0000_0000_0000; w.write_u16(mark); - for &out in outs { - let id = self.ensure_id(graph, out)?; + for out in outs { + let id = self.ensure_id(out.clone())?; w.write_id(id); } let deps = build.discovered_ins(); w.write_u16(deps.len() as u16); - for &dep in deps { - let id = self.ensure_id(graph, dep)?; + for dep in deps { + let id = self.ensure_id(dep.clone())?; w.write_id(id); } @@ -190,9 +192,9 @@ impl<'a> Reader<'a> { fn read_path(&mut self, len: usize) -> std::io::Result<()> { let name = self.read_str(len)?; // No canonicalization needed, paths were written canonicalized. - let fileid = self.graph.files.id_from_canonical(name); - let dbid = self.ids.fileids.push(fileid); - self.ids.db_ids.insert(fileid, dbid); + let file = self.graph.files.id_from_canonical(name); + let dbid = self.ids.fileids.push(file.clone()); + self.ids.db_ids.insert(file.as_ref() as *const graph::File, dbid); Ok(()) } @@ -217,7 +219,7 @@ impl<'a> Reader<'a> { // keep reading to parse through it. continue; } - match self.graph.file(self.ids.fileids[fileid]).input { + match *self.ids.fileids[fileid].input.lock().unwrap() { None => { obsolete = true; } @@ -241,7 +243,7 @@ impl<'a> Reader<'a> { let mut deps = Vec::with_capacity(len as usize); for _ in 0..len { let id = self.read_id()?; - deps.push(self.ids.fileids[id]); + deps.push(self.ids.fileids[id].clone()); } let hash = BuildHash(self.read_u64()?); diff --git a/src/graph.rs b/src/graph.rs index 8bd3483..a07fc50 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,9 +1,11 @@ //! The build graph, a graph between files and commands. +use rustc_hash::{FxHashMap, FxHasher}; + use crate::{ concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, hash::BuildHash, trace }; -use std::path::{Path, PathBuf}; +use std::{hash::BuildHasherDefault, path::{Path, PathBuf}, sync::Mutex}; use std::time::SystemTime; use std::{collections::HashMap, sync::Arc}; @@ -41,12 +43,12 @@ impl From for BuildId { } /// A single file referenced as part of a build. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default)] pub struct File { /// Canonical path to the file. pub name: String, /// The Build that generates this file, if any. - pub input: Option, + pub input: Mutex>, /// The Builds that depend on this file as an input. pub dependents: ConcurrentLinkedList, } @@ -81,7 +83,7 @@ pub struct BuildIns { /// This is mostly to simplify some of the iteration and is a little more /// memory efficient than three separate Vecs, but it is kept internal to /// Build and only exposed via methods on Build. - pub ids: Vec, + pub ids: Vec>, pub explicit: usize, pub implicit: usize, pub order_only: usize, @@ -92,7 +94,7 @@ pub struct BuildIns { /// Output files from a Build. pub struct BuildOuts { /// Similar to ins, we keep both explicit and implicit outs in one Vec. - pub ids: Vec, + pub ids: Vec>, pub explicit: usize, } @@ -102,15 +104,15 @@ impl BuildOuts { /// this function removes duplicates from the output list. pub fn remove_duplicates(&mut self) { let mut ids = Vec::new(); - for (i, &id) in self.ids.iter().enumerate() { - if self.ids[0..i].iter().any(|&prev| prev == id) { + for (i, id) in self.ids.iter().enumerate() { + if self.ids[0..i].iter().any(|prev| std::ptr::eq(prev.as_ref(), id.as_ref())) { // Skip over duplicate. if i < self.explicit { self.explicit -= 1; } continue; } - ids.push(id); + ids.push(id.clone()); } self.ids = ids; } @@ -118,30 +120,39 @@ impl BuildOuts { #[cfg(test)] mod tests { - fn fileids(ids: Vec) -> Vec { - ids.into_iter().map(FileId::from).collect() + use super::*; + + fn assert_file_arc_vecs_equal(a: Vec>, b: Vec>) { + for (x, y) in a.into_iter().zip(b.into_iter()) { + if !Arc::ptr_eq(&x, &y) { + panic!("File vecs not equal"); + } + } } - use super::*; #[test] fn remove_dups_explicit() { + let file1 = Arc::new(File::default()); + let file2 = Arc::new(File::default()); let mut outs = BuildOuts { - ids: fileids(vec![1, 1, 2]), + ids: vec![file1.clone(), file1.clone(), file2.clone()], explicit: 2, }; outs.remove_duplicates(); - assert_eq!(outs.ids, fileids(vec![1, 2])); + assert_file_arc_vecs_equal(outs.ids, vec![file1, file2]); assert_eq!(outs.explicit, 1); } #[test] fn remove_dups_implicit() { + let file1 = Arc::new(File::default()); + let file2 = Arc::new(File::default()); let mut outs = BuildOuts { - ids: fileids(vec![1, 2, 1]), + ids: vec![file1.clone(), file2.clone(), file1.clone()], explicit: 2, }; outs.remove_duplicates(); - assert_eq!(outs.ids, fileids(vec![1, 2])); + assert_file_arc_vecs_equal(outs.ids, vec![file1, file2]); assert_eq!(outs.explicit, 2); } } @@ -174,7 +185,7 @@ pub struct Build { pub ins: BuildIns, /// Additional inputs discovered from a previous build. - discovered_ins: Vec, + discovered_ins: Vec>, /// Output files. pub outs: BuildOuts, @@ -197,13 +208,13 @@ impl Build { } /// Input paths that appear in `$in`. - pub fn explicit_ins(&self) -> &[FileId] { + pub fn explicit_ins(&self) -> &[Arc] { &self.ins.ids[0..self.ins.explicit] } /// Input paths that, if changed, invalidate the output. /// Note this omits discovered_ins, which also invalidate the output. - pub fn dirtying_ins(&self) -> &[FileId] { + pub fn dirtying_ins(&self) -> &[Arc] { &self.ins.ids[0..(self.ins.explicit + self.ins.implicit)] } @@ -211,7 +222,7 @@ impl Build { /// Distinct from dirtying_ins in that it includes order-only dependencies. /// Note that we don't order on discovered_ins, because they're not allowed to /// affect build order. - pub fn ordering_ins(&self) -> &[FileId] { + pub fn ordering_ins(&self) -> &[Arc] { &self.ins.ids[0..(self.ins.order_only + self.ins.explicit + self.ins.implicit)] } @@ -219,13 +230,25 @@ impl Build { /// Validation inputs will be built whenever this Build is built, but this Build will not /// wait for them to complete before running. The validation inputs can fail to build, which /// will cause the overall build to fail. - pub fn validation_ins(&self) -> &[FileId] { + pub fn validation_ins(&self) -> &[Arc] { &self.ins.ids[(self.ins.order_only + self.ins.explicit + self.ins.implicit)..] } + fn vecs_of_arcs_eq(a: &Vec>, b: &Vec>) -> bool { + if a.len() != b.len() { + return false; + } + for (x, y) in a.iter().zip(b.iter()) { + if !Arc::ptr_eq(x, y) { + return false; + } + } + return true; + } + /// Potentially update discovered_ins with a new set of deps, returning true if they changed. - pub fn update_discovered(&mut self, deps: Vec) -> bool { - if deps == self.discovered_ins { + pub fn update_discovered(&mut self, deps: Vec>) -> bool { + if Self::vecs_of_arcs_eq(&deps, &self.discovered_ins) { false } else { self.set_discovered_ins(deps); @@ -233,22 +256,22 @@ impl Build { } } - pub fn set_discovered_ins(&mut self, deps: Vec) { + pub fn set_discovered_ins(&mut self, deps: Vec>) { self.discovered_ins = deps; } /// Input paths that were discovered after building, for use in the next build. - pub fn discovered_ins(&self) -> &[FileId] { + pub fn discovered_ins(&self) -> &[Arc] { &self.discovered_ins } /// Output paths that appear in `$out`. - pub fn explicit_outs(&self) -> &[FileId] { + pub fn explicit_outs(&self) -> &[Arc] { &self.outs.ids[0..self.outs.explicit] } /// Output paths that are updated when the build runs. - pub fn outs(&self) -> &[FileId] { + pub fn outs(&self) -> &[Arc] { &self.outs.ids } } @@ -264,53 +287,31 @@ pub struct Graph { /// Split from Graph for lifetime reasons. #[derive(Default)] pub struct GraphFiles { - pub by_id: DenseMap, - by_name: dashmap::DashMap, + by_name: dashmap::DashMap>, } impl Graph { pub fn from_uninitialized_builds_and_files( builds: Vec, - files: ( - dashmap::DashMap, - dashmap::DashMap, - ), + files: dashmap::DashMap>, ) -> anyhow::Result { - let files_by_name = files.0; - let files_by_id_orig = files.1; - let files_by_id = trace::scope("create files_by_id", || { - let mut files_by_id = - DenseMap::new_sized(FileId::from(files_by_id_orig.len()), File::default()); - for (id, file) in files_by_id_orig.into_iter() { - files_by_id[id] = file; - } - files_by_id - }); let result = Graph { builds: DenseMap::from_vec(builds), files: GraphFiles { - by_name: files_by_name, - by_id: files_by_id, + by_name: files, }, }; Ok(result) } - /// Look up a file by its FileId. - pub fn file(&self, id: FileId) -> &File { - &self.files.by_id[id] - } - /// Add a new Build, generating a BuildId for it. - pub fn initialize_build( - files_by_id: &dashmap::DashMap, build: &mut Build, ) -> anyhow::Result<()> { let new_id = build.id; let mut fixup_dups = false; - for id in &build.outs.ids { - let f = &mut files_by_id.get_mut(id).unwrap(); - match f.input { + for f in &build.outs.ids { + let mut input = f.input.lock().unwrap(); + match *input { Some(prev) if prev == new_id => { fixup_dups = true; println!( @@ -328,7 +329,7 @@ impl Graph { //self.builds[prev].location ); } - None => f.input = Some(new_id), + None => *input = Some(new_id), } } if fixup_dups { @@ -340,8 +341,8 @@ impl Graph { impl GraphFiles { /// Look up a file by its name. Name must have been canonicalized already. - pub fn lookup(&self, file: &str) -> Option { - self.by_name.get(file).map(|x| *x) + pub fn lookup(&self, file: &str) -> Option> { + self.by_name.get(file).map(|x| x.clone()) } /// Look up a file by its name, adding it if not already present. @@ -351,24 +352,26 @@ impl GraphFiles { /// of this function that accepts string references that is more optimized /// for the case where the entry already exists. But so far, all of our /// usages of this function have an owned string easily accessible anyways. - pub fn id_from_canonical(&mut self, file: String) -> FileId { + pub fn id_from_canonical(&mut self, file: String) -> Arc { // TODO: so many string copies :< match self.by_name.entry(file) { - dashmap::mapref::entry::Entry::Occupied(o) => *o.get(), + dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), dashmap::mapref::entry::Entry::Vacant(v) => { - let id = self.by_id.push(File { - name: v.key().clone(), - input: None, - dependents: ConcurrentLinkedList::new(), - }); - v.insert(id); - id + let mut f = File::default(); + f.name = v.key().clone(); + let f = Arc::new(f); + v.insert(f.clone()); + f } } } - pub fn all_ids(&self) -> impl Iterator { - (0..self.by_id.next_id().0).map(|id| FileId(id)) + pub fn all_files(&self) -> impl Iterator> + '_ { + self.by_name.iter().map(|x| x.clone()) + } + + pub fn num_files(&self) -> usize { + self.by_name.len() } } @@ -399,20 +402,21 @@ pub fn stat(path: &Path) -> std::io::Result { /// Gathered state of on-disk files. /// Due to discovered deps this map may grow after graph initialization. -pub struct FileState(DenseMap>); +pub struct FileState(FxHashMap<*const File, Option>); impl FileState { pub fn new(graph: &Graph) -> Self { - FileState(DenseMap::new_sized(graph.files.by_id.next_id(), None)) + let hm = HashMap::with_capacity_and_hasher(graph.files.num_files(), BuildHasherDefault::::default()); + FileState(hm) } - pub fn get(&self, id: FileId) -> Option { - self.0.lookup(id).copied().unwrap_or(None) + pub fn get(&self, id: &File) -> Option { + self.0.get(&(id as *const File)).copied().flatten() } - pub fn stat(&mut self, id: FileId, path: &Path) -> anyhow::Result { + pub fn stat(&mut self, id: &File, path: &Path) -> anyhow::Result { let mtime = stat(path).map_err(|err| anyhow::anyhow!("stat {:?}: {}", path, err))?; - self.0.set_grow(id, Some(mtime), None); + self.0.insert(id as *const File, Some(mtime)); Ok(mtime) } } diff --git a/src/hash.rs b/src/hash.rs index 23ea8d5..b10c4b5 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -4,12 +4,9 @@ //! See "Manifests instead of mtime order" in //! https://neugierig.org/software/blog/2022/03/n2.html -use crate::graph::{Build, FileId, FileState, GraphFiles, MTime, RspFile}; +use crate::graph::{self, Build, FileId, FileState, GraphFiles, MTime, RspFile}; use std::{ - collections::hash_map::DefaultHasher, - fmt::Write, - hash::{Hash, Hasher}, - time::SystemTime, + collections::hash_map::DefaultHasher, fmt::Write, hash::{Hash, Hasher}, sync::Arc, time::SystemTime }; /// Hash value used to identify a given instance of a Build's execution; @@ -24,20 +21,18 @@ trait Manifest { fn write_files( &mut self, desc: &str, - files: &GraphFiles, file_state: &FileState, - ids: &[FileId], + ids: &[Arc], ); fn write_rsp(&mut self, rspfile: &RspFile); fn write_cmdline(&mut self, cmdline: &str); } fn get_fileid_status<'a>( - files: &'a GraphFiles, file_state: &FileState, - id: FileId, + id: &'a graph::File, ) -> (&'a str, SystemTime) { - let name = &files.by_id[id].name; + let name = &id.name; let mtime = file_state .get(id) .unwrap_or_else(|| panic!("no state for {:?}", name)); @@ -72,12 +67,11 @@ impl Manifest for TerseHash { fn write_files<'a>( &mut self, _desc: &str, - files: &GraphFiles, file_state: &FileState, - ids: &[FileId], + ids: &[Arc], ) { - for &id in ids { - let (name, mtime) = get_fileid_status(files, file_state, id); + for id in ids { + let (name, mtime) = get_fileid_status(file_state, &id); self.write_string(name); mtime.hash(&mut self.0); } @@ -96,26 +90,25 @@ impl Manifest for TerseHash { fn build_manifest( manifest: &mut M, - files: &GraphFiles, file_state: &FileState, build: &Build, ) { - manifest.write_files("in", files, file_state, build.dirtying_ins()); - manifest.write_files("discovered", files, file_state, build.discovered_ins()); + manifest.write_files("in", file_state, build.dirtying_ins()); + manifest.write_files("discovered", file_state, build.discovered_ins()); manifest.write_cmdline(build.cmdline.as_deref().unwrap_or("")); if let Some(rspfile) = &build.rspfile { manifest.write_rsp(rspfile); } - manifest.write_files("out", files, file_state, build.outs()); + manifest.write_files("out", file_state, build.outs()); } // Hashes the inputs of a build to compute a signature. // Prerequisite: all referenced files have already been stat()ed and are present. // (It doesn't make sense to hash a build with missing files, because it's out // of date regardless of the state of the other files.) -pub fn hash_build(files: &GraphFiles, file_state: &FileState, build: &Build) -> BuildHash { +pub fn hash_build(file_state: &FileState, build: &Build) -> BuildHash { let mut hasher = TerseHash::default(); - build_manifest(&mut hasher, files, file_state, build); + build_manifest(&mut hasher, file_state, build); hasher.finish() } @@ -129,13 +122,12 @@ impl Manifest for ExplainHash { fn write_files<'a>( &mut self, desc: &str, - files: &GraphFiles, file_state: &FileState, - ids: &[FileId], + ids: &[Arc], ) { writeln!(&mut self.text, "{desc}:").unwrap(); - for &id in ids { - let (name, mtime) = get_fileid_status(files, file_state, id); + for id in ids { + let (name, mtime) = get_fileid_status(file_state, &id); let millis = mtime .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -159,8 +151,8 @@ impl Manifest for ExplainHash { /// Logs human-readable state of all the inputs used for hashing a given build. /// Used for "-d explain" debugging output. -pub fn explain_hash_build(files: &GraphFiles, file_state: &FileState, build: &Build) -> String { +pub fn explain_hash_build(file_state: &FileState, build: &Build) -> String { let mut explainer = ExplainHash::default(); - build_manifest(&mut explainer, files, file_state, build); + build_manifest(&mut explainer, file_state, build); explainer.text } diff --git a/src/load.rs b/src/load.rs index 6c91a12..e9c00c9 100644 --- a/src/load.rs +++ b/src/load.rs @@ -181,7 +181,11 @@ fn add_build<'text>( let ins = graph::BuildIns { ids: ins .into_iter() - .map(|x| files.id_from_canonical_and_add_dependant(x, build_id)) + .map(|x| { + let f = files.id_from_canonical(x); + f.dependents.prepend(build_id); + f + }) .collect(), explicit: b.explicit_ins, implicit: b.implicit_ins, @@ -212,7 +216,7 @@ fn add_build<'text>( build.rspfile = rspfile; build.pool = pool; - graph::Graph::initialize_build(&files.by_id, &mut build)?; + graph::Graph::initialize_build(&mut build)?; Ok(SubninjaResults { builds: vec![build], @@ -221,67 +225,34 @@ fn add_build<'text>( } struct Files { - by_name: dashmap::DashMap, - by_id: dashmap::DashMap, - next_id: AtomicU32, + by_name: dashmap::DashMap>, next_build_id: AtomicUsize, } impl Files { pub fn new() -> Self { Self { by_name: dashmap::DashMap::new(), - by_id: dashmap::DashMap::new(), - next_id: AtomicU32::new(0), next_build_id: AtomicUsize::new(0), } } - pub fn id_from_canonical(&self, file: String) -> FileId { + pub fn id_from_canonical(&self, file: String) -> Arc { match self.by_name.entry(file) { - dashmap::mapref::entry::Entry::Occupied(o) => *o.get(), + dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), dashmap::mapref::entry::Entry::Vacant(v) => { - let id = self - .next_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let id = FileId::from(id); let mut f = graph::File::default(); f.name = v.key().clone(); - self.by_id.insert(id, f); - v.insert(id); - id - } - } - } - - pub fn id_from_canonical_and_add_dependant(&self, file: String, build: BuildId) -> FileId { - match self.by_name.entry(file) { - dashmap::mapref::entry::Entry::Occupied(o) => { - let id = *o.get(); - self.by_id.get(&id).unwrap().dependents.prepend(build); - id - }, - dashmap::mapref::entry::Entry::Vacant(v) => { - let id = self - .next_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let id = FileId::from(id); - let mut f = graph::File::default(); - f.name = v.key().clone(); - f.dependents.prepend(build); - self.by_id.insert(id, f); - v.insert(id); - id + let f = Arc::new(f); + v.insert(f.clone()); + f } } } pub fn into_maps( self, - ) -> ( - dashmap::DashMap, - dashmap::DashMap, - ) { - (self.by_name, self.by_id) + ) -> dashmap::DashMap> { + self.by_name } pub fn create_build_id(&self) -> BuildId { @@ -295,7 +266,7 @@ impl Files { #[derive(Default)] struct SubninjaResults<'text> { pub builds: Vec, - defaults: Vec, + defaults: Vec>, builddir: Option, pools: SmallMap<&'text str, usize>, } @@ -544,7 +515,7 @@ pub struct State { pub graph: graph::Graph, pub db: db::Writer, pub hashes: graph::Hashes, - pub default: Vec, + pub default: Vec>, pub pools: SmallMap, } diff --git a/src/run.rs b/src/run.rs index efa8e9e..4b95926 100644 --- a/src/run.rs +++ b/src/run.rs @@ -35,7 +35,7 @@ fn build( // Attempt to rebuild build.ninja. let mut build_file_target = work.lookup(&build_filename); - if let Some(target) = build_file_target { + if let Some(target) = build_file_target.clone() { work.want_file(target)?; match trace::scope("work.run", || work.run())? { None => return Ok(None), @@ -67,9 +67,11 @@ fn build( let target = work .lookup(name) .ok_or_else(|| anyhow::anyhow!("unknown path requested: {:?}", name))?; - if Some(target) == build_file_target { - // Already built above. - continue; + if let Some(build_file_target) = build_file_target.as_ref() { + if std::ptr::eq(build_file_target.as_ref(), target.as_ref()) { + // Already built above. + continue; + } } work.want_file(target)?; } diff --git a/src/work.rs b/src/work.rs index 1ab75c7..acb6fff 100644 --- a/src/work.rs +++ b/src/work.rs @@ -6,6 +6,7 @@ use crate::{ }; use std::collections::HashSet; use std::collections::VecDeque; +use std::sync::Arc; /// Build steps go through this sequence of states. /// See "Build states" in the design notes. @@ -197,10 +198,10 @@ impl BuildStates { /// Visits a BuildId that is an input to the desired output. /// Will recursively visit its own inputs. - fn want_build( + fn want_build<'a>( &mut self, graph: &Graph, - stack: &mut Vec, + stack: &mut Vec>, id: BuildId, ) -> anyhow::Result<()> { if self.get(id) != BuildState::Unknown { @@ -212,16 +213,16 @@ impl BuildStates { // Any Build that doesn't depend on an output of another Build is ready. let mut ready = true; - for &id in build.ordering_ins() { - self.want_file(graph, stack, id)?; - ready = ready && graph.file(id).input.is_none(); + for file in build.ordering_ins() { + self.want_file(graph, stack, file.clone())?; + ready = ready && file.input.lock().unwrap().is_none(); } - for &id in build.validation_ins() { + for file in build.validation_ins() { // This build doesn't technically depend on the validation inputs, so // allocate a new stack. Validation inputs could in theory depend on this build's // outputs. let mut stack = Vec::new(); - self.want_file(graph, &mut stack, id)?; + self.want_file(graph, &mut stack, file.clone())?; } if ready { @@ -235,21 +236,23 @@ impl BuildStates { pub fn want_file( &mut self, graph: &Graph, - stack: &mut Vec, - id: FileId, + stack: &mut Vec>, + file: Arc, ) -> anyhow::Result<()> { // Check for a dependency cycle. - if let Some(cycle) = stack.iter().position(|&sid| sid == id) { + if let Some(cycle) = stack.iter().position(|f| std::ptr::eq(f.as_ref(), file.as_ref())) { let mut err = "dependency cycle: ".to_string(); - for &id in stack[cycle..].iter() { - err.push_str(&format!("{} -> ", graph.file(id).name)); + for file in stack[cycle..].iter() { + err.push_str(&format!("{} -> ", file.name)); } - err.push_str(&graph.file(id).name); + err.push_str(&file.name); anyhow::bail!(err); } - if let Some(bid) = graph.file(id).input { - stack.push(id); + let input_guard = file.input.lock().unwrap(); + if let Some(bid) = *input_guard { + drop(input_guard); + stack.push(file.clone()); self.want_build(graph, stack, bid)?; stack.pop(); } @@ -345,23 +348,24 @@ impl<'a> Work<'a> { } } - pub fn lookup(&mut self, name: &str) -> Option { + pub fn lookup(&mut self, name: &str) -> Option> { self.graph.files.lookup(&canon_path(name)) } - pub fn want_file(&mut self, id: FileId) -> anyhow::Result<()> { + pub fn want_file(&mut self, file: Arc) -> anyhow::Result<()> { let mut stack = Vec::new(); - self.build_states.want_file(&self.graph, &mut stack, id) + self.build_states.want_file(&self.graph, &mut stack, file) } - pub fn want_every_file(&mut self, exclude: Option) -> anyhow::Result<()> { - for id in self.graph.files.all_ids() { - if let Some(exclude) = exclude { - if id == exclude { + pub fn want_every_file(&mut self, exclude: Option>) -> anyhow::Result<()> { + for id in self.graph.files.all_files() { + if let Some(exclude) = exclude.as_ref() { + if std::ptr::eq(id.as_ref(), exclude.as_ref()) { continue; } } - self.want_file(id)?; + let mut stack = Vec::new(); + self.build_states.want_file(&self.graph, &mut stack, id.clone())?; } Ok(()) } @@ -371,9 +375,8 @@ impl<'a> Work<'a> { fn recheck_ready(&self, id: BuildId) -> bool { let build = &self.graph.builds[id]; // println!("recheck {:?} {} ({}...)", id, build.location, self.graph.file(build.outs()[0]).name); - for &id in build.ordering_ins() { - let file = self.graph.file(id); - match file.input { + for file in build.ordering_ins() { + match *file.input.lock().unwrap() { None => { // Only generated inputs contribute to readiness. continue; @@ -397,19 +400,18 @@ impl<'a> Work<'a> { &mut self, id: BuildId, discovered: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result>> { let build = &self.graph.builds[id]; - let ids = if discovered { + let files = if discovered { build.discovered_ins() } else { build.dirtying_ins() }; - for &id in ids { - let mtime = match self.file_state.get(id) { + for file in files { + let mtime = match self.file_state.get(file.as_ref()) { Some(mtime) => mtime, None => { - let file = self.graph.file(id); - if file.input.is_some() { + if file.input.lock().unwrap().is_some() { // This dep is generated by some other build step, but the // build graph didn't cause that other build step to be // visited first. This is an error in the build file. @@ -428,11 +430,11 @@ impl<'a> Work<'a> { file.name ); } - self.file_state.stat(id, file.path())? + self.file_state.stat(file.as_ref(), file.path())? } }; if mtime == MTime::Missing { - return Ok(Some(id)); + return Ok(Some(file.clone())); } } Ok(None) @@ -442,18 +444,18 @@ impl<'a> Work<'a> { /// Postcondition: all outputs have been stat()ed. fn record_finished(&mut self, id: BuildId, result: task::TaskResult) -> anyhow::Result<()> { // Clean up the deps discovered from the task. - let mut deps = Vec::new(); + let mut deps: Vec> = Vec::new(); if let Some(names) = result.discovered_deps { for name in names { let fileid = self.graph.files.id_from_canonical(canon_path(name)); // Filter duplicates from the file list. - if deps.contains(&fileid) { + if deps.iter().find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())).is_some() { continue; } // Filter out any deps that were already dirtying in the build file. // Note that it's allowed to have a duplicate against an order-only // dep; see `discover_existing_dep` test. - if self.graph.builds[id].dirtying_ins().contains(&fileid) { + if self.graph.builds[id].dirtying_ins().iter().find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())).is_some() { continue; } deps.push(fileid); @@ -467,7 +469,7 @@ impl<'a> Work<'a> { anyhow::bail!( "{}: depfile references nonexistent {}", self.graph.builds[id].location, - self.graph.file(missing).name + missing.name ); } } @@ -475,7 +477,7 @@ impl<'a> Work<'a> { let input_was_missing = self.graph.builds[id] .dirtying_ins() .iter() - .any(|&id| self.file_state.get(id).unwrap() == MTime::Missing); + .any(|file| self.file_state.get(file.as_ref()).unwrap() == MTime::Missing); // Update any cached state of the output files to reflect their new state. let output_was_missing = self.stat_all_outputs(id)?.is_some(); @@ -487,7 +489,7 @@ impl<'a> Work<'a> { } let build = &self.graph.builds[id]; - let hash = hash::hash_build(&self.graph.files, &self.file_state, build); + let hash = hash::hash_build(&self.file_state, build); self.db.write_build(&self.graph, id, hash)?; Ok(()) @@ -499,12 +501,12 @@ impl<'a> Work<'a> { self.build_states.set(id, build, BuildState::Done); let mut dependents = HashSet::new(); - for &id in build.outs() { - for &id in self.graph.file(id).dependents.iter() { - if self.build_states.get(id) != BuildState::Want { + for file in build.outs() { + for &file in file.dependents.iter() { + if self.build_states.get(file) != BuildState::Want { continue; } - dependents.insert(id); + dependents.insert(file); } } for id in dependents { @@ -519,14 +521,13 @@ impl<'a> Work<'a> { /// Stat all the outputs of a build. /// Called before it's run (for determining whether it's up to date) and /// after (to see if it touched any outputs). - fn stat_all_outputs(&mut self, id: BuildId) -> anyhow::Result> { + fn stat_all_outputs(&mut self, id: BuildId) -> anyhow::Result>> { let build = &self.graph.builds[id]; let mut missing = None; - for &id in build.outs() { - let file = self.graph.file(id); - let mtime = self.file_state.stat(id, file.path())?; + for file in build.outs() { + let mtime = self.file_state.stat(file.as_ref(), file.path())?; if mtime == MTime::Missing && missing.is_none() { - missing = Some(id); + missing = Some(file.clone()); } } Ok(missing) @@ -538,13 +539,12 @@ impl<'a> Work<'a> { /// Returns a build error if any required input files are missing. /// Otherwise returns the missing id if any expected but not required files, /// e.g. outputs, are missing, implying that the build needs to be executed. - fn check_build_files_missing(&mut self, id: BuildId) -> anyhow::Result> { + fn check_build_files_missing(&mut self, id: BuildId) -> anyhow::Result>> { // Ensure we have state for all input files. if let Some(missing) = self.ensure_input_files(id, false)? { - let file = self.graph.file(missing); - if file.input.is_none() { + if missing.input.lock().unwrap().is_none() { let build = &self.graph.builds[id]; - anyhow::bail!("{}: input {} missing", build.location, file.name); + anyhow::bail!("{}: input {} missing", build.location, missing.name); } return Ok(Some(missing)); } @@ -605,7 +605,7 @@ impl<'a> Work<'a> { self.progress.log(&format!( "explain: {}: input {} missing", build.location, - self.graph.file(missing).name + missing.name )); } return Ok(true); @@ -630,13 +630,12 @@ impl<'a> Work<'a> { Some(prev_hash) => prev_hash, }; - let hash = hash::hash_build(&self.graph.files, &self.file_state, build); + let hash = hash::hash_build(&self.file_state, build); if prev_hash != hash { if self.options.explain { self.progress .log(&format!("explain: {}: manifest changed", build.location)); self.progress.log(&hash::explain_hash_build( - &self.graph.files, &self.file_state, build, )); @@ -650,10 +649,10 @@ impl<'a> Work<'a> { /// Create the parent directories of a given list of fileids. /// Used to create directories used for outputs. /// TODO: do this within the thread executing the subtask? - fn create_parent_dirs(&self, ids: &[FileId]) -> anyhow::Result<()> { + fn create_parent_dirs(&self, ids: &[Arc]) -> anyhow::Result<()> { let mut dirs: Vec<&std::path::Path> = Vec::new(); - for &out in ids { - if let Some(parent) = self.graph.file(out).path().parent() { + for out in ids { + if let Some(parent) = out.path().parent() { if dirs.iter().any(|&p| p == parent) { continue; } From 248d161adeb4831e9a997f75c432b5573a909223 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 14 Feb 2024 10:09:17 -0800 Subject: [PATCH 04/29] Switch to parallel iterators These turn out to be faster than spawning individual tasks, even if it requires more single-threaded parts of the code. --- src/concurrent_linked_list.rs | 32 ++++-- src/db.rs | 8 +- src/file_pool.rs | 24 +++-- src/graph.rs | 29 ++++-- src/hash.rs | 38 ++----- src/load.rs | 188 +++++++++++++++++----------------- src/parse.rs | 10 +- src/work.rs | 30 ++++-- 8 files changed, 190 insertions(+), 169 deletions(-) diff --git a/src/concurrent_linked_list.rs b/src/concurrent_linked_list.rs index 52bffde..2ea5152 100644 --- a/src/concurrent_linked_list.rs +++ b/src/concurrent_linked_list.rs @@ -1,4 +1,10 @@ -use std::{borrow::Borrow, fmt::Debug, marker::PhantomData, ptr::null_mut, sync::atomic::{AtomicPtr, Ordering}}; +use std::{ + borrow::Borrow, + fmt::Debug, + marker::PhantomData, + ptr::null_mut, + sync::atomic::{AtomicPtr, Ordering}, +}; /// ConcurrentLinkedList is a linked list that can only be prepended to or /// iterated over. prepend() accepts an &self instead of an &mut self, and can @@ -20,7 +26,7 @@ impl ConcurrentLinkedList { } pub fn prepend(&self, val: T) { - let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode { val, next: null_mut(), })); @@ -28,7 +34,11 @@ impl ConcurrentLinkedList { let old_head = self.head.load(Ordering::SeqCst); unsafe { (*new_head).next = old_head; - if self.head.compare_exchange_weak(old_head, new_head, Ordering::SeqCst, Ordering::SeqCst).is_ok() { + if self + .head + .compare_exchange_weak(old_head, new_head, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { break; } } @@ -45,7 +55,9 @@ impl ConcurrentLinkedList { impl Default for ConcurrentLinkedList { fn default() -> Self { - Self { head: Default::default() } + Self { + head: Default::default(), + } } } @@ -53,23 +65,27 @@ impl Clone for ConcurrentLinkedList { fn clone(&self) -> Self { let mut iter = self.iter(); match iter.next() { - None => Self { head: AtomicPtr::new(null_mut()) }, + None => Self { + head: AtomicPtr::new(null_mut()), + }, Some(x) => { - let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + let new_head = Box::into_raw(Box::new(ConcurrentLinkedListNode { val: x.clone(), next: null_mut(), })); let mut new_tail = new_head; for x in iter { unsafe { - (*new_tail).next = Box::into_raw(Box::new(ConcurrentLinkedListNode{ + (*new_tail).next = Box::into_raw(Box::new(ConcurrentLinkedListNode { val: x.clone(), next: null_mut(), })); new_tail = (*new_tail).next; } } - Self { head: AtomicPtr::new(new_head) } + Self { + head: AtomicPtr::new(new_head), + } } } } diff --git a/src/db.rs b/src/db.rs index 7c014c0..3d043d8 100644 --- a/src/db.rs +++ b/src/db.rs @@ -117,7 +117,9 @@ impl Writer { Some(&id) => id, None => { let id = self.ids.fileids.push(file.clone()); - self.ids.db_ids.insert(file.as_ref() as *const graph::File, id); + self.ids + .db_ids + .insert(file.as_ref() as *const graph::File, id); self.write_path(&file.name)?; id } @@ -194,7 +196,9 @@ impl<'a> Reader<'a> { // No canonicalization needed, paths were written canonicalized. let file = self.graph.files.id_from_canonical(name); let dbid = self.ids.fileids.push(file.clone()); - self.ids.db_ids.insert(file.as_ref() as *const graph::File, dbid); + self.ids + .db_ids + .insert(file.as_ref() as *const graph::File, dbid); Ok(()) } diff --git a/src/file_pool.rs b/src/file_pool.rs index 4f7e4c1..0ad986c 100644 --- a/src/file_pool.rs +++ b/src/file_pool.rs @@ -1,7 +1,15 @@ -use core::slice; -use std::{os::fd::{AsFd, AsRawFd}, path::Path, ptr::null_mut, sync::Mutex}; use anyhow::bail; -use libc::{c_void, mmap, munmap, strerror, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, PROT_READ, PROT_WRITE, _SC_PAGESIZE}; +use core::slice; +use libc::{ + c_void, mmap, munmap, strerror, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, + PROT_READ, PROT_WRITE, _SC_PAGESIZE, +}; +use std::{ + os::fd::{AsFd, AsRawFd}, + path::Path, + ptr::null_mut, + sync::Mutex, +}; /// FilePool is a datastucture that is intended to hold onto byte buffers and give out immutable /// references to them. But it can also accept new byte buffers while old ones are still lent out. @@ -21,7 +29,7 @@ impl FilePool { } pub fn read_file(&self, path: &Path) -> anyhow::Result<&[u8]> { - let page_size = unsafe {sysconf(_SC_PAGESIZE)} as usize; + let page_size = unsafe { sysconf(_SC_PAGESIZE) } as usize; let file = std::fs::File::open(path)?; let fd = file.as_fd().as_raw_fd(); let file_size = file.metadata()?.len() as usize; @@ -38,7 +46,9 @@ impl FilePool { page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, - -1, 0); + -1, + 0, + ); if addr2 == MAP_FAILED { bail!("mmap failed"); } @@ -46,7 +56,7 @@ impl FilePool { // The manpages say the extra bytes past the end of the file are // zero-filled, but just to make sure: assert!(*(addr.add(file_size) as *mut u8) == 0); - + let files = &mut self.files.lock().unwrap(); files.push((addr, mapping_size)); @@ -58,7 +68,7 @@ impl FilePool { // SAFETY: Sync isn't implemented automatically because we have a *mut pointer, // but that pointer isn't used at all aside from the drop implementation, so // we won't have data races. -unsafe impl Sync for FilePool{} +unsafe impl Sync for FilePool {} impl Drop for FilePool { fn drop(&mut self) { diff --git a/src/graph.rs b/src/graph.rs index a07fc50..69d4450 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -3,11 +3,18 @@ use rustc_hash::{FxHashMap, FxHasher}; use crate::{ - concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, hash::BuildHash, trace + concurrent_linked_list::ConcurrentLinkedList, + densemap::{self, DenseMap}, + hash::BuildHash, + trace, }; -use std::{hash::BuildHasherDefault, path::{Path, PathBuf}, sync::Mutex}; use std::time::SystemTime; use std::{collections::HashMap, sync::Arc}; +use std::{ + hash::BuildHasherDefault, + path::{Path, PathBuf}, + sync::Mutex, +}; /// Id for File nodes in the Graph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -105,7 +112,10 @@ impl BuildOuts { pub fn remove_duplicates(&mut self) { let mut ids = Vec::new(); for (i, id) in self.ids.iter().enumerate() { - if self.ids[0..i].iter().any(|prev| std::ptr::eq(prev.as_ref(), id.as_ref())) { + if self.ids[0..i] + .iter() + .any(|prev| std::ptr::eq(prev.as_ref(), id.as_ref())) + { // Skip over duplicate. if i < self.explicit { self.explicit -= 1; @@ -297,16 +307,12 @@ impl Graph { ) -> anyhow::Result { let result = Graph { builds: DenseMap::from_vec(builds), - files: GraphFiles { - by_name: files, - }, + files: GraphFiles { by_name: files }, }; Ok(result) } - pub fn initialize_build( - build: &mut Build, - ) -> anyhow::Result<()> { + pub fn initialize_build(build: &mut Build) -> anyhow::Result<()> { let new_id = build.id; let mut fixup_dups = false; for f in &build.outs.ids { @@ -406,7 +412,10 @@ pub struct FileState(FxHashMap<*const File, Option>); impl FileState { pub fn new(graph: &Graph) -> Self { - let hm = HashMap::with_capacity_and_hasher(graph.files.num_files(), BuildHasherDefault::::default()); + let hm = HashMap::with_capacity_and_hasher( + graph.files.num_files(), + BuildHasherDefault::::default(), + ); FileState(hm) } diff --git a/src/hash.rs b/src/hash.rs index b10c4b5..21dfbf2 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -6,7 +6,11 @@ use crate::graph::{self, Build, FileId, FileState, GraphFiles, MTime, RspFile}; use std::{ - collections::hash_map::DefaultHasher, fmt::Write, hash::{Hash, Hasher}, sync::Arc, time::SystemTime + collections::hash_map::DefaultHasher, + fmt::Write, + hash::{Hash, Hasher}, + sync::Arc, + time::SystemTime, }; /// Hash value used to identify a given instance of a Build's execution; @@ -18,20 +22,12 @@ pub struct BuildHash(pub u64); /// implement it a second time for "-d explain" debug purposes. trait Manifest { /// Write a list of files+mtimes. desc is used only for "-d explain" output. - fn write_files( - &mut self, - desc: &str, - file_state: &FileState, - ids: &[Arc], - ); + fn write_files(&mut self, desc: &str, file_state: &FileState, ids: &[Arc]); fn write_rsp(&mut self, rspfile: &RspFile); fn write_cmdline(&mut self, cmdline: &str); } -fn get_fileid_status<'a>( - file_state: &FileState, - id: &'a graph::File, -) -> (&'a str, SystemTime) { +fn get_fileid_status<'a>(file_state: &FileState, id: &'a graph::File) -> (&'a str, SystemTime) { let name = &id.name; let mtime = file_state .get(id) @@ -64,12 +60,7 @@ impl TerseHash { } impl Manifest for TerseHash { - fn write_files<'a>( - &mut self, - _desc: &str, - file_state: &FileState, - ids: &[Arc], - ) { + fn write_files<'a>(&mut self, _desc: &str, file_state: &FileState, ids: &[Arc]) { for id in ids { let (name, mtime) = get_fileid_status(file_state, &id); self.write_string(name); @@ -88,11 +79,7 @@ impl Manifest for TerseHash { } } -fn build_manifest( - manifest: &mut M, - file_state: &FileState, - build: &Build, -) { +fn build_manifest(manifest: &mut M, file_state: &FileState, build: &Build) { manifest.write_files("in", file_state, build.dirtying_ins()); manifest.write_files("discovered", file_state, build.discovered_ins()); manifest.write_cmdline(build.cmdline.as_deref().unwrap_or("")); @@ -119,12 +106,7 @@ struct ExplainHash { } impl Manifest for ExplainHash { - fn write_files<'a>( - &mut self, - desc: &str, - file_state: &FileState, - ids: &[Arc], - ) { + fn write_files<'a>(&mut self, desc: &str, file_state: &FileState, ids: &[Arc]) { writeln!(&mut self.text, "{desc}:").unwrap(); for id in ids { let (name, mtime) = get_fileid_status(file_state, &id); diff --git a/src/load.rs b/src/load.rs index e9c00c9..8acbcbe 100644 --- a/src/load.rs +++ b/src/load.rs @@ -15,7 +15,11 @@ use crate::{ use anyhow::{anyhow, bail}; use rayon::prelude::*; use rustc_hash::FxHashMap; -use std::{borrow::Cow, path::Path, sync::{atomic::AtomicUsize, mpsc::TryRecvError}}; +use std::{ + borrow::Cow, + path::Path, + sync::{atomic::AtomicUsize, mpsc::TryRecvError}, +}; use std::{ cell::UnsafeCell, cmp::Ordering, @@ -119,10 +123,10 @@ impl<'text> Scope<'text> { fn add_build<'text>( files: &Files, - filename: Arc, + filename: &Arc, scope: &Scope, b: parse::Build, -) -> anyhow::Result> { +) -> anyhow::Result { let ins: Vec<_> = b .ins .iter() @@ -202,7 +206,7 @@ fn add_build<'text>( let mut build = graph::Build::new( build_id, graph::FileLoc { - filename, + filename: filename.clone(), line: b.line, }, ins, @@ -218,10 +222,7 @@ fn add_build<'text>( graph::Graph::initialize_build(&mut build)?; - Ok(SubninjaResults { - builds: vec![build], - ..SubninjaResults::default() - }) + Ok(build) } struct Files { @@ -249,9 +250,7 @@ impl Files { } } - pub fn into_maps( - self, - ) -> dashmap::DashMap> { + pub fn into_maps(self) -> dashmap::DashMap> { self.by_name } @@ -296,44 +295,42 @@ where }, ); } - let parse_results = parse( - num_threads, - file_pool, - file_pool.read_file(&path)?, - &mut scope, - executor, - )?; - let (sender, receiver) = std::sync::mpsc::channel::>>(); + let parse_results = trace::scope("parse", || { + parse( + num_threads, + file_pool, + file_pool.read_file(&path)?, + &mut scope, + executor, + ) + })?; let scope = Arc::new(scope); - for sn in parse_results.subninjas.into_iter() { - let scope = scope.clone(); - let sender = sender.clone(); - executor.spawn(move |executor| { + let mut subninja_results = parse_results + .subninjas + .into_par_iter() + .map(|sn| { let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position)); - sender - .send(subninja( - num_threads, - files, - file_pool, - file, - Some(ParentScopeReference(scope, sn.scope_position)), - executor, - )) - .unwrap(); - }); - } + subninja( + num_threads, + files, + file_pool, + file, + Some(ParentScopeReference(scope.clone(), sn.scope_position)), + executor, + ) + }) + .collect::>>()?; + let filename = Arc::new(path); - for build in parse_results.builds.into_iter() { - let filename = filename.clone(); - let scope = scope.clone(); - let sender = sender.clone(); - executor.spawn(move |_| { - sender - .send(add_build(files, filename, &scope, build)) - .unwrap(); - }); - } let mut results = SubninjaResults::default(); + + let builds = parse_results.builds; + results.builds = trace::scope("add builds", || { + builds + .into_par_iter() + .map(|build| add_build(files, &filename, &scope, build)) + .collect::>>() + })?; results.pools = parse_results.pools; for default in parse_results.defaults.into_iter() { let scope = scope.clone(); @@ -352,35 +349,23 @@ where } } - drop(sender); - - let mut err = None; - loop { - match receiver.try_recv() { - Ok(Err(e)) => { - if err.is_none() { - err = Some(Err(e)) - } - }, - Ok(Ok(new_results)) => { - results.builds.extend(new_results.builds); - results.defaults.extend(new_results.defaults); - for (name, depth) in new_results.pools.into_iter() { - add_pool(&mut results.pools, name, depth)?; - } - }, - // We can't risk having any tasks blocked on other tasks, lest - // the thread pool fill up with only blocked tasks. - Err(TryRecvError::Empty) => {rayon::yield_now();}, - Err(TryRecvError::Disconnected) => break, + results.builds.par_extend( + subninja_results + .par_iter_mut() + .flat_map(|x| std::mem::take(&mut x.builds)), + ); + results.defaults.par_extend( + subninja_results + .par_iter_mut() + .flat_map(|x| std::mem::take(&mut x.defaults)), + ); + for new_results in subninja_results { + for (name, depth) in new_results.pools.into_iter() { + add_pool(&mut results.pools, name, depth)?; } } - if let Some(e) = err { - e - } else { - Ok(results) - } + Ok(results) } fn include<'thread, 'text>( @@ -447,22 +432,35 @@ where { let chunks = parse::split_manifest_into_chunks(bytes, num_threads); - let mut receivers = Vec::with_capacity(chunks.len()); - - for chunk in chunks.into_iter() { - let (sender, receiver) = std::sync::mpsc::channel::>>(); - receivers.push(receiver); - executor.spawn(move |_| { + let receivers = chunks + .into_par_iter() + .map(|chunk| { let mut parser = parse::Parser::new(chunk); - parser.read_to_channel(sender); + parser.read_all() }) - } + .collect::>>>(); + + let Ok(receivers) = receivers else { + // TODO: Call format_parse_error + bail!(receivers.unwrap_err().msg); + }; - let mut i = 0; let mut results = ParseResults::default(); - while i < receivers.len() { - match receivers[i].try_recv() { - Ok(Ok(Statement::VariableAssignment(mut variable_assignment))) => { + + results.builds.reserve( + receivers + .par_iter() + .flatten() + .map(|x| match x { + Statement::Build(_) => 1, + _ => 0, + }) + .sum(), + ); + + for stmt in receivers.into_iter().flatten() { + match stmt { + Statement::VariableAssignment(mut variable_assignment) => { variable_assignment.scope_position = scope.get_and_inc_scope_position(); match scope.variables.entry(variable_assignment.name) { Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), @@ -471,42 +469,37 @@ where } } } - Ok(Ok(Statement::Include(i))) => trace::scope("include", || -> anyhow::Result<()> { + Statement::Include(i) => trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; results.merge(new_results)?; Ok(()) })?, - Ok(Ok(Statement::Subninja(mut subninja))) => trace::scope("subninja", || { + Statement::Subninja(mut subninja) => trace::scope("subninja", || { subninja.scope_position = scope.get_and_inc_scope_position(); results.subninjas.push(subninja); }), - Ok(Ok(Statement::Default(mut default))) => { + Statement::Default(mut default) => { default.scope_position = scope.get_and_inc_scope_position(); results.defaults.push(default); } - Ok(Ok(Statement::Rule(mut rule))) => { + Statement::Rule(mut rule) => { rule.scope_position = scope.get_and_inc_scope_position(); match scope.rules.entry(rule.name) { Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), Entry::Vacant(e) => e.insert(rule), }; } - Ok(Ok(Statement::Build(mut build))) => { + Statement::Build(mut build) => { build.scope_position = scope.get_and_inc_scope_position(); results.builds.push(build); } - Ok(Ok(Statement::Pool(pool))) => { + Statement::Pool(pool) => { add_pool(&mut results.pools, pool.name, pool.depth)?; } - // TODO: Call format_parse_error - Ok(Err(e)) => bail!(e.msg), - // We can't risk having any tasks blocked on other tasks, lest - // the thread pool fill up with only blocked tasks. - Err(TryRecvError::Empty) => {rayon::yield_now();}, - Err(TryRecvError::Disconnected) => {i += 1;}, }; } + Ok(results) } @@ -543,10 +536,13 @@ pub fn read(build_filename: &str) -> anyhow::Result { None, executor, )?; - results.builds.par_sort_unstable_by_key(|b| b.id.index()); + trace::scope("sort builds", || { + results.builds.par_sort_unstable_by_key(|b| b.id.index()) + }); Ok(results) }) })?; + drop(pool); let mut graph = trace::scope("loader.from_uninitialized_builds_and_files", || { Graph::from_uninitialized_builds_and_files(builds, files.into_maps()) })?; diff --git a/src/parse.rs b/src/parse.rs index 7581fa1..3c34473 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -62,10 +62,7 @@ pub struct VariableAssignment<'text> { unsafe impl Sync for VariableAssignment<'_> {} impl<'text> VariableAssignment<'text> { - fn new( - name: &'text str, - unevaluated: EvalString<&'text str>, - ) -> Self { + fn new(name: &'text str, unevaluated: EvalString<&'text str>) -> Self { Self { name, unevaluated, @@ -209,10 +206,7 @@ impl<'text> Parser<'text> { } "pool" => return Ok(Some(Statement::Pool(self.read_pool()?))), ident => { - let result = VariableAssignment::new( - ident, - self.read_vardef()?, - ); + let result = VariableAssignment::new(ident, self.read_vardef()?); return Ok(Some(Statement::VariableAssignment(result))); } } diff --git a/src/work.rs b/src/work.rs index acb6fff..3ea55ba 100644 --- a/src/work.rs +++ b/src/work.rs @@ -240,7 +240,10 @@ impl BuildStates { file: Arc, ) -> anyhow::Result<()> { // Check for a dependency cycle. - if let Some(cycle) = stack.iter().position(|f| std::ptr::eq(f.as_ref(), file.as_ref())) { + if let Some(cycle) = stack + .iter() + .position(|f| std::ptr::eq(f.as_ref(), file.as_ref())) + { let mut err = "dependency cycle: ".to_string(); for file in stack[cycle..].iter() { err.push_str(&format!("{} -> ", file.name)); @@ -365,7 +368,8 @@ impl<'a> Work<'a> { } } let mut stack = Vec::new(); - self.build_states.want_file(&self.graph, &mut stack, id.clone())?; + self.build_states + .want_file(&self.graph, &mut stack, id.clone())?; } Ok(()) } @@ -449,13 +453,22 @@ impl<'a> Work<'a> { for name in names { let fileid = self.graph.files.id_from_canonical(canon_path(name)); // Filter duplicates from the file list. - if deps.iter().find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())).is_some() { + if deps + .iter() + .find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())) + .is_some() + { continue; } // Filter out any deps that were already dirtying in the build file. // Note that it's allowed to have a duplicate against an order-only // dep; see `discover_existing_dep` test. - if self.graph.builds[id].dirtying_ins().iter().find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())).is_some() { + if self.graph.builds[id] + .dirtying_ins() + .iter() + .find(|x| std::ptr::eq(x.as_ref(), fileid.as_ref())) + .is_some() + { continue; } deps.push(fileid); @@ -604,8 +617,7 @@ impl<'a> Work<'a> { if self.options.explain { self.progress.log(&format!( "explain: {}: input {} missing", - build.location, - missing.name + build.location, missing.name )); } return Ok(true); @@ -635,10 +647,8 @@ impl<'a> Work<'a> { if self.options.explain { self.progress .log(&format!("explain: {}: manifest changed", build.location)); - self.progress.log(&hash::explain_hash_build( - &self.file_state, - build, - )); + self.progress + .log(&hash::explain_hash_build(&self.file_state, build)); } return Ok(true); } From a0600fdc7509c43e46925f979d3e78399c0add29 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Fri, 16 Feb 2024 14:47:09 -0800 Subject: [PATCH 05/29] Use Arc for filenames Using Arc allows us to not copy the string into both the hashmap key and the File object. --- src/graph.rs | 13 +++++++------ src/load.rs | 6 +++--- src/work.rs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 69d4450..ac38c23 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -53,7 +53,7 @@ impl From for BuildId { #[derive(Debug, Default)] pub struct File { /// Canonical path to the file. - pub name: String, + pub name: Arc, /// The Build that generates this file, if any. pub input: Mutex>, /// The Builds that depend on this file as an input. @@ -62,7 +62,7 @@ pub struct File { impl File { pub fn path(&self) -> &Path { - Path::new(&self.name) + Path::new(self.name.as_ref()) } } @@ -297,13 +297,13 @@ pub struct Graph { /// Split from Graph for lifetime reasons. #[derive(Default)] pub struct GraphFiles { - by_name: dashmap::DashMap>, + by_name: dashmap::DashMap, Arc>, } impl Graph { pub fn from_uninitialized_builds_and_files( builds: Vec, - files: dashmap::DashMap>, + files: dashmap::DashMap, Arc>, ) -> anyhow::Result { let result = Graph { builds: DenseMap::from_vec(builds), @@ -347,8 +347,8 @@ impl Graph { impl GraphFiles { /// Look up a file by its name. Name must have been canonicalized already. - pub fn lookup(&self, file: &str) -> Option> { - self.by_name.get(file).map(|x| x.clone()) + pub fn lookup(&self, file: String) -> Option> { + self.by_name.get(&Arc::new(file)).map(|x| x.clone()) } /// Look up a file by its name, adding it if not already present. @@ -359,6 +359,7 @@ impl GraphFiles { /// for the case where the entry already exists. But so far, all of our /// usages of this function have an owned string easily accessible anyways. pub fn id_from_canonical(&mut self, file: String) -> Arc { + let file = Arc::new(file); // TODO: so many string copies :< match self.by_name.entry(file) { dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), diff --git a/src/load.rs b/src/load.rs index 8acbcbe..f2703dc 100644 --- a/src/load.rs +++ b/src/load.rs @@ -226,7 +226,7 @@ fn add_build<'text>( } struct Files { - by_name: dashmap::DashMap>, + by_name: dashmap::DashMap, Arc>, next_build_id: AtomicUsize, } impl Files { @@ -238,7 +238,7 @@ impl Files { } pub fn id_from_canonical(&self, file: String) -> Arc { - match self.by_name.entry(file) { + match self.by_name.entry(Arc::new(file)) { dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), dashmap::mapref::entry::Entry::Vacant(v) => { let mut f = graph::File::default(); @@ -250,7 +250,7 @@ impl Files { } } - pub fn into_maps(self) -> dashmap::DashMap> { + pub fn into_maps(self) -> dashmap::DashMap, Arc> { self.by_name } diff --git a/src/work.rs b/src/work.rs index 3ea55ba..a94fbe1 100644 --- a/src/work.rs +++ b/src/work.rs @@ -352,7 +352,7 @@ impl<'a> Work<'a> { } pub fn lookup(&mut self, name: &str) -> Option> { - self.graph.files.lookup(&canon_path(name)) + self.graph.files.lookup(canon_path(name)) } pub fn want_file(&mut self, file: Arc) -> anyhow::Result<()> { From 0748d21ddc487c841496b5220bfeda1f3241d77c Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Sun, 18 Feb 2024 15:36:31 -0800 Subject: [PATCH 06/29] Don't flatten build vector Keep the statements around as a Vec> even after parsing, because copying all the builds into a new vector takes too long and causes more memory usage. --- src/load.rs | 68 ++++++++++++++++++++++++++++++---------------------- src/parse.rs | 1 + 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/load.rs b/src/load.rs index f2703dc..e65a4bb 100644 --- a/src/load.rs +++ b/src/load.rs @@ -324,11 +324,17 @@ where let filename = Arc::new(path); let mut results = SubninjaResults::default(); - let builds = parse_results.builds; + let statements = parse_results.statements; results.builds = trace::scope("add builds", || { - builds + statements .into_par_iter() - .map(|build| add_build(files, &filename, &scope, build)) + .flatten() + .filter_map(|stmt| { + match stmt { + Statement::Build(build) => Some(add_build(files, &filename, &scope, build)), + _ => None, + } + }) .collect::>>() })?; results.pools = parse_results.pools; @@ -402,15 +408,15 @@ fn add_pool<'text>( #[derive(Default)] struct ParseResults<'text> { - builds: Vec>, + statements: Vec>>, defaults: Vec>, subninjas: Vec>, pools: SmallMap<&'text str, usize>, } impl<'text> ParseResults<'text> { - pub fn merge(&mut self, other: ParseResults<'text>) -> anyhow::Result<()> { - self.builds.extend(other.builds); + pub fn merge(&mut self, mut other: ParseResults<'text>) -> anyhow::Result<()> { + self.statements.append(&mut other.statements); self.defaults.extend(other.defaults); self.subninjas.extend(other.subninjas); for (name, depth) in other.pools.into_iter() { @@ -432,7 +438,7 @@ where { let chunks = parse::split_manifest_into_chunks(bytes, num_threads); - let receivers = chunks + let statements = chunks .into_par_iter() .map(|chunk| { let mut parser = parse::Parser::new(chunk); @@ -440,27 +446,19 @@ where }) .collect::>>>(); - let Ok(receivers) = receivers else { + let Ok(mut statements) = statements else { // TODO: Call format_parse_error - bail!(receivers.unwrap_err().msg); + bail!(statements.unwrap_err().msg); }; let mut results = ParseResults::default(); - results.builds.reserve( - receivers - .par_iter() - .flatten() - .map(|x| match x { - Statement::Build(_) => 1, - _ => 0, - }) - .sum(), - ); - - for stmt in receivers.into_iter().flatten() { + for stmt in statements.iter_mut().flatten() { match stmt { - Statement::VariableAssignment(mut variable_assignment) => { + stmt @ Statement::VariableAssignment(_) => { + let Statement::VariableAssignment(mut variable_assignment) = std::mem::replace(stmt, Statement::EmptyStatement) else { + unreachable!(); + }; variable_assignment.scope_position = scope.get_and_inc_scope_position(); match scope.variables.entry(variable_assignment.name) { Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), @@ -469,37 +467,51 @@ where } } } - Statement::Include(i) => trace::scope("include", || -> anyhow::Result<()> { + Statement::Include(ref i) => trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; + // Things will be out of order here, but we don't care about + // order for builds, defaults, subninjas, or pools, as long + // as their scope_position is correct. results.merge(new_results)?; Ok(()) })?, - Statement::Subninja(mut subninja) => trace::scope("subninja", || { + stmt @ Statement::Subninja(_) => trace::scope("subninja", || { + let Statement::Subninja(mut subninja) = std::mem::replace(stmt, Statement::EmptyStatement) else { + unreachable!(); + }; subninja.scope_position = scope.get_and_inc_scope_position(); results.subninjas.push(subninja); }), - Statement::Default(mut default) => { + stmt @ Statement::Default(_) => { + let Statement::Default(mut default) = std::mem::replace(stmt, Statement::EmptyStatement) else { + unreachable!(); + }; default.scope_position = scope.get_and_inc_scope_position(); results.defaults.push(default); } - Statement::Rule(mut rule) => { + stmt @ Statement::Rule(_) => { + let Statement::Rule(mut rule) = std::mem::replace(stmt, Statement::EmptyStatement) else { + unreachable!(); + }; rule.scope_position = scope.get_and_inc_scope_position(); match scope.rules.entry(rule.name) { Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), Entry::Vacant(e) => e.insert(rule), }; } - Statement::Build(mut build) => { + Statement::Build(ref mut build) => { build.scope_position = scope.get_and_inc_scope_position(); - results.builds.push(build); } Statement::Pool(pool) => { add_pool(&mut results.pools, pool.name, pool.depth)?; } + Statement::EmptyStatement => {}, }; } + results.statements.append(&mut statements); + Ok(results) } diff --git a/src/parse.rs b/src/parse.rs index 3c34473..21edcb4 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -110,6 +110,7 @@ pub struct IncludeOrSubninja<'text> { #[derive(Debug)] pub enum Statement<'text> { + EmptyStatement, Rule(Rule<'text>), Build(Build<'text>), Default(DefaultStmt<'text>), From 3ebeec70745d7c373d2f7601c76c797b66183dc3 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Tue, 20 Feb 2024 14:49:40 -0800 Subject: [PATCH 07/29] Parse by chunks Another attempt to reduce copies / memory usage --- src/load.rs | 278 ++++++++++++++++++++++++++++----------------------- src/parse.rs | 90 ++++++++++++++--- src/trace.rs | 12 +++ 3 files changed, 241 insertions(+), 139 deletions(-) diff --git a/src/load.rs b/src/load.rs index e65a4bb..38ceba7 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,24 +1,13 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, - densemap::Index, - eval::{EvalPart, EvalString}, - file_pool::FilePool, - graph::{BuildId, FileId, Graph, RspFile}, - parse::{Build, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, - scanner, - scanner::ParseResult, - smallmap::SmallMap, - {db, eval, graph, parse, trace}, + canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, Build, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; use rustc_hash::FxHashMap; use std::{ - borrow::Cow, - path::Path, - sync::{atomic::AtomicUsize, mpsc::TryRecvError}, + borrow::Cow, default, path::Path, sync::{atomic::AtomicUsize, mpsc::TryRecvError}, time::Instant }; use std::{ cell::UnsafeCell, @@ -48,11 +37,19 @@ impl<'text> eval::Env for BuildImplicitVars<'text> { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct ScopePosition(pub usize); +impl ScopePosition { + pub fn add(&self, other: ScopePosition) -> ScopePosition { + ScopePosition(self.0 + other.0) + } +} + +#[derive(Debug)] pub struct ParentScopeReference<'text>(pub Arc>, pub ScopePosition); +#[derive(Debug)] pub struct Scope<'text> { parent: Option>, rules: HashMap<&'text str, Rule<'text>>, @@ -125,8 +122,10 @@ fn add_build<'text>( files: &Files, filename: &Arc, scope: &Scope, - b: parse::Build, + b: &mut parse::Build, + base_position: ScopePosition, ) -> anyhow::Result { + b.scope_position.0 += base_position.0; let ins: Vec<_> = b .ins .iter() @@ -285,31 +284,42 @@ where let top_level_scope = parent_scope.is_none(); let mut scope = Scope::new(parent_scope); if top_level_scope { - let position = scope.get_and_inc_scope_position(); scope.rules.insert( "phony", Rule { name: "phony", vars: SmallMap::default(), - scope_position: position, + scope_position: ScopePosition(0), }, ); } - let parse_results = trace::scope("parse", || { + let mut parse_results = trace::scope("parse", || { parse( num_threads, file_pool, file_pool.read_file(&path)?, &mut scope, + // to account for the phony rule + if top_level_scope { ScopePosition(1) } else { ScopePosition(0) }, executor, ) })?; + + for clump in &mut parse_results { + for mut rule in std::mem::take(&mut clump.rules).into_iter() { + rule.scope_position = rule.scope_position.add(clump.base_position); + match scope.rules.entry(rule.name) { + Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + Entry::Vacant(e) => e.insert(rule), + }; + } + } + let scope = Arc::new(scope); - let mut subninja_results = parse_results - .subninjas - .into_par_iter() - .map(|sn| { - let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position)); + let mut subninja_results = parse_results.par_iter() + .flat_map(|x| x.subninjas.par_iter().zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))) + .map(|(sn, base_position)| { + let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position.add(base_position))); subninja( num_threads, files, @@ -324,42 +334,42 @@ where let filename = Arc::new(path); let mut results = SubninjaResults::default(); - let statements = parse_results.statements; + for clump in &parse_results { + for pool in &clump.pools { + add_pool(&mut results.pools, pool.name, pool.depth)?; + } + for default in clump.defaults.iter() { + let scope = scope.clone(); + results.defaults.extend(default.files.iter().map(|x| { + let path = canon_path(x.evaluate(&[], &scope, default.scope_position.add(clump.base_position))); + files.id_from_canonical(path) + })); + } + } + results.builds = trace::scope("add builds", || { - statements - .into_par_iter() - .flatten() - .filter_map(|stmt| { - match stmt { - Statement::Build(build) => Some(add_build(files, &filename, &scope, build)), - _ => None, - } + parse_results + .par_iter_mut() + .flat_map(|x| { + let num_builds = x.builds.len(); + x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) }) + .map(|(build, base_position)| add_build(files, &filename, &scope, build, base_position)) .collect::>>() })?; - results.pools = parse_results.pools; - for default in parse_results.defaults.into_iter() { - let scope = scope.clone(); - results.defaults.extend(default.files.iter().map(|x| { - let path = canon_path(x.evaluate(&[], &scope, default.scope_position)); - files.id_from_canonical(path) - })); - } // Only the builddir in the outermost scope is respected if top_level_scope { let mut build_dir = String::new(); - scope.evaluate(&mut build_dir, "builddir", scope.get_last_scope_position()); + scope.evaluate(&mut build_dir, "builddir", ScopePosition(parse_results.iter().map(|x| x.used_scope_positions).sum::())); if !build_dir.is_empty() { results.builddir = Some(build_dir); } } - results.builds.par_extend( - subninja_results - .par_iter_mut() - .flat_map(|x| std::mem::take(&mut x.builds)), - ); + for sn in &mut subninja_results { + results.builds.append(&mut sn.builds); + } results.defaults.par_extend( subninja_results .par_iter_mut() @@ -379,8 +389,9 @@ fn include<'thread, 'text>( file_pool: &'text FilePool, path: String, scope: &mut Scope<'text>, + clump_base_position: ScopePosition, executor: &rayon::Scope<'thread>, -) -> anyhow::Result> +) -> anyhow::Result>> where 'text: 'thread, { @@ -390,6 +401,7 @@ where file_pool, file_pool.read_file(&path)?, scope, + clump_base_position, executor, ) } @@ -406,111 +418,123 @@ fn add_pool<'text>( Ok(()) } -#[derive(Default)] -struct ParseResults<'text> { - statements: Vec>>, - defaults: Vec>, - subninjas: Vec>, - pools: SmallMap<&'text str, usize>, -} - -impl<'text> ParseResults<'text> { - pub fn merge(&mut self, mut other: ParseResults<'text>) -> anyhow::Result<()> { - self.statements.append(&mut other.statements); - self.defaults.extend(other.defaults); - self.subninjas.extend(other.subninjas); - for (name, depth) in other.pools.into_iter() { - add_pool(&mut self.pools, name, depth)?; - } - Ok(()) - } -} - fn parse<'thread, 'text>( num_threads: usize, file_pool: &'text FilePool, bytes: &'text [u8], scope: &mut Scope<'text>, + mut clump_base_position: ScopePosition, executor: &rayon::Scope<'thread>, -) -> anyhow::Result> +) -> anyhow::Result>> where 'text: 'thread, { let chunks = parse::split_manifest_into_chunks(bytes, num_threads); - let statements = chunks + let statements: ParseResult>> = chunks .into_par_iter() .map(|chunk| { let mut parser = parse::Parser::new(chunk); - parser.read_all() - }) - .collect::>>>(); + parser.read_clumps() + }).collect(); let Ok(mut statements) = statements else { // TODO: Call format_parse_error bail!(statements.unwrap_err().msg); }; - let mut results = ParseResults::default(); + let mut results = Vec::new(); - for stmt in statements.iter_mut().flatten() { + let start = Instant::now(); + for stmt in statements.into_iter().flatten() { match stmt { - stmt @ Statement::VariableAssignment(_) => { - let Statement::VariableAssignment(mut variable_assignment) = std::mem::replace(stmt, Statement::EmptyStatement) else { - unreachable!(); - }; - variable_assignment.scope_position = scope.get_and_inc_scope_position(); - match scope.variables.entry(variable_assignment.name) { - Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), - Entry::Vacant(e) => { - e.insert(vec![variable_assignment]); + ClumpOrInclude::Clump(mut clump) => { + // Variable assignemnts must be added to the scope now, because + // they may be referenced by a later include. Everything else + // can be handled after all parsing is done. + for mut variable_assignment in std::mem::take(&mut clump.assignments).into_iter() { + variable_assignment.scope_position.0 += clump_base_position.0; + match scope.variables.entry(variable_assignment.name) { + Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), + Entry::Vacant(e) => { + e.insert(vec![variable_assignment]); + } } } - } - Statement::Include(ref i) => trace::scope("include", || -> anyhow::Result<()> { - let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); - let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; - // Things will be out of order here, but we don't care about - // order for builds, defaults, subninjas, or pools, as long - // as their scope_position is correct. - results.merge(new_results)?; - Ok(()) - })?, - stmt @ Statement::Subninja(_) => trace::scope("subninja", || { - let Statement::Subninja(mut subninja) = std::mem::replace(stmt, Statement::EmptyStatement) else { - unreachable!(); - }; - subninja.scope_position = scope.get_and_inc_scope_position(); - results.subninjas.push(subninja); - }), - stmt @ Statement::Default(_) => { - let Statement::Default(mut default) = std::mem::replace(stmt, Statement::EmptyStatement) else { - unreachable!(); - }; - default.scope_position = scope.get_and_inc_scope_position(); - results.defaults.push(default); - } - stmt @ Statement::Rule(_) => { - let Statement::Rule(mut rule) = std::mem::replace(stmt, Statement::EmptyStatement) else { - unreachable!(); - }; - rule.scope_position = scope.get_and_inc_scope_position(); - match scope.rules.entry(rule.name) { - Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), - Entry::Vacant(e) => e.insert(rule), - }; - } - Statement::Build(ref mut build) => { - build.scope_position = scope.get_and_inc_scope_position(); - } - Statement::Pool(pool) => { - add_pool(&mut results.pools, pool.name, pool.depth)?; - } - Statement::EmptyStatement => {}, - }; + clump.base_position = clump_base_position; + clump_base_position.0 += clump.used_scope_positions; + results.push(clump); + }, + ClumpOrInclude::Include(i) => { + trace::scope("include", || -> anyhow::Result<()> { + let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position)); + let mut new_results = include(num_threads, file_pool, evaluated, scope, clump_base_position, executor)?; + clump_base_position.0 += new_results.iter().map(|c| c.used_scope_positions).sum::(); + // Things will be out of order here, but we don't care about + // order for builds, defaults, subninjas, or pools, as long + // as their scope_position is correct. + results.append(&mut new_results); + clump_base_position.0 += 1; + Ok(()) + })?; + }, + } + // match stmt { + // stmt @ Statement::VariableAssignment(_) => { + // let Statement::VariableAssignment(mut variable_assignment) = std::mem::replace(stmt, Statement::EmptyStatement) else { + // unreachable!(); + // }; + // variable_assignment.scope_position = scope.get_and_inc_scope_position(); + // match scope.variables.entry(variable_assignment.name) { + // Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), + // Entry::Vacant(e) => { + // e.insert(vec![variable_assignment]); + // } + // } + // } + // Statement::Include(ref i) => trace::scope("include", || -> anyhow::Result<()> { + // let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); + // let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; + // // Things will be out of order here, but we don't care about + // // order for builds, defaults, subninjas, or pools, as long + // // as their scope_position is correct. + // results.merge(new_results)?; + // Ok(()) + // })?, + // stmt @ Statement::Subninja(_) => trace::scope("subninja", || { + // let Statement::Subninja(mut subninja) = std::mem::replace(stmt, Statement::EmptyStatement) else { + // unreachable!(); + // }; + // subninja.scope_position = scope.get_and_inc_scope_position(); + // results.subninjas.push(subninja); + // }), + // stmt @ Statement::Default(_) => { + // let Statement::Default(mut default) = std::mem::replace(stmt, Statement::EmptyStatement) else { + // unreachable!(); + // }; + // default.scope_position = scope.get_and_inc_scope_position(); + // results.defaults.push(default); + // } + // stmt @ Statement::Rule(_) => { + // let Statement::Rule(mut rule) = std::mem::replace(stmt, Statement::EmptyStatement) else { + // unreachable!(); + // }; + // rule.scope_position = scope.get_and_inc_scope_position(); + // match scope.rules.entry(rule.name) { + // Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + // Entry::Vacant(e) => e.insert(rule), + // }; + // } + // Statement::Build(ref mut build) => { + // build.scope_position = scope.get_and_inc_scope_position(); + // } + // Statement::Pool(pool) => { + // add_pool(&mut results.pools, pool.name, pool.depth)?; + // } + // Statement::EmptyStatement => {}, + // }; } - - results.statements.append(&mut statements); + trace::write_complete("parse loop", start, Instant::now()); Ok(results) } diff --git a/src/parse.rs b/src/parse.rs index 21edcb4..4deb57a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -120,6 +120,35 @@ pub enum Statement<'text> { VariableAssignment(VariableAssignment<'text>), } +#[derive(Default, Debug)] +pub struct Clump<'text> { + pub assignments: Vec>, + pub rules: Vec>, + pub pools: Vec>, + pub defaults: Vec>, + pub builds: Vec>, + pub subninjas: Vec>, + pub used_scope_positions: usize, + pub base_position: ScopePosition, +} + +impl<'text> Clump<'text> { + pub fn is_empty(&self) -> bool { + self.assignments.is_empty() && + self.rules.is_empty() && + self.pools.is_empty() && + self.defaults.is_empty() && + self.builds.is_empty() && + self.subninjas.is_empty() + } +} + +#[derive(Debug)] +pub enum ClumpOrInclude<'text> { + Clump(Clump<'text>), + Include(EvalString<&'text str>), +} + pub struct Parser<'text> { scanner: Scanner<'text>, buf_len: usize, @@ -149,20 +178,57 @@ impl<'text> Parser<'text> { Ok(result) } - pub fn read_to_channel( - &mut self, - sender: std::sync::mpsc::Sender>>, - ) { - loop { - match self.read() { - Ok(None) => return, - Ok(Some(stmt)) => sender.send(Ok(stmt)).unwrap(), - Err(e) => { - sender.send(Err(e)).unwrap(); - return; - } + pub fn read_clumps(&mut self) -> ParseResult>> { + let mut result = Vec::new(); + let mut clump = Clump::default(); + let mut position = ScopePosition(0); + while let Some(stmt) = self.read()? { + match stmt { + Statement::EmptyStatement => {}, + Statement::Rule(mut r) => { + r.scope_position = position; + position.0 += 1; + clump.rules.push(r); + }, + Statement::Build(mut b) => { + b.scope_position = position; + position.0 += 1; + clump.builds.push(b); + }, + Statement::Default(mut d) => { + d.scope_position = position; + position.0 += 1; + clump.defaults.push(d); + }, + Statement::Include(i) => { + if !clump.is_empty() { + clump.used_scope_positions = position.0; + result.push(ClumpOrInclude::Clump(clump)); + clump = Clump::default(); + position = ScopePosition(0); + } + result.push(ClumpOrInclude::Include(i.file)); + }, + Statement::Subninja(mut s) => { + s.scope_position = position; + position.0 += 1; + clump.subninjas.push(s); + }, + Statement::Pool(p) => { + clump.pools.push(p); + }, + Statement::VariableAssignment(mut v) => { + v.scope_position = position; + position.0 += 1; + clump.assignments.push(v); + }, } } + if !clump.is_empty() { + clump.used_scope_positions = position.0; + result.push(ClumpOrInclude::Clump(clump)); + } + Ok(result) } pub fn read(&mut self) -> ParseResult>> { diff --git a/src/trace.rs b/src/trace.rs index c862bfc..a43f5c5 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -119,6 +119,18 @@ pub fn scope(name: &'static str, f: impl FnOnce() -> T) -> T { } } + +#[inline] +pub fn write_complete(name: &'static str, start: Instant, end: Instant) { + // Safety: accessing global mut, not threadsafe. + unsafe { + match &mut TRACE { + None => (), + Some(t) => t.write_complete(name, 0, start, end), + } + } +} + pub fn close() { if_enabled(|t| t.close()); } From 740f5931822da7506503bfc2d41f0028c971383e Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 21 Feb 2024 11:56:31 -0800 Subject: [PATCH 08/29] Remove parse::Build, using graph::Build instead Using the final graph::Build object is faster due to the need for fewer copies between different vector types. It will also allow us to only evaluate the bindings of the Build objects we actually use. --- src/graph.rs | 56 ++++++++----- src/load.rs | 216 +++++++++++++++++------------------------------- src/parse.rs | 121 +++++++++++++++++---------- src/smallmap.rs | 12 +++ src/trace.rs | 14 +++- 5 files changed, 214 insertions(+), 205 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index ac38c23..68bde4b 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -3,10 +3,7 @@ use rustc_hash::{FxHashMap, FxHasher}; use crate::{ - concurrent_linked_list::ConcurrentLinkedList, - densemap::{self, DenseMap}, - hash::BuildHash, - trace, + concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::EvalString, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap, trace }; use std::time::SystemTime; use std::{collections::HashMap, sync::Arc}; @@ -85,12 +82,14 @@ pub struct RspFile { } /// Input files to a Build. +#[derive(Debug)] pub struct BuildIns { /// Internally we stuff explicit/implicit/order-only ins all into one Vec. /// This is mostly to simplify some of the iteration and is a little more /// memory efficient than three separate Vecs, but it is kept internal to /// Build and only exposed via methods on Build. pub ids: Vec>, + pub unevaluated: Vec>, pub explicit: usize, pub implicit: usize, pub order_only: usize, @@ -99,9 +98,11 @@ pub struct BuildIns { } /// Output files from a Build. +#[derive(Debug)] pub struct BuildOuts { /// Similar to ins, we keep both explicit and implicit outs in one Vec. pub ids: Vec>, + pub unevaluated: Vec>, pub explicit: usize, } @@ -146,6 +147,7 @@ mod tests { let file2 = Arc::new(File::default()); let mut outs = BuildOuts { ids: vec![file1.clone(), file1.clone(), file2.clone()], + unevaluated: Vec::new(), explicit: 2, }; outs.remove_duplicates(); @@ -159,6 +161,7 @@ mod tests { let file2 = Arc::new(File::default()); let mut outs = BuildOuts { ids: vec![file1.clone(), file2.clone(), file1.clone()], + unevaluated: Vec::new(), explicit: 2, }; outs.remove_duplicates(); @@ -168,9 +171,18 @@ mod tests { } /// A single build action, generating File outputs from File inputs with a command. +#[derive(Debug)] pub struct Build { pub id: BuildId, + pub scope_position: ScopePosition, + + pub rule: String, + + // The unevaluated variable bindings. They're stored unevalated so that + // we don't have to evaluate all bindings on all builds. + pub bindings: SmallMap>, + /// Source location this Build was declared. pub location: FileLoc, @@ -195,27 +207,28 @@ pub struct Build { pub ins: BuildIns, /// Additional inputs discovered from a previous build. - discovered_ins: Vec>, + // TODO: Make private again + pub discovered_ins: Vec>, /// Output files. pub outs: BuildOuts, } impl Build { - pub fn new(id: BuildId, loc: FileLoc, ins: BuildIns, outs: BuildOuts) -> Self { - Build { - id, - location: loc, - desc: None, - cmdline: None, - depfile: None, - parse_showincludes: false, - rspfile: None, - pool: None, - ins, - discovered_ins: Vec::new(), - outs, - } - } + // pub fn new(id: BuildId, loc: FileLoc, ins: BuildIns, outs: BuildOuts) -> Self { + // Build { + // id, + // location: loc, + // desc: None, + // cmdline: None, + // depfile: None, + // parse_showincludes: false, + // rspfile: None, + // pool: None, + // ins, + // discovered_ins: Vec::new(), + // outs, + // } + // } /// Input paths that appear in `$in`. pub fn explicit_ins(&self) -> &[Arc] { @@ -315,6 +328,9 @@ impl Graph { pub fn initialize_build(build: &mut Build) -> anyhow::Result<()> { let new_id = build.id; let mut fixup_dups = false; + for input in &build.ins.ids { + input.dependents.prepend(new_id); + } for f in &build.outs.ids { let mut input = f.input.lock().unwrap(); match *input { diff --git a/src/load.rs b/src/load.rs index 38ceba7..d39f93d 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,7 +1,7 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, Build, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace + canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; @@ -120,35 +120,32 @@ impl<'text> Scope<'text> { fn add_build<'text>( files: &Files, - filename: &Arc, scope: &Scope, - b: &mut parse::Build, + b: &mut graph::Build, base_position: ScopePosition, -) -> anyhow::Result { +) -> anyhow::Result<()> { b.scope_position.0 += base_position.0; - let ins: Vec<_> = b - .ins + let ins: Vec<_> = b.ins.unevaluated .iter() - .map(|x| canon_path(x.evaluate(&[&b.vars], scope, b.scope_position))) + .map(|x| canon_path(x.evaluate(&[&b.bindings], scope, b.scope_position))) .collect(); - let outs: Vec<_> = b - .outs + let outs: Vec<_> = b.outs.unevaluated .iter() - .map(|x| canon_path(x.evaluate(&[&b.vars], scope, b.scope_position))) + .map(|x| canon_path(x.evaluate(&[&b.bindings], scope, b.scope_position))) .collect(); - let rule = match scope.get_rule(b.rule, b.scope_position) { + let rule = match scope.get_rule(&b.rule, b.scope_position) { Some(r) => r, None => bail!("unknown rule {:?}", b.rule), }; let implicit_vars = BuildImplicitVars { - explicit_ins: &ins[..b.explicit_ins], - explicit_outs: &outs[..b.explicit_outs], + explicit_ins: &ins[..b.ins.explicit], + explicit_outs: &outs[..b.outs.explicit], }; // temp variable in order to not move all of b into the closure - let build_vars = &b.vars; + let build_vars = &b.bindings; let lookup = |key: &str| -> Option { // Look up `key = ...` binding in build and rule block. Some(match rule.vars.get(key) { @@ -179,49 +176,21 @@ fn add_build<'text>( _ => bail!("rspfile and rspfile_content need to be both specified"), }; - let build_id = files.create_build_id(); + b.ins.ids = ins.into_iter() + .map(|x| files.id_from_canonical(x)) + .collect(); + b.outs.ids = outs.into_iter() + .map(|x| files.id_from_canonical(x)) + .collect(); - let ins = graph::BuildIns { - ids: ins - .into_iter() - .map(|x| { - let f = files.id_from_canonical(x); - f.dependents.prepend(build_id); - f - }) - .collect(), - explicit: b.explicit_ins, - implicit: b.implicit_ins, - order_only: b.order_only_ins, - // validation is implied by the other counts - }; - let outs = graph::BuildOuts { - ids: outs - .into_iter() - .map(|x| files.id_from_canonical(x)) - .collect(), - explicit: b.explicit_outs, - }; - let mut build = graph::Build::new( - build_id, - graph::FileLoc { - filename: filename.clone(), - line: b.line, - }, - ins, - outs, - ); - - build.cmdline = cmdline; - build.desc = desc; - build.depfile = depfile; - build.parse_showincludes = parse_showincludes; - build.rspfile = rspfile; - build.pool = pool; - - graph::Graph::initialize_build(&mut build)?; - - Ok(build) + b.cmdline = cmdline; + b.desc = desc; + b.depfile = depfile; + b.parse_showincludes = parse_showincludes; + b.rspfile = rspfile; + b.pool = pool; + + Ok(()) } struct Files { @@ -263,7 +232,7 @@ impl Files { #[derive(Default)] struct SubninjaResults<'text> { - pub builds: Vec, + pub builds: Vec>, defaults: Vec>, builddir: Option, pools: SmallMap<&'text str, usize>, @@ -293,11 +262,13 @@ where }, ); } + let filename = Arc::new(path); let mut parse_results = trace::scope("parse", || { parse( + &filename, num_threads, file_pool, - file_pool.read_file(&path)?, + file_pool.read_file(&filename)?, &mut scope, // to account for the phony rule if top_level_scope { ScopePosition(1) } else { ScopePosition(0) }, @@ -331,7 +302,6 @@ where }) .collect::>>()?; - let filename = Arc::new(path); let mut results = SubninjaResults::default(); for clump in &parse_results { @@ -347,17 +317,22 @@ where } } - results.builds = trace::scope("add builds", || { + results.builds = Vec::new(); + results.builds.push(trace::scope("add builds", || { parse_results .par_iter_mut() .flat_map(|x| { let num_builds = x.builds.len(); - x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) + std::mem::take(&mut x.builds).into_par_iter().zip(rayon::iter::repeatn(x.base_position, num_builds)) + }) + .map(|(mut build, base_position)| -> anyhow::Result { + add_build(files, &scope, &mut build, base_position)?; + Ok(build) }) - .map(|(build, base_position)| add_build(files, &filename, &scope, build, base_position)) .collect::>>() - })?; - + })?); + trace::write_instant("Right after add_builds"); + // Only the builddir in the outermost scope is respected if top_level_scope { let mut build_dir = String::new(); @@ -367,24 +342,31 @@ where } } - for sn in &mut subninja_results { - results.builds.append(&mut sn.builds); - } - results.defaults.par_extend( - subninja_results - .par_iter_mut() - .flat_map(|x| std::mem::take(&mut x.defaults)), - ); - for new_results in subninja_results { - for (name, depth) in new_results.pools.into_iter() { - add_pool(&mut results.pools, name, depth)?; + trace::scope("Extend subninja results", || -> anyhow::Result<()> { + results.builds.par_extend( + subninja_results + .par_iter_mut() + .flat_map(|x| std::mem::take(&mut x.builds)), + ); + results.defaults.par_extend( + subninja_results + .par_iter_mut() + .flat_map(|x| std::mem::take(&mut x.defaults)), + ); + for new_results in subninja_results { + for (name, depth) in new_results.pools.into_iter() { + add_pool(&mut results.pools, name, depth)?; + } } - } + Ok(()) + })?; + trace::write_instant("End of subninja"); Ok(results) } fn include<'thread, 'text>( + filename: &Arc, num_threads: usize, file_pool: &'text FilePool, path: String, @@ -397,6 +379,7 @@ where { let path = PathBuf::from(path); parse( + filename, num_threads, file_pool, file_pool.read_file(&path)?, @@ -419,6 +402,7 @@ fn add_pool<'text>( } fn parse<'thread, 'text>( + filename: &Arc, num_threads: usize, file_pool: &'text FilePool, bytes: &'text [u8], @@ -434,7 +418,7 @@ where let statements: ParseResult>> = chunks .into_par_iter() .map(|chunk| { - let mut parser = parse::Parser::new(chunk); + let mut parser = parse::Parser::new(chunk, filename.clone()); parser.read_clumps() }).collect(); @@ -468,7 +452,7 @@ where ClumpOrInclude::Include(i) => { trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position)); - let mut new_results = include(num_threads, file_pool, evaluated, scope, clump_base_position, executor)?; + let mut new_results = include(filename, num_threads, file_pool, evaluated, scope, clump_base_position, executor)?; clump_base_position.0 += new_results.iter().map(|c| c.used_scope_positions).sum::(); // Things will be out of order here, but we don't care about // order for builds, defaults, subninjas, or pools, as long @@ -479,60 +463,6 @@ where })?; }, } - // match stmt { - // stmt @ Statement::VariableAssignment(_) => { - // let Statement::VariableAssignment(mut variable_assignment) = std::mem::replace(stmt, Statement::EmptyStatement) else { - // unreachable!(); - // }; - // variable_assignment.scope_position = scope.get_and_inc_scope_position(); - // match scope.variables.entry(variable_assignment.name) { - // Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), - // Entry::Vacant(e) => { - // e.insert(vec![variable_assignment]); - // } - // } - // } - // Statement::Include(ref i) => trace::scope("include", || -> anyhow::Result<()> { - // let evaluated = canon_path(i.file.evaluate(&[], &scope, i.scope_position)); - // let new_results = include(num_threads, file_pool, evaluated, scope, executor)?; - // // Things will be out of order here, but we don't care about - // // order for builds, defaults, subninjas, or pools, as long - // // as their scope_position is correct. - // results.merge(new_results)?; - // Ok(()) - // })?, - // stmt @ Statement::Subninja(_) => trace::scope("subninja", || { - // let Statement::Subninja(mut subninja) = std::mem::replace(stmt, Statement::EmptyStatement) else { - // unreachable!(); - // }; - // subninja.scope_position = scope.get_and_inc_scope_position(); - // results.subninjas.push(subninja); - // }), - // stmt @ Statement::Default(_) => { - // let Statement::Default(mut default) = std::mem::replace(stmt, Statement::EmptyStatement) else { - // unreachable!(); - // }; - // default.scope_position = scope.get_and_inc_scope_position(); - // results.defaults.push(default); - // } - // stmt @ Statement::Rule(_) => { - // let Statement::Rule(mut rule) = std::mem::replace(stmt, Statement::EmptyStatement) else { - // unreachable!(); - // }; - // rule.scope_position = scope.get_and_inc_scope_position(); - // match scope.rules.entry(rule.name) { - // Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), - // Entry::Vacant(e) => e.insert(rule), - // }; - // } - // Statement::Build(ref mut build) => { - // build.scope_position = scope.get_and_inc_scope_position(); - // } - // Statement::Pool(pool) => { - // add_pool(&mut results.pools, pool.name, pool.depth)?; - // } - // Statement::EmptyStatement => {}, - // }; } trace::write_complete("parse loop", start, Instant::now()); @@ -557,12 +487,12 @@ pub fn read(build_filename: &str) -> anyhow::Result { let pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build()?; - let SubninjaResults { - builds, + let (SubninjaResults { defaults, builddir, pools, - } = trace::scope("loader.read_file", || -> anyhow::Result { + .. + }, builds) = trace::scope("loader.read_file", || -> anyhow::Result<(SubninjaResults, Vec)> { pool.scope(|executor: &rayon::Scope| { let mut results = subninja( num_threads, @@ -572,13 +502,21 @@ pub fn read(build_filename: &str) -> anyhow::Result { None, executor, )?; - trace::scope("sort builds", || { - results.builds.par_sort_unstable_by_key(|b| b.id.index()) - }); - Ok(results) + trace::scope("initialize builds", || { + let mut builds = Vec::with_capacity(results.builds.iter().map(|x| x.len()).sum()); + for build_vec in &mut results.builds { + builds.append(build_vec); + } + builds.par_iter_mut().enumerate().try_for_each(|(id, build)| { + build.id = BuildId::from(id); + graph::Graph::initialize_build(build) + })?; + Ok((results, builds)) + }) }) })?; drop(pool); + let mut graph = trace::scope("loader.from_uninitialized_builds_and_files", || { Graph::from_uninitialized_builds_and_files(builds, files.into_maps()) })?; diff --git a/src/parse.rs b/src/parse.rs index 4deb57a..43173d8 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,15 +6,12 @@ //! text, marked with the lifetime `'text`. use crate::{ - eval::{EvalPart, EvalString}, - load::{Scope, ScopePosition}, - scanner::{ParseError, ParseResult, Scanner}, - smallmap::SmallMap, + eval::{EvalPart, EvalString}, graph::{Build, BuildId, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseError, ParseResult, Scanner}, smallmap::SmallMap }; use std::{ cell::UnsafeCell, - path::Path, - sync::{atomic::AtomicBool, Mutex}, + path::{Path, PathBuf}, + sync::{atomic::AtomicBool, Arc, Mutex}, }; /// A list of variable bindings, as expressed with syntax like: @@ -28,20 +25,20 @@ pub struct Rule<'text> { pub scope_position: ScopePosition, } -#[derive(Debug, PartialEq)] -pub struct Build<'text> { - pub rule: &'text str, - pub line: usize, - pub outs: Vec>, - pub explicit_outs: usize, - pub ins: Vec>, - pub explicit_ins: usize, - pub implicit_ins: usize, - pub order_only_ins: usize, - pub validation_ins: usize, - pub vars: VarList<'text>, - pub scope_position: ScopePosition, -} +// #[derive(Debug, PartialEq)] +// pub struct Build<'text> { +// pub rule: &'text str, +// pub line: usize, +// pub outs: Vec>, +// pub explicit_outs: usize, +// pub ins: Vec>, +// pub explicit_ins: usize, +// pub implicit_ins: usize, +// pub order_only_ins: usize, +// pub validation_ins: usize, +// pub vars: VarList<'text>, +// pub scope_position: ScopePosition, +// } #[derive(Debug, PartialEq)] pub struct Pool<'text> { @@ -112,7 +109,7 @@ pub struct IncludeOrSubninja<'text> { pub enum Statement<'text> { EmptyStatement, Rule(Rule<'text>), - Build(Build<'text>), + Build(Build), Default(DefaultStmt<'text>), Include(IncludeOrSubninja<'text>), Subninja(IncludeOrSubninja<'text>), @@ -126,7 +123,7 @@ pub struct Clump<'text> { pub rules: Vec>, pub pools: Vec>, pub defaults: Vec>, - pub builds: Vec>, + pub builds: Vec, pub subninjas: Vec>, pub used_scope_positions: usize, pub base_position: ScopePosition, @@ -150,6 +147,7 @@ pub enum ClumpOrInclude<'text> { } pub struct Parser<'text> { + filename: Arc, scanner: Scanner<'text>, buf_len: usize, /// Reading EvalStrings is very hot when parsing, so we always read into @@ -158,8 +156,9 @@ pub struct Parser<'text> { } impl<'text> Parser<'text> { - pub fn new(buf: &'text [u8]) -> Parser<'text> { + pub fn new(buf: &'text [u8], filename: Arc) -> Parser<'text> { Parser { + filename, scanner: Scanner::new(buf), buf_len: buf.len(), eval_buf: Vec::with_capacity(16), @@ -383,15 +382,30 @@ impl<'text> Parser<'text> { Ok(()) } - fn read_build(&mut self) -> ParseResult> { + fn read_owned_unevaluated_paths_to( + &mut self, + v: &mut Vec>, + ) -> ParseResult<()> { + self.skip_spaces(); + while self.scanner.peek() != ':' + && self.scanner.peek() != '|' + && !self.scanner.peek_newline() + { + v.push(self.read_eval(true)?.into_owned()); + self.skip_spaces(); + } + Ok(()) + } + + fn read_build(&mut self) -> ParseResult { let line = self.scanner.line; let mut outs = Vec::new(); - self.read_unevaluated_paths_to(&mut outs)?; + self.read_owned_unevaluated_paths_to(&mut outs)?; let explicit_outs = outs.len(); if self.scanner.peek() == '|' { self.scanner.next(); - self.read_unevaluated_paths_to(&mut outs)?; + self.read_owned_unevaluated_paths_to(&mut outs)?; } self.scanner.expect(':')?; @@ -399,7 +413,7 @@ impl<'text> Parser<'text> { let rule = self.read_ident()?; let mut ins = Vec::new(); - self.read_unevaluated_paths_to(&mut ins)?; + self.read_owned_unevaluated_paths_to(&mut ins)?; let explicit_ins = ins.len(); if self.scanner.peek() == '|' { @@ -408,7 +422,7 @@ impl<'text> Parser<'text> { if peek == '|' || peek == '@' { self.scanner.back(); } else { - self.read_unevaluated_paths_to(&mut ins)?; + self.read_owned_unevaluated_paths_to(&mut ins)?; } } let implicit_ins = ins.len() - explicit_ins; @@ -419,7 +433,7 @@ impl<'text> Parser<'text> { self.scanner.back(); } else { self.scanner.expect('|')?; - self.read_unevaluated_paths_to(&mut ins)?; + self.read_owned_unevaluated_paths_to(&mut ins)?; } } let order_only_ins = ins.len() - implicit_ins - explicit_ins; @@ -427,7 +441,7 @@ impl<'text> Parser<'text> { if self.scanner.peek() == '|' { self.scanner.next(); self.scanner.expect('@')?; - self.read_unevaluated_paths_to(&mut ins)?; + self.read_owned_unevaluated_paths_to(&mut ins)?; } let validation_ins = ins.len() - order_only_ins - implicit_ins - explicit_ins; @@ -436,17 +450,33 @@ impl<'text> Parser<'text> { let vars = self.read_scoped_vars(|_| true)?; Ok(Build { - rule, - line, - outs, - explicit_outs, - ins, - explicit_ins, - implicit_ins, - order_only_ins, - validation_ins, - vars, + id: BuildId::from(0), + rule: rule.to_owned(), scope_position: ScopePosition(0), + bindings: vars.to_owned(), + location: FileLoc { + filename: self.filename.clone(), + line, + }, + desc: None, + cmdline: None, + depfile: None, + parse_showincludes: false, + rspfile: None, + pool: None, + ins: BuildIns { + ids: Vec::new(), + unevaluated: ins, + explicit: explicit_ins, + implicit: implicit_ins, + order_only: order_only_ins, + }, + discovered_ins: Vec::new(), + outs: BuildOuts { + ids: Vec::new(), + unevaluated: outs, + explicit: explicit_outs, + }, }) } @@ -701,7 +731,7 @@ mod tests { fn parse_defaults() { test_for_line_endings(&["var = 3", "default a b$var c", ""], |test_case| { let mut buf = test_case_buffer(test_case); - let mut parser = Parser::new(&mut buf); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); match parser.read().unwrap().unwrap() { Statement::VariableAssignment(_) => {} stmt => panic!("expected variable assignment, got {:?}", stmt), @@ -724,7 +754,7 @@ mod tests { #[test] fn parse_dot_in_eval() { let mut buf = test_case_buffer("x = $y.z\n"); - let mut parser = Parser::new(&mut buf); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); let Ok(Some(Statement::VariableAssignment(x))) = parser.read() else { panic!("Fail"); }; @@ -738,7 +768,7 @@ mod tests { #[test] fn parse_dot_in_rule() { let mut buf = test_case_buffer("rule x.y\n command = x\n"); - let mut parser = Parser::new(&mut buf); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); let Ok(Some(Statement::Rule(stmt))) = parser.read() else { panic!("Fail"); }; @@ -753,11 +783,12 @@ mod tests { #[test] fn parse_trailing_newline() { let mut buf = test_case_buffer("build$\n foo$\n : $\n touch $\n\n"); - let mut parser = Parser::new(&mut buf); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); let stmt = parser.read().unwrap().unwrap(); + let rule = "touch".to_owned(); assert!(matches!( stmt, - Statement::Build(Build { rule: "touch", .. }) + Statement::Build(Build { rule, .. }) )); } } diff --git a/src/smallmap.rs b/src/smallmap.rs index 3b358ac..76d4376 100644 --- a/src/smallmap.rs +++ b/src/smallmap.rs @@ -4,6 +4,8 @@ use std::{borrow::Borrow, fmt::Debug}; +use crate::eval::EvalString; + /// A map-like object implemented as a list of pairs, for cases where the /// number of entries in the map is small. #[derive(Debug)] @@ -95,3 +97,13 @@ impl PartialEq for SmallMap { self.0 == other.0 } } + +impl SmallMap<&str, EvalString<&str>> { + pub fn to_owned(self) -> SmallMap> { + let mut result = SmallMap::default(); + for (k, v) in self.into_iter() { + result.insert(k.to_owned(), v.into_owned()); + } + result + } +} diff --git a/src/trace.rs b/src/trace.rs index a43f5c5..ffc357d 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -58,6 +58,7 @@ impl Trace { /* These functions were useful when developing, but are currently unused. + */ pub fn write_instant(&mut self, name: &str) { self.write_event_prefix(name, Instant::now()); @@ -79,7 +80,7 @@ impl Trace { } writeln!(self.w, "}}}}").unwrap(); } - */ + fn close(&mut self) { self.write_complete("main", 0, self.start, Instant::now()); @@ -119,6 +120,17 @@ pub fn scope(name: &'static str, f: impl FnOnce() -> T) -> T { } } +#[inline] +pub fn write_instant(name: &'static str) { + // Safety: accessing global mut, not threadsafe. + unsafe { + match &mut TRACE { + None => (), + Some(t) => t.write_instant(name), + } + } +} + #[inline] pub fn write_complete(name: &'static str, start: Instant, end: Instant) { From 127cb25c4ce6b6b5d3bf3cb5464b1fd377b2b724 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 21 Feb 2024 15:53:39 -0800 Subject: [PATCH 09/29] Standardize on clumps --- src/load.rs | 151 +++++++++++++++++++++++---------------------------- src/parse.rs | 6 +- 2 files changed, 73 insertions(+), 84 deletions(-) diff --git a/src/load.rs b/src/load.rs index d39f93d..a88aa8d 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,7 +1,7 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace + canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, Clump, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; @@ -232,10 +232,8 @@ impl Files { #[derive(Default)] struct SubninjaResults<'text> { - pub builds: Vec>, - defaults: Vec>, + clumps: Vec>, builddir: Option, - pools: SmallMap<&'text str, usize>, } fn subninja<'thread, 'text>( @@ -276,93 +274,65 @@ where ) })?; + let scope = Arc::new(scope); + for clump in &mut parse_results { - for mut rule in std::mem::take(&mut clump.rules).into_iter() { - rule.scope_position = rule.scope_position.add(clump.base_position); - match scope.rules.entry(rule.name) { - Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), - Entry::Vacant(e) => e.insert(rule), - }; + let base_position = clump.base_position; + for default in clump.defaults.iter_mut() { + let scope = scope.clone(); + default.evaluated = default.files.iter().map(|x| { + let path = canon_path(x.evaluate(&[], &scope, default.scope_position.add(base_position))); + files.id_from_canonical(path) + }).collect(); } } - let scope = Arc::new(scope); + parse_results + .par_iter_mut() + .flat_map(|x| { + let num_builds = x.builds.len(); + x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) + }) + .try_for_each(|(mut build, base_position)| -> anyhow::Result<()> { + add_build(files, &scope, &mut build, base_position) + })?; + let mut subninja_results = parse_results.par_iter() .flat_map(|x| x.subninjas.par_iter().zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))) - .map(|(sn, base_position)| { + .map(|(sn, base_position)| -> anyhow::Result> { let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position.add(base_position))); - subninja( + Ok(subninja( num_threads, files, file_pool, file, Some(ParentScopeReference(scope.clone(), sn.scope_position)), executor, - ) + )?.clumps) }) - .collect::>>()?; + .collect::>>>>()?; - let mut results = SubninjaResults::default(); - - for clump in &parse_results { - for pool in &clump.pools { - add_pool(&mut results.pools, pool.name, pool.depth)?; - } - for default in clump.defaults.iter() { - let scope = scope.clone(); - results.defaults.extend(default.files.iter().map(|x| { - let path = canon_path(x.evaluate(&[], &scope, default.scope_position.add(clump.base_position))); - files.id_from_canonical(path) - })); - } + for subninja_result in &mut subninja_results { + parse_results.append(subninja_result); } - - results.builds = Vec::new(); - results.builds.push(trace::scope("add builds", || { - parse_results - .par_iter_mut() - .flat_map(|x| { - let num_builds = x.builds.len(); - std::mem::take(&mut x.builds).into_par_iter().zip(rayon::iter::repeatn(x.base_position, num_builds)) - }) - .map(|(mut build, base_position)| -> anyhow::Result { - add_build(files, &scope, &mut build, base_position)?; - Ok(build) - }) - .collect::>>() - })?); - trace::write_instant("Right after add_builds"); // Only the builddir in the outermost scope is respected - if top_level_scope { + let build_dir = if top_level_scope { let mut build_dir = String::new(); - scope.evaluate(&mut build_dir, "builddir", ScopePosition(parse_results.iter().map(|x| x.used_scope_positions).sum::())); + scope.evaluate(&mut build_dir, "builddir", ScopePosition(usize::MAX)); if !build_dir.is_empty() { - results.builddir = Some(build_dir); + Some(build_dir) + } else { + None } - } - - trace::scope("Extend subninja results", || -> anyhow::Result<()> { - results.builds.par_extend( - subninja_results - .par_iter_mut() - .flat_map(|x| std::mem::take(&mut x.builds)), - ); - results.defaults.par_extend( - subninja_results - .par_iter_mut() - .flat_map(|x| std::mem::take(&mut x.defaults)), - ); - for new_results in subninja_results { - for (name, depth) in new_results.pools.into_iter() { - add_pool(&mut results.pools, name, depth)?; - } - } - Ok(()) - })?; + } else { + None + }; - trace::write_instant("End of subninja"); - Ok(results) + Ok(SubninjaResults { + clumps: parse_results, + builddir: build_dir, + }) } fn include<'thread, 'text>( @@ -422,7 +392,7 @@ where parser.read_clumps() }).collect(); - let Ok(mut statements) = statements else { + let Ok(statements) = statements else { // TODO: Call format_parse_error bail!(statements.unwrap_err().msg); }; @@ -434,8 +404,8 @@ where match stmt { ClumpOrInclude::Clump(mut clump) => { // Variable assignemnts must be added to the scope now, because - // they may be referenced by a later include. Everything else - // can be handled after all parsing is done. + // they may be referenced by a later include. Also add rules + // while we're at it, to avoid some copies later on. for mut variable_assignment in std::mem::take(&mut clump.assignments).into_iter() { variable_assignment.scope_position.0 += clump_base_position.0; match scope.variables.entry(variable_assignment.name) { @@ -445,6 +415,15 @@ where } } } + for mut rule in std::mem::take(&mut clump.rules).into_iter() { + rule.scope_position.0 += clump_base_position.0; + match scope.rules.entry(rule.name) { + Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + Entry::Vacant(e) => { + e.insert(rule); + }, + } + } clump.base_position = clump_base_position; clump_base_position.0 += clump.used_scope_positions; results.push(clump); @@ -487,12 +466,7 @@ pub fn read(build_filename: &str) -> anyhow::Result { let pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build()?; - let (SubninjaResults { - defaults, - builddir, - pools, - .. - }, builds) = trace::scope("loader.read_file", || -> anyhow::Result<(SubninjaResults, Vec)> { + let (defaults, builddir, pools, builds) = trace::scope("loader.read_file", || -> anyhow::Result<_> { pool.scope(|executor: &rayon::Scope| { let mut results = subninja( num_threads, @@ -502,16 +476,29 @@ pub fn read(build_filename: &str) -> anyhow::Result { None, executor, )?; + + let mut pools = SmallMap::default(); + let mut defaults = Vec::new(); + let mut num_builds = 0; + for clump in &mut results.clumps { + for pool in &clump.pools { + add_pool(&mut pools, pool.name, pool.depth)?; + } + for default in &mut clump.defaults { + defaults.append(&mut default.evaluated); + } + num_builds += clump.builds.len(); + } trace::scope("initialize builds", || { - let mut builds = Vec::with_capacity(results.builds.iter().map(|x| x.len()).sum()); - for build_vec in &mut results.builds { - builds.append(build_vec); + let mut builds = Vec::with_capacity(num_builds); + for clump in &mut results.clumps { + builds.append(&mut clump.builds); } builds.par_iter_mut().enumerate().try_for_each(|(id, build)| { build.id = BuildId::from(id); graph::Graph::initialize_build(build) })?; - Ok((results, builds)) + Ok((defaults, results.builddir, pools, builds)) }) }) })?; diff --git a/src/parse.rs b/src/parse.rs index 43173d8..5e7f3d8 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,7 +6,7 @@ //! text, marked with the lifetime `'text`. use crate::{ - eval::{EvalPart, EvalString}, graph::{Build, BuildId, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseError, ParseResult, Scanner}, smallmap::SmallMap + eval::{EvalPart, EvalString}, graph::{self, Build, BuildId, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseError, ParseResult, Scanner}, smallmap::SmallMap }; use std::{ cell::UnsafeCell, @@ -93,9 +93,10 @@ impl<'text> VariableAssignment<'text> { } } -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct DefaultStmt<'text> { pub files: Vec>, + pub evaluated: Vec>, pub scope_position: ScopePosition, } @@ -490,6 +491,7 @@ impl<'text> Parser<'text> { self.scanner.expect('\n')?; Ok(DefaultStmt { files, + evaluated: Vec::new(), scope_position: ScopePosition(0), }) } From fbfe7ee24d725619e756f3991f41068b174b420a Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 21 Feb 2024 16:54:06 -0800 Subject: [PATCH 10/29] Reserve some vector sizes and add traces --- src/load.rs | 143 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 82 insertions(+), 61 deletions(-) diff --git a/src/load.rs b/src/load.rs index a88aa8d..7f2dcd4 100644 --- a/src/load.rs +++ b/src/load.rs @@ -195,13 +195,11 @@ fn add_build<'text>( struct Files { by_name: dashmap::DashMap, Arc>, - next_build_id: AtomicUsize, } impl Files { pub fn new() -> Self { Self { by_name: dashmap::DashMap::new(), - next_build_id: AtomicUsize::new(0), } } @@ -221,13 +219,6 @@ impl Files { pub fn into_maps(self) -> dashmap::DashMap, Arc> { self.by_name } - - pub fn create_build_id(&self) -> BuildId { - let id = self - .next_build_id - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); - BuildId::from(id) - } } #[derive(Default)] @@ -287,15 +278,17 @@ where } } - parse_results - .par_iter_mut() - .flat_map(|x| { - let num_builds = x.builds.len(); - x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) - }) - .try_for_each(|(mut build, base_position)| -> anyhow::Result<()> { - add_build(files, &scope, &mut build, base_position) - })?; + trace::scope("add builds", || -> anyhow::Result<()> { + parse_results + .par_iter_mut() + .flat_map(|x| { + let num_builds = x.builds.len(); + x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) + }) + .try_for_each(|(mut build, base_position)| -> anyhow::Result<()> { + add_build(files, &scope, &mut build, base_position) + }) + })?; let mut subninja_results = parse_results.par_iter() .flat_map(|x| x.subninjas.par_iter().zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))) @@ -329,6 +322,8 @@ where None }; + //std::mem::forget(scope); + Ok(SubninjaResults { clumps: parse_results, builddir: build_dir, @@ -360,11 +355,11 @@ where } fn add_pool<'text>( - pools: &mut SmallMap<&'text str, usize>, - name: &'text str, + pools: &mut SmallMap, + name: String, depth: usize, ) -> anyhow::Result<()> { - if let Some(_) = pools.get(name) { + if let Some(_) = pools.get(&name) { bail!("duplicate pool {}", name); } pools.insert(name, depth); @@ -397,33 +392,61 @@ where bail!(statements.unwrap_err().msg); }; - let mut results = Vec::new(); - let start = Instant::now(); + + let mut num_rules = 0; + let mut num_variables = 0; + let mut num_clumps = 0; + for clumps in &statements { + num_clumps += clumps.len(); + for clump_or_include in clumps { + if let ClumpOrInclude::Clump(clump) = clump_or_include { + num_rules += clump.rules.len(); + num_variables += clump.assignments.len(); + } + } + } + + scope.rules.reserve(num_rules); + scope.variables.reserve(num_variables); + + let mut results = Vec::with_capacity(num_clumps); + for stmt in statements.into_iter().flatten() { match stmt { ClumpOrInclude::Clump(mut clump) => { // Variable assignemnts must be added to the scope now, because // they may be referenced by a later include. Also add rules // while we're at it, to avoid some copies later on. - for mut variable_assignment in std::mem::take(&mut clump.assignments).into_iter() { - variable_assignment.scope_position.0 += clump_base_position.0; - match scope.variables.entry(variable_assignment.name) { - Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), - Entry::Vacant(e) => { - e.insert(vec![variable_assignment]); + let rules = std::mem::take(&mut clump.rules); + let assignments = std::mem::take(&mut clump.assignments); + let scope_rules = &mut scope.rules; + let scope_variables = &mut scope.variables; + rayon::join( + || { + for mut variable_assignment in assignments.into_iter() { + variable_assignment.scope_position.0 += clump_base_position.0; + match scope_variables.entry(variable_assignment.name) { + Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), + Entry::Vacant(e) => { + e.insert(vec![variable_assignment]); + } + } } + }, + || -> anyhow::Result<()> { + for mut rule in rules.into_iter() { + rule.scope_position.0 += clump_base_position.0; + match scope_rules.entry(rule.name) { + Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + Entry::Vacant(e) => { + e.insert(rule); + }, + } + } + Ok(()) } - } - for mut rule in std::mem::take(&mut clump.rules).into_iter() { - rule.scope_position.0 += clump_base_position.0; - match scope.rules.entry(rule.name) { - Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), - Entry::Vacant(e) => { - e.insert(rule); - }, - } - } + ).1?; clump.base_position = clump_base_position; clump_base_position.0 += clump.used_scope_positions; results.push(clump); @@ -480,20 +503,26 @@ pub fn read(build_filename: &str) -> anyhow::Result { let mut pools = SmallMap::default(); let mut defaults = Vec::new(); let mut num_builds = 0; - for clump in &mut results.clumps { - for pool in &clump.pools { - add_pool(&mut pools, pool.name, pool.depth)?; - } - for default in &mut clump.defaults { - defaults.append(&mut default.evaluated); - } - num_builds += clump.builds.len(); - } - trace::scope("initialize builds", || { - let mut builds = Vec::with_capacity(num_builds); + trace::scope("add pools and defaults", || -> anyhow::Result<()> { for clump in &mut results.clumps { - builds.append(&mut clump.builds); + for pool in &clump.pools { + add_pool(&mut pools, pool.name.to_owned(), pool.depth)?; + } + for default in &mut clump.defaults { + defaults.append(&mut default.evaluated); + } + num_builds += clump.builds.len(); } + Ok(()) + })?; + trace::scope("initialize builds", || { + let mut builds = trace::scope("allocate and concat builds", || { + let mut builds = Vec::with_capacity(num_builds); + for clump in &mut results.clumps { + builds.append(&mut clump.builds); + } + builds + }); builds.par_iter_mut().enumerate().try_for_each(|(id, build)| { build.id = BuildId::from(id); graph::Graph::initialize_build(build) @@ -502,11 +531,8 @@ pub fn read(build_filename: &str) -> anyhow::Result { }) }) })?; - drop(pool); - let mut graph = trace::scope("loader.from_uninitialized_builds_and_files", || { - Graph::from_uninitialized_builds_and_files(builds, files.into_maps()) - })?; + let mut graph = Graph::from_uninitialized_builds_and_files(builds, files.into_maps())?; let mut hashes = graph::Hashes::default(); let db = trace::scope("db::open", || { let mut db_path = PathBuf::from(".n2_db"); @@ -520,16 +546,11 @@ pub fn read(build_filename: &str) -> anyhow::Result { }) .map_err(|err| anyhow!("load .n2_db: {}", err))?; - let mut owned_pools = SmallMap::with_capacity(pools.len()); - for pool in pools.iter() { - owned_pools.insert(pool.0.to_owned(), pool.1); - } - Ok(State { graph, db, hashes, default: defaults, - pools: owned_pools, + pools, }) } From 1e994ee35ce43d23dc679ab5d46597f3a378bc09 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 21 Feb 2024 17:38:49 -0800 Subject: [PATCH 11/29] Fix bug with evaluating variables from parent scope --- src/load.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/load.rs b/src/load.rs index 7f2dcd4..fd438b4 100644 --- a/src/load.rs +++ b/src/load.rs @@ -113,7 +113,7 @@ impl<'text> Scope<'text> { // position, so check the parent scope if there is one. } if let Some(parent) = &self.parent { - parent.0.evaluate(result, varname, position); + parent.0.evaluate(result, varname, parent.1); } } } From 488957657de5af5d31b13982a12aa47f620c5d24 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 21 Feb 2024 17:47:56 -0800 Subject: [PATCH 12/29] Box Builds This saves ~0.3 seconds of merging the vecs of builds. --- src/graph.rs | 4 ++-- src/parse.rs | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 68bde4b..b9092b2 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -302,7 +302,7 @@ impl Build { /// The build graph: owns Files/Builds and maps FileIds/BuildIds to them. #[derive(Default)] pub struct Graph { - pub builds: DenseMap, + pub builds: DenseMap>, pub files: GraphFiles, } @@ -315,7 +315,7 @@ pub struct GraphFiles { impl Graph { pub fn from_uninitialized_builds_and_files( - builds: Vec, + builds: Vec>, files: dashmap::DashMap, Arc>, ) -> anyhow::Result { let result = Graph { diff --git a/src/parse.rs b/src/parse.rs index 5e7f3d8..c07d255 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -110,7 +110,7 @@ pub struct IncludeOrSubninja<'text> { pub enum Statement<'text> { EmptyStatement, Rule(Rule<'text>), - Build(Build), + Build(Box), Default(DefaultStmt<'text>), Include(IncludeOrSubninja<'text>), Subninja(IncludeOrSubninja<'text>), @@ -124,7 +124,7 @@ pub struct Clump<'text> { pub rules: Vec>, pub pools: Vec>, pub defaults: Vec>, - pub builds: Vec, + pub builds: Vec>, pub subninjas: Vec>, pub used_scope_positions: usize, pub base_position: ScopePosition, @@ -255,7 +255,7 @@ impl<'text> Parser<'text> { self.skip_spaces(); match ident { "rule" => return Ok(Some(Statement::Rule(self.read_rule()?))), - "build" => return Ok(Some(Statement::Build(self.read_build()?))), + "build" => return Ok(Some(Statement::Build(Box::new(self.read_build()?)))), "default" => return Ok(Some(Statement::Default(self.read_default()?))), "include" => { let result = IncludeOrSubninja { @@ -787,10 +787,9 @@ mod tests { let mut buf = test_case_buffer("build$\n foo$\n : $\n touch $\n\n"); let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); let stmt = parser.read().unwrap().unwrap(); - let rule = "touch".to_owned(); - assert!(matches!( - stmt, - Statement::Build(Build { rule, .. }) - )); + let Statement::Build(stmt) = stmt else { + panic!("Wasn't a build"); + }; + assert_eq!(stmt.rule, "touch"); } } From b1213e68c980ee95571b4883774de5ee34a73f03 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 22 Feb 2024 11:17:13 -0800 Subject: [PATCH 13/29] Switch to FxHashMap and deallocate scopes in parallel --- src/load.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/load.rs b/src/load.rs index fd438b4..5a3c635 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,22 +1,21 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, densemap::Index, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, stat, BuildId, FileId, Graph, RspFile}, parse::{self, Clump, ClumpOrInclude, DefaultStmt, IncludeOrSubninja, Rule, Statement, VariableAssignment}, scanner::{self, ParseResult}, smallmap::SmallMap, trace + canon::canon_path, db, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, BuildId, Graph, RspFile}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::ParseResult, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; use rustc_hash::FxHashMap; use std::{ - borrow::Cow, default, path::Path, sync::{atomic::AtomicUsize, mpsc::TryRecvError}, time::Instant + borrow::Cow, path::Path, time::Instant }; use std::{ - cell::UnsafeCell, cmp::Ordering, - collections::{hash_map::Entry, HashMap}, - sync::{Arc, Mutex}, + collections::hash_map::Entry, + sync::Arc, thread::available_parallelism, }; -use std::{path::PathBuf, sync::atomic::AtomicU32}; +use std::path::PathBuf; /// A variable lookup environment for magic $in/$out variables. struct BuildImplicitVars<'a> { @@ -52,7 +51,7 @@ pub struct ParentScopeReference<'text>(pub Arc>, pub ScopePosition) #[derive(Debug)] pub struct Scope<'text> { parent: Option>, - rules: HashMap<&'text str, Rule<'text>>, + rules: FxHashMap<&'text str, Rule<'text>>, variables: FxHashMap<&'text str, Vec>>, next_free_position: ScopePosition, } @@ -61,7 +60,7 @@ impl<'text> Scope<'text> { pub fn new(parent: Option>) -> Self { Self { parent, - rules: HashMap::new(), + rules: FxHashMap::default(), variables: FxHashMap::default(), next_free_position: ScopePosition(0), } @@ -293,13 +292,14 @@ where let mut subninja_results = parse_results.par_iter() .flat_map(|x| x.subninjas.par_iter().zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))) .map(|(sn, base_position)| -> anyhow::Result> { - let file = canon_path(sn.file.evaluate(&[], &scope, sn.scope_position.add(base_position))); + let position = sn.scope_position.add(base_position); + let file = canon_path(sn.file.evaluate(&[], &scope, position)); Ok(subninja( num_threads, files, file_pool, file, - Some(ParentScopeReference(scope.clone(), sn.scope_position)), + Some(ParentScopeReference(scope.clone(), position)), executor, )?.clumps) }) @@ -322,7 +322,12 @@ where None }; - //std::mem::forget(scope); + // Deallocating the scope can take some time (~200ms on android's files). + // We want to do that in the background, but rayon::spawn requires static + // lifetimes. We know that dropping doesn't attempt to dereference any + // pointers to the text, so unsafely cast the scope to the static lifetime. + let scope = unsafe { std::mem::transmute::>, Arc>>(scope)}; + rayon::spawn(move || drop(scope)); Ok(SubninjaResults { clumps: parse_results, From ec3845505d7d87e8386bc35b217eecb227c78b91 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 22 Feb 2024 15:35:06 -0800 Subject: [PATCH 14/29] Defer build binding evaluation This allows us to not spend time evaluating all the build bindings, but we still need to evaluate all the global variables if we want to not keep the file in memory. --- src/db.rs | 2 +- src/file_pool.rs | 2 +- src/graph.rs | 103 +++++++++++++++++++-------------- src/hash.rs | 21 +++---- src/load.rs | 145 +++++++++++------------------------------------ src/parse.rs | 64 ++++++++++----------- src/progress.rs | 25 ++++---- src/task.rs | 11 ++-- src/trace.rs | 60 ++++++++++---------- src/work.rs | 20 ++++--- 10 files changed, 196 insertions(+), 257 deletions(-) diff --git a/src/db.rs b/src/db.rs index 3d043d8..4fd7c67 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,7 +3,7 @@ use crate::graph; use crate::{ - densemap, densemap::DenseMap, graph::BuildId, graph::FileId, graph::Graph, graph::Hashes, + densemap, densemap::DenseMap, graph::BuildId, graph::Graph, graph::Hashes, hash::BuildHash, }; use anyhow::{anyhow, bail}; diff --git a/src/file_pool.rs b/src/file_pool.rs index 0ad986c..5218bf1 100644 --- a/src/file_pool.rs +++ b/src/file_pool.rs @@ -1,7 +1,7 @@ use anyhow::bail; use core::slice; use libc::{ - c_void, mmap, munmap, strerror, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, + c_void, mmap, munmap, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, PROT_READ, PROT_WRITE, _SC_PAGESIZE, }; use std::{ diff --git a/src/graph.rs b/src/graph.rs index b9092b2..e2b80cd 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,11 +1,12 @@ //! The build graph, a graph between files and commands. +use anyhow::bail; use rustc_hash::{FxHashMap, FxHasher}; use crate::{ - concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::EvalString, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap, trace + concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::{EvalPart, EvalString}, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap }; -use std::time::SystemTime; +use std::{borrow::Cow, time::SystemTime}; use std::{collections::HashMap, sync::Arc}; use std::{ hash::BuildHasherDefault, @@ -13,25 +14,6 @@ use std::{ sync::Mutex, }; -/// Id for File nodes in the Graph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct FileId(u32); -impl densemap::Index for FileId { - fn index(&self) -> usize { - self.0 as usize - } -} -impl From for FileId { - fn from(u: usize) -> FileId { - FileId(u as u32) - } -} -impl From for FileId { - fn from(u: u32) -> FileId { - FileId(u) - } -} - /// Id for Build nodes in the Graph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct BuildId(u32); @@ -170,11 +152,32 @@ mod tests { } } + +/// A variable lookup environment for magic $in/$out variables. +struct BuildImplicitVars<'a> { + explicit_ins: &'a [Arc], + explicit_outs: &'a [Arc], +} +impl<'text> crate::eval::Env for BuildImplicitVars<'text> { + fn get_var(&self, var: &str) -> Option>> { + let string_to_evalstring = + |s: String| Some(EvalString::new(vec![EvalPart::Literal(Cow::Owned(s))])); + match var { + "in" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join(" ")), + "in_newline" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join("\n")), + "out" => string_to_evalstring(self.explicit_outs.iter().map(|x| x.name.as_str()).collect::>().join(" ")), + "out_newline" => string_to_evalstring(self.explicit_outs.iter().map(|x| x.name.as_str()).collect::>().join("\n")), + _ => None, + } + } +} + /// A single build action, generating File outputs from File inputs with a command. #[derive(Debug)] pub struct Build { pub id: BuildId, + pub scope: Option>, pub scope_position: ScopePosition, pub rule: String, @@ -186,24 +189,6 @@ pub struct Build { /// Source location this Build was declared. pub location: FileLoc, - /// User-provided description of the build step. - pub desc: Option, - - /// Command line to run. Absent for phony builds. - pub cmdline: Option, - - /// Path to generated `.d` file, if any. - pub depfile: Option, - - /// If true, extract "/showIncludes" lines from output. - pub parse_showincludes: bool, - - // Struct that contains the path to the rsp file and its contents, if any. - pub rspfile: Option, - - /// Pool to execute this build in, if any. - pub pool: Option, - pub ins: BuildIns, /// Additional inputs discovered from a previous build. @@ -297,16 +282,52 @@ impl Build { pub fn outs(&self) -> &[Arc] { &self.outs.ids } + + pub fn get_binding(&self, key: &str) -> Option { + let implicit_vars = BuildImplicitVars { + explicit_ins: &self.ins.ids[..self.ins.explicit], + explicit_outs: &self.outs.ids[..self.outs.explicit], + }; + let scope = self.scope.as_ref().unwrap(); + let rule = scope.get_rule(&self.rule, self.scope_position).unwrap(); + Some(match rule.vars.get(key) { + Some(val) => val.evaluate(&[&implicit_vars, &self.bindings], scope, self.scope_position), + None => self.bindings.get(key)?.evaluate(&[], scope, self.scope_position), + }) + } + + pub fn get_rspfile(&self) -> anyhow::Result> { + let rspfile_path = self.get_binding("rspfile"); + let rspfile_content = self.get_binding("rspfile_content"); + let rspfile = match (rspfile_path, rspfile_content) { + (None, None) => None, + (Some(path), Some(content)) => Some(RspFile { + path: std::path::PathBuf::from(path), + content, + }), + _ => bail!("rspfile and rspfile_content need to be both specified"), + }; + Ok(rspfile) + } + + pub fn get_parse_showincludes(&self) -> anyhow::Result { + Ok(match self.get_binding("deps").as_deref() { + None => false, + Some("gcc") => false, + Some("msvc") => true, + Some(other) => bail!("invalid deps attribute {:?}", other), + }) + } } -/// The build graph: owns Files/Builds and maps FileIds/BuildIds to them. +/// The build graph: owns Files/Builds and maps BuildIds to them. #[derive(Default)] pub struct Graph { pub builds: DenseMap>, pub files: GraphFiles, } -/// Files identified by FileId, as well as mapping string filenames to them. +/// Files identified by their string names. /// Split from Graph for lifetime reasons. #[derive(Default)] pub struct GraphFiles { diff --git a/src/hash.rs b/src/hash.rs index 21dfbf2..523f5d8 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -4,7 +4,7 @@ //! See "Manifests instead of mtime order" in //! https://neugierig.org/software/blog/2022/03/n2.html -use crate::graph::{self, Build, FileId, FileState, GraphFiles, MTime, RspFile}; +use crate::graph::{self, Build, FileState, MTime, RspFile}; use std::{ collections::hash_map::DefaultHasher, fmt::Write, @@ -79,24 +79,25 @@ impl Manifest for TerseHash { } } -fn build_manifest(manifest: &mut M, file_state: &FileState, build: &Build) { +fn build_manifest(manifest: &mut M, file_state: &FileState, build: &Build) -> anyhow::Result<()> { manifest.write_files("in", file_state, build.dirtying_ins()); manifest.write_files("discovered", file_state, build.discovered_ins()); - manifest.write_cmdline(build.cmdline.as_deref().unwrap_or("")); - if let Some(rspfile) = &build.rspfile { + manifest.write_cmdline(build.get_binding("command").as_deref().unwrap_or("")); + if let Some(rspfile) = &build.get_rspfile()? { manifest.write_rsp(rspfile); } manifest.write_files("out", file_state, build.outs()); + Ok(()) } // Hashes the inputs of a build to compute a signature. // Prerequisite: all referenced files have already been stat()ed and are present. // (It doesn't make sense to hash a build with missing files, because it's out // of date regardless of the state of the other files.) -pub fn hash_build(file_state: &FileState, build: &Build) -> BuildHash { +pub fn hash_build(file_state: &FileState, build: &Build) -> anyhow::Result { let mut hasher = TerseHash::default(); - build_manifest(&mut hasher, file_state, build); - hasher.finish() + build_manifest(&mut hasher, file_state, build)?; + Ok(hasher.finish()) } /// A BuildHasher that records human-readable text for "-d explain" debugging. @@ -133,8 +134,8 @@ impl Manifest for ExplainHash { /// Logs human-readable state of all the inputs used for hashing a given build. /// Used for "-d explain" debugging output. -pub fn explain_hash_build(file_state: &FileState, build: &Build) -> String { +pub fn explain_hash_build(file_state: &FileState, build: &Build) -> anyhow::Result { let mut explainer = ExplainHash::default(); - build_manifest(&mut explainer, file_state, build); - explainer.text + build_manifest(&mut explainer, file_state, build)?; + Ok(explainer.text) } diff --git a/src/load.rs b/src/load.rs index 5a3c635..52c5e7b 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,13 +1,13 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, eval::{self, EvalPart, EvalString}, file_pool::FilePool, graph::{self, BuildId, Graph, RspFile}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::ParseResult, smallmap::SmallMap, trace + canon::canon_path, db, file_pool::FilePool, graph::{self, BuildId, Graph}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::ParseResult, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; use rustc_hash::FxHashMap; use std::{ - borrow::Cow, path::Path, time::Instant + path::Path, time::Instant }; use std::{ cmp::Ordering, @@ -17,25 +17,6 @@ use std::{ }; use std::path::PathBuf; -/// A variable lookup environment for magic $in/$out variables. -struct BuildImplicitVars<'a> { - explicit_ins: &'a [String], - explicit_outs: &'a [String], -} -impl<'text> eval::Env for BuildImplicitVars<'text> { - fn get_var(&self, var: &str) -> Option>> { - let string_to_evalstring = - |s: String| Some(EvalString::new(vec![EvalPart::Literal(Cow::Owned(s))])); - match var { - "in" => string_to_evalstring(self.explicit_ins.join(" ")), - "in_newline" => string_to_evalstring(self.explicit_ins.join("\n")), - "out" => string_to_evalstring(self.explicit_outs.join(" ")), - "out_newline" => string_to_evalstring(self.explicit_outs.join("\n")), - _ => None, - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct ScopePosition(pub usize); @@ -46,18 +27,18 @@ impl ScopePosition { } #[derive(Debug)] -pub struct ParentScopeReference<'text>(pub Arc>, pub ScopePosition); +pub struct ParentScopeReference(pub Arc, pub ScopePosition); #[derive(Debug)] -pub struct Scope<'text> { - parent: Option>, - rules: FxHashMap<&'text str, Rule<'text>>, - variables: FxHashMap<&'text str, Vec>>, +pub struct Scope { + parent: Option, + rules: FxHashMap, + variables: FxHashMap>, next_free_position: ScopePosition, } -impl<'text> Scope<'text> { - pub fn new(parent: Option>) -> Self { +impl Scope { + pub fn new(parent: Option) -> Self { Self { parent, rules: FxHashMap::default(), @@ -76,7 +57,7 @@ impl<'text> Scope<'text> { self.next_free_position } - pub fn get_rule(&self, name: &'text str, position: ScopePosition) -> Option<&Rule> { + pub fn get_rule(&self, name: &str, position: ScopePosition) -> Option<&Rule> { match self.rules.get(name) { Some(rule) if rule.scope_position.0 < position.0 => Some(rule), Some(_) | None => self @@ -87,7 +68,7 @@ impl<'text> Scope<'text> { } } - pub fn evaluate(&self, result: &mut String, varname: &'text str, position: ScopePosition) { + pub fn evaluate(&self, result: &mut String, varname: &str, position: ScopePosition) { if let Some(variables) = self.variables.get(varname) { let i = variables .binary_search_by(|x| { @@ -119,75 +100,18 @@ impl<'text> Scope<'text> { fn add_build<'text>( files: &Files, - scope: &Scope, + scope: Arc, b: &mut graph::Build, base_position: ScopePosition, ) -> anyhow::Result<()> { b.scope_position.0 += base_position.0; - let ins: Vec<_> = b.ins.unevaluated - .iter() - .map(|x| canon_path(x.evaluate(&[&b.bindings], scope, b.scope_position))) - .collect(); - let outs: Vec<_> = b.outs.unevaluated - .iter() - .map(|x| canon_path(x.evaluate(&[&b.bindings], scope, b.scope_position))) - .collect(); - - let rule = match scope.get_rule(&b.rule, b.scope_position) { - Some(r) => r, - None => bail!("unknown rule {:?}", b.rule), - }; - - let implicit_vars = BuildImplicitVars { - explicit_ins: &ins[..b.ins.explicit], - explicit_outs: &outs[..b.outs.explicit], - }; - - // temp variable in order to not move all of b into the closure - let build_vars = &b.bindings; - let lookup = |key: &str| -> Option { - // Look up `key = ...` binding in build and rule block. - Some(match rule.vars.get(key) { - Some(val) => val.evaluate(&[&implicit_vars, build_vars], scope, b.scope_position), - None => build_vars.get(key)?.evaluate(&[], scope, b.scope_position), - }) - }; - - let cmdline = lookup("command"); - let desc = lookup("description"); - let depfile = lookup("depfile"); - let parse_showincludes = match lookup("deps").as_deref() { - None => false, - Some("gcc") => false, - Some("msvc") => true, - Some(other) => bail!("invalid deps attribute {:?}", other), - }; - let pool = lookup("pool"); - - let rspfile_path = lookup("rspfile"); - let rspfile_content = lookup("rspfile_content"); - let rspfile = match (rspfile_path, rspfile_content) { - (None, None) => None, - (Some(path), Some(content)) => Some(RspFile { - path: std::path::PathBuf::from(path), - content, - }), - _ => bail!("rspfile and rspfile_content need to be both specified"), - }; - - b.ins.ids = ins.into_iter() - .map(|x| files.id_from_canonical(x)) + b.ins.ids = b.ins.unevaluated.iter() + .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); - b.outs.ids = outs.into_iter() - .map(|x| files.id_from_canonical(x)) + b.outs.ids = b.outs.unevaluated.iter() + .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); - - b.cmdline = cmdline; - b.desc = desc; - b.depfile = depfile; - b.parse_showincludes = parse_showincludes; - b.rspfile = rspfile; - b.pool = pool; + b.scope = Some(scope); Ok(()) } @@ -231,7 +155,7 @@ fn subninja<'thread, 'text>( files: &'thread Files, file_pool: &'text FilePool, path: String, - parent_scope: Option>, + parent_scope: Option, executor: &rayon::Scope<'thread>, ) -> anyhow::Result> where @@ -242,9 +166,8 @@ where let mut scope = Scope::new(parent_scope); if top_level_scope { scope.rules.insert( - "phony", + "phony".to_owned(), Rule { - name: "phony", vars: SmallMap::default(), scope_position: ScopePosition(0), }, @@ -277,6 +200,11 @@ where } } + scope.variables.par_iter().flat_map(|x| x.1.par_iter()).for_each(|x| { + let mut result = String::new(); + x.evaluate(&mut result, &scope); + }); + trace::scope("add builds", || -> anyhow::Result<()> { parse_results .par_iter_mut() @@ -285,7 +213,7 @@ where x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) }) .try_for_each(|(mut build, base_position)| -> anyhow::Result<()> { - add_build(files, &scope, &mut build, base_position) + add_build(files, scope.clone(), &mut build, base_position) }) })?; @@ -308,7 +236,7 @@ where for subninja_result in &mut subninja_results { parse_results.append(subninja_result); } - + // Only the builddir in the outermost scope is respected let build_dir = if top_level_scope { let mut build_dir = String::new(); @@ -322,13 +250,6 @@ where None }; - // Deallocating the scope can take some time (~200ms on android's files). - // We want to do that in the background, but rayon::spawn requires static - // lifetimes. We know that dropping doesn't attempt to dereference any - // pointers to the text, so unsafely cast the scope to the static lifetime. - let scope = unsafe { std::mem::transmute::>, Arc>>(scope)}; - rayon::spawn(move || drop(scope)); - Ok(SubninjaResults { clumps: parse_results, builddir: build_dir, @@ -340,7 +261,7 @@ fn include<'thread, 'text>( num_threads: usize, file_pool: &'text FilePool, path: String, - scope: &mut Scope<'text>, + scope: &mut Scope, clump_base_position: ScopePosition, executor: &rayon::Scope<'thread>, ) -> anyhow::Result>> @@ -376,7 +297,7 @@ fn parse<'thread, 'text>( num_threads: usize, file_pool: &'text FilePool, bytes: &'text [u8], - scope: &mut Scope<'text>, + scope: &mut Scope, mut clump_base_position: ScopePosition, executor: &rayon::Scope<'thread>, ) -> anyhow::Result>> @@ -429,9 +350,9 @@ where let scope_variables = &mut scope.variables; rayon::join( || { - for mut variable_assignment in assignments.into_iter() { + for (name, mut variable_assignment) in assignments.into_iter() { variable_assignment.scope_position.0 += clump_base_position.0; - match scope_variables.entry(variable_assignment.name) { + match scope_variables.entry(name) { Entry::Occupied(mut e) => e.get_mut().push(variable_assignment), Entry::Vacant(e) => { e.insert(vec![variable_assignment]); @@ -440,10 +361,10 @@ where } }, || -> anyhow::Result<()> { - for mut rule in rules.into_iter() { + for (name, mut rule) in rules.into_iter() { rule.scope_position.0 += clump_base_position.0; - match scope_rules.entry(rule.name) { - Entry::Occupied(_) => bail!("duplicate rule '{}'", rule.name), + match scope_rules.entry(name) { + Entry::Occupied(e) => bail!("duplicate rule '{}'", e.key()), Entry::Vacant(e) => { e.insert(rule); }, diff --git a/src/parse.rs b/src/parse.rs index c07d255..b92dbe7 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -19,9 +19,8 @@ use std::{ pub type VarList<'text> = SmallMap<&'text str, EvalString<&'text str>>; #[derive(Debug, PartialEq)] -pub struct Rule<'text> { - pub name: &'text str, - pub vars: VarList<'text>, +pub struct Rule { + pub vars: SmallMap>, pub scope_position: ScopePosition, } @@ -47,21 +46,19 @@ pub struct Pool<'text> { } #[derive(Debug)] -pub struct VariableAssignment<'text> { - pub name: &'text str, - pub unevaluated: EvalString<&'text str>, +pub struct VariableAssignment { + pub unevaluated: EvalString<&'static str>, pub scope_position: ScopePosition, pub evaluated: UnsafeCell>, pub is_evaluated: AtomicBool, pub lock: Mutex<()>, } -unsafe impl Sync for VariableAssignment<'_> {} +unsafe impl Sync for VariableAssignment {} -impl<'text> VariableAssignment<'text> { - fn new(name: &'text str, unevaluated: EvalString<&'text str>) -> Self { +impl VariableAssignment { + fn new(unevaluated: EvalString<&'static str>) -> Self { Self { - name, unevaluated, scope_position: ScopePosition(0), evaluated: UnsafeCell::new(None), @@ -109,19 +106,19 @@ pub struct IncludeOrSubninja<'text> { #[derive(Debug)] pub enum Statement<'text> { EmptyStatement, - Rule(Rule<'text>), + Rule((String, Rule)), Build(Box), Default(DefaultStmt<'text>), Include(IncludeOrSubninja<'text>), Subninja(IncludeOrSubninja<'text>), Pool(Pool<'text>), - VariableAssignment(VariableAssignment<'text>), + VariableAssignment((String, VariableAssignment)), } #[derive(Default, Debug)] pub struct Clump<'text> { - pub assignments: Vec>, - pub rules: Vec>, + pub assignments: Vec<(String, VariableAssignment)>, + pub rules: Vec<(String, Rule)>, pub pools: Vec>, pub defaults: Vec>, pub builds: Vec>, @@ -186,7 +183,7 @@ impl<'text> Parser<'text> { match stmt { Statement::EmptyStatement => {}, Statement::Rule(mut r) => { - r.scope_position = position; + r.1.scope_position = position; position.0 += 1; clump.rules.push(r); }, @@ -218,7 +215,7 @@ impl<'text> Parser<'text> { clump.pools.push(p); }, Statement::VariableAssignment(mut v) => { - v.scope_position = position; + v.1.scope_position = position; position.0 += 1; clump.assignments.push(v); }, @@ -273,8 +270,12 @@ impl<'text> Parser<'text> { } "pool" => return Ok(Some(Statement::Pool(self.read_pool()?))), ident => { - let result = VariableAssignment::new(ident, self.read_vardef()?); - return Ok(Some(Statement::VariableAssignment(result))); + let x = self.read_vardef()?; + let x = unsafe { + std::mem::transmute::, EvalString<&'static str>>(x) + }; + let result = VariableAssignment::new(x); + return Ok(Some(Statement::VariableAssignment((ident.to_owned(), result)))); } } } @@ -319,7 +320,7 @@ impl<'text> Parser<'text> { Ok(vars) } - fn read_rule(&mut self) -> ParseResult> { + fn read_rule(&mut self) -> ParseResult<(String, Rule)> { let name = self.read_ident()?; self.scanner.skip('\r'); self.scanner.expect('\n')?; @@ -339,11 +340,10 @@ impl<'text> Parser<'text> { | "msvc_deps_prefix" ) })?; - Ok(Rule { - name, - vars, + Ok((name.to_owned(), Rule { + vars: vars.to_owned(), scope_position: ScopePosition(0), - }) + })) } fn read_pool(&mut self) -> ParseResult> { @@ -444,7 +444,6 @@ impl<'text> Parser<'text> { self.scanner.expect('@')?; self.read_owned_unevaluated_paths_to(&mut ins)?; } - let validation_ins = ins.len() - order_only_ins - implicit_ins - explicit_ins; self.scanner.skip('\r'); self.scanner.expect('\n')?; @@ -453,18 +452,13 @@ impl<'text> Parser<'text> { Ok(Build { id: BuildId::from(0), rule: rule.to_owned(), + scope: None, scope_position: ScopePosition(0), bindings: vars.to_owned(), location: FileLoc { filename: self.filename.clone(), line, }, - desc: None, - cmdline: None, - depfile: None, - parse_showincludes: false, - rspfile: None, - pool: None, ins: BuildIns { ids: Vec::new(), unevaluated: ins, @@ -757,10 +751,10 @@ mod tests { fn parse_dot_in_eval() { let mut buf = test_case_buffer("x = $y.z\n"); let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); - let Ok(Some(Statement::VariableAssignment(x))) = parser.read() else { + let Ok(Some(Statement::VariableAssignment((name, x)))) = parser.read() else { panic!("Fail"); }; - assert_eq!(x.name, "x"); + assert_eq!(name, "x"); assert_eq!( x.unevaluated, EvalString::new(vec![EvalPart::VarRef("y"), EvalPart::Literal(".z"),]) @@ -771,14 +765,14 @@ mod tests { fn parse_dot_in_rule() { let mut buf = test_case_buffer("rule x.y\n command = x\n"); let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); - let Ok(Some(Statement::Rule(stmt))) = parser.read() else { + let Ok(Some(Statement::Rule((name, stmt)))) = parser.read() else { panic!("Fail"); }; - assert_eq!(stmt.name, "x.y"); + assert_eq!(name, "x.y"); assert_eq!(stmt.vars.len(), 1); assert_eq!( stmt.vars.get("command"), - Some(&EvalString::new(vec![EvalPart::Literal("x"),])) + Some(&EvalString::new(vec![EvalPart::Literal("x".to_owned()),])) ); } diff --git a/src/progress.rs b/src/progress.rs index 6ba1418..a7e8efe 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -14,12 +14,11 @@ use std::time::Duration; use std::time::Instant; /// Compute the message to display on the console for a given build. -pub fn build_message(build: &Build) -> &str { +pub fn build_message(build: &Build) -> String { build - .desc - .as_ref() + .get_binding("description") .filter(|desc| !desc.is_empty()) - .unwrap_or_else(|| build.cmdline.as_ref().unwrap()) + .unwrap_or_else(|| build.get_binding("command").unwrap()) } /// Trait for build progress notifications. @@ -80,11 +79,11 @@ impl Progress for DumbConsoleProgress { } fn task_started(&mut self, id: BuildId, build: &Build) { - self.log(if self.verbose { - build.cmdline.as_ref().unwrap() + if self.verbose { + self.log(build.get_binding("command").as_ref().unwrap()); } else { - build_message(build) - }); + self.log(&build_message(build)); + } self.last_started = Some(id); } @@ -98,11 +97,11 @@ impl Progress for DumbConsoleProgress { if result.output.is_empty() || self.last_started == Some(id) { // Output is empty, or we just printed the command, don't print it again. } else { - self.log(build_message(build)) + self.log(&build_message(build)) } } - Termination::Interrupted => self.log(&format!("interrupted: {}", build_message(build))), - Termination::Failure => self.log(&format!("failed: {}", build_message(build))), + Termination::Interrupted => self.log(&format!("interrupted: {}", &build_message(build))), + Termination::Failure => self.log(&format!("failed: {}", &build_message(build))), }; if !result.output.is_empty() { std::io::stdout().write_all(&result.output).unwrap(); @@ -228,7 +227,7 @@ impl FancyState { fn task_started(&mut self, id: BuildId, build: &Build) { if self.verbose { - self.log(build.cmdline.as_ref().unwrap()); + self.log(build.get_binding("command").as_ref().unwrap()); } let message = build_message(build); self.tasks.push_back(Task { @@ -254,7 +253,7 @@ impl FancyState { if result.output.is_empty() { // Common case: don't show anything. } else { - self.log(build_message(build)) + self.log(&build_message(build)) } } Termination::Interrupted => self.log(&format!("interrupted: {}", build_message(build))), diff --git a/src/task.rs b/src/task.rs index dfb818b..03392f6 100644 --- a/src/task.rs +++ b/src/task.rs @@ -210,11 +210,11 @@ impl Runner { self.running > 0 } - pub fn start(&mut self, id: BuildId, build: &Build) { - let cmdline = build.cmdline.clone().unwrap(); - let depfile = build.depfile.clone().map(PathBuf::from); - let rspfile = build.rspfile.clone(); - let parse_showincludes = build.parse_showincludes; + pub fn start(&mut self, id: BuildId, build: &Build) -> anyhow::Result<()> { + let cmdline = build.get_binding("command").clone().unwrap(); + let depfile = build.get_binding("depfile").clone().map(PathBuf::from); + let rspfile = build.get_rspfile()?; + let parse_showincludes = build.get_parse_showincludes()?; let tid = self.tids.claim(); let tx = self.tx.clone(); @@ -246,6 +246,7 @@ impl Runner { let _ = tx.send(Message::Done(task)); }); self.running += 1; + Ok(()) } /// Wait for a build to complete. May block for a long time. diff --git a/src/trace.rs b/src/trace.rs index ffc357d..ee74514 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -60,26 +60,26 @@ impl Trace { These functions were useful when developing, but are currently unused. */ - pub fn write_instant(&mut self, name: &str) { - self.write_event_prefix(name, Instant::now()); - writeln!(self.w, "\"ph\":\"i\"}}").unwrap(); - } - - pub fn write_counts<'a>( - &mut self, - name: &str, - counts: impl Iterator, - ) { - self.write_event_prefix(name, Instant::now()); - write!(self.w, "\"ph\":\"C\", \"args\":{{").unwrap(); - for (i, (name, count)) in counts.enumerate() { - if i > 0 { - write!(self.w, ",").unwrap(); - } - write!(self.w, "\"{}\":{}", name, count).unwrap(); - } - writeln!(self.w, "}}}}").unwrap(); - } + // pub fn write_instant(&mut self, name: &str) { + // self.write_event_prefix(name, Instant::now()); + // writeln!(self.w, "\"ph\":\"i\"}}").unwrap(); + // } + + // pub fn write_counts<'a>( + // &mut self, + // name: &str, + // counts: impl Iterator, + // ) { + // self.write_event_prefix(name, Instant::now()); + // write!(self.w, "\"ph\":\"C\", \"args\":{{").unwrap(); + // for (i, (name, count)) in counts.enumerate() { + // if i > 0 { + // write!(self.w, ",").unwrap(); + // } + // write!(self.w, "\"{}\":{}", name, count).unwrap(); + // } + // writeln!(self.w, "}}}}").unwrap(); + // } fn close(&mut self) { @@ -120,16 +120,16 @@ pub fn scope(name: &'static str, f: impl FnOnce() -> T) -> T { } } -#[inline] -pub fn write_instant(name: &'static str) { - // Safety: accessing global mut, not threadsafe. - unsafe { - match &mut TRACE { - None => (), - Some(t) => t.write_instant(name), - } - } -} +// #[inline] +// pub fn write_instant(name: &'static str) { +// // Safety: accessing global mut, not threadsafe. +// unsafe { +// match &mut TRACE { +// None => (), +// Some(t) => t.write_instant(name), +// } +// } +// } #[inline] diff --git a/src/work.rs b/src/work.rs index a94fbe1..c13c41d 100644 --- a/src/work.rs +++ b/src/work.rs @@ -72,6 +72,7 @@ impl StateCounts { /// Each running build is running "in" a pool; there's a default unbounded /// pool for builds that don't specify one. /// See "Tracking build state" in the design notes. +#[derive(Debug)] struct PoolState { /// A queue of builds that are ready to be executed in this pool. queued: VecDeque, @@ -140,7 +141,7 @@ impl BuildStates { let prev = std::mem::replace(&mut self.states[id], state); // We skip user-facing counters for phony builds. - let skip_ui_count = build.cmdline.is_none(); + let skip_ui_count = build.get_binding("command").is_none(); // println!("{:?} {:?}=>{:?} {:?}", id, prev, state, self.counts); if prev == BuildState::Unknown { @@ -270,7 +271,8 @@ impl BuildStates { /// Look up a PoolState by name. fn get_pool(&mut self, build: &Build) -> Option<&mut PoolState> { - let name = build.pool.as_deref().unwrap_or(""); + let owned_name = build.get_binding("pool"); + let name = owned_name.as_deref().unwrap_or(""); for (key, pool) in self.pools.iter_mut() { if key == name { return Some(pool); @@ -289,7 +291,7 @@ impl BuildStates { build.location, // Unnamed pool lookups always succeed, this error is about // named pools. - build.pool.as_ref().unwrap() + build.get_binding("pool").as_ref().unwrap() ) })?; pool.queued.push_back(id); @@ -502,7 +504,7 @@ impl<'a> Work<'a> { } let build = &self.graph.builds[id]; - let hash = hash::hash_build(&self.file_state, build); + let hash = hash::hash_build(&self.file_state, build)?; self.db.write_build(&self.graph, id, hash)?; Ok(()) @@ -602,7 +604,7 @@ impl<'a> Work<'a> { /// Prereq: any dependent input is already generated. fn check_build_dirty(&mut self, id: BuildId) -> anyhow::Result { let build = &self.graph.builds[id]; - let phony = build.cmdline.is_none(); + let phony = build.get_binding("command").is_none(); let file_missing = if phony { self.check_build_files_missing_phony(id)?; return Ok(false); // Phony builds never need to run anything. @@ -642,13 +644,13 @@ impl<'a> Work<'a> { Some(prev_hash) => prev_hash, }; - let hash = hash::hash_build(&self.file_state, build); + let hash = hash::hash_build(&self.file_state, build)?; if prev_hash != hash { if self.options.explain { self.progress .log(&format!("explain: {}: manifest changed", build.location)); self.progress - .log(&hash::explain_hash_build(&self.file_state, build)); + .log(&hash::explain_hash_build(&self.file_state, build)?); } return Ok(true); } @@ -703,7 +705,7 @@ impl<'a> Work<'a> { let build = &self.graph.builds[id]; self.build_states.set(id, build, BuildState::Running); self.create_parent_dirs(build.outs())?; - runner.start(id, build); + runner.start(id, build)?; self.progress.task_started(id, build); made_progress = true; } @@ -747,7 +749,7 @@ impl<'a> Work<'a> { let build = &self.graph.builds[task.buildid]; trace::if_enabled(|t| { let desc = progress::build_message(build); - t.write_complete(desc, task.tid + 1, task.span.0, task.span.1); + t.write_complete(&desc, task.tid + 1, task.span.0, task.span.1); }); self.progress From d10ab6d6272c0f04f9254b8c6ca5cf29a3c7f4d3 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 22 Feb 2024 15:51:52 -0800 Subject: [PATCH 15/29] Don't own the unevaluated build ins/outs We only need these within the lifetime of 'text, so we can make them references instead. --- src/graph.rs | 4 ++-- src/load.rs | 4 ++++ src/parse.rs | 35 ++++++++++++++--------------------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index e2b80cd..592f438 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -71,7 +71,7 @@ pub struct BuildIns { /// memory efficient than three separate Vecs, but it is kept internal to /// Build and only exposed via methods on Build. pub ids: Vec>, - pub unevaluated: Vec>, + pub unevaluated: Vec>, pub explicit: usize, pub implicit: usize, pub order_only: usize, @@ -84,7 +84,7 @@ pub struct BuildIns { pub struct BuildOuts { /// Similar to ins, we keep both explicit and implicit outs in one Vec. pub ids: Vec>, - pub unevaluated: Vec>, + pub unevaluated: Vec>, pub explicit: usize, } diff --git a/src/load.rs b/src/load.rs index 52c5e7b..8ee80e6 100644 --- a/src/load.rs +++ b/src/load.rs @@ -111,6 +111,10 @@ fn add_build<'text>( b.outs.ids = b.outs.unevaluated.iter() .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); + // The unevaluated values actually have a lifetime of 'text, not 'static, + // so clear them so they don't accidentally get used later. + b.ins.unevaluated.clear(); + b.outs.unevaluated.clear(); b.scope = Some(scope); Ok(()) diff --git a/src/parse.rs b/src/parse.rs index b92dbe7..e048322 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -383,30 +383,15 @@ impl<'text> Parser<'text> { Ok(()) } - fn read_owned_unevaluated_paths_to( - &mut self, - v: &mut Vec>, - ) -> ParseResult<()> { - self.skip_spaces(); - while self.scanner.peek() != ':' - && self.scanner.peek() != '|' - && !self.scanner.peek_newline() - { - v.push(self.read_eval(true)?.into_owned()); - self.skip_spaces(); - } - Ok(()) - } - fn read_build(&mut self) -> ParseResult { let line = self.scanner.line; let mut outs = Vec::new(); - self.read_owned_unevaluated_paths_to(&mut outs)?; + self.read_unevaluated_paths_to(&mut outs)?; let explicit_outs = outs.len(); if self.scanner.peek() == '|' { self.scanner.next(); - self.read_owned_unevaluated_paths_to(&mut outs)?; + self.read_unevaluated_paths_to(&mut outs)?; } self.scanner.expect(':')?; @@ -414,7 +399,7 @@ impl<'text> Parser<'text> { let rule = self.read_ident()?; let mut ins = Vec::new(); - self.read_owned_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut ins)?; let explicit_ins = ins.len(); if self.scanner.peek() == '|' { @@ -423,7 +408,7 @@ impl<'text> Parser<'text> { if peek == '|' || peek == '@' { self.scanner.back(); } else { - self.read_owned_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut ins)?; } } let implicit_ins = ins.len() - explicit_ins; @@ -434,7 +419,7 @@ impl<'text> Parser<'text> { self.scanner.back(); } else { self.scanner.expect('|')?; - self.read_owned_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut ins)?; } } let order_only_ins = ins.len() - implicit_ins - explicit_ins; @@ -442,13 +427,21 @@ impl<'text> Parser<'text> { if self.scanner.peek() == '|' { self.scanner.next(); self.scanner.expect('@')?; - self.read_owned_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut ins)?; } self.scanner.skip('\r'); self.scanner.expect('\n')?; let vars = self.read_scoped_vars(|_| true)?; + // We will evaluate the ins/outs into owned strings before 'text is over, + // and we don't want to attach the 'text lifetime to Build. So instead, + // unsafely cast the lifetime to 'static. + let (ins, outs) = unsafe { + (std::mem::transmute::>, Vec>>(ins), + std::mem::transmute::>, Vec>>(outs)) + }; + Ok(Build { id: BuildId::from(0), rule: rule.to_owned(), From 4389ee7cccddc25b12c91e78032cb8ba0d7fbc65 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Thu, 22 Feb 2024 16:12:36 -0800 Subject: [PATCH 16/29] Remove executor argument --- src/load.rs | 11 ++--------- src/parse.rs | 2 -- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/load.rs b/src/load.rs index 8ee80e6..3e5d786 100644 --- a/src/load.rs +++ b/src/load.rs @@ -160,7 +160,6 @@ fn subninja<'thread, 'text>( file_pool: &'text FilePool, path: String, parent_scope: Option, - executor: &rayon::Scope<'thread>, ) -> anyhow::Result> where 'text: 'thread, @@ -187,7 +186,6 @@ where &mut scope, // to account for the phony rule if top_level_scope { ScopePosition(1) } else { ScopePosition(0) }, - executor, ) })?; @@ -232,7 +230,6 @@ where file_pool, file, Some(ParentScopeReference(scope.clone(), position)), - executor, )?.clumps) }) .collect::>>>>()?; @@ -267,7 +264,6 @@ fn include<'thread, 'text>( path: String, scope: &mut Scope, clump_base_position: ScopePosition, - executor: &rayon::Scope<'thread>, ) -> anyhow::Result>> where 'text: 'thread, @@ -280,7 +276,6 @@ where file_pool.read_file(&path)?, scope, clump_base_position, - executor, ) } @@ -303,7 +298,6 @@ fn parse<'thread, 'text>( bytes: &'text [u8], scope: &mut Scope, mut clump_base_position: ScopePosition, - executor: &rayon::Scope<'thread>, ) -> anyhow::Result>> where 'text: 'thread, @@ -384,7 +378,7 @@ where ClumpOrInclude::Include(i) => { trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position)); - let mut new_results = include(filename, num_threads, file_pool, evaluated, scope, clump_base_position, executor)?; + let mut new_results = include(filename, num_threads, file_pool, evaluated, scope, clump_base_position)?; clump_base_position.0 += new_results.iter().map(|c| c.used_scope_positions).sum::(); // Things will be out of order here, but we don't care about // order for builds, defaults, subninjas, or pools, as long @@ -420,14 +414,13 @@ pub fn read(build_filename: &str) -> anyhow::Result { .num_threads(num_threads) .build()?; let (defaults, builddir, pools, builds) = trace::scope("loader.read_file", || -> anyhow::Result<_> { - pool.scope(|executor: &rayon::Scope| { + pool.scope(|_| { let mut results = subninja( num_threads, &files, &file_pool, build_filename, None, - executor, )?; let mut pools = SmallMap::default(); diff --git a/src/parse.rs b/src/parse.rs index e048322..95e520f 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -105,7 +105,6 @@ pub struct IncludeOrSubninja<'text> { #[derive(Debug)] pub enum Statement<'text> { - EmptyStatement, Rule((String, Rule)), Build(Box), Default(DefaultStmt<'text>), @@ -181,7 +180,6 @@ impl<'text> Parser<'text> { let mut position = ScopePosition(0); while let Some(stmt) = self.read()? { match stmt { - Statement::EmptyStatement => {}, Statement::Rule(mut r) => { r.1.scope_position = position; position.0 += 1; From 49e371abdcce085e012c749028bca2d3c08614ba Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Fri, 23 Feb 2024 15:22:35 -0800 Subject: [PATCH 17/29] Put unevaluated build ins/outs in 1 vec --- src/graph.rs | 12 ++++++++---- src/load.rs | 7 +++---- src/parse.rs | 33 ++++++++++++++++----------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 592f438..b601b35 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -71,7 +71,6 @@ pub struct BuildIns { /// memory efficient than three separate Vecs, but it is kept internal to /// Build and only exposed via methods on Build. pub ids: Vec>, - pub unevaluated: Vec>, pub explicit: usize, pub implicit: usize, pub order_only: usize, @@ -84,8 +83,8 @@ pub struct BuildIns { pub struct BuildOuts { /// Similar to ins, we keep both explicit and implicit outs in one Vec. pub ids: Vec>, - pub unevaluated: Vec>, pub explicit: usize, + pub implicit: usize, } impl BuildOuts { @@ -109,6 +108,10 @@ impl BuildOuts { } self.ids = ids; } + + pub fn num_outs(&self) -> usize { + self.explicit + self.implicit + } } #[cfg(test)] @@ -129,8 +132,8 @@ mod tests { let file2 = Arc::new(File::default()); let mut outs = BuildOuts { ids: vec![file1.clone(), file1.clone(), file2.clone()], - unevaluated: Vec::new(), explicit: 2, + implicit: 0, }; outs.remove_duplicates(); assert_file_arc_vecs_equal(outs.ids, vec![file1, file2]); @@ -143,8 +146,8 @@ mod tests { let file2 = Arc::new(File::default()); let mut outs = BuildOuts { ids: vec![file1.clone(), file2.clone(), file1.clone()], - unevaluated: Vec::new(), explicit: 2, + implicit: 0, }; outs.remove_duplicates(); assert_file_arc_vecs_equal(outs.ids, vec![file1, file2]); @@ -179,6 +182,7 @@ pub struct Build { pub scope: Option>, pub scope_position: ScopePosition, + pub unevaluated_outs_and_ins: Vec>, pub rule: String, diff --git a/src/load.rs b/src/load.rs index 3e5d786..d291c41 100644 --- a/src/load.rs +++ b/src/load.rs @@ -105,16 +105,15 @@ fn add_build<'text>( base_position: ScopePosition, ) -> anyhow::Result<()> { b.scope_position.0 += base_position.0; - b.ins.ids = b.ins.unevaluated.iter() + b.outs.ids = b.unevaluated_outs_and_ins[..b.outs.num_outs()].iter() .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); - b.outs.ids = b.outs.unevaluated.iter() + b.ins.ids = b.unevaluated_outs_and_ins[b.outs.num_outs()..].iter() .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); // The unevaluated values actually have a lifetime of 'text, not 'static, // so clear them so they don't accidentally get used later. - b.ins.unevaluated.clear(); - b.outs.unevaluated.clear(); + b.unevaluated_outs_and_ins.clear(); b.scope = Some(scope); Ok(()) diff --git a/src/parse.rs b/src/parse.rs index 95e520f..5cd83f4 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -383,22 +383,22 @@ impl<'text> Parser<'text> { fn read_build(&mut self) -> ParseResult { let line = self.scanner.line; - let mut outs = Vec::new(); - self.read_unevaluated_paths_to(&mut outs)?; - let explicit_outs = outs.len(); + let mut outs_and_ins = Vec::new(); + self.read_unevaluated_paths_to(&mut outs_and_ins)?; + let explicit_outs = outs_and_ins.len(); if self.scanner.peek() == '|' { self.scanner.next(); - self.read_unevaluated_paths_to(&mut outs)?; + self.read_unevaluated_paths_to(&mut outs_and_ins)?; } + let implicit_outs = outs_and_ins.len() - explicit_outs; self.scanner.expect(':')?; self.skip_spaces(); let rule = self.read_ident()?; - let mut ins = Vec::new(); - self.read_unevaluated_paths_to(&mut ins)?; - let explicit_ins = ins.len(); + self.read_unevaluated_paths_to(&mut outs_and_ins)?; + let explicit_ins = outs_and_ins.len() - implicit_outs - explicit_outs; if self.scanner.peek() == '|' { self.scanner.next(); @@ -406,10 +406,10 @@ impl<'text> Parser<'text> { if peek == '|' || peek == '@' { self.scanner.back(); } else { - self.read_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut outs_and_ins)?; } } - let implicit_ins = ins.len() - explicit_ins; + let implicit_ins = outs_and_ins.len() - explicit_ins - implicit_outs - explicit_outs; if self.scanner.peek() == '|' { self.scanner.next(); @@ -417,15 +417,15 @@ impl<'text> Parser<'text> { self.scanner.back(); } else { self.scanner.expect('|')?; - self.read_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut outs_and_ins)?; } } - let order_only_ins = ins.len() - implicit_ins - explicit_ins; + let order_only_ins = outs_and_ins.len() - implicit_ins - explicit_ins - implicit_outs - explicit_outs; if self.scanner.peek() == '|' { self.scanner.next(); self.scanner.expect('@')?; - self.read_unevaluated_paths_to(&mut ins)?; + self.read_unevaluated_paths_to(&mut outs_and_ins)?; } self.scanner.skip('\r'); @@ -435,9 +435,8 @@ impl<'text> Parser<'text> { // We will evaluate the ins/outs into owned strings before 'text is over, // and we don't want to attach the 'text lifetime to Build. So instead, // unsafely cast the lifetime to 'static. - let (ins, outs) = unsafe { - (std::mem::transmute::>, Vec>>(ins), - std::mem::transmute::>, Vec>>(outs)) + let outs_and_ins = unsafe { + std::mem::transmute::>, Vec>>(outs_and_ins) }; Ok(Build { @@ -452,7 +451,6 @@ impl<'text> Parser<'text> { }, ins: BuildIns { ids: Vec::new(), - unevaluated: ins, explicit: explicit_ins, implicit: implicit_ins, order_only: order_only_ins, @@ -460,9 +458,10 @@ impl<'text> Parser<'text> { discovered_ins: Vec::new(), outs: BuildOuts { ids: Vec::new(), - unevaluated: outs, explicit: explicit_outs, + implicit: implicit_outs, }, + unevaluated_outs_and_ins: outs_and_ins, }) } From 983f4984ea1073d911cd73723b2f7ab271e52a40 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Fri, 23 Feb 2024 16:04:46 -0800 Subject: [PATCH 18/29] Make the files map use FxHasher --- src/graph.rs | 4 ++-- src/load.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index b601b35..11dc3cb 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -335,13 +335,13 @@ pub struct Graph { /// Split from Graph for lifetime reasons. #[derive(Default)] pub struct GraphFiles { - by_name: dashmap::DashMap, Arc>, + by_name: dashmap::DashMap, Arc, BuildHasherDefault>, } impl Graph { pub fn from_uninitialized_builds_and_files( builds: Vec>, - files: dashmap::DashMap, Arc>, + files: dashmap::DashMap, Arc, BuildHasherDefault>, ) -> anyhow::Result { let result = Graph { builds: DenseMap::from_vec(builds), diff --git a/src/load.rs b/src/load.rs index d291c41..01557e8 100644 --- a/src/load.rs +++ b/src/load.rs @@ -5,9 +5,9 @@ use crate::{ }; use anyhow::{anyhow, bail}; use rayon::prelude::*; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHasher}; use std::{ - path::Path, time::Instant + hash::BuildHasherDefault, path::Path, time::Instant }; use std::{ cmp::Ordering, @@ -120,12 +120,12 @@ fn add_build<'text>( } struct Files { - by_name: dashmap::DashMap, Arc>, + by_name: dashmap::DashMap, Arc, BuildHasherDefault>, } impl Files { pub fn new() -> Self { Self { - by_name: dashmap::DashMap::new(), + by_name: dashmap::DashMap::with_hasher(BuildHasherDefault::default()), } } @@ -142,7 +142,7 @@ impl Files { } } - pub fn into_maps(self) -> dashmap::DashMap, Arc> { + pub fn into_maps(self) -> dashmap::DashMap, Arc, BuildHasherDefault> { self.by_name } } From 9212a9b0bf18bc4a6b1f53fb6a966732cd73c83d Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Mon, 26 Feb 2024 11:46:30 -0800 Subject: [PATCH 19/29] Use unwrap_err_unchecked in Scope::evaluate Surprisingly this seems to have a measurable ~0.1s performance increase. Also removed some unwraps in the VariableAssignment's evaluation, but those didn't seem to help as much. --- src/load.rs | 6 ++++-- src/parse.rs | 10 +++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/load.rs b/src/load.rs index 01557e8..f64cbda 100644 --- a/src/load.rs +++ b/src/load.rs @@ -82,8 +82,10 @@ impl Scope { // before it. So return Greater instead of Equal. Ordering::Greater } - }) - .unwrap_err(); + }); + // SAFETY: We never return Ordering::Equal above, so this will + // always be an error + let i = unsafe { i.unwrap_err_unchecked() }; let i = std::cmp::min(i, variables.len() - 1); if variables[i].scope_position.0 < position.0 { variables[i].evaluate(result, &self); diff --git a/src/parse.rs b/src/parse.rs index 5cd83f4..e5d0438 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -49,7 +49,7 @@ pub struct Pool<'text> { pub struct VariableAssignment { pub unevaluated: EvalString<&'static str>, pub scope_position: ScopePosition, - pub evaluated: UnsafeCell>, + pub evaluated: UnsafeCell, pub is_evaluated: AtomicBool, pub lock: Mutex<()>, } @@ -61,7 +61,7 @@ impl VariableAssignment { Self { unevaluated, scope_position: ScopePosition(0), - evaluated: UnsafeCell::new(None), + evaluated: UnsafeCell::new(String::new()), is_evaluated: AtomicBool::new(false), lock: Mutex::new(()), } @@ -70,18 +70,18 @@ impl VariableAssignment { pub fn evaluate(&self, result: &mut String, scope: &Scope) { unsafe { if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { - result.push_str((*self.evaluated.get()).as_ref().unwrap()); + result.push_str(&(*self.evaluated.get())); return; } let guard = self.lock.lock().unwrap(); if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { - result.push_str((*self.evaluated.get()).as_ref().unwrap()); + result.push_str(&(*self.evaluated.get())); return; } let cache = self.unevaluated.evaluate(&[], scope, self.scope_position); result.push_str(&cache); - *self.evaluated.get() = Some(cache); + *self.evaluated.get() = cache; self.is_evaluated .store(true, std::sync::atomic::Ordering::Relaxed); From 5fdd13cf0dc1893d8c60d7b2ae98736cf6a149fe Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Mon, 26 Feb 2024 12:36:58 -0800 Subject: [PATCH 20/29] Unmap files in parallel unmapping files is slow. --- src/file_pool.rs | 1 + src/load.rs | 31 ++++++++++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/file_pool.rs b/src/file_pool.rs index 5218bf1..eb7dbf2 100644 --- a/src/file_pool.rs +++ b/src/file_pool.rs @@ -69,6 +69,7 @@ impl FilePool { // but that pointer isn't used at all aside from the drop implementation, so // we won't have data races. unsafe impl Sync for FilePool {} +unsafe impl Send for FilePool {} impl Drop for FilePool { fn drop(&mut self) { diff --git a/src/load.rs b/src/load.rs index f64cbda..5447669 100644 --- a/src/load.rs +++ b/src/load.rs @@ -107,15 +107,17 @@ fn add_build<'text>( base_position: ScopePosition, ) -> anyhow::Result<()> { b.scope_position.0 += base_position.0; - b.outs.ids = b.unevaluated_outs_and_ins[..b.outs.num_outs()].iter() + let num_outs = b.outs.num_outs(); + b.outs.ids = b.unevaluated_outs_and_ins[..num_outs].iter() .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); - b.ins.ids = b.unevaluated_outs_and_ins[b.outs.num_outs()..].iter() + b.ins.ids = b.unevaluated_outs_and_ins[num_outs..].iter() .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) .collect(); // The unevaluated values actually have a lifetime of 'text, not 'static, // so clear them so they don't accidentally get used later. b.unevaluated_outs_and_ins.clear(); + b.unevaluated_outs_and_ins.shrink_to_fit(); b.scope = Some(scope); Ok(()) @@ -439,19 +441,26 @@ pub fn read(build_filename: &str) -> anyhow::Result { } Ok(()) })?; - trace::scope("initialize builds", || { - let mut builds = trace::scope("allocate and concat builds", || { - let mut builds = Vec::with_capacity(num_builds); - for clump in &mut results.clumps { - builds.append(&mut clump.builds); - } - builds - }); + let mut builds = trace::scope("allocate and concat builds", || { + let mut builds = Vec::with_capacity(num_builds); + for clump in &mut results.clumps { + builds.append(&mut clump.builds); + } + builds + }); + let builddir = results.builddir.take(); + drop(results); + // Turns out munmap is rather slow, unmapping the android ninja + // files takes ~150ms. Do this in parallel with initialize_build. + rayon::spawn(move || { + drop(file_pool); + }); + trace::scope("initialize builds", move || { builds.par_iter_mut().enumerate().try_for_each(|(id, build)| { build.id = BuildId::from(id); graph::Graph::initialize_build(build) })?; - Ok((defaults, results.builddir, pools, builds)) + Ok((defaults, builddir, pools, builds)) }) }) })?; From 86dacb3fd6d69d1f156321d9efb8d5458beca72d Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Mon, 26 Feb 2024 14:17:23 -0800 Subject: [PATCH 21/29] Call format_parse_error again --- src/concurrent_linked_list.rs | 2 +- src/depfile.rs | 6 ++- src/graph.rs | 46 ++++++++++++----------- src/load.rs | 14 ++++--- src/parse.rs | 40 ++++++++------------ src/scanner.rs | 69 ++++++++++++++++++----------------- src/task.rs | 6 +-- 7 files changed, 92 insertions(+), 91 deletions(-) diff --git a/src/concurrent_linked_list.rs b/src/concurrent_linked_list.rs index 2ea5152..0c15408 100644 --- a/src/concurrent_linked_list.rs +++ b/src/concurrent_linked_list.rs @@ -100,7 +100,7 @@ impl Debug for ConcurrentLinkedList { impl Drop for ConcurrentLinkedList { fn drop(&mut self) { - let mut cur = self.head.load(Ordering::Relaxed); + let mut cur = self.head.swap(null_mut(), Ordering::Relaxed); while !cur.is_null() { unsafe { // Re-box it so that box will call Drop and deallocate the memory diff --git a/src/depfile.rs b/src/depfile.rs index b241cb6..ef46a31 100644 --- a/src/depfile.rs +++ b/src/depfile.rs @@ -85,13 +85,15 @@ pub fn parse<'a>(scanner: &mut Scanner<'a>) -> ParseResult) -> Result>, String> { buf.push(0); - let mut scanner = Scanner::new(buf); - parse(&mut scanner).map_err(|err| scanner.format_parse_error(Path::new("test"), err)) + let mut scanner = Scanner::new(buf, 0); + parse(&mut scanner).map_err(|err| format_parse_error(0, buf, Path::new("test"), err)) } fn must_parse(buf: &mut Vec) -> SmallMap<&str, Vec<&str>> { diff --git a/src/graph.rs b/src/graph.rs index 11dc3cb..3c3a318 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -196,28 +196,33 @@ pub struct Build { pub ins: BuildIns, /// Additional inputs discovered from a previous build. - // TODO: Make private again - pub discovered_ins: Vec>, + discovered_ins: Vec>, /// Output files. pub outs: BuildOuts, } impl Build { - // pub fn new(id: BuildId, loc: FileLoc, ins: BuildIns, outs: BuildOuts) -> Self { - // Build { - // id, - // location: loc, - // desc: None, - // cmdline: None, - // depfile: None, - // parse_showincludes: false, - // rspfile: None, - // pool: None, - // ins, - // discovered_ins: Vec::new(), - // outs, - // } - // } + pub fn new( + rule: String, + bindings: SmallMap>, + location: FileLoc, + ins: BuildIns, + outs: BuildOuts, + unevaluated_outs_and_ins: Vec>, + ) -> Self { + Build { + id: BuildId::from(0), + rule, + scope: None, + scope_position: ScopePosition(0), + bindings, + location, + ins: ins, + discovered_ins: Vec::new(), + outs: outs, + unevaluated_outs_and_ins, + } + } /// Input paths that appear in `$in`. pub fn explicit_ins(&self) -> &[Arc] { @@ -366,14 +371,12 @@ impl Graph { build.location, f.name, ); } - Some(prev) => { + Some(_) => { let location = build.location.clone(); anyhow::bail!( - "{}: {:?} is already an output at ", // {} + "{}: {:?} is already an output of another build", location, f.name, - // TODO - //self.builds[prev].location ); } None => *input = Some(new_id), @@ -401,7 +404,6 @@ impl GraphFiles { /// usages of this function have an owned string easily accessible anyways. pub fn id_from_canonical(&mut self, file: String) -> Arc { let file = Arc::new(file); - // TODO: so many string copies :< match self.by_name.entry(file) { dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), dashmap::mapref::entry::Entry::Vacant(v) => { diff --git a/src/load.rs b/src/load.rs index 5447669..66da569 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,7 +1,7 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, file_pool::FilePool, graph::{self, BuildId, Graph}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::ParseResult, smallmap::SmallMap, trace + canon::canon_path, db, file_pool::FilePool, graph::{self, BuildId, Graph}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::{format_parse_error, ParseResult}, smallmap::SmallMap, trace }; use anyhow::{anyhow, bail}; use rayon::prelude::*; @@ -308,15 +308,17 @@ where let chunks = parse::split_manifest_into_chunks(bytes, num_threads); let statements: ParseResult>> = chunks - .into_par_iter() - .map(|chunk| { - let mut parser = parse::Parser::new(chunk, filename.clone()); + .par_iter() + .enumerate() + .map(|(i, chunk)| { + let mut parser = parse::Parser::new(chunk, filename.clone(), i); parser.read_clumps() }).collect(); let Ok(statements) = statements else { - // TODO: Call format_parse_error - bail!(statements.unwrap_err().msg); + let err = statements.unwrap_err(); + let ofs = chunks[..err.chunk_index].iter().map(|x| x.len()).sum(); + bail!(format_parse_error(ofs, chunks[err.chunk_index], filename, err)); }; let start = Instant::now(); diff --git a/src/parse.rs b/src/parse.rs index e5d0438..7a4274d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,11 +6,11 @@ //! text, marked with the lifetime `'text`. use crate::{ - eval::{EvalPart, EvalString}, graph::{self, Build, BuildId, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseError, ParseResult, Scanner}, smallmap::SmallMap + eval::{EvalPart, EvalString}, graph::{self, Build, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseResult, Scanner}, smallmap::SmallMap }; use std::{ cell::UnsafeCell, - path::{Path, PathBuf}, + path::PathBuf, sync::{atomic::AtomicBool, Arc, Mutex}, }; @@ -153,19 +153,15 @@ pub struct Parser<'text> { } impl<'text> Parser<'text> { - pub fn new(buf: &'text [u8], filename: Arc) -> Parser<'text> { + pub fn new(buf: &'text [u8], filename: Arc, chunk_index: usize) -> Parser<'text> { Parser { filename, - scanner: Scanner::new(buf), + scanner: Scanner::new(buf, chunk_index), buf_len: buf.len(), eval_buf: Vec::with_capacity(16), } } - pub fn format_parse_error(&self, filename: &Path, err: ParseError) -> String { - self.scanner.format_parse_error(filename, err) - } - pub fn read_all(&mut self) -> ParseResult>> { let mut result = Vec::new(); while let Some(stmt) = self.read()? { @@ -439,30 +435,26 @@ impl<'text> Parser<'text> { std::mem::transmute::>, Vec>>(outs_and_ins) }; - Ok(Build { - id: BuildId::from(0), - rule: rule.to_owned(), - scope: None, - scope_position: ScopePosition(0), - bindings: vars.to_owned(), - location: FileLoc { + Ok(Build::new( + rule.to_owned(), + vars.to_owned(), + FileLoc { filename: self.filename.clone(), line, }, - ins: BuildIns { + BuildIns { ids: Vec::new(), explicit: explicit_ins, implicit: implicit_ins, order_only: order_only_ins, }, - discovered_ins: Vec::new(), - outs: BuildOuts { + BuildOuts { ids: Vec::new(), explicit: explicit_outs, implicit: implicit_outs, }, - unevaluated_outs_and_ins: outs_and_ins, - }) + outs_and_ins + )) } fn read_default(&mut self) -> ParseResult> { @@ -717,7 +709,7 @@ mod tests { fn parse_defaults() { test_for_line_endings(&["var = 3", "default a b$var c", ""], |test_case| { let mut buf = test_case_buffer(test_case); - let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja")), 0); match parser.read().unwrap().unwrap() { Statement::VariableAssignment(_) => {} stmt => panic!("expected variable assignment, got {:?}", stmt), @@ -740,7 +732,7 @@ mod tests { #[test] fn parse_dot_in_eval() { let mut buf = test_case_buffer("x = $y.z\n"); - let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja")), 0); let Ok(Some(Statement::VariableAssignment((name, x)))) = parser.read() else { panic!("Fail"); }; @@ -754,7 +746,7 @@ mod tests { #[test] fn parse_dot_in_rule() { let mut buf = test_case_buffer("rule x.y\n command = x\n"); - let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja")), 0); let Ok(Some(Statement::Rule((name, stmt)))) = parser.read() else { panic!("Fail"); }; @@ -769,7 +761,7 @@ mod tests { #[test] fn parse_trailing_newline() { let mut buf = test_case_buffer("build$\n foo$\n : $\n touch $\n\n"); - let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja"))); + let mut parser = Parser::new(&mut buf, Arc::new(PathBuf::from("build.ninja")), 0); let stmt = parser.read().unwrap().unwrap(); let Statement::Build(stmt) = stmt else { panic!("Wasn't a build"); diff --git a/src/scanner.rs b/src/scanner.rs index bfea31b..4229a68 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -6,6 +6,7 @@ use std::{io::Read, path::Path}; pub struct ParseError { pub msg: String, ofs: usize, + pub chunk_index: usize, } pub type ParseResult = Result; @@ -13,14 +14,16 @@ pub struct Scanner<'a> { buf: &'a [u8], pub ofs: usize, pub line: usize, + pub chunk_index: usize, } impl<'a> Scanner<'a> { - pub fn new(buf: &'a [u8]) -> Self { + pub fn new(buf: &'a [u8], chunk_index: usize) -> Self { Scanner { buf, ofs: 0, line: 1, + chunk_index, } } @@ -88,46 +91,46 @@ impl<'a> Scanner<'a> { Err(ParseError { msg: msg.into(), ofs: self.ofs, + chunk_index: self.chunk_index, }) } +} - pub fn format_parse_error(&self, filename: &Path, err: ParseError) -> String { - let mut ofs = 0; - let lines = self.buf.split(|&c| c == b'\n'); - for (line_number, line) in lines.enumerate() { - if ofs + line.len() >= err.ofs { - let mut msg = "parse error: ".to_string(); - msg.push_str(&err.msg); - msg.push('\n'); - - let prefix = format!("{}:{}: ", filename.display(), line_number + 1); - msg.push_str(&prefix); +pub fn format_parse_error(mut ofs: usize, buf: &[u8], filename: &Path, err: ParseError) -> String { + let lines = buf.split(|&c| c == b'\n'); + for (line_number, line) in lines.enumerate() { + if ofs + line.len() >= err.ofs { + let mut msg = "parse error: ".to_string(); + msg.push_str(&err.msg); + msg.push('\n'); - let mut context = unsafe { std::str::from_utf8_unchecked(line) }; - let mut col = err.ofs - ofs; - if col > 40 { - // Trim beginning of line to fit it on screen. - msg.push_str("..."); - context = &context[col - 20..]; - col = 3 + 20; - } - if context.len() > 40 { - context = &context[0..40]; - msg.push_str(context); - msg.push_str("..."); - } else { - msg.push_str(context); - } - msg.push('\n'); + let prefix = format!("{}:{}: ", filename.display(), line_number + 1); + msg.push_str(&prefix); - msg.push_str(&" ".repeat(prefix.len() + col)); - msg.push_str("^\n"); - return msg; + let mut context = unsafe { std::str::from_utf8_unchecked(line) }; + let mut col = err.ofs - ofs; + if col > 40 { + // Trim beginning of line to fit it on screen. + msg.push_str("..."); + context = &context[col - 20..]; + col = 3 + 20; } - ofs += line.len() + 1; + if context.len() > 40 { + context = &context[0..40]; + msg.push_str(context); + msg.push_str("..."); + } else { + msg.push_str(context); + } + msg.push('\n'); + + msg.push_str(&" ".repeat(prefix.len() + col)); + msg.push_str("^\n"); + return msg; } - panic!("invalid offset when formatting error") + ofs += line.len() + 1; } + panic!("invalid offset when formatting error") } /// Scanner wants its input buffer to end in a trailing nul. diff --git a/src/task.rs b/src/task.rs index 03392f6..04550e6 100644 --- a/src/task.rs +++ b/src/task.rs @@ -12,7 +12,7 @@ use crate::{ depfile, graph::{Build, BuildId, RspFile}, process, - scanner::{self, Scanner}, + scanner::{self, format_parse_error, Scanner}, }; use anyhow::{anyhow, bail}; use std::path::{Path, PathBuf}; @@ -46,9 +46,9 @@ fn read_depfile(path: &Path) -> anyhow::Result> { Err(e) => bail!("read {}: {}", path.display(), e), }; - let mut scanner = Scanner::new(&bytes); + let mut scanner = Scanner::new(&bytes, 0); let parsed_deps = depfile::parse(&mut scanner) - .map_err(|err| anyhow!(scanner.format_parse_error(path, err)))?; + .map_err(|err| anyhow!(format_parse_error(0, &bytes, path, err)))?; // TODO verify deps refers to correct output let deps: Vec = parsed_deps .values() From d9d54f9f27c0d24d7a6c38cd9d8f842efcfcbb6e Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Mon, 26 Feb 2024 16:17:50 -0800 Subject: [PATCH 22/29] Fix bug in evaluation order --- src/load.rs | 33 +++++++---------------- tests/e2e/include_and_subninja.rs | 45 +++++++++++++++++++++++++++++++ tests/e2e/mod.rs | 2 ++ 3 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 tests/e2e/include_and_subninja.rs diff --git a/src/load.rs b/src/load.rs index 66da569..eca7ba9 100644 --- a/src/load.rs +++ b/src/load.rs @@ -17,13 +17,16 @@ use std::{ }; use std::path::PathBuf; -#[derive(Debug, Copy, Clone, PartialEq, Default)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord)] pub struct ScopePosition(pub usize); impl ScopePosition { pub fn add(&self, other: ScopePosition) -> ScopePosition { ScopePosition(self.0 + other.0) } + pub fn add_usize(&self, other: usize) -> ScopePosition { + ScopePosition(self.0 + other) + } } #[derive(Debug)] @@ -70,23 +73,11 @@ impl Scope { pub fn evaluate(&self, result: &mut String, varname: &str, position: ScopePosition) { if let Some(variables) = self.variables.get(varname) { - let i = variables - .binary_search_by(|x| { - if x.scope_position.0 < position.0 { - Ordering::Less - } else if x.scope_position.0 > position.0 { - Ordering::Greater - } else { - // If we're evaluating a variable assignment, we don't want to - // get the same assignment, but instead, we want the one just - // before it. So return Greater instead of Equal. - Ordering::Greater - } - }); - // SAFETY: We never return Ordering::Equal above, so this will - // always be an error - let i = unsafe { i.unwrap_err_unchecked() }; - let i = std::cmp::min(i, variables.len() - 1); + let i = variables.binary_search_by_key(&position, |x| x.scope_position); + let i = match i { + Ok(i) => std::cmp::max(i, 1)-1, + Err(i) => std::cmp::min(i, variables.len() - 1), + }; if variables[i].scope_position.0 < position.0 { variables[i].evaluate(result, &self); return; @@ -384,12 +375,8 @@ where trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position)); let mut new_results = include(filename, num_threads, file_pool, evaluated, scope, clump_base_position)?; - clump_base_position.0 += new_results.iter().map(|c| c.used_scope_positions).sum::(); - // Things will be out of order here, but we don't care about - // order for builds, defaults, subninjas, or pools, as long - // as their scope_position is correct. + clump_base_position = new_results.last().map(|c| c.base_position.add_usize(c.used_scope_positions)).unwrap_or(clump_base_position); results.append(&mut new_results); - clump_base_position.0 += 1; Ok(()) })?; }, diff --git a/tests/e2e/include_and_subninja.rs b/tests/e2e/include_and_subninja.rs new file mode 100644 index 0000000..4c0ea48 --- /dev/null +++ b/tests/e2e/include_and_subninja.rs @@ -0,0 +1,45 @@ +use crate::e2e::{n2_command, TestSpace, TOUCH_RULE}; + +#[cfg(unix)] +#[test] +fn include_creates_new_variable_with_dependency() -> anyhow::Result<()> { + let space = TestSpace::new()?; + space.write("build.ninja", " +rule write_file + command = echo $contents > $out + +a = foo +include included.ninja +build out: write_file + contents = $b + +")?; + space.write("included.ninja", " +b = $a bar +")?; + space.run_expect(&mut n2_command(vec!["out"]))?; + assert_eq!(space.read("out").unwrap(), b"foo bar\n"); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn include_creates_edits_existing_variable() -> anyhow::Result<()> { + let space = TestSpace::new()?; + space.write("build.ninja", " +rule write_file + command = echo $contents > $out + +a = foo +include included.ninja +build out: write_file + contents = $a + +")?; + space.write("included.ninja", " +a = $a bar +")?; + space.run_expect(&mut n2_command(vec!["out"]))?; + assert_eq!(space.read("out").unwrap(), b"foo bar\n"); + Ok(()) +} diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index a1e7e7f..94801a7 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -3,6 +3,7 @@ mod basic; mod directories; mod discovered; +mod include_and_subninja; mod missing; mod regen; mod validations; @@ -99,6 +100,7 @@ impl TestSpace { print_output(&out); anyhow::bail!("build failed, status {}", out.status); } + print_output(&out); Ok(out) } From 01c246d4cfb138078f5e31d1999593c45af9e3d8 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Mon, 26 Feb 2024 16:36:05 -0800 Subject: [PATCH 23/29] Add a test for subninjas --- tests/e2e/include_and_subninja.rs | 25 +++++++++++++++++++++++++ tests/e2e/mod.rs | 1 - 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/e2e/include_and_subninja.rs b/tests/e2e/include_and_subninja.rs index 4c0ea48..b1191cd 100644 --- a/tests/e2e/include_and_subninja.rs +++ b/tests/e2e/include_and_subninja.rs @@ -43,3 +43,28 @@ a = $a bar assert_eq!(space.read("out").unwrap(), b"foo bar\n"); Ok(()) } + +#[cfg(unix)] +#[test] +fn subninja_doesnt_affect_variables_in_parent_scope() -> anyhow::Result<()> { + let space = TestSpace::new()?; + space.write("build.ninja", " +rule write_file + command = echo $contents > $out + +a = foo +subninja subninja.ninja +build out: write_file + contents = $a + +")?; + space.write("subninja.ninja", " +a = bar +build out2: write_file + contents = $a +")?; + space.run_expect(&mut n2_command(vec!["out", "out2"]))?; + assert_eq!(space.read("out").unwrap(), b"foo\n"); + assert_eq!(space.read("out2").unwrap(), b"bar\n"); + Ok(()) +} diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index 94801a7..956076f 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -100,7 +100,6 @@ impl TestSpace { print_output(&out); anyhow::bail!("build failed, status {}", out.status); } - print_output(&out); Ok(out) } From a99843a9075fcffa950fd08f65df31eba4a2efa8 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Fri, 16 Feb 2024 17:53:13 -0800 Subject: [PATCH 24/29] Double parse evalstrings So that we don't have to allocate memory for their parsed representations. --- src/eval.rs | 66 +++++++++-------------- src/graph.rs | 2 +- src/parse.rs | 144 ++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 127 insertions(+), 85 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index a02bf68..c1ebe3b 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -3,6 +3,8 @@ use crate::load::Scope; use crate::load::ScopePosition; +use crate::parse::parse_eval; +use crate::parse::Parser; use crate::smallmap::SmallMap; use std::borrow::Borrow; use std::borrow::Cow; @@ -26,10 +28,10 @@ pub enum EvalPart> { /// expanded evals, like top-level bindings, and EvalString, which is /// used for delayed evals like in `rule` blocks. #[derive(Debug, PartialEq)] -pub struct EvalString>(Vec>); +pub struct EvalString>(T); impl> EvalString { - pub fn new(parts: Vec>) -> Self { - EvalString(parts) + pub fn new(inner: T) -> Self { + EvalString(inner) } fn evaluate_inner( @@ -39,7 +41,7 @@ impl> EvalString { scope: &Scope, position: ScopePosition, ) { - for part in &self.0 { + for part in self.parse() { match part { EvalPart::Literal(s) => result.push_str(s.as_ref()), EvalPart::VarRef(v) => { @@ -71,52 +73,34 @@ impl> EvalString { } pub fn maybe_literal(&self) -> Option<&T> { - match &self.0[..] { - [EvalPart::Literal(x)] => Some(x), - _ => None, + if self.0.as_ref().contains('$') { + None + } else { + Some(&self.0) } } + + + pub fn parse(&self) -> impl Iterator> { + parse_eval(self.0.as_ref()) + } } impl EvalString<&str> { pub fn into_owned(self) -> EvalString { - EvalString( - self.0 - .into_iter() - .map(|part| match part { - EvalPart::Literal(s) => EvalPart::Literal(s.to_owned()), - EvalPart::VarRef(s) => EvalPart::VarRef(s.to_owned()), - }) - .collect(), - ) + EvalString(self.0.to_owned()) } } impl EvalString { pub fn as_cow(&self) -> EvalString> { - EvalString( - self.0 - .iter() - .map(|part| match part { - EvalPart::Literal(s) => EvalPart::Literal(Cow::Borrowed(s.as_ref())), - EvalPart::VarRef(s) => EvalPart::VarRef(Cow::Borrowed(s.as_ref())), - }) - .collect(), - ) + EvalString(Cow::Borrowed(self.0.as_str())) } } impl EvalString<&str> { pub fn as_cow(&self) -> EvalString> { - EvalString( - self.0 - .iter() - .map(|part| match part { - EvalPart::Literal(s) => EvalPart::Literal(Cow::Borrowed(*s)), - EvalPart::VarRef(s) => EvalPart::VarRef(Cow::Borrowed(*s)), - }) - .collect(), - ) + EvalString(Cow::Borrowed(self.0)) } } @@ -132,10 +116,10 @@ impl + PartialEq> Env for SmallMap> { } } -impl Env for SmallMap<&str, String> { - fn get_var(&self, var: &str) -> Option>> { - Some(EvalString::new(vec![EvalPart::Literal( - std::borrow::Cow::Borrowed(self.get(var)?), - )])) - } -} +// impl Env for SmallMap<&str, String> { +// fn get_var(&self, var: &str) -> Option>> { +// Some(EvalString::new(vec![EvalPart::Literal( +// std::borrow::Cow::Borrowed(self.get(var)?), +// )])) +// } +// } diff --git a/src/graph.rs b/src/graph.rs index 3c3a318..2dfab4f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -164,7 +164,7 @@ struct BuildImplicitVars<'a> { impl<'text> crate::eval::Env for BuildImplicitVars<'text> { fn get_var(&self, var: &str) -> Option>> { let string_to_evalstring = - |s: String| Some(EvalString::new(vec![EvalPart::Literal(Cow::Owned(s))])); + |s: String| Some(EvalString::new(Cow::Owned(s))); match var { "in" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join(" ")), "in_newline" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join("\n")), diff --git a/src/parse.rs b/src/parse.rs index 7a4274d..5dcb831 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -147,9 +147,6 @@ pub struct Parser<'text> { filename: Arc, scanner: Scanner<'text>, buf_len: usize, - /// Reading EvalStrings is very hot when parsing, so we always read into - /// this buffer and then clone it afterwards. - eval_buf: Vec>, } impl<'text> Parser<'text> { @@ -158,7 +155,6 @@ impl<'text> Parser<'text> { filename, scanner: Scanner::new(buf, chunk_index), buf_len: buf.len(), - eval_buf: Vec::with_capacity(16), } } @@ -286,7 +282,7 @@ impl<'text> Parser<'text> { if self.scanner.peek_newline() { self.scanner.skip('\r'); self.scanner.expect('\n')?; - return Ok(EvalString::new(Vec::new())); + return Ok(EvalString::new("")); } let result = self.read_eval(false); self.scanner.skip('\r'); @@ -504,8 +500,9 @@ impl<'text> Parser<'text> { /// stop_at_path_separators is set, without consuming the character that /// caused it to stop. fn read_eval(&mut self, stop_at_path_separators: bool) -> ParseResult> { - self.eval_buf.clear(); + let start = self.scanner.ofs; let mut ofs = self.scanner.ofs; + let mut found_content = false; // This match block is copied twice, with the only difference being the check for // spaces, colons, and pipes in the stop_at_path_separators version. We could remove the // duplication by adding a match branch like `' ' | ':' | '|' if stop_at_path_separators =>` @@ -525,13 +522,8 @@ impl<'text> Parser<'text> { break self.scanner.ofs; } '$' => { - let end = self.scanner.ofs - 1; - if end > ofs { - self.eval_buf - .push(EvalPart::Literal(self.scanner.slice(ofs, end))); - } - let escape = self.read_escape()?; - self.eval_buf.push(escape); + self.read_escape()?; + found_content = true; ofs = self.scanner.ofs; } _ => {} @@ -550,13 +542,8 @@ impl<'text> Parser<'text> { break self.scanner.ofs; } '$' => { - let end = self.scanner.ofs - 1; - if end > ofs { - self.eval_buf - .push(EvalPart::Literal(self.scanner.slice(ofs, end))); - } - let escape = self.read_escape()?; - self.eval_buf.push(escape); + self.read_escape()?; + found_content = true; ofs = self.scanner.ofs; } _ => {} @@ -564,13 +551,12 @@ impl<'text> Parser<'text> { } }; if end > ofs { - self.eval_buf - .push(EvalPart::Literal(self.scanner.slice(ofs, end))); + found_content = true; } - if self.eval_buf.is_empty() { + if !found_content { return self.scanner.parse_error(format!("Expected a string")); } - Ok(EvalString::new(self.eval_buf.clone())) + Ok(EvalString::new(self.scanner.slice(start, end))) } /// Read a variable name as found after a '$' in an eval. @@ -578,7 +564,7 @@ impl<'text> Parser<'text> { /// period allowed(!), I guess because we expect things like /// foo = $bar.d /// to parse as a reference to $bar. - fn read_simple_varname(&mut self) -> ParseResult<&'text str> { + fn read_simple_varname(&mut self) -> ParseResult<()> { let start = self.scanner.ofs; while matches!(self.scanner.read(), 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-') {} self.scanner.back(); @@ -586,21 +572,17 @@ impl<'text> Parser<'text> { if end == start { return self.scanner.parse_error("failed to scan variable name"); } - Ok(self.scanner.slice(start, end)) + Ok(()) } /// Read and interpret the text following a '$' escape character. - fn read_escape(&mut self) -> ParseResult> { + fn read_escape(&mut self) -> ParseResult<()> { Ok(match self.scanner.read() { '\n' | '\r' => { self.scanner.skip_spaces(); - EvalPart::Literal(self.scanner.slice(0, 0)) - } - ' ' | '$' | ':' => { - EvalPart::Literal(self.scanner.slice(self.scanner.ofs - 1, self.scanner.ofs)) } + ' ' | '$' | ':' => (), '{' => { - let start = self.scanner.ofs; loop { match self.scanner.read() { '\0' => return self.scanner.parse_error("unexpected EOF"), @@ -608,14 +590,11 @@ impl<'text> Parser<'text> { _ => {} } } - let end = self.scanner.ofs - 1; - EvalPart::VarRef(self.scanner.slice(start, end)) } _ => { // '$' followed by some other text. self.scanner.back(); - let var = self.read_simple_varname()?; - EvalPart::VarRef(var) + self.read_simple_varname()?; } }) } @@ -687,6 +666,85 @@ fn find_start_of_next_manifest_chunk(buf: &[u8], prospective_start: usize) -> us } } +struct EvalParser<'a> { + buf: &'a [u8], + offset: usize, +} + +impl<'a> EvalParser<'a> { + fn peek(&self) -> u8 { + unsafe { *self.buf.get_unchecked(self.offset) } + } + fn read(&mut self) -> u8 { + let c = self.peek(); + self.offset += 1; + c + } + fn slice(&self, start: usize, end: usize) -> &'a str { + unsafe { std::str::from_utf8_unchecked(self.buf.get_unchecked(start..end)) } + } +} + +impl<'a> Iterator for EvalParser<'a> { + type Item = EvalPart<&'a str>; + + fn next(&mut self) -> Option { + let mut start = self.offset; + while self.offset < self.buf.len() { + match self.peek() { + b'$' => { + if self.offset > start { + return Some(EvalPart::Literal(self.slice(start, self.offset))) + } + self.offset += 1; + match self.peek() { + b'\n' | b'\r' => { + self.offset += 1; + while self.offset < self.buf.len() && self.peek() == b' ' { + self.offset += 1; + } + start = self.offset; + } + b' ' | b'$' | b':' => { + start = self.offset; + self.offset += 1; + } + b'{' => { + self.offset += 1; + start = self.offset; + while self.read() != b'}' {} + let end = self.offset - 1; + return Some(EvalPart::VarRef(self.slice(start, end))); + } + _ => { + // '$' followed by some other text. + start = self.offset; + while self.offset < self.buf.len() && matches!(self.peek(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-') { + self.offset += 1; + } + return Some(EvalPart::VarRef(self.slice(start, self.offset))) + } + } + } + _ => self.offset += 1, + } + } + if self.offset > start { + return Some(EvalPart::Literal(self.slice(start, self.offset))) + } + None + } +} + +// Returns an iterator over teh EvalParts in the given string. Note that the +// string must be a valid EvalString, or undefined behavior will occur. +pub fn parse_eval(buf: &str) -> impl Iterator> { + return EvalParser { + buf: buf.as_bytes(), + offset: 0, + } +} + #[cfg(test)] mod tests { use super::*; @@ -719,11 +777,11 @@ mod tests { stmt => panic!("expected default, got {:?}", stmt), }; assert_eq!( - default, + default.iter().map(|x| x.parse().collect::>()).collect::>(), vec![ - EvalString::new(vec![EvalPart::Literal("a")]), - EvalString::new(vec![EvalPart::Literal("b"), EvalPart::VarRef("var")]), - EvalString::new(vec![EvalPart::Literal("c")]), + vec![EvalPart::Literal("a")], + vec![EvalPart::Literal("b"), EvalPart::VarRef("var")], + vec![EvalPart::Literal("c")], ] ); }); @@ -738,8 +796,8 @@ mod tests { }; assert_eq!(name, "x"); assert_eq!( - x.unevaluated, - EvalString::new(vec![EvalPart::VarRef("y"), EvalPart::Literal(".z"),]) + x.unevaluated.parse().collect::>(), + vec![EvalPart::VarRef("y"), EvalPart::Literal(".z")] ); } @@ -754,7 +812,7 @@ mod tests { assert_eq!(stmt.vars.len(), 1); assert_eq!( stmt.vars.get("command"), - Some(&EvalString::new(vec![EvalPart::Literal("x".to_owned()),])) + Some(&EvalString::new("x".to_owned())) ); } From 1718839e7877e7e1add116772df58e7342c311f0 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Tue, 27 Feb 2024 11:42:32 -0800 Subject: [PATCH 25/29] owned evalstring parsing --- src/eval.rs | 16 +++++++++++++++- src/graph.rs | 2 +- src/load.rs | 1 - src/parse.rs | 17 ++++++++--------- src/scanner.rs | 7 +++++-- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index c1ebe3b..ea822a1 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -4,7 +4,6 @@ use crate::load::Scope; use crate::load::ScopePosition; use crate::parse::parse_eval; -use crate::parse::Parser; use crate::smallmap::SmallMap; use std::borrow::Borrow; use std::borrow::Cow; @@ -23,6 +22,21 @@ pub enum EvalPart> { VarRef(T), } +impl EvalPart<&str> { + pub fn into_owned(&self) -> EvalPart { + match self { + EvalPart::Literal(x) => EvalPart::Literal((*x).to_owned()), + EvalPart::VarRef(x) => EvalPart::VarRef((*x).to_owned()), + } + } +} + +impl> Default for EvalPart { + fn default() -> Self { + EvalPart::Literal(T::default()) + } +} + /// A parsed but unexpanded variable-reference string, e.g. "cc $in -o $out". /// This is generic to support EvalString<&str>, which is used for immediately- /// expanded evals, like top-level bindings, and EvalString, which is diff --git a/src/graph.rs b/src/graph.rs index 2dfab4f..c7278b9 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -4,7 +4,7 @@ use anyhow::bail; use rustc_hash::{FxHashMap, FxHasher}; use crate::{ - concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::{EvalPart, EvalString}, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap + concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::EvalString, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap }; use std::{borrow::Cow, time::SystemTime}; use std::{collections::HashMap, sync::Arc}; diff --git a/src/load.rs b/src/load.rs index eca7ba9..1a52446 100644 --- a/src/load.rs +++ b/src/load.rs @@ -10,7 +10,6 @@ use std::{ hash::BuildHasherDefault, path::Path, time::Instant }; use std::{ - cmp::Ordering, collections::hash_map::Entry, sync::Arc, thread::available_parallelism, diff --git a/src/parse.rs b/src/parse.rs index 5dcb831..36609d6 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -294,8 +294,8 @@ impl<'text> Parser<'text> { fn read_scoped_vars( &mut self, variable_name_validator: fn(var: &str) -> bool, - ) -> ParseResult> { - let mut vars = VarList::default(); + ) -> ParseResult>> { + let mut vars = SmallMap::default(); while self.scanner.peek() == ' ' { self.scanner.skip_spaces(); let name = self.read_ident()?; @@ -304,8 +304,8 @@ impl<'text> Parser<'text> { .parse_error(format!("unexpected variable {:?}", name))?; } self.skip_spaces(); - let val = self.read_vardef()?; - vars.insert(name, val); + let val = self.read_vardef()?.into_owned(); + vars.insert(name.to_owned(), val); } Ok(vars) } @@ -331,7 +331,7 @@ impl<'text> Parser<'text> { ) })?; Ok((name.to_owned(), Rule { - vars: vars.to_owned(), + vars, scope_position: ScopePosition(0), })) } @@ -351,7 +351,7 @@ impl<'text> Parser<'text> { None => { return self .scanner - .parse_error(format!("pool depth must be a literal string")) + .parse_error(format!("pool depth must be a literal string, got: {:?}", val)) } } } @@ -363,8 +363,7 @@ impl<'text> Parser<'text> { v: &mut Vec>, ) -> ParseResult<()> { self.skip_spaces(); - while self.scanner.peek() != ':' - && self.scanner.peek() != '|' + while !matches!(self.scanner.peek(), ':' | '|') && !self.scanner.peek_newline() { v.push(self.read_eval(true)?); @@ -433,7 +432,7 @@ impl<'text> Parser<'text> { Ok(Build::new( rule.to_owned(), - vars.to_owned(), + vars, FileLoc { filename: self.filename.clone(), line, diff --git a/src/scanner.rs b/src/scanner.rs index 4229a68..bb5539e 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -34,25 +34,28 @@ impl<'a> Scanner<'a> { unsafe { *self.buf.get_unchecked(self.ofs) as char } } pub fn peek_newline(&self) -> bool { - if self.peek() == '\n' { + let peek = self.peek(); + if peek == '\n' { return true; } if self.ofs >= self.buf.len() - 1 { return false; } let peek2 = unsafe { *self.buf.get_unchecked(self.ofs + 1) as char }; - self.peek() == '\r' && peek2 == '\n' + peek == '\r' && peek2 == '\n' } pub fn next(&mut self) { if self.peek() == '\n' { self.line += 1; } + #[cfg(debug_assertions)] if self.ofs == self.buf.len() { panic!("scanned past end") } self.ofs += 1; } pub fn back(&mut self) { + #[cfg(debug_assertions)] if self.ofs == 0 { panic!("back at start") } From 04ec995a6c14ad1896965766a40b8d4526db0816 Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Tue, 27 Feb 2024 16:21:58 -0800 Subject: [PATCH 26/29] Fix bug where build paths were evaluated an extra time This should also improve performance of evaluation in general slightly. --- src/eval.rs | 61 +++++++++++----------------------------------- src/graph.rs | 46 ++++++++++++++++++++++++++-------- src/hash.rs | 2 +- src/parse.rs | 8 +++--- src/progress.rs | 8 +++--- src/task.rs | 4 +-- src/work.rs | 8 +++--- tests/e2e/basic.rs | 18 ++++++++++++++ 8 files changed, 82 insertions(+), 73 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index ea822a1..91f7f55 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -6,13 +6,12 @@ use crate::load::ScopePosition; use crate::parse::parse_eval; use crate::smallmap::SmallMap; use std::borrow::Borrow; -use std::borrow::Cow; /// An environment providing a mapping of variable name to variable value. /// This represents one "frame" of evaluation context, a given EvalString may /// need multiple environments in order to be fully expanded. pub trait Env { - fn get_var(&self, var: &str) -> Option>>; + fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn Env], scope: &Scope, position: ScopePosition); } /// One token within an EvalString, either literal text or a variable reference. @@ -48,26 +47,14 @@ impl> EvalString { EvalString(inner) } - fn evaluate_inner( - &self, - result: &mut String, - envs: &[&dyn Env], - scope: &Scope, - position: ScopePosition, - ) { + pub fn evaluate_inner(&self, result: &mut String, envs: &[&dyn Env], scope: &Scope, position: ScopePosition) { for part in self.parse() { match part { EvalPart::Literal(s) => result.push_str(s.as_ref()), EvalPart::VarRef(v) => { - let mut found = false; - for (i, env) in envs.iter().enumerate() { - if let Some(v) = env.get_var(v.as_ref()) { - v.evaluate_inner(result, &envs[i + 1..], scope, position); - found = true; - break; - } - } - if !found { + if let Some(env) = envs.first() { + env.evaluate_var(result, v.as_ref(), &envs[1..], scope, position); + } else { scope.evaluate(result, v.as_ref(), position); } } @@ -106,34 +93,14 @@ impl EvalString<&str> { } } -impl EvalString { - pub fn as_cow(&self) -> EvalString> { - EvalString(Cow::Borrowed(self.0.as_str())) - } -} - -impl EvalString<&str> { - pub fn as_cow(&self) -> EvalString> { - EvalString(Cow::Borrowed(self.0)) - } -} - -impl + PartialEq> Env for SmallMap> { - fn get_var(&self, var: &str) -> Option>> { - Some(self.get(var)?.as_cow()) - } -} - -impl + PartialEq> Env for SmallMap> { - fn get_var(&self, var: &str) -> Option>> { - Some(self.get(var)?.as_cow()) +impl + PartialEq, V: AsRef> Env for SmallMap> { + fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn Env], scope: &Scope, position: ScopePosition) { + if let Some(v) = self.get(var) { + v.evaluate_inner(result, envs, scope, position); + } else if let Some(env) = envs.first() { + env.evaluate_var(result, var, &envs[1..], scope, position); + } else { + scope.evaluate(result, var, position); + } } } - -// impl Env for SmallMap<&str, String> { -// fn get_var(&self, var: &str) -> Option>> { -// Some(EvalString::new(vec![EvalPart::Literal( -// std::borrow::Cow::Borrowed(self.get(var)?), -// )])) -// } -// } diff --git a/src/graph.rs b/src/graph.rs index c7278b9..449d62f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -6,7 +6,7 @@ use rustc_hash::{FxHashMap, FxHasher}; use crate::{ concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::EvalString, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap }; -use std::{borrow::Cow, time::SystemTime}; +use std::time::SystemTime; use std::{collections::HashMap, sync::Arc}; use std::{ hash::BuildHasherDefault, @@ -162,15 +162,25 @@ struct BuildImplicitVars<'a> { explicit_outs: &'a [Arc], } impl<'text> crate::eval::Env for BuildImplicitVars<'text> { - fn get_var(&self, var: &str) -> Option>> { - let string_to_evalstring = - |s: String| Some(EvalString::new(Cow::Owned(s))); + fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn crate::eval::Env], scope: &Scope, position: ScopePosition) { + let mut common = |files: &[Arc], sep: &'static str| { + for (i, file) in files.iter().enumerate() { + if i > 0 { + result.push_str(sep); + } + result.push_str(&file.name); + } + }; match var { - "in" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join(" ")), - "in_newline" => string_to_evalstring(self.explicit_ins.iter().map(|x| x.name.as_str()).collect::>().join("\n")), - "out" => string_to_evalstring(self.explicit_outs.iter().map(|x| x.name.as_str()).collect::>().join(" ")), - "out_newline" => string_to_evalstring(self.explicit_outs.iter().map(|x| x.name.as_str()).collect::>().join("\n")), - _ => None, + "in" => common(self.explicit_ins, " "), + "in_newline" => common(self.explicit_ins, "\n"), + "out" => common(self.explicit_outs, " "), + "out_newline" => common(self.explicit_outs, "\n"), + _ => if let Some(env) = envs.first() { + env.evaluate_var(result, var, &envs[1..], scope, position); + } else { + scope.evaluate(result, var, position); + }, } } } @@ -292,7 +302,7 @@ impl Build { &self.outs.ids } - pub fn get_binding(&self, key: &str) -> Option { + fn get_binding(&self, key: &str) -> Option { let implicit_vars = BuildImplicitVars { explicit_ins: &self.ins.ids[..self.ins.explicit], explicit_outs: &self.outs.ids[..self.outs.explicit], @@ -327,6 +337,22 @@ impl Build { Some(other) => bail!("invalid deps attribute {:?}", other), }) } + + pub fn get_cmdline(&self) -> Option { + self.get_binding("command") + } + + pub fn get_description(&self) -> Option { + self.get_binding("description") + } + + pub fn get_depfile(&self) -> Option { + self.get_binding("depfile") + } + + pub fn get_pool(&self) -> Option { + self.get_binding("pool") + } } /// The build graph: owns Files/Builds and maps BuildIds to them. diff --git a/src/hash.rs b/src/hash.rs index 523f5d8..cfef8a9 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -82,7 +82,7 @@ impl Manifest for TerseHash { fn build_manifest(manifest: &mut M, file_state: &FileState, build: &Build) -> anyhow::Result<()> { manifest.write_files("in", file_state, build.dirtying_ins()); manifest.write_files("discovered", file_state, build.discovered_ins()); - manifest.write_cmdline(build.get_binding("command").as_deref().unwrap_or("")); + manifest.write_cmdline(build.get_cmdline().as_deref().unwrap_or("")); if let Some(rspfile) = &build.get_rspfile()? { manifest.write_rsp(rspfile); } diff --git a/src/parse.rs b/src/parse.rs index 36609d6..901303d 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -79,13 +79,11 @@ impl VariableAssignment { return; } - let cache = self.unevaluated.evaluate(&[], scope, self.scope_position); - result.push_str(&cache); - *self.evaluated.get() = cache; - self.is_evaluated - .store(true, std::sync::atomic::Ordering::Relaxed); + self.unevaluated.evaluate_inner(&mut *self.evaluated.get(), &[], scope, self.scope_position); + self.is_evaluated.store(true, std::sync::atomic::Ordering::Relaxed); drop(guard); + result.push_str(&(*self.evaluated.get())); } } } diff --git a/src/progress.rs b/src/progress.rs index a7e8efe..644607f 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -16,9 +16,9 @@ use std::time::Instant; /// Compute the message to display on the console for a given build. pub fn build_message(build: &Build) -> String { build - .get_binding("description") + .get_description() .filter(|desc| !desc.is_empty()) - .unwrap_or_else(|| build.get_binding("command").unwrap()) + .unwrap_or_else(|| build.get_cmdline().unwrap()) } /// Trait for build progress notifications. @@ -80,7 +80,7 @@ impl Progress for DumbConsoleProgress { fn task_started(&mut self, id: BuildId, build: &Build) { if self.verbose { - self.log(build.get_binding("command").as_ref().unwrap()); + self.log(build.get_cmdline().as_ref().unwrap()); } else { self.log(&build_message(build)); } @@ -227,7 +227,7 @@ impl FancyState { fn task_started(&mut self, id: BuildId, build: &Build) { if self.verbose { - self.log(build.get_binding("command").as_ref().unwrap()); + self.log(build.get_cmdline().as_ref().unwrap()); } let message = build_message(build); self.tasks.push_back(Task { diff --git a/src/task.rs b/src/task.rs index 04550e6..1091f79 100644 --- a/src/task.rs +++ b/src/task.rs @@ -211,8 +211,8 @@ impl Runner { } pub fn start(&mut self, id: BuildId, build: &Build) -> anyhow::Result<()> { - let cmdline = build.get_binding("command").clone().unwrap(); - let depfile = build.get_binding("depfile").clone().map(PathBuf::from); + let cmdline = build.get_cmdline().clone().unwrap(); + let depfile = build.get_depfile().clone().map(PathBuf::from); let rspfile = build.get_rspfile()?; let parse_showincludes = build.get_parse_showincludes()?; diff --git a/src/work.rs b/src/work.rs index c13c41d..dab3273 100644 --- a/src/work.rs +++ b/src/work.rs @@ -141,7 +141,7 @@ impl BuildStates { let prev = std::mem::replace(&mut self.states[id], state); // We skip user-facing counters for phony builds. - let skip_ui_count = build.get_binding("command").is_none(); + let skip_ui_count = build.get_cmdline().is_none(); // println!("{:?} {:?}=>{:?} {:?}", id, prev, state, self.counts); if prev == BuildState::Unknown { @@ -271,7 +271,7 @@ impl BuildStates { /// Look up a PoolState by name. fn get_pool(&mut self, build: &Build) -> Option<&mut PoolState> { - let owned_name = build.get_binding("pool"); + let owned_name = build.get_pool(); let name = owned_name.as_deref().unwrap_or(""); for (key, pool) in self.pools.iter_mut() { if key == name { @@ -291,7 +291,7 @@ impl BuildStates { build.location, // Unnamed pool lookups always succeed, this error is about // named pools. - build.get_binding("pool").as_ref().unwrap() + build.get_pool().as_ref().unwrap() ) })?; pool.queued.push_back(id); @@ -604,7 +604,7 @@ impl<'a> Work<'a> { /// Prereq: any dependent input is already generated. fn check_build_dirty(&mut self, id: BuildId) -> anyhow::Result { let build = &self.graph.builds[id]; - let phony = build.get_binding("command").is_none(); + let phony = build.get_cmdline().is_none(); let file_missing = if phony { self.check_build_files_missing_phony(id)?; return Ok(false); // Phony builds never need to run anything. diff --git a/tests/e2e/basic.rs b/tests/e2e/basic.rs index cdff1b3..edaaacf 100644 --- a/tests/e2e/basic.rs +++ b/tests/e2e/basic.rs @@ -183,6 +183,24 @@ rule echo Ok(()) } +#[cfg(unix)] +#[test] +fn dollar_in_filename() -> anyhow::Result<()> { + let space = TestSpace::new()?; + space.write( + "build.ninja", + " +# need a special touch rule that escapes the $ for the shell +rule touch + command = touch '$out' +build out$$foo: touch +", + )?; + space.run_expect(&mut n2_command(vec!["out$foo"]))?; + assert!(space.read("out$foo").is_ok()); + Ok(()) +} + #[test] fn explain() -> anyhow::Result<()> { let space = TestSpace::new()?; From 0d9bf017583fa74007dfbcba45c81a77c8c1383c Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Tue, 27 Feb 2024 17:27:13 -0800 Subject: [PATCH 27/29] Cleanup and prepare for submitting --- src/db.rs | 3 +- src/densemap.rs | 22 --- src/eval.rs | 46 ++--- src/file_pool.rs | 6 +- src/graph.rs | 61 +++++-- src/hash.rs | 6 +- src/load.rs | 284 ++++++++++++++++-------------- src/parse.rs | 189 +++++++++++--------- src/progress.rs | 4 +- src/scanner.rs | 4 +- src/smallmap.rs | 31 ++-- src/trace.rs | 64 +++---- tests/e2e/include_and_subninja.rs | 44 +++-- 13 files changed, 403 insertions(+), 361 deletions(-) diff --git a/src/db.rs b/src/db.rs index 4fd7c67..7157f4b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,8 +3,7 @@ use crate::graph; use crate::{ - densemap, densemap::DenseMap, graph::BuildId, graph::Graph, graph::Hashes, - hash::BuildHash, + densemap, densemap::DenseMap, graph::BuildId, graph::Graph, graph::Hashes, hash::BuildHash, }; use anyhow::{anyhow, bail}; use std::collections::HashMap; diff --git a/src/densemap.rs b/src/densemap.rs index d4afb65..b95b044 100644 --- a/src/densemap.rs +++ b/src/densemap.rs @@ -44,13 +44,6 @@ impl DenseMap { } } - pub fn with_capacity(c: usize) -> Self { - Self { - vec: Vec::with_capacity(c), - key_type: PhantomData, - } - } - pub fn lookup(&self, k: K) -> Option<&V> { self.vec.get(k.index()) } @@ -64,21 +57,6 @@ impl DenseMap { self.vec.push(val); id } - - pub fn keys(&self) -> impl Iterator { - (0..self.vec.len()).map(|x| K::from(x)) - } - - pub fn iter(&self) -> impl Iterator { - self.vec.iter().enumerate().map(|(i, v)| (K::from(i), v)) - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.vec - .iter_mut() - .enumerate() - .map(|(i, v)| (K::from(i), v)) - } } impl DenseMap { diff --git a/src/eval.rs b/src/eval.rs index 91f7f55..be2a8e4 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -3,7 +3,7 @@ use crate::load::Scope; use crate::load::ScopePosition; -use crate::parse::parse_eval; +use crate::parse::EvalParser; use crate::smallmap::SmallMap; use std::borrow::Borrow; @@ -11,7 +11,14 @@ use std::borrow::Borrow; /// This represents one "frame" of evaluation context, a given EvalString may /// need multiple environments in order to be fully expanded. pub trait Env { - fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn Env], scope: &Scope, position: ScopePosition); + fn evaluate_var( + &self, + result: &mut String, + var: &str, + envs: &[&dyn Env], + scope: &Scope, + position: ScopePosition, + ); } /// One token within an EvalString, either literal text or a variable reference. @@ -21,21 +28,6 @@ pub enum EvalPart> { VarRef(T), } -impl EvalPart<&str> { - pub fn into_owned(&self) -> EvalPart { - match self { - EvalPart::Literal(x) => EvalPart::Literal((*x).to_owned()), - EvalPart::VarRef(x) => EvalPart::VarRef((*x).to_owned()), - } - } -} - -impl> Default for EvalPart { - fn default() -> Self { - EvalPart::Literal(T::default()) - } -} - /// A parsed but unexpanded variable-reference string, e.g. "cc $in -o $out". /// This is generic to support EvalString<&str>, which is used for immediately- /// expanded evals, like top-level bindings, and EvalString, which is @@ -47,7 +39,13 @@ impl> EvalString { EvalString(inner) } - pub fn evaluate_inner(&self, result: &mut String, envs: &[&dyn Env], scope: &Scope, position: ScopePosition) { + pub fn evaluate_inner( + &self, + result: &mut String, + envs: &[&dyn Env], + scope: &Scope, + position: ScopePosition, + ) { for part in self.parse() { match part { EvalPart::Literal(s) => result.push_str(s.as_ref()), @@ -81,9 +79,8 @@ impl> EvalString { } } - pub fn parse(&self) -> impl Iterator> { - parse_eval(self.0.as_ref()) + EvalParser::new(self.0.as_ref().as_bytes()) } } @@ -94,7 +91,14 @@ impl EvalString<&str> { } impl + PartialEq, V: AsRef> Env for SmallMap> { - fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn Env], scope: &Scope, position: ScopePosition) { + fn evaluate_var( + &self, + result: &mut String, + var: &str, + envs: &[&dyn Env], + scope: &Scope, + position: ScopePosition, + ) { if let Some(v) = self.get(var) { v.evaluate_inner(result, envs, scope, position); } else if let Some(env) = envs.first() { diff --git a/src/file_pool.rs b/src/file_pool.rs index eb7dbf2..6c8d1ad 100644 --- a/src/file_pool.rs +++ b/src/file_pool.rs @@ -1,8 +1,8 @@ use anyhow::bail; use core::slice; use libc::{ - c_void, mmap, munmap, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, - PROT_READ, PROT_WRITE, _SC_PAGESIZE, + c_void, mmap, munmap, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, PROT_READ, + PROT_WRITE, _SC_PAGESIZE, }; use std::{ os::fd::{AsFd, AsRawFd}, @@ -11,7 +11,7 @@ use std::{ sync::Mutex, }; -/// FilePool is a datastucture that is intended to hold onto byte buffers and give out immutable +/// FilePool is a datastructure that is intended to hold onto byte buffers and give out immutable /// references to them. But it can also accept new byte buffers while old ones are still lent out. /// This requires interior mutability / unsafe code. Appending to a Vec while references to other /// elements are held is generally unsafe, because the Vec can reallocate all the prior elements diff --git a/src/graph.rs b/src/graph.rs index 449d62f..b4b289b 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -4,7 +4,12 @@ use anyhow::bail; use rustc_hash::{FxHashMap, FxHasher}; use crate::{ - concurrent_linked_list::ConcurrentLinkedList, densemap::{self, DenseMap}, eval::EvalString, hash::BuildHash, load::{Scope, ScopePosition}, smallmap::SmallMap + concurrent_linked_list::ConcurrentLinkedList, + densemap::{self, DenseMap}, + eval::EvalString, + hash::BuildHash, + load::{Scope, ScopePosition}, + smallmap::SmallMap, }; use std::time::SystemTime; use std::{collections::HashMap, sync::Arc}; @@ -155,14 +160,20 @@ mod tests { } } - /// A variable lookup environment for magic $in/$out variables. struct BuildImplicitVars<'a> { explicit_ins: &'a [Arc], explicit_outs: &'a [Arc], } impl<'text> crate::eval::Env for BuildImplicitVars<'text> { - fn evaluate_var(&self, result: &mut String, var: &str, envs: &[&dyn crate::eval::Env], scope: &Scope, position: ScopePosition) { + fn evaluate_var( + &self, + result: &mut String, + var: &str, + envs: &[&dyn crate::eval::Env], + scope: &Scope, + position: ScopePosition, + ) { let mut common = |files: &[Arc], sep: &'static str| { for (i, file) in files.iter().enumerate() { if i > 0 { @@ -176,11 +187,13 @@ impl<'text> crate::eval::Env for BuildImplicitVars<'text> { "in_newline" => common(self.explicit_ins, "\n"), "out" => common(self.explicit_outs, " "), "out_newline" => common(self.explicit_outs, "\n"), - _ => if let Some(env) = envs.first() { - env.evaluate_var(result, var, &envs[1..], scope, position); - } else { - scope.evaluate(result, var, position); - }, + _ => { + if let Some(env) = envs.first() { + env.evaluate_var(result, var, &envs[1..], scope, position); + } else { + scope.evaluate(result, var, position); + } + } } } } @@ -190,8 +203,19 @@ impl<'text> crate::eval::Env for BuildImplicitVars<'text> { pub struct Build { pub id: BuildId, + /// The scope that this build is part of. Used when evaluating the build's + /// bindings. pub scope: Option>, + + /// The position of this build in the scope. Used when evaluating the + /// build's bindings. pub scope_position: ScopePosition, + + /// The unevalated output/input files. These strings really have a lifetime + /// of 'text, but we use 'static so that we don't need to add 'text to the + /// build itself. We unsafely cast 'text strings to 'static. The strings + /// are evaluated and this vec is cleared before the lifetime of 'text is + /// over. pub unevaluated_outs_and_ins: Vec>, pub rule: String, @@ -203,6 +227,7 @@ pub struct Build { /// Source location this Build was declared. pub location: FileLoc, + /// Input files. pub ins: BuildIns, /// Additional inputs discovered from a previous build. @@ -310,8 +335,15 @@ impl Build { let scope = self.scope.as_ref().unwrap(); let rule = scope.get_rule(&self.rule, self.scope_position).unwrap(); Some(match rule.vars.get(key) { - Some(val) => val.evaluate(&[&implicit_vars, &self.bindings], scope, self.scope_position), - None => self.bindings.get(key)?.evaluate(&[], scope, self.scope_position), + Some(val) => val.evaluate( + &[&implicit_vars, &self.bindings], + scope, + self.scope_position, + ), + None => self + .bindings + .get(key)? + .evaluate(&[], scope, self.scope_position), }) } @@ -370,13 +402,10 @@ pub struct GraphFiles { } impl Graph { - pub fn from_uninitialized_builds_and_files( - builds: Vec>, - files: dashmap::DashMap, Arc, BuildHasherDefault>, - ) -> anyhow::Result { + pub fn new(builds: Vec>, files: GraphFiles) -> anyhow::Result { let result = Graph { builds: DenseMap::from_vec(builds), - files: GraphFiles { by_name: files }, + files, }; Ok(result) } @@ -428,7 +457,7 @@ impl GraphFiles { /// of this function that accepts string references that is more optimized /// for the case where the entry already exists. But so far, all of our /// usages of this function have an owned string easily accessible anyways. - pub fn id_from_canonical(&mut self, file: String) -> Arc { + pub fn id_from_canonical(&self, file: String) -> Arc { let file = Arc::new(file); match self.by_name.entry(file) { dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), diff --git a/src/hash.rs b/src/hash.rs index cfef8a9..5306ab3 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -79,7 +79,11 @@ impl Manifest for TerseHash { } } -fn build_manifest(manifest: &mut M, file_state: &FileState, build: &Build) -> anyhow::Result<()> { +fn build_manifest( + manifest: &mut M, + file_state: &FileState, + build: &Build, +) -> anyhow::Result<()> { manifest.write_files("in", file_state, build.dirtying_ins()); manifest.write_files("discovered", file_state, build.discovered_ins()); manifest.write_cmdline(build.get_cmdline().as_deref().unwrap_or("")); diff --git a/src/load.rs b/src/load.rs index 1a52446..5806195 100644 --- a/src/load.rs +++ b/src/load.rs @@ -1,20 +1,20 @@ //! Graph loading: runs .ninja parsing and constructs the build graph from it. use crate::{ - canon::canon_path, db, file_pool::FilePool, graph::{self, BuildId, Graph}, parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, scanner::{format_parse_error, ParseResult}, smallmap::SmallMap, trace + canon::canon_path, + db, + file_pool::FilePool, + graph::{self, BuildId, Graph, GraphFiles}, + parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment}, + scanner::{format_parse_error, ParseResult}, + smallmap::SmallMap, + trace, }; use anyhow::{anyhow, bail}; use rayon::prelude::*; -use rustc_hash::{FxHashMap, FxHasher}; -use std::{ - hash::BuildHasherDefault, path::Path, time::Instant -}; -use std::{ - collections::hash_map::Entry, - sync::Arc, - thread::available_parallelism, -}; -use std::path::PathBuf; +use rustc_hash::FxHashMap; +use std::path::{Path, PathBuf}; +use std::{collections::hash_map::Entry, sync::Arc, thread::available_parallelism}; #[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord)] pub struct ScopePosition(pub usize); @@ -74,7 +74,7 @@ impl Scope { if let Some(variables) = self.variables.get(varname) { let i = variables.binary_search_by_key(&position, |x| x.scope_position); let i = match i { - Ok(i) => std::cmp::max(i, 1)-1, + Ok(i) => std::cmp::max(i, 1) - 1, Err(i) => std::cmp::min(i, variables.len() - 1), }; if variables[i].scope_position.0 < position.0 { @@ -90,19 +90,33 @@ impl Scope { } } -fn add_build<'text>( - files: &Files, +fn evaluate_build_files<'text>( + files: &GraphFiles, scope: Arc, b: &mut graph::Build, base_position: ScopePosition, ) -> anyhow::Result<()> { b.scope_position.0 += base_position.0; let num_outs = b.outs.num_outs(); - b.outs.ids = b.unevaluated_outs_and_ins[..num_outs].iter() - .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) + b.outs.ids = b.unevaluated_outs_and_ins[..num_outs] + .iter() + .map(|x| { + files.id_from_canonical(canon_path(x.evaluate( + &[&b.bindings], + &scope, + b.scope_position, + ))) + }) .collect(); - b.ins.ids = b.unevaluated_outs_and_ins[num_outs..].iter() - .map(|x| files.id_from_canonical(canon_path(x.evaluate(&[&b.bindings], &scope, b.scope_position)))) + b.ins.ids = b.unevaluated_outs_and_ins[num_outs..] + .iter() + .map(|x| { + files.id_from_canonical(canon_path(x.evaluate( + &[&b.bindings], + &scope, + b.scope_position, + ))) + }) .collect(); // The unevaluated values actually have a lifetime of 'text, not 'static, // so clear them so they don't accidentally get used later. @@ -113,34 +127,6 @@ fn add_build<'text>( Ok(()) } -struct Files { - by_name: dashmap::DashMap, Arc, BuildHasherDefault>, -} -impl Files { - pub fn new() -> Self { - Self { - by_name: dashmap::DashMap::with_hasher(BuildHasherDefault::default()), - } - } - - pub fn id_from_canonical(&self, file: String) -> Arc { - match self.by_name.entry(Arc::new(file)) { - dashmap::mapref::entry::Entry::Occupied(o) => o.get().clone(), - dashmap::mapref::entry::Entry::Vacant(v) => { - let mut f = graph::File::default(); - f.name = v.key().clone(); - let f = Arc::new(f); - v.insert(f.clone()); - f - } - } - } - - pub fn into_maps(self) -> dashmap::DashMap, Arc, BuildHasherDefault> { - self.by_name - } -} - #[derive(Default)] struct SubninjaResults<'text> { clumps: Vec>, @@ -149,7 +135,7 @@ struct SubninjaResults<'text> { fn subninja<'thread, 'text>( num_threads: usize, - files: &'thread Files, + files: &'thread GraphFiles, file_pool: &'text FilePool, path: String, parent_scope: Option, @@ -178,7 +164,11 @@ where file_pool.read_file(&filename)?, &mut scope, // to account for the phony rule - if top_level_scope { ScopePosition(1) } else { ScopePosition(0) }, + if top_level_scope { + ScopePosition(1) + } else { + ScopePosition(0) + }, ) })?; @@ -188,32 +178,53 @@ where let base_position = clump.base_position; for default in clump.defaults.iter_mut() { let scope = scope.clone(); - default.evaluated = default.files.iter().map(|x| { - let path = canon_path(x.evaluate(&[], &scope, default.scope_position.add(base_position))); - files.id_from_canonical(path) - }).collect(); + default.evaluated = default + .files + .iter() + .map(|x| { + let path = canon_path(x.evaluate( + &[], + &scope, + default.scope_position.add(base_position), + )); + files.id_from_canonical(path) + }) + .collect(); } } - scope.variables.par_iter().flat_map(|x| x.1.par_iter()).for_each(|x| { - let mut result = String::new(); - x.evaluate(&mut result, &scope); - }); - - trace::scope("add builds", || -> anyhow::Result<()> { + trace::scope("evaluate builds' files", || -> anyhow::Result<()> { parse_results .par_iter_mut() .flat_map(|x| { let num_builds = x.builds.len(); - x.builds.par_iter_mut().zip(rayon::iter::repeatn(x.base_position, num_builds)) + x.builds + .par_iter_mut() + .zip(rayon::iter::repeatn(x.base_position, num_builds)) }) .try_for_each(|(mut build, base_position)| -> anyhow::Result<()> { - add_build(files, scope.clone(), &mut build, base_position) + evaluate_build_files(files, scope.clone(), &mut build, base_position) }) })?; - let mut subninja_results = parse_results.par_iter() - .flat_map(|x| x.subninjas.par_iter().zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))) + // The unevaluated values of scoped variables have a lifetime of 'static + // for simplicity in the code, but in actuality their lifetime is 'text. + // We need to evaluate all the variables before the lifetime of 'text ends. + scope + .variables + .par_iter() + .flat_map(|x| x.1.par_iter()) + .for_each(|x| { + x.pre_evaluate(&scope); + }); + + let mut subninja_results = parse_results + .par_iter() + .flat_map(|x| { + x.subninjas + .par_iter() + .zip(rayon::iter::repeatn(x.base_position, x.subninjas.len())) + }) .map(|(sn, base_position)| -> anyhow::Result> { let position = sn.scope_position.add(base_position); let file = canon_path(sn.file.evaluate(&[], &scope, position)); @@ -223,7 +234,8 @@ where file_pool, file, Some(ParentScopeReference(scope.clone(), position)), - )?.clumps) + )? + .clumps) }) .collect::>>>>()?; @@ -272,18 +284,6 @@ where ) } -fn add_pool<'text>( - pools: &mut SmallMap, - name: String, - depth: usize, -) -> anyhow::Result<()> { - if let Some(_) = pools.get(&name) { - bail!("duplicate pool {}", name); - } - pools.insert(name, depth); - Ok(()) -} - fn parse<'thread, 'text>( filename: &Arc, num_threads: usize, @@ -303,16 +303,20 @@ where .map(|(i, chunk)| { let mut parser = parse::Parser::new(chunk, filename.clone(), i); parser.read_clumps() - }).collect(); + }) + .collect(); let Ok(statements) = statements else { let err = statements.unwrap_err(); let ofs = chunks[..err.chunk_index].iter().map(|x| x.len()).sum(); - bail!(format_parse_error(ofs, chunks[err.chunk_index], filename, err)); + bail!(format_parse_error( + ofs, + chunks[err.chunk_index], + filename, + err + )); }; - let start = Instant::now(); - let mut num_rules = 0; let mut num_variables = 0; let mut num_clumps = 0; @@ -334,7 +338,7 @@ where for stmt in statements.into_iter().flatten() { match stmt { ClumpOrInclude::Clump(mut clump) => { - // Variable assignemnts must be added to the scope now, because + // Variable assignments must be added to the scope now, because // they may be referenced by a later include. Also add rules // while we're at it, to avoid some copies later on. let rules = std::mem::take(&mut clump.rules); @@ -360,28 +364,38 @@ where Entry::Occupied(e) => bail!("duplicate rule '{}'", e.key()), Entry::Vacant(e) => { e.insert(rule); - }, + } } } Ok(()) - } - ).1?; + }, + ) + .1?; clump.base_position = clump_base_position; clump_base_position.0 += clump.used_scope_positions; results.push(clump); - }, + } ClumpOrInclude::Include(i) => { trace::scope("include", || -> anyhow::Result<()> { let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position)); - let mut new_results = include(filename, num_threads, file_pool, evaluated, scope, clump_base_position)?; - clump_base_position = new_results.last().map(|c| c.base_position.add_usize(c.used_scope_positions)).unwrap_or(clump_base_position); + let mut new_results = include( + filename, + num_threads, + file_pool, + evaluated, + scope, + clump_base_position, + )?; + clump_base_position = new_results + .last() + .map(|c| c.base_position.add_usize(c.used_scope_positions)) + .unwrap_or(clump_base_position); results.append(&mut new_results); Ok(()) })?; - }, + } } } - trace::write_complete("parse loop", start, Instant::now()); Ok(results) } @@ -399,61 +413,61 @@ pub struct State { pub fn read(build_filename: &str) -> anyhow::Result { let build_filename = canon_path(build_filename); let file_pool = FilePool::new(); - let files = Files::new(); + let files = GraphFiles::default(); let num_threads = available_parallelism()?.get(); let pool = rayon::ThreadPoolBuilder::new() .num_threads(num_threads) .build()?; - let (defaults, builddir, pools, builds) = trace::scope("loader.read_file", || -> anyhow::Result<_> { - pool.scope(|_| { - let mut results = subninja( - num_threads, - &files, - &file_pool, - build_filename, - None, - )?; - - let mut pools = SmallMap::default(); - let mut defaults = Vec::new(); - let mut num_builds = 0; - trace::scope("add pools and defaults", || -> anyhow::Result<()> { - for clump in &mut results.clumps { - for pool in &clump.pools { - add_pool(&mut pools, pool.name.to_owned(), pool.depth)?; - } - for default in &mut clump.defaults { - defaults.append(&mut default.evaluated); + let (defaults, builddir, pools, builds) = + trace::scope("loader.read_file", || -> anyhow::Result<_> { + pool.scope(|_| { + let mut results = subninja(num_threads, &files, &file_pool, build_filename, None)?; + + let mut pools = SmallMap::default(); + let mut defaults = Vec::new(); + let mut num_builds = 0; + trace::scope("add pools and defaults", || -> anyhow::Result<()> { + for clump in &mut results.clumps { + for pool in &clump.pools { + if !pools.insert_if_absent(pool.name.to_owned(), pool.depth) { + bail!("duplicate pool {}", pool.name); + } + } + for default in &mut clump.defaults { + defaults.append(&mut default.evaluated); + } + num_builds += clump.builds.len(); } - num_builds += clump.builds.len(); - } - Ok(()) - })?; - let mut builds = trace::scope("allocate and concat builds", || { - let mut builds = Vec::with_capacity(num_builds); - for clump in &mut results.clumps { - builds.append(&mut clump.builds); - } - builds - }); - let builddir = results.builddir.take(); - drop(results); - // Turns out munmap is rather slow, unmapping the android ninja - // files takes ~150ms. Do this in parallel with initialize_build. - rayon::spawn(move || { - drop(file_pool); - }); - trace::scope("initialize builds", move || { - builds.par_iter_mut().enumerate().try_for_each(|(id, build)| { - build.id = BuildId::from(id); - graph::Graph::initialize_build(build) + Ok(()) })?; - Ok((defaults, builddir, pools, builds)) + let mut builds = trace::scope("allocate and concat builds", || { + let mut builds = Vec::with_capacity(num_builds); + for clump in &mut results.clumps { + builds.append(&mut clump.builds); + } + builds + }); + let builddir = results.builddir.take(); + drop(results); + // Turns out munmap is rather slow, unmapping the android ninja + // files takes ~150ms. Do this in parallel with initialize_build. + rayon::spawn(move || { + drop(file_pool); + }); + trace::scope("initialize builds", move || { + builds + .par_iter_mut() + .enumerate() + .try_for_each(|(id, build)| { + build.id = BuildId::from(id); + graph::Graph::initialize_build(build) + })?; + Ok((defaults, builddir, pools, builds)) + }) }) - }) - })?; + })?; - let mut graph = Graph::from_uninitialized_builds_and_files(builds, files.into_maps())?; + let mut graph = Graph::new(builds, files)?; let mut hashes = graph::Hashes::default(); let db = trace::scope("db::open", || { let mut db_path = PathBuf::from(".n2_db"); diff --git a/src/parse.rs b/src/parse.rs index 901303d..91b54ba 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -6,7 +6,11 @@ //! text, marked with the lifetime `'text`. use crate::{ - eval::{EvalPart, EvalString}, graph::{self, Build, BuildIns, BuildOuts, FileLoc}, load::{Scope, ScopePosition}, scanner::{ParseResult, Scanner}, smallmap::SmallMap + eval::{EvalPart, EvalString}, + graph::{self, Build, BuildIns, BuildOuts, FileLoc}, + load::{Scope, ScopePosition}, + scanner::{ParseResult, Scanner}, + smallmap::SmallMap, }; use std::{ cell::UnsafeCell, @@ -14,11 +18,7 @@ use std::{ sync::{atomic::AtomicBool, Arc, Mutex}, }; -/// A list of variable bindings, as expressed with syntax like: -/// key = $val -pub type VarList<'text> = SmallMap<&'text str, EvalString<&'text str>>; - -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct Rule { pub vars: SmallMap>, pub scope_position: ScopePosition, @@ -54,6 +54,8 @@ pub struct VariableAssignment { pub lock: Mutex<()>, } +// SAFETY: Sync is not automatically implemented because of the UnsafeCell, +// but our usage of the UnsafeCell is guarded byhind a Mutex. unsafe impl Sync for VariableAssignment {} impl VariableAssignment { @@ -68,23 +70,35 @@ impl VariableAssignment { } pub fn evaluate(&self, result: &mut String, scope: &Scope) { + self.pre_evaluate(scope); unsafe { - if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { - result.push_str(&(*self.evaluated.get())); - return; - } - let guard = self.lock.lock().unwrap(); - if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { - result.push_str(&(*self.evaluated.get())); - return; - } + result.push_str(&(*self.evaluated.get())); + } + } - self.unevaluated.evaluate_inner(&mut *self.evaluated.get(), &[], scope, self.scope_position); - self.is_evaluated.store(true, std::sync::atomic::Ordering::Relaxed); + // This is the same as evaluate, but doesn't give any results back. + // Used to evalutate all the variables before 'text is over, as unevaluated + // should really have a 'text lifetime. + pub fn pre_evaluate(&self, scope: &Scope) { + if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + let guard = self.lock.lock().unwrap(); + if self.is_evaluated.load(std::sync::atomic::Ordering::Relaxed) { + return; + } - drop(guard); - result.push_str(&(*self.evaluated.get())); + unsafe { + self.unevaluated.evaluate_inner( + &mut *self.evaluated.get(), + &[], + scope, + self.scope_position, + ); } + self.is_evaluated + .store(true, std::sync::atomic::Ordering::Relaxed); + drop(guard); } } @@ -112,6 +126,9 @@ pub enum Statement<'text> { VariableAssignment((String, VariableAssignment)), } +// Grouping the parse results into clumps allows us to do fewer vector +// concatenations when merging results of different chunks or different files +// together. #[derive(Default, Debug)] pub struct Clump<'text> { pub assignments: Vec<(String, VariableAssignment)>, @@ -126,12 +143,12 @@ pub struct Clump<'text> { impl<'text> Clump<'text> { pub fn is_empty(&self) -> bool { - self.assignments.is_empty() && - self.rules.is_empty() && - self.pools.is_empty() && - self.defaults.is_empty() && - self.builds.is_empty() && - self.subninjas.is_empty() + self.assignments.is_empty() + && self.rules.is_empty() + && self.pools.is_empty() + && self.defaults.is_empty() + && self.builds.is_empty() + && self.subninjas.is_empty() } } @@ -156,14 +173,6 @@ impl<'text> Parser<'text> { } } - pub fn read_all(&mut self) -> ParseResult>> { - let mut result = Vec::new(); - while let Some(stmt) = self.read()? { - result.push(stmt) - } - Ok(result) - } - pub fn read_clumps(&mut self) -> ParseResult>> { let mut result = Vec::new(); let mut clump = Clump::default(); @@ -174,17 +183,17 @@ impl<'text> Parser<'text> { r.1.scope_position = position; position.0 += 1; clump.rules.push(r); - }, + } Statement::Build(mut b) => { b.scope_position = position; position.0 += 1; clump.builds.push(b); - }, + } Statement::Default(mut d) => { d.scope_position = position; position.0 += 1; clump.defaults.push(d); - }, + } Statement::Include(i) => { if !clump.is_empty() { clump.used_scope_positions = position.0; @@ -193,20 +202,20 @@ impl<'text> Parser<'text> { position = ScopePosition(0); } result.push(ClumpOrInclude::Include(i.file)); - }, + } Statement::Subninja(mut s) => { s.scope_position = position; position.0 += 1; clump.subninjas.push(s); - }, + } Statement::Pool(p) => { clump.pools.push(p); - }, + } Statement::VariableAssignment(mut v) => { v.1.scope_position = position; position.0 += 1; clump.assignments.push(v); - }, + } } } if !clump.is_empty() { @@ -259,11 +268,22 @@ impl<'text> Parser<'text> { "pool" => return Ok(Some(Statement::Pool(self.read_pool()?))), ident => { let x = self.read_vardef()?; + // SAFETY: We need to make sure we call evaluate + // or pre_evaluate on all VariableAssignments before + // the lifetime of 'text is over. After evaluating, + // the VariableAssignments will cache their owned + // Strings. let x = unsafe { - std::mem::transmute::, EvalString<&'static str>>(x) + std::mem::transmute::< + EvalString<&'text str>, + EvalString<&'static str>, + >(x) }; let result = VariableAssignment::new(x); - return Ok(Some(Statement::VariableAssignment((ident.to_owned(), result)))); + return Ok(Some(Statement::VariableAssignment(( + ident.to_owned(), + result, + )))); } } } @@ -328,10 +348,13 @@ impl<'text> Parser<'text> { | "msvc_deps_prefix" ) })?; - Ok((name.to_owned(), Rule { - vars, - scope_position: ScopePosition(0), - })) + Ok(( + name.to_owned(), + Rule { + vars, + scope_position: ScopePosition(0), + }, + )) } fn read_pool(&mut self) -> ParseResult> { @@ -347,9 +370,10 @@ impl<'text> Parser<'text> { Err(err) => return self.scanner.parse_error(format!("pool depth: {}", err)), }, None => { - return self - .scanner - .parse_error(format!("pool depth must be a literal string, got: {:?}", val)) + return self.scanner.parse_error(format!( + "pool depth must be a literal string, got: {:?}", + val + )) } } } @@ -361,9 +385,7 @@ impl<'text> Parser<'text> { v: &mut Vec>, ) -> ParseResult<()> { self.skip_spaces(); - while !matches!(self.scanner.peek(), ':' | '|') - && !self.scanner.peek_newline() - { + while !matches!(self.scanner.peek(), ':' | '|') && !self.scanner.peek_newline() { v.push(self.read_eval(true)?); self.skip_spaces(); } @@ -409,7 +431,8 @@ impl<'text> Parser<'text> { self.read_unevaluated_paths_to(&mut outs_and_ins)?; } } - let order_only_ins = outs_and_ins.len() - implicit_ins - explicit_ins - implicit_outs - explicit_outs; + let order_only_ins = + outs_and_ins.len() - implicit_ins - explicit_ins - implicit_outs - explicit_outs; if self.scanner.peek() == '|' { self.scanner.next(); @@ -421,11 +444,13 @@ impl<'text> Parser<'text> { self.scanner.expect('\n')?; let vars = self.read_scoped_vars(|_| true)?; - // We will evaluate the ins/outs into owned strings before 'text is over, - // and we don't want to attach the 'text lifetime to Build. So instead, - // unsafely cast the lifetime to 'static. + // SAFETY: We will evaluate the ins/outs into owned strings before 'text + // is over, and we don't want to attach the 'text lifetime to Build. So + // instead, unsafely cast the lifetime to 'static. let outs_and_ins = unsafe { - std::mem::transmute::>, Vec>>(outs_and_ins) + std::mem::transmute::>, Vec>>( + outs_and_ins, + ) }; Ok(Build::new( @@ -446,7 +471,7 @@ impl<'text> Parser<'text> { explicit: explicit_outs, implicit: implicit_outs, }, - outs_and_ins + outs_and_ins, )) } @@ -579,15 +604,13 @@ impl<'text> Parser<'text> { self.scanner.skip_spaces(); } ' ' | '$' | ':' => (), - '{' => { - loop { - match self.scanner.read() { - '\0' => return self.scanner.parse_error("unexpected EOF"), - '}' => break, - _ => {} - } + '{' => loop { + match self.scanner.read() { + '\0' => return self.scanner.parse_error("unexpected EOF"), + '}' => break, + _ => {} } - } + }, _ => { // '$' followed by some other text. self.scanner.back(); @@ -663,20 +686,28 @@ fn find_start_of_next_manifest_chunk(buf: &[u8], prospective_start: usize) -> us } } -struct EvalParser<'a> { +// An iterator over the EvalParts in the given string. Note that the +// string must be a valid EvalString, or undefined behavior will occur. +pub struct EvalParser<'a> { buf: &'a [u8], offset: usize, } impl<'a> EvalParser<'a> { + pub fn new(buf: &'a [u8]) -> Self { + Self { buf, offset: 0 } + } + fn peek(&self) -> u8 { unsafe { *self.buf.get_unchecked(self.offset) } } + fn read(&mut self) -> u8 { let c = self.peek(); self.offset += 1; c } + fn slice(&self, start: usize, end: usize) -> &'a str { unsafe { std::str::from_utf8_unchecked(self.buf.get_unchecked(start..end)) } } @@ -691,7 +722,7 @@ impl<'a> Iterator for EvalParser<'a> { match self.peek() { b'$' => { if self.offset > start { - return Some(EvalPart::Literal(self.slice(start, self.offset))) + return Some(EvalPart::Literal(self.slice(start, self.offset))); } self.offset += 1; match self.peek() { @@ -716,10 +747,12 @@ impl<'a> Iterator for EvalParser<'a> { _ => { // '$' followed by some other text. start = self.offset; - while self.offset < self.buf.len() && matches!(self.peek(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-') { + while self.offset < self.buf.len() + && matches!(self.peek(), b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-') + { self.offset += 1; } - return Some(EvalPart::VarRef(self.slice(start, self.offset))) + return Some(EvalPart::VarRef(self.slice(start, self.offset))); } } } @@ -727,21 +760,12 @@ impl<'a> Iterator for EvalParser<'a> { } } if self.offset > start { - return Some(EvalPart::Literal(self.slice(start, self.offset))) + return Some(EvalPart::Literal(self.slice(start, self.offset))); } None } } -// Returns an iterator over teh EvalParts in the given string. Note that the -// string must be a valid EvalString, or undefined behavior will occur. -pub fn parse_eval(buf: &str) -> impl Iterator> { - return EvalParser { - buf: buf.as_bytes(), - offset: 0, - } -} - #[cfg(test)] mod tests { use super::*; @@ -774,7 +798,10 @@ mod tests { stmt => panic!("expected default, got {:?}", stmt), }; assert_eq!( - default.iter().map(|x| x.parse().collect::>()).collect::>(), + default + .iter() + .map(|x| x.parse().collect::>()) + .collect::>(), vec![ vec![EvalPart::Literal("a")], vec![EvalPart::Literal("b"), EvalPart::VarRef("var")], diff --git a/src/progress.rs b/src/progress.rs index 644607f..6533f95 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -100,7 +100,9 @@ impl Progress for DumbConsoleProgress { self.log(&build_message(build)) } } - Termination::Interrupted => self.log(&format!("interrupted: {}", &build_message(build))), + Termination::Interrupted => { + self.log(&format!("interrupted: {}", &build_message(build))) + } Termination::Failure => self.log(&format!("failed: {}", &build_message(build))), }; if !result.output.is_empty() { diff --git a/src/scanner.rs b/src/scanner.rs index bb5539e..717b388 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -2,9 +2,9 @@ use std::{io::Read, path::Path}; -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct ParseError { - pub msg: String, + msg: String, ofs: usize, pub chunk_index: usize, } diff --git a/src/smallmap.rs b/src/smallmap.rs index 76d4376..88ec041 100644 --- a/src/smallmap.rs +++ b/src/smallmap.rs @@ -4,11 +4,8 @@ use std::{borrow::Borrow, fmt::Debug}; -use crate::eval::EvalString; - /// A map-like object implemented as a list of pairs, for cases where the /// number of entries in the map is small. -#[derive(Debug)] pub struct SmallMap(Vec<(K, V)>); impl SmallMap { @@ -37,6 +34,17 @@ impl SmallMap { self.0.push((k, v)); } + // returns true if value was inserted, false if the key was already present. + pub fn insert_if_absent(&mut self, k: K, v: V) -> bool { + for (ik, _) in self.0.iter_mut() { + if *ik == k { + return false; + } + } + self.0.push((k, v)); + true + } + pub fn get(&self, q: &Q) -> Option<&V> where K: Borrow, @@ -90,20 +98,3 @@ impl PartialEq for SmallMap { return self.0 == other.0; } } - -// TODO: Make this not order-sensitive -impl PartialEq for SmallMap { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -impl SmallMap<&str, EvalString<&str>> { - pub fn to_owned(self) -> SmallMap> { - let mut result = SmallMap::default(); - for (k, v) in self.into_iter() { - result.insert(k.to_owned(), v.into_owned()); - } - result - } -} diff --git a/src/trace.rs b/src/trace.rs index ee74514..c862bfc 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -58,29 +58,28 @@ impl Trace { /* These functions were useful when developing, but are currently unused. - */ - // pub fn write_instant(&mut self, name: &str) { - // self.write_event_prefix(name, Instant::now()); - // writeln!(self.w, "\"ph\":\"i\"}}").unwrap(); - // } + pub fn write_instant(&mut self, name: &str) { + self.write_event_prefix(name, Instant::now()); + writeln!(self.w, "\"ph\":\"i\"}}").unwrap(); + } - // pub fn write_counts<'a>( - // &mut self, - // name: &str, - // counts: impl Iterator, - // ) { - // self.write_event_prefix(name, Instant::now()); - // write!(self.w, "\"ph\":\"C\", \"args\":{{").unwrap(); - // for (i, (name, count)) in counts.enumerate() { - // if i > 0 { - // write!(self.w, ",").unwrap(); - // } - // write!(self.w, "\"{}\":{}", name, count).unwrap(); - // } - // writeln!(self.w, "}}}}").unwrap(); - // } - + pub fn write_counts<'a>( + &mut self, + name: &str, + counts: impl Iterator, + ) { + self.write_event_prefix(name, Instant::now()); + write!(self.w, "\"ph\":\"C\", \"args\":{{").unwrap(); + for (i, (name, count)) in counts.enumerate() { + if i > 0 { + write!(self.w, ",").unwrap(); + } + write!(self.w, "\"{}\":{}", name, count).unwrap(); + } + writeln!(self.w, "}}}}").unwrap(); + } + */ fn close(&mut self) { self.write_complete("main", 0, self.start, Instant::now()); @@ -120,29 +119,6 @@ pub fn scope(name: &'static str, f: impl FnOnce() -> T) -> T { } } -// #[inline] -// pub fn write_instant(name: &'static str) { -// // Safety: accessing global mut, not threadsafe. -// unsafe { -// match &mut TRACE { -// None => (), -// Some(t) => t.write_instant(name), -// } -// } -// } - - -#[inline] -pub fn write_complete(name: &'static str, start: Instant, end: Instant) { - // Safety: accessing global mut, not threadsafe. - unsafe { - match &mut TRACE { - None => (), - Some(t) => t.write_complete(name, 0, start, end), - } - } -} - pub fn close() { if_enabled(|t| t.close()); } diff --git a/tests/e2e/include_and_subninja.rs b/tests/e2e/include_and_subninja.rs index b1191cd..425cfac 100644 --- a/tests/e2e/include_and_subninja.rs +++ b/tests/e2e/include_and_subninja.rs @@ -1,10 +1,12 @@ -use crate::e2e::{n2_command, TestSpace, TOUCH_RULE}; +use crate::e2e::{n2_command, TestSpace}; #[cfg(unix)] #[test] fn include_creates_new_variable_with_dependency() -> anyhow::Result<()> { let space = TestSpace::new()?; - space.write("build.ninja", " + space.write( + "build.ninja", + " rule write_file command = echo $contents > $out @@ -13,10 +15,14 @@ include included.ninja build out: write_file contents = $b -")?; - space.write("included.ninja", " +", + )?; + space.write( + "included.ninja", + " b = $a bar -")?; +", + )?; space.run_expect(&mut n2_command(vec!["out"]))?; assert_eq!(space.read("out").unwrap(), b"foo bar\n"); Ok(()) @@ -26,7 +32,9 @@ b = $a bar #[test] fn include_creates_edits_existing_variable() -> anyhow::Result<()> { let space = TestSpace::new()?; - space.write("build.ninja", " + space.write( + "build.ninja", + " rule write_file command = echo $contents > $out @@ -35,10 +43,14 @@ include included.ninja build out: write_file contents = $a -")?; - space.write("included.ninja", " +", + )?; + space.write( + "included.ninja", + " a = $a bar -")?; +", + )?; space.run_expect(&mut n2_command(vec!["out"]))?; assert_eq!(space.read("out").unwrap(), b"foo bar\n"); Ok(()) @@ -48,7 +60,9 @@ a = $a bar #[test] fn subninja_doesnt_affect_variables_in_parent_scope() -> anyhow::Result<()> { let space = TestSpace::new()?; - space.write("build.ninja", " + space.write( + "build.ninja", + " rule write_file command = echo $contents > $out @@ -57,12 +71,16 @@ subninja subninja.ninja build out: write_file contents = $a -")?; - space.write("subninja.ninja", " +", + )?; + space.write( + "subninja.ninja", + " a = bar build out2: write_file contents = $a -")?; +", + )?; space.run_expect(&mut n2_command(vec!["out", "out2"]))?; assert_eq!(space.read("out").unwrap(), b"foo\n"); assert_eq!(space.read("out2").unwrap(), b"bar\n"); From 98867a2bc99f18a5bdc8db281ca65e5660ff6b5b Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 28 Feb 2024 10:01:28 -0800 Subject: [PATCH 28/29] Fall back to reading instead of mmap on windows Windows doesn't have support for mmap (at least not with the same apis) --- src/file_pool.rs | 174 +++++++++++++++++++++++++++++------------------ 1 file changed, 109 insertions(+), 65 deletions(-) diff --git a/src/file_pool.rs b/src/file_pool.rs index 6c8d1ad..499812c 100644 --- a/src/file_pool.rs +++ b/src/file_pool.rs @@ -1,83 +1,127 @@ use anyhow::bail; use core::slice; -use libc::{ - c_void, mmap, munmap, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, PROT_READ, - PROT_WRITE, _SC_PAGESIZE, -}; -use std::{ - os::fd::{AsFd, AsRawFd}, - path::Path, - ptr::null_mut, - sync::Mutex, -}; +use std::{path::Path, sync::Mutex}; -/// FilePool is a datastructure that is intended to hold onto byte buffers and give out immutable -/// references to them. But it can also accept new byte buffers while old ones are still lent out. -/// This requires interior mutability / unsafe code. Appending to a Vec while references to other -/// elements are held is generally unsafe, because the Vec can reallocate all the prior elements -/// to a new memory location. But if the elements themselves are pointers to stable memory, the -/// contents of those pointers can be referenced safely. This also requires guarding the outer -/// Vec with a Mutex so that two threads don't append to it at the same time. -pub struct FilePool { - files: Mutex>, -} -impl FilePool { - pub fn new() -> FilePool { - FilePool { - files: Mutex::new(Vec::new()), - } +#[cfg(unix)] +mod mmap { + use super::*; + use libc::{ + c_void, mmap, munmap, sysconf, MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, + PROT_READ, PROT_WRITE, _SC_PAGESIZE, + }; + use std::{ + os::fd::{AsFd, AsRawFd}, + ptr::null_mut, + }; + /// FilePool is a datastructure that is intended to hold onto byte buffers and give out immutable + /// references to them. But it can also accept new byte buffers while old ones are still lent out. + /// This requires interior mutability / unsafe code. Appending to a Vec while references to other + /// elements are held is generally unsafe, because the Vec can reallocate all the prior elements + /// to a new memory location. But if the elements themselves are pointers to stable memory, the + /// contents of those pointers can be referenced safely. This also requires guarding the outer + /// Vec with a Mutex so that two threads don't append to it at the same time. + pub struct FilePool { + files: Mutex>, } - - pub fn read_file(&self, path: &Path) -> anyhow::Result<&[u8]> { - let page_size = unsafe { sysconf(_SC_PAGESIZE) } as usize; - let file = std::fs::File::open(path)?; - let fd = file.as_fd().as_raw_fd(); - let file_size = file.metadata()?.len() as usize; - let mapping_size = (file_size + page_size).next_multiple_of(page_size); - unsafe { - // size + 1 to add a null terminator. - let addr = mmap(null_mut(), mapping_size, PROT_READ, MAP_PRIVATE, fd, 0); - if addr == MAP_FAILED { - bail!("mmap failed"); + impl FilePool { + pub fn new() -> FilePool { + FilePool { + files: Mutex::new(Vec::new()), } + } + + pub fn read_file(&self, path: &Path) -> anyhow::Result<&[u8]> { + let page_size = unsafe { sysconf(_SC_PAGESIZE) } as usize; + let file = std::fs::File::open(path)?; + let fd = file.as_fd().as_raw_fd(); + let file_size = file.metadata()?.len() as usize; + let mapping_size = (file_size + page_size).next_multiple_of(page_size); + unsafe { + // size + 1 to add a null terminator. + let addr = mmap(null_mut(), mapping_size, PROT_READ, MAP_PRIVATE, fd, 0); + if addr == MAP_FAILED { + bail!("mmap failed"); + } + + let addr2 = mmap( + addr.add(mapping_size).sub(page_size), + page_size, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, + 0, + ); + if addr2 == MAP_FAILED { + bail!("mmap failed"); + } + *(addr.add(mapping_size).sub(page_size) as *mut u8) = 0; + // The manpages say the extra bytes past the end of the file are + // zero-filled, but just to make sure: + assert!(*(addr.add(file_size) as *mut u8) == 0); + + let files = &mut self.files.lock().unwrap(); + files.push((addr, mapping_size)); - let addr2 = mmap( - addr.add(mapping_size).sub(page_size), - page_size, - PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, - -1, - 0, - ); - if addr2 == MAP_FAILED { - bail!("mmap failed"); + Ok(slice::from_raw_parts(addr as *mut u8, file_size + 1)) } - *(addr.add(mapping_size).sub(page_size) as *mut u8) = 0; - // The manpages say the extra bytes past the end of the file are - // zero-filled, but just to make sure: - assert!(*(addr.add(file_size) as *mut u8) == 0); + } + } - let files = &mut self.files.lock().unwrap(); - files.push((addr, mapping_size)); + // SAFETY: Sync isn't implemented automatically because we have a *mut pointer, + // but that pointer isn't used at all aside from the drop implementation, so + // we won't have data races. + unsafe impl Sync for FilePool {} + unsafe impl Send for FilePool {} - Ok(slice::from_raw_parts(addr as *mut u8, file_size + 1)) + impl Drop for FilePool { + fn drop(&mut self) { + let files = self.files.lock().unwrap(); + for &(addr, len) in files.iter() { + unsafe { + munmap(addr, len); + } + } } } } -// SAFETY: Sync isn't implemented automatically because we have a *mut pointer, -// but that pointer isn't used at all aside from the drop implementation, so -// we won't have data races. -unsafe impl Sync for FilePool {} -unsafe impl Send for FilePool {} +#[cfg(not(unix))] +mod read { + use crate::scanner::read_file_with_nul; -impl Drop for FilePool { - fn drop(&mut self) { - let files = self.files.lock().unwrap(); - for &(addr, len) in files.iter() { - unsafe { - munmap(addr, len); + use super::*; + + /// FilePool is a datastructure that is intended to hold onto byte buffers and give out immutable + /// references to them. But it can also accept new byte buffers while old ones are still lent out. + /// This requires interior mutability / unsafe code. Appending to a Vec while references to other + /// elements are held is generally unsafe, because the Vec can reallocate all the prior elements + /// to a new memory location. But if the elements themselves are unchanging Vecs, the + /// contents of those Vecs can be referenced safely. This also requires guarding the outer + /// Vec with a Mutex so that two threads don't append to it at the same time. + pub struct FilePool { + files: Mutex>>, + } + + impl FilePool { + pub fn new() -> FilePool { + FilePool { + files: Mutex::new(Vec::new()), } } + + pub fn read_file(&self, path: &Path) -> anyhow::Result<&[u8]> { + let bytes = read_file_with_nul(path)?; + let addr = bytes.as_ptr(); + let len = bytes.len(); + self.files.lock().unwrap().push(bytes); + + unsafe { Ok(slice::from_raw_parts(addr as *mut u8, len)) } + } } } + +#[cfg(unix)] +pub use mmap::FilePool; + +#[cfg(not(unix))] +pub use read::FilePool; From 9cc13a8532385c19c65c70595acd5669cb45993a Mon Sep 17 00:00:00 2001 From: Cole Faust Date: Wed, 28 Feb 2024 10:11:28 -0800 Subject: [PATCH 29/29] Update rust to 1.73.0 Needed for usize::next_multiple_of(). --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 654296c..0df6b41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@1.70.0 + - uses: dtolnay/rust-toolchain@1.73.0 with: components: rustfmt - name: Check formatting diff --git a/Cargo.toml b/Cargo.toml index 87d091f..1d8bda2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/evmar/n2" # https://github.com/evmar/n2/issues/74 # Note: if we bump this, may need to bump .github/workflows/ci.yml version too. -rust-version = "1.70.0" +rust-version = "1.73.0" description = "a ninja compatible build system" [dependencies]