forked from extern/nushell
Make EngineState clone cheaper with Arc on all of the heavy objects (#12229)
# Description This makes many of the larger objects in `EngineState` into `Arc`, and uses `Arc::make_mut` to do clone-on-write if the reference is not unique. This is generally very cheap, giving us the best of both worlds - allowing us to mutate without cloning if we have an exclusive reference, and cloning if we don't. This started as more of a curiosity for me after remembering that `Arc::make_mut` exists and can make using `Arc` for mostly immutable data that sometimes needs to be changed very convenient, and also after hearing someone complain about memory usage on Discord - this is a somewhat significant win for that. The exact objects that were wrapped in `Arc`: - `files`, `file_contents` - the strings and byte buffers - `decls` - the whole `Vec`, but mostly to avoid lots of individual `malloc()` calls on Clone rather than for memory usage - `blocks` - the blocks themselves, rather than the outer Vec - `modules` - the modules themselves, rather than the outer Vec - `env_vars`, `previous_env_vars` - the entire maps - `config` The changes required were relatively minimal, but this is a breaking API change. In particular, blocks are added as Arcs, to allow the parser cache functionality to work. With my normal nu config, running on Linux, this saves me about 15 MiB of process memory usage when running interactively (65 MiB → 50 MiB). This also makes quick command executions cheaper, particularly since every REPL loop now involves a clone of the engine state so that we can recover from a panic. It also reduces memory usage where engine state needs to be cloned and sent to another thread or kept within an iterator. # User-Facing Changes Shouldn't be any, since it's all internal stuff, but it does change some public interfaces so it's a breaking change
This commit is contained in:
@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Argument, Expr, ExternalArgument, RecordItem};
|
||||
@ -325,7 +327,7 @@ impl Expression {
|
||||
expr.replace_span(working_set, replaced, new_span);
|
||||
}
|
||||
Expr::Block(block_id) => {
|
||||
let mut block = working_set.get_block(*block_id).clone();
|
||||
let mut block = (**working_set.get_block(*block_id)).clone();
|
||||
|
||||
for pipeline in block.pipelines.iter_mut() {
|
||||
for element in pipeline.elements.iter_mut() {
|
||||
@ -333,10 +335,10 @@ impl Expression {
|
||||
}
|
||||
}
|
||||
|
||||
*block_id = working_set.add_block(block);
|
||||
*block_id = working_set.add_block(Arc::new(block));
|
||||
}
|
||||
Expr::Closure(block_id) => {
|
||||
let mut block = working_set.get_block(*block_id).clone();
|
||||
let mut block = (**working_set.get_block(*block_id)).clone();
|
||||
|
||||
for pipeline in block.pipelines.iter_mut() {
|
||||
for element in pipeline.elements.iter_mut() {
|
||||
@ -344,7 +346,7 @@ impl Expression {
|
||||
}
|
||||
}
|
||||
|
||||
*block_id = working_set.add_block(block);
|
||||
*block_id = working_set.add_block(Arc::new(block));
|
||||
}
|
||||
Expr::Binary(_) => {}
|
||||
Expr::Bool(_) => {}
|
||||
@ -429,7 +431,7 @@ impl Expression {
|
||||
}
|
||||
}
|
||||
Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
|
||||
let mut block = working_set.get_block(*block_id).clone();
|
||||
let mut block = (**working_set.get_block(*block_id)).clone();
|
||||
|
||||
for pipeline in block.pipelines.iter_mut() {
|
||||
for element in pipeline.elements.iter_mut() {
|
||||
@ -437,7 +439,7 @@ impl Expression {
|
||||
}
|
||||
}
|
||||
|
||||
*block_id = working_set.add_block(block);
|
||||
*block_id = working_set.add_block(Arc::new(block));
|
||||
}
|
||||
Expr::Table(headers, cells) => {
|
||||
for header in headers {
|
||||
|
@ -64,23 +64,29 @@ impl Clone for IsDebugging {
|
||||
/// will refer to the corresponding IDs rather than their definitions directly. At runtime, this means
|
||||
/// less copying and smaller structures.
|
||||
///
|
||||
/// Many of the larger objects in this structure are stored within `Arc` to decrease the cost of
|
||||
/// cloning `EngineState`. While `Arc`s are generally immutable, they can be modified using
|
||||
/// `Arc::make_mut`, which automatically clones to a new allocation if there are other copies of
|
||||
/// the `Arc` already in use, but will let us modify the `Arc` directly if we have the only
|
||||
/// reference to it.
|
||||
///
|
||||
/// Note that the runtime stack is not part of this global state. Runtime stacks are handled differently,
|
||||
/// but they also rely on using IDs rather than full definitions.
|
||||
#[derive(Clone)]
|
||||
pub struct EngineState {
|
||||
files: Vec<(String, usize, usize)>,
|
||||
file_contents: Vec<(Vec<u8>, usize, usize)>,
|
||||
files: Vec<(Arc<String>, usize, usize)>,
|
||||
file_contents: Vec<(Arc<Vec<u8>>, usize, usize)>,
|
||||
pub(super) virtual_paths: Vec<(String, VirtualPath)>,
|
||||
vars: Vec<Variable>,
|
||||
decls: Vec<Box<dyn Command + 'static>>,
|
||||
pub(super) blocks: Vec<Block>,
|
||||
pub(super) modules: Vec<Module>,
|
||||
decls: Arc<Vec<Box<dyn Command + 'static>>>,
|
||||
pub(super) blocks: Vec<Arc<Block>>,
|
||||
pub(super) modules: Vec<Arc<Module>>,
|
||||
usage: Usage,
|
||||
pub scope: ScopeFrame,
|
||||
pub ctrlc: Option<Arc<AtomicBool>>,
|
||||
pub env_vars: EnvVars,
|
||||
pub previous_env_vars: HashMap<String, Value>,
|
||||
pub config: Config,
|
||||
pub env_vars: Arc<EnvVars>,
|
||||
pub previous_env_vars: Arc<HashMap<String, Value>>,
|
||||
pub config: Arc<Config>,
|
||||
pub pipeline_externals_state: Arc<(AtomicU32, AtomicU32)>,
|
||||
pub repl_state: Arc<Mutex<ReplState>>,
|
||||
pub table_decl_id: Option<usize>,
|
||||
@ -122,9 +128,11 @@ impl EngineState {
|
||||
Variable::new(Span::new(0, 0), Type::Any, false),
|
||||
Variable::new(Span::new(0, 0), Type::Any, false),
|
||||
],
|
||||
decls: vec![],
|
||||
decls: Arc::new(vec![]),
|
||||
blocks: vec![],
|
||||
modules: vec![Module::new(DEFAULT_OVERLAY_NAME.as_bytes().to_vec())],
|
||||
modules: vec![Arc::new(Module::new(
|
||||
DEFAULT_OVERLAY_NAME.as_bytes().to_vec(),
|
||||
))],
|
||||
usage: Usage::new(),
|
||||
// make sure we have some default overlay:
|
||||
scope: ScopeFrame::with_empty_overlay(
|
||||
@ -133,11 +141,13 @@ impl EngineState {
|
||||
false,
|
||||
),
|
||||
ctrlc: None,
|
||||
env_vars: [(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
previous_env_vars: HashMap::new(),
|
||||
config: Config::default(),
|
||||
env_vars: Arc::new(
|
||||
[(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
previous_env_vars: Arc::new(HashMap::new()),
|
||||
config: Arc::new(Config::default()),
|
||||
pipeline_externals_state: Arc::new((AtomicU32::new(0), AtomicU32::new(0))),
|
||||
repl_state: Arc::new(Mutex::new(ReplState {
|
||||
buffer: "".to_string(),
|
||||
@ -175,12 +185,16 @@ impl EngineState {
|
||||
self.files.extend(delta.files);
|
||||
self.file_contents.extend(delta.file_contents);
|
||||
self.virtual_paths.extend(delta.virtual_paths);
|
||||
self.decls.extend(delta.decls);
|
||||
self.vars.extend(delta.vars);
|
||||
self.blocks.extend(delta.blocks);
|
||||
self.modules.extend(delta.modules);
|
||||
self.usage.merge_with(delta.usage);
|
||||
|
||||
// Avoid potentially cloning the Arc if we aren't adding anything
|
||||
if !delta.decls.is_empty() {
|
||||
Arc::make_mut(&mut self.decls).extend(delta.decls);
|
||||
}
|
||||
|
||||
let first = delta.scope.remove(0);
|
||||
|
||||
for (delta_name, delta_overlay) in first.clone().overlays {
|
||||
@ -268,7 +282,7 @@ impl EngineState {
|
||||
|
||||
for mut scope in stack.env_vars.drain(..) {
|
||||
for (overlay_name, mut env) in scope.drain() {
|
||||
if let Some(env_vars) = self.env_vars.get_mut(&overlay_name) {
|
||||
if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
|
||||
// Updating existing overlay
|
||||
for (k, v) in env.drain() {
|
||||
if k == "config" {
|
||||
@ -276,7 +290,7 @@ impl EngineState {
|
||||
// Instead, mutate a clone of it with into_config(), and put THAT in env_vars.
|
||||
let mut new_record = v.clone();
|
||||
let (config, error) = new_record.into_config(&self.config);
|
||||
self.config = config;
|
||||
self.config = Arc::new(config);
|
||||
config_updated = true;
|
||||
env_vars.insert(k, new_record);
|
||||
if let Some(e) = error {
|
||||
@ -288,7 +302,7 @@ impl EngineState {
|
||||
}
|
||||
} else {
|
||||
// Pushing a new overlay
|
||||
self.env_vars.insert(overlay_name, env);
|
||||
Arc::make_mut(&mut self.env_vars).insert(overlay_name, env);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -422,10 +436,10 @@ impl EngineState {
|
||||
pub fn add_env_var(&mut self, name: String, val: Value) {
|
||||
let overlay_name = String::from_utf8_lossy(self.last_overlay_name(&[])).to_string();
|
||||
|
||||
if let Some(env_vars) = self.env_vars.get_mut(&overlay_name) {
|
||||
if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) {
|
||||
env_vars.insert(name, val);
|
||||
} else {
|
||||
self.env_vars
|
||||
Arc::make_mut(&mut self.env_vars)
|
||||
.insert(overlay_name, [(name, val)].into_iter().collect());
|
||||
}
|
||||
}
|
||||
@ -752,7 +766,7 @@ impl EngineState {
|
||||
self.update_plugin_gc_configs(&conf.plugin_gc);
|
||||
}
|
||||
|
||||
self.config = conf;
|
||||
self.config = Arc::new(conf);
|
||||
}
|
||||
|
||||
/// Fetch the configuration for a plugin
|
||||
@ -867,7 +881,7 @@ impl EngineState {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Arc<Block> {
|
||||
self.blocks
|
||||
.get(block_id)
|
||||
.expect("internal error: missing block")
|
||||
@ -878,7 +892,7 @@ impl EngineState {
|
||||
/// Prefer to use [`.get_block()`] in most cases - `BlockId`s that don't exist are normally a
|
||||
/// compiler error. This only exists to stop plugins from crashing the engine if they send us
|
||||
/// something invalid.
|
||||
pub fn try_get_block(&self, block_id: BlockId) -> Option<&Block> {
|
||||
pub fn try_get_block(&self, block_id: BlockId) -> Option<&Arc<Block>> {
|
||||
self.blocks.get(block_id)
|
||||
}
|
||||
|
||||
@ -902,7 +916,7 @@ impl EngineState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn files(&self) -> impl Iterator<Item = &(String, usize, usize)> {
|
||||
pub fn files(&self) -> impl Iterator<Item = &(Arc<String>, usize, usize)> {
|
||||
self.files.iter()
|
||||
}
|
||||
|
||||
@ -911,9 +925,10 @@ impl EngineState {
|
||||
let next_span_end = next_span_start + contents.len();
|
||||
|
||||
self.file_contents
|
||||
.push((contents, next_span_start, next_span_end));
|
||||
.push((Arc::new(contents), next_span_start, next_span_end));
|
||||
|
||||
self.files.push((filename, next_span_start, next_span_end));
|
||||
self.files
|
||||
.push((Arc::new(filename), next_span_start, next_span_end));
|
||||
|
||||
self.num_files() - 1
|
||||
}
|
||||
@ -953,7 +968,7 @@ impl EngineState {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn get_file_contents(&self) -> &[(Vec<u8>, usize, usize)] {
|
||||
pub fn get_file_contents(&self) -> &[(Arc<Vec<u8>>, usize, usize)] {
|
||||
&self.file_contents
|
||||
}
|
||||
|
||||
@ -1051,8 +1066,8 @@ mod engine_state_tests {
|
||||
engine_state.merge_delta(delta)?;
|
||||
|
||||
assert_eq!(engine_state.num_files(), 2);
|
||||
assert_eq!(&engine_state.files[0].0, "test.nu");
|
||||
assert_eq!(&engine_state.files[1].0, "child.nu");
|
||||
assert_eq!(&*engine_state.files[0].0, "test.nu");
|
||||
assert_eq!(&*engine_state.files[1].0, "child.nu");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -12,13 +12,13 @@ use crate::RegisteredPlugin;
|
||||
/// can be applied to the global state to update it to contain both previous state and the state held
|
||||
/// within the delta.
|
||||
pub struct StateDelta {
|
||||
pub(super) files: Vec<(String, usize, usize)>,
|
||||
pub(crate) file_contents: Vec<(Vec<u8>, usize, usize)>,
|
||||
pub(super) files: Vec<(Arc<String>, usize, usize)>,
|
||||
pub(crate) file_contents: Vec<(Arc<Vec<u8>>, usize, usize)>,
|
||||
pub(super) virtual_paths: Vec<(String, VirtualPath)>,
|
||||
pub(super) vars: Vec<Variable>, // indexed by VarId
|
||||
pub(super) decls: Vec<Box<dyn Command>>, // indexed by DeclId
|
||||
pub blocks: Vec<Block>, // indexed by BlockId
|
||||
pub(super) modules: Vec<Module>, // indexed by ModuleId
|
||||
pub blocks: Vec<Arc<Block>>, // indexed by BlockId
|
||||
pub(super) modules: Vec<Arc<Module>>, // indexed by ModuleId
|
||||
pub(super) usage: Usage,
|
||||
pub scope: Vec<ScopeFrame>,
|
||||
#[cfg(feature = "plugin")]
|
||||
@ -131,7 +131,7 @@ impl StateDelta {
|
||||
self.scope.pop();
|
||||
}
|
||||
|
||||
pub fn get_file_contents(&self) -> &[(Vec<u8>, usize, usize)] {
|
||||
pub fn get_file_contents(&self) -> &[(Arc<Vec<u8>>, usize, usize)] {
|
||||
&self.file_contents
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_block(&mut self, block: Block) -> BlockId {
|
||||
pub fn add_block(&mut self, block: Arc<Block>) -> BlockId {
|
||||
self.delta.blocks.push(block);
|
||||
|
||||
self.num_blocks() - 1
|
||||
@ -263,7 +263,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
pub fn add_module(&mut self, name: &str, module: Module, comments: Vec<Span>) -> ModuleId {
|
||||
let name = name.as_bytes().to_vec();
|
||||
|
||||
self.delta.modules.push(module);
|
||||
self.delta.modules.push(Arc::new(module));
|
||||
let module_id = self.num_modules() - 1;
|
||||
|
||||
if !comments.is_empty() {
|
||||
@ -296,7 +296,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
self.permanent_state.next_span_start()
|
||||
}
|
||||
|
||||
pub fn files(&'a self) -> impl Iterator<Item = &(String, usize, usize)> {
|
||||
pub fn files(&'a self) -> impl Iterator<Item = &(Arc<String>, usize, usize)> {
|
||||
self.permanent_state.files().chain(self.delta.files.iter())
|
||||
}
|
||||
|
||||
@ -320,7 +320,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
pub fn add_file(&mut self, filename: String, contents: &[u8]) -> FileId {
|
||||
// First, look for the file to see if we already have it
|
||||
for (idx, (fname, file_start, file_end)) in self.files().enumerate() {
|
||||
if fname == &filename {
|
||||
if **fname == filename {
|
||||
let prev_contents = self.get_span_contents(Span::new(*file_start, *file_end));
|
||||
if prev_contents == contents {
|
||||
return idx;
|
||||
@ -331,13 +331,15 @@ impl<'a> StateWorkingSet<'a> {
|
||||
let next_span_start = self.next_span_start();
|
||||
let next_span_end = next_span_start + contents.len();
|
||||
|
||||
self.delta
|
||||
.file_contents
|
||||
.push((contents.to_vec(), next_span_start, next_span_end));
|
||||
self.delta.file_contents.push((
|
||||
Arc::new(contents.to_vec()),
|
||||
next_span_start,
|
||||
next_span_end,
|
||||
));
|
||||
|
||||
self.delta
|
||||
.files
|
||||
.push((filename, next_span_start, next_span_end));
|
||||
.push((Arc::new(filename), next_span_start, next_span_end));
|
||||
|
||||
self.num_files() - 1
|
||||
}
|
||||
@ -353,7 +355,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
let (file_id, ..) = self
|
||||
.files()
|
||||
.enumerate()
|
||||
.find(|(_, (fname, _, _))| fname == filename)?;
|
||||
.find(|(_, (fname, _, _))| **fname == filename)?;
|
||||
|
||||
Some(self.get_span_for_file(file_id))
|
||||
}
|
||||
@ -626,8 +628,8 @@ impl<'a> StateWorkingSet<'a> {
|
||||
pub fn list_env(&self) -> Vec<String> {
|
||||
let mut env_vars = vec![];
|
||||
|
||||
for env_var in self.permanent_state.env_vars.clone().into_iter() {
|
||||
env_vars.push(env_var.0)
|
||||
for env_var in self.permanent_state.env_vars.iter() {
|
||||
env_vars.push(env_var.0.clone());
|
||||
}
|
||||
|
||||
env_vars
|
||||
@ -742,7 +744,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Arc<Block> {
|
||||
let num_permanent_blocks = self.permanent_state.num_blocks();
|
||||
if block_id < num_permanent_blocks {
|
||||
self.permanent_state.get_block(block_id)
|
||||
@ -774,6 +776,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
self.delta
|
||||
.blocks
|
||||
.get_mut(block_id - num_permanent_blocks)
|
||||
.map(Arc::make_mut)
|
||||
.expect("internal error: missing block")
|
||||
}
|
||||
}
|
||||
@ -957,7 +960,7 @@ impl<'a> StateWorkingSet<'a> {
|
||||
build_usage(&comment_lines)
|
||||
}
|
||||
|
||||
pub fn find_block_by_span(&self, span: Span) -> Option<Block> {
|
||||
pub fn find_block_by_span(&self, span: Span) -> Option<Arc<Block>> {
|
||||
for block in &self.delta.blocks {
|
||||
if Some(span) == block.span {
|
||||
return Some(block.clone());
|
||||
@ -1063,7 +1066,7 @@ impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
|
||||
}
|
||||
|
||||
let data = span_contents.data();
|
||||
if filename == "<cli>" {
|
||||
if **filename == "<cli>" {
|
||||
if debugging {
|
||||
let success_cli = "Successfully read CLI span";
|
||||
dbg!(success_cli, String::from_utf8_lossy(data));
|
||||
@ -1081,7 +1084,7 @@ impl<'a> miette::SourceCode for &StateWorkingSet<'a> {
|
||||
dbg!(success_file);
|
||||
}
|
||||
return Ok(Box::new(miette::MietteSpanContents::new_named(
|
||||
filename.clone(),
|
||||
(**filename).clone(),
|
||||
data,
|
||||
retranslated,
|
||||
span_contents.line(),
|
||||
|
Reference in New Issue
Block a user