mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-13 05:42:59 +00:00
Start on refactoring BackgroundScanner
to be generic over fs
This commit is contained in:
parent
2fa63a3a50
commit
d5361299ad
1 changed files with 146 additions and 58 deletions
|
@ -22,6 +22,7 @@ use gpui::{
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::{
|
use postage::{
|
||||||
|
broadcast,
|
||||||
prelude::{Sink, Stream},
|
prelude::{Sink, Stream},
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
|
@ -153,7 +154,7 @@ struct InMemoryEntry {
|
||||||
struct InMemoryFsState {
|
struct InMemoryFsState {
|
||||||
entries: BTreeMap<PathBuf, InMemoryEntry>,
|
entries: BTreeMap<PathBuf, InMemoryEntry>,
|
||||||
next_inode: u64,
|
next_inode: u64,
|
||||||
events_tx: watch::Sender<()>,
|
events_tx: broadcast::Sender<fsevent::Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InMemoryFsState {
|
impl InMemoryFsState {
|
||||||
|
@ -168,16 +169,26 @@ impl InMemoryFsState {
|
||||||
Err(anyhow!("invalid "))
|
Err(anyhow!("invalid "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn emit_event(&mut self, path: &Path) {
|
||||||
|
let _ = self
|
||||||
|
.events_tx
|
||||||
|
.send(fsevent::Event {
|
||||||
|
event_id: 0,
|
||||||
|
flags: fsevent::StreamFlags::empty(),
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InMemoryFs {
|
pub struct InMemoryFs {
|
||||||
state: RwLock<InMemoryFsState>,
|
state: RwLock<InMemoryFsState>,
|
||||||
events_rx: watch::Receiver<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InMemoryFs {
|
impl InMemoryFs {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (events_tx, events_rx) = watch::channel();
|
let (events_tx, _) = broadcast::channel(2048);
|
||||||
let mut entries = BTreeMap::new();
|
let mut entries = BTreeMap::new();
|
||||||
entries.insert(
|
entries.insert(
|
||||||
Path::new("/").to_path_buf(),
|
Path::new("/").to_path_buf(),
|
||||||
|
@ -195,7 +206,6 @@ impl InMemoryFs {
|
||||||
next_inode: 1,
|
next_inode: 1,
|
||||||
events_tx,
|
events_tx,
|
||||||
}),
|
}),
|
||||||
events_rx,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,8 +225,33 @@ impl InMemoryFs {
|
||||||
content: None,
|
content: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
state.emit_event(path).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn remove(&self, path: &Path) -> Result<()> {
|
||||||
|
let mut state = self.state.write().await;
|
||||||
|
state.validate_path(path)?;
|
||||||
|
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
state.entries.retain(|path, _| {
|
||||||
|
if path.starts_with(path) {
|
||||||
|
paths.push(path.to_path_buf());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for path in paths {
|
||||||
|
state.emit_event(&path).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn events(&self) -> broadcast::Receiver<fsevent::Event> {
|
||||||
|
self.state.read().await.events_tx.subscribe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
@ -267,6 +302,7 @@ impl Fs for InMemoryFs {
|
||||||
} else {
|
} else {
|
||||||
entry.content = Some(text.chunks().collect());
|
entry.content = Some(text.chunks().collect());
|
||||||
entry.mtime = SystemTime::now();
|
entry.mtime = SystemTime::now();
|
||||||
|
state.emit_event(path).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,6 +318,7 @@ impl Fs for InMemoryFs {
|
||||||
content: Some(text.chunks().collect()),
|
content: Some(text.chunks().collect()),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
state.emit_event(path).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -291,7 +328,7 @@ impl Fs for InMemoryFs {
|
||||||
enum ScanState {
|
enum ScanState {
|
||||||
Idle,
|
Idle,
|
||||||
Scanning,
|
Scanning,
|
||||||
Err(Arc<io::Error>),
|
Err(Arc<anyhow::Error>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Worktree {
|
pub enum Worktree {
|
||||||
|
@ -331,7 +368,7 @@ impl Worktree {
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let fs = Arc::new(OsFs);
|
let fs = Arc::new(OsFs);
|
||||||
let (mut tree, scan_states_tx) = LocalWorktree::new(path, languages, fs, cx);
|
let (mut tree, scan_states_tx) = LocalWorktree::new(path, languages, fs.clone(), cx);
|
||||||
let (event_stream, event_stream_handle) = fsevent::EventStream::new(
|
let (event_stream, event_stream_handle) = fsevent::EventStream::new(
|
||||||
&[tree.snapshot.abs_path.as_ref()],
|
&[tree.snapshot.abs_path.as_ref()],
|
||||||
Duration::from_millis(100),
|
Duration::from_millis(100),
|
||||||
|
@ -339,7 +376,7 @@ impl Worktree {
|
||||||
let background_snapshot = tree.background_snapshot.clone();
|
let background_snapshot = tree.background_snapshot.clone();
|
||||||
let id = tree.id;
|
let id = tree.id;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let scanner = BackgroundScanner::new(background_snapshot, scan_states_tx, id);
|
let scanner = BackgroundScanner::new(fs, background_snapshot, scan_states_tx, id);
|
||||||
scanner.run(event_stream);
|
scanner.run(event_stream);
|
||||||
});
|
});
|
||||||
tree._event_stream_handle = Some(event_stream_handle);
|
tree._event_stream_handle = Some(event_stream_handle);
|
||||||
|
@ -356,7 +393,14 @@ impl Worktree {
|
||||||
let (tree, scan_states_tx) = LocalWorktree::new(path, languages, fs.clone(), cx);
|
let (tree, scan_states_tx) = LocalWorktree::new(path, languages, fs.clone(), cx);
|
||||||
let background_snapshot = tree.background_snapshot.clone();
|
let background_snapshot = tree.background_snapshot.clone();
|
||||||
let id = tree.id;
|
let id = tree.id;
|
||||||
cx.background().spawn(async move {}).detach();
|
let fs = fs.clone();
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move {
|
||||||
|
let events_rx = fs.events().await;
|
||||||
|
let scanner = BackgroundScanner::new(fs, background_snapshot, scan_states_tx, id);
|
||||||
|
scanner.run_test(events_rx).await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
Worktree::Local(tree)
|
Worktree::Local(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1933,14 +1977,21 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackgroundScanner {
|
struct BackgroundScanner {
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
snapshot: Arc<Mutex<Snapshot>>,
|
snapshot: Arc<Mutex<Snapshot>>,
|
||||||
notify: Sender<ScanState>,
|
notify: Sender<ScanState>,
|
||||||
thread_pool: scoped_pool::Pool,
|
thread_pool: scoped_pool::Pool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundScanner {
|
impl BackgroundScanner {
|
||||||
fn new(snapshot: Arc<Mutex<Snapshot>>, notify: Sender<ScanState>, worktree_id: usize) -> Self {
|
fn new(
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
snapshot: Arc<Mutex<Snapshot>>,
|
||||||
|
notify: Sender<ScanState>,
|
||||||
|
worktree_id: usize,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
fs,
|
||||||
snapshot,
|
snapshot,
|
||||||
notify,
|
notify,
|
||||||
thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)),
|
thread_pool: scoped_pool::Pool::new(16, format!("worktree-{}-scanner", worktree_id)),
|
||||||
|
@ -1960,7 +2011,7 @@ impl BackgroundScanner {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = self.scan_dirs() {
|
if let Err(err) = smol::block_on(self.scan_dirs()) {
|
||||||
if smol::block_on(self.notify.send(ScanState::Err(Arc::new(err)))).is_err() {
|
if smol::block_on(self.notify.send(ScanState::Err(Arc::new(err)))).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1975,7 +2026,7 @@ impl BackgroundScanner {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.process_events(events) {
|
if !smol::block_on(self.process_events(events)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1987,46 +2038,82 @@ impl BackgroundScanner {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_dirs(&mut self) -> io::Result<()> {
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
self.snapshot.lock().scan_id += 1;
|
async fn run_test(mut self, mut events_rx: broadcast::Receiver<fsevent::Event>) {
|
||||||
|
if self.notify.send(ScanState::Scanning).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = self.scan_dirs().await {
|
||||||
|
if self
|
||||||
|
.notify
|
||||||
|
.send(ScanState::Err(Arc::new(err)))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notify.send(ScanState::Idle).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(event) = events_rx.recv().await {
|
||||||
|
let mut events = vec![event];
|
||||||
|
while let Ok(event) = events_rx.try_recv() {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notify.send(ScanState::Scanning).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.process_events(events).await {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.notify.send(ScanState::Idle).await.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scan_dirs(&mut self) -> Result<()> {
|
||||||
|
let next_entry_id;
|
||||||
|
{
|
||||||
|
let mut snapshot = self.snapshot.lock();
|
||||||
|
snapshot.scan_id += 1;
|
||||||
|
next_entry_id = snapshot.next_entry_id.clone();
|
||||||
|
}
|
||||||
|
|
||||||
let path: Arc<Path> = Arc::from(Path::new(""));
|
let path: Arc<Path> = Arc::from(Path::new(""));
|
||||||
let abs_path = self.abs_path();
|
let abs_path = self.abs_path();
|
||||||
let metadata = fs::metadata(&abs_path)?;
|
|
||||||
let inode = metadata.ino();
|
|
||||||
let is_symlink = fs::symlink_metadata(&abs_path)?.file_type().is_symlink();
|
|
||||||
let is_dir = metadata.file_type().is_dir();
|
|
||||||
let mtime = metadata.modified()?;
|
|
||||||
|
|
||||||
// After determining whether the root entry is a file or a directory, populate the
|
// After determining whether the root entry is a file or a directory, populate the
|
||||||
// snapshot's "root name", which will be used for the purpose of fuzzy matching.
|
// snapshot's "root name", which will be used for the purpose of fuzzy matching.
|
||||||
let mut root_name = abs_path
|
let mut root_name = abs_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||||
|
let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
|
||||||
|
let entry = self
|
||||||
|
.fs
|
||||||
|
.entry(root_char_bag, &next_entry_id, path.clone(), &abs_path)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("root entry does not exist"))?;
|
||||||
|
let is_dir = entry.is_dir();
|
||||||
if is_dir {
|
if is_dir {
|
||||||
root_name.push('/');
|
root_name.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
|
|
||||||
let next_entry_id;
|
|
||||||
{
|
{
|
||||||
let mut snapshot = self.snapshot.lock();
|
let mut snapshot = self.snapshot.lock();
|
||||||
snapshot.root_name = root_name;
|
snapshot.root_name = root_name;
|
||||||
snapshot.root_char_bag = root_char_bag;
|
snapshot.root_char_bag = root_char_bag;
|
||||||
next_entry_id = snapshot.next_entry_id.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.snapshot.lock().insert_entry(entry);
|
||||||
if is_dir {
|
if is_dir {
|
||||||
self.snapshot.lock().insert_entry(Entry {
|
|
||||||
id: next_entry_id.fetch_add(1, SeqCst),
|
|
||||||
kind: EntryKind::PendingDir,
|
|
||||||
path: path.clone(),
|
|
||||||
inode,
|
|
||||||
mtime,
|
|
||||||
is_symlink,
|
|
||||||
is_ignored: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let (tx, rx) = crossbeam_channel::unbounded();
|
let (tx, rx) = crossbeam_channel::unbounded();
|
||||||
tx.send(ScanJob {
|
tx.send(ScanJob {
|
||||||
abs_path: abs_path.to_path_buf(),
|
abs_path: abs_path.to_path_buf(),
|
||||||
|
@ -2041,31 +2128,23 @@ impl BackgroundScanner {
|
||||||
for _ in 0..self.thread_pool.thread_count() {
|
for _ in 0..self.thread_pool.thread_count() {
|
||||||
pool.execute(|| {
|
pool.execute(|| {
|
||||||
while let Ok(job) = rx.recv() {
|
while let Ok(job) = rx.recv() {
|
||||||
if let Err(err) =
|
if let Err(err) = smol::block_on(self.scan_dir(
|
||||||
self.scan_dir(root_char_bag, next_entry_id.clone(), &job)
|
root_char_bag,
|
||||||
{
|
next_entry_id.clone(),
|
||||||
|
&job,
|
||||||
|
)) {
|
||||||
log::error!("error scanning {:?}: {}", job.abs_path, err);
|
log::error!("error scanning {:?}: {}", job.abs_path, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
self.snapshot.lock().insert_entry(Entry {
|
|
||||||
id: next_entry_id.fetch_add(1, SeqCst),
|
|
||||||
kind: EntryKind::File(char_bag_for_path(root_char_bag, &path)),
|
|
||||||
path,
|
|
||||||
inode,
|
|
||||||
mtime,
|
|
||||||
is_symlink,
|
|
||||||
is_ignored: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scan_dir(
|
async fn scan_dir(
|
||||||
&self,
|
&self,
|
||||||
root_char_bag: CharBag,
|
root_char_bag: CharBag,
|
||||||
next_entry_id: Arc<AtomicUsize>,
|
next_entry_id: Arc<AtomicUsize>,
|
||||||
|
@ -2164,7 +2243,7 @@ impl BackgroundScanner {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
|
async fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
|
||||||
let mut snapshot = self.snapshot();
|
let mut snapshot = self.snapshot();
|
||||||
snapshot.scan_id += 1;
|
snapshot.scan_id += 1;
|
||||||
|
|
||||||
|
@ -2207,12 +2286,16 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match smol::block_on(OsFs.entry(
|
match self
|
||||||
|
.fs
|
||||||
|
.entry(
|
||||||
snapshot.root_char_bag,
|
snapshot.root_char_bag,
|
||||||
&next_entry_id,
|
&next_entry_id,
|
||||||
path.clone(),
|
path.clone(),
|
||||||
&event.path,
|
&event.path,
|
||||||
)) {
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Some(mut fs_entry)) => {
|
Ok(Some(mut fs_entry)) => {
|
||||||
let is_dir = fs_entry.is_dir();
|
let is_dir = fs_entry.is_dir();
|
||||||
let ignore_stack = snapshot.ignore_stack_for_path(&path, is_dir);
|
let ignore_stack = snapshot.ignore_stack_for_path(&path, is_dir);
|
||||||
|
@ -2245,8 +2328,11 @@ impl BackgroundScanner {
|
||||||
for _ in 0..self.thread_pool.thread_count() {
|
for _ in 0..self.thread_pool.thread_count() {
|
||||||
pool.execute(|| {
|
pool.execute(|| {
|
||||||
while let Ok(job) = scan_queue_rx.recv() {
|
while let Ok(job) = scan_queue_rx.recv() {
|
||||||
if let Err(err) = self.scan_dir(root_char_bag, next_entry_id.clone(), &job)
|
if let Err(err) = smol::block_on(self.scan_dir(
|
||||||
{
|
root_char_bag,
|
||||||
|
next_entry_id.clone(),
|
||||||
|
&job,
|
||||||
|
)) {
|
||||||
log::error!("error scanning {:?}: {}", job.abs_path, err);
|
log::error!("error scanning {:?}: {}", job.abs_path, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3061,6 +3147,7 @@ mod tests {
|
||||||
|
|
||||||
let (notify_tx, _notify_rx) = smol::channel::unbounded();
|
let (notify_tx, _notify_rx) = smol::channel::unbounded();
|
||||||
let mut scanner = BackgroundScanner::new(
|
let mut scanner = BackgroundScanner::new(
|
||||||
|
Arc::new(OsFs),
|
||||||
Arc::new(Mutex::new(Snapshot {
|
Arc::new(Mutex::new(Snapshot {
|
||||||
id: 0,
|
id: 0,
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
@ -3076,7 +3163,7 @@ mod tests {
|
||||||
notify_tx,
|
notify_tx,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
scanner.scan_dirs().unwrap();
|
smol::block_on(scanner.scan_dirs()).unwrap();
|
||||||
scanner.snapshot().check_invariants();
|
scanner.snapshot().check_invariants();
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
@ -3086,7 +3173,7 @@ mod tests {
|
||||||
let len = rng.gen_range(0..=events.len());
|
let len = rng.gen_range(0..=events.len());
|
||||||
let to_deliver = events.drain(0..len).collect::<Vec<_>>();
|
let to_deliver = events.drain(0..len).collect::<Vec<_>>();
|
||||||
log::info!("Delivering events: {:#?}", to_deliver);
|
log::info!("Delivering events: {:#?}", to_deliver);
|
||||||
scanner.process_events(to_deliver);
|
smol::block_on(scanner.process_events(to_deliver));
|
||||||
scanner.snapshot().check_invariants();
|
scanner.snapshot().check_invariants();
|
||||||
} else {
|
} else {
|
||||||
events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap());
|
events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap());
|
||||||
|
@ -3094,11 +3181,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::info!("Quiescing: {:#?}", events);
|
log::info!("Quiescing: {:#?}", events);
|
||||||
scanner.process_events(events);
|
smol::block_on(scanner.process_events(events));
|
||||||
scanner.snapshot().check_invariants();
|
scanner.snapshot().check_invariants();
|
||||||
|
|
||||||
let (notify_tx, _notify_rx) = smol::channel::unbounded();
|
let (notify_tx, _notify_rx) = smol::channel::unbounded();
|
||||||
let mut new_scanner = BackgroundScanner::new(
|
let mut new_scanner = BackgroundScanner::new(
|
||||||
|
scanner.fs.clone(),
|
||||||
Arc::new(Mutex::new(Snapshot {
|
Arc::new(Mutex::new(Snapshot {
|
||||||
id: 0,
|
id: 0,
|
||||||
scan_id: 0,
|
scan_id: 0,
|
||||||
|
@ -3114,7 +3202,7 @@ mod tests {
|
||||||
notify_tx,
|
notify_tx,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
new_scanner.scan_dirs().unwrap();
|
smol::block_on(new_scanner.scan_dirs()).unwrap();
|
||||||
assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec());
|
assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue