comparison rust/hg-core/src/dirstate_tree/status.rs @ 47475:94e38822d395

status: Extend read_dir caching to directories with ignored files See code comments Differential Revision: https://phab.mercurial-scm.org/D10909
author Simon Sapin <simon.sapin@octobus.net>
date Thu, 24 Jun 2021 21:54:14 +0200
parents c657beacdf2e
children f2a9db29cb2d
comparison
equal deleted inserted replaced
47474:c657beacdf2e 47475:94e38822d395
188 // All states that we care about listing have corresponding 188 // All states that we care about listing have corresponding
189 // dirstate entries. 189 // dirstate entries.
190 // This happens for example with `hg status -mard`. 190 // This happens for example with `hg status -mard`.
191 return true; 191 return true;
192 } 192 }
193 if let Some(cached_mtime) = cached_directory_mtime { 193 if !self.options.list_ignored
194 // The dirstate contains a cached mtime for this directory, set by 194 && self.ignore_patterns_have_changed == Some(false)
195 // a previous run of the `status` algorithm which found this 195 {
196 // directory eligible for `read_dir` caching. 196 if let Some(cached_mtime) = cached_directory_mtime {
197 if let Some(meta) = directory_metadata { 197 // The dirstate contains a cached mtime for this directory, set
198 if let Ok(current_mtime) = meta.modified() { 198 // by a previous run of the `status` algorithm which found this
199 if current_mtime == cached_mtime.into() { 199 // directory eligible for `read_dir` caching.
200 // The mtime of that directory has not changed since 200 if let Some(meta) = directory_metadata {
201 // then, which means that the 201 if let Ok(current_mtime) = meta.modified() {
202 // results of `read_dir` should also 202 if current_mtime == cached_mtime.into() {
203 // be unchanged. 203 // The mtime of that directory has not changed
204 return true; 204 // since then, which means that the results of
205 // `read_dir` should also be unchanged.
206 return true;
207 }
205 } 208 }
206 } 209 }
207 } 210 }
208 } 211 }
209 false 212 false
210 } 213 }
211 214
212 /// Returns whether the filesystem directory was found to have any entry 215 /// Returns whether all child entries of the filesystem directory have a
213 /// that does not have a corresponding dirstate tree node. 216 /// corresponding dirstate node or are ignored.
214 fn traverse_fs_directory_and_dirstate( 217 fn traverse_fs_directory_and_dirstate(
215 &self, 218 &self,
216 has_ignored_ancestor: bool, 219 has_ignored_ancestor: bool,
217 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>, 220 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
218 directory_hg_path: &BorrowedPath<'tree, 'on_disk>, 221 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
246 } 249 }
247 } 250 }
248 }) 251 })
249 .collect::<Result<_, _>>()?; 252 .collect::<Result<_, _>>()?;
250 253
251 // Conservatively don’t let the caller assume that there aren’t 254 // We don’t know, so conservatively say this isn’t the case
252 // any, since we don’t know. 255 let children_all_have_dirstate_node_or_are_ignored = false;
253 let directory_has_any_fs_only_entry = true; 256
254 257 return Ok(children_all_have_dirstate_node_or_are_ignored);
255 return Ok(directory_has_any_fs_only_entry);
256 } 258 }
257 259
258 let mut fs_entries = if let Ok(entries) = self.read_dir( 260 let mut fs_entries = if let Ok(entries) = self.read_dir(
259 directory_hg_path, 261 directory_hg_path,
260 directory_fs_path, 262 directory_fs_path,
293 }, 295 },
294 ) 296 )
295 .par_bridge() 297 .par_bridge()
296 .map(|pair| { 298 .map(|pair| {
297 use itertools::EitherOrBoth::*; 299 use itertools::EitherOrBoth::*;
298 let is_fs_only = pair.is_right(); 300 let has_dirstate_node_or_is_ignored;
299 match pair { 301 match pair {
300 Both(dirstate_node, fs_entry) => self 302 Both(dirstate_node, fs_entry) => {
301 .traverse_fs_and_dirstate( 303 self.traverse_fs_and_dirstate(
302 &fs_entry.full_path, 304 &fs_entry.full_path,
303 &fs_entry.metadata, 305 &fs_entry.metadata,
304 dirstate_node, 306 dirstate_node,
305 has_ignored_ancestor, 307 has_ignored_ancestor,
306 )?, 308 )?;
309 has_dirstate_node_or_is_ignored = true
310 }
307 Left(dirstate_node) => { 311 Left(dirstate_node) => {
308 self.traverse_dirstate_only(dirstate_node)? 312 self.traverse_dirstate_only(dirstate_node)?;
309 } 313 has_dirstate_node_or_is_ignored = true;
310 Right(fs_entry) => self.traverse_fs_only( 314 }
311 has_ignored_ancestor, 315 Right(fs_entry) => {
312 directory_hg_path, 316 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
313 fs_entry, 317 has_ignored_ancestor,
314 ), 318 directory_hg_path,
315 } 319 fs_entry,
316 Ok(is_fs_only) 320 )
321 }
322 }
323 Ok(has_dirstate_node_or_is_ignored)
317 }) 324 })
318 .try_reduce(|| false, |a, b| Ok(a || b)) 325 .try_reduce(|| true, |a, b| Ok(a && b))
319 } 326 }
320 327
321 fn traverse_fs_and_dirstate( 328 fn traverse_fs_and_dirstate(
322 &self, 329 &self,
323 fs_path: &Path, 330 fs_path: &Path,
346 .traversed 353 .traversed
347 .push(hg_path.detach_from_tree()) 354 .push(hg_path.detach_from_tree())
348 } 355 }
349 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path); 356 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
350 let is_at_repo_root = false; 357 let is_at_repo_root = false;
351 let directory_has_any_fs_only_entry = self 358 let children_all_have_dirstate_node_or_are_ignored = self
352 .traverse_fs_directory_and_dirstate( 359 .traverse_fs_directory_and_dirstate(
353 is_ignored, 360 is_ignored,
354 dirstate_node.children(self.dmap.on_disk)?, 361 dirstate_node.children(self.dmap.on_disk)?,
355 hg_path, 362 hg_path,
356 fs_path, 363 fs_path,
357 Some(fs_metadata), 364 Some(fs_metadata),
358 dirstate_node.cached_directory_mtime(), 365 dirstate_node.cached_directory_mtime(),
359 is_at_repo_root, 366 is_at_repo_root,
360 )?; 367 )?;
361 self.maybe_save_directory_mtime( 368 self.maybe_save_directory_mtime(
362 directory_has_any_fs_only_entry, 369 children_all_have_dirstate_node_or_are_ignored,
363 fs_metadata, 370 fs_metadata,
364 dirstate_node, 371 dirstate_node,
365 )? 372 )?
366 } else { 373 } else {
367 if file_or_symlink && self.matcher.matches(hg_path) { 374 if file_or_symlink && self.matcher.matches(hg_path) {
392 EntryState::Unknown => unreachable!(), 399 EntryState::Unknown => unreachable!(),
393 } 400 }
394 } else { 401 } else {
395 // `node.entry.is_none()` indicates a "directory" 402 // `node.entry.is_none()` indicates a "directory"
396 // node, but the filesystem has a file 403 // node, but the filesystem has a file
397 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path) 404 self.mark_unknown_or_ignored(
405 has_ignored_ancestor,
406 hg_path,
407 );
398 } 408 }
399 } 409 }
400 410
401 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter() 411 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
402 { 412 {
406 Ok(()) 416 Ok(())
407 } 417 }
408 418
409 fn maybe_save_directory_mtime( 419 fn maybe_save_directory_mtime(
410 &self, 420 &self,
411 directory_has_any_fs_only_entry: bool, 421 children_all_have_dirstate_node_or_are_ignored: bool,
412 directory_metadata: &std::fs::Metadata, 422 directory_metadata: &std::fs::Metadata,
413 dirstate_node: NodeRef<'tree, 'on_disk>, 423 dirstate_node: NodeRef<'tree, 'on_disk>,
414 ) -> Result<(), DirstateV2ParseError> { 424 ) -> Result<(), DirstateV2ParseError> {
415 if !directory_has_any_fs_only_entry { 425 if children_all_have_dirstate_node_or_are_ignored {
416 // All filesystem directory entries from `read_dir` have a 426 // All filesystem directory entries from `read_dir` have a
417 // corresponding node in the dirstate, so we can reconstitute the 427 // corresponding node in the dirstate, so we can reconstitute the
418 // names of those entries without calling `read_dir` again. 428 // names of those entries without calling `read_dir` again.
419 if let (Some(status_start), Ok(directory_mtime)) = ( 429 if let (Some(status_start), Ok(directory_mtime)) = (
420 &self.filesystem_time_at_status_start, 430 &self.filesystem_time_at_status_start,
582 } 592 }
583 } 593 }
584 } 594 }
585 595
586 /// Something in the filesystem has no corresponding dirstate node 596 /// Something in the filesystem has no corresponding dirstate node
597 ///
598 /// Returns whether that path is ignored
587 fn traverse_fs_only( 599 fn traverse_fs_only(
588 &self, 600 &self,
589 has_ignored_ancestor: bool, 601 has_ignored_ancestor: bool,
590 directory_hg_path: &HgPath, 602 directory_hg_path: &HgPath,
591 fs_entry: &DirEntry, 603 fs_entry: &DirEntry,
592 ) { 604 ) -> bool {
593 let hg_path = directory_hg_path.join(&fs_entry.base_name); 605 let hg_path = directory_hg_path.join(&fs_entry.base_name);
594 let file_type = fs_entry.metadata.file_type(); 606 let file_type = fs_entry.metadata.file_type();
595 let file_or_symlink = file_type.is_file() || file_type.is_symlink(); 607 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
596 if file_type.is_dir() { 608 if file_type.is_dir() {
597 let is_ignored = 609 let is_ignored =
614 children_fs_entries.par_iter().for_each(|child_fs_entry| { 626 children_fs_entries.par_iter().for_each(|child_fs_entry| {
615 self.traverse_fs_only( 627 self.traverse_fs_only(
616 is_ignored, 628 is_ignored,
617 &hg_path, 629 &hg_path,
618 child_fs_entry, 630 child_fs_entry,
619 ) 631 );
620 }) 632 })
621 } 633 }
622 } 634 }
623 if self.options.collect_traversed_dirs { 635 if self.options.collect_traversed_dirs {
624 self.outcome.lock().unwrap().traversed.push(hg_path.into()) 636 self.outcome.lock().unwrap().traversed.push(hg_path.into())
625 } 637 }
626 } else if file_or_symlink && self.matcher.matches(&hg_path) { 638 is_ignored
627 self.mark_unknown_or_ignored( 639 } else {
628 has_ignored_ancestor, 640 if file_or_symlink {
629 &BorrowedPath::InMemory(&hg_path), 641 if self.matcher.matches(&hg_path) {
630 ) 642 self.mark_unknown_or_ignored(
631 } 643 has_ignored_ancestor,
632 } 644 &BorrowedPath::InMemory(&hg_path),
633 645 )
646 } else {
647 // We haven’t computed whether this path is ignored. It
648 // might not be, and a future run of status might have a
649 // different matcher that matches it. So treat it as not
650 // ignored. That is, inhibit readdir caching of the parent
651 // directory.
652 false
653 }
654 } else {
655 // This is neither a directory, a plain file, or a symlink.
656 // Treat it like an ignored file.
657 true
658 }
659 }
660 }
661
662 /// Returns whether that path is ignored
634 fn mark_unknown_or_ignored( 663 fn mark_unknown_or_ignored(
635 &self, 664 &self,
636 has_ignored_ancestor: bool, 665 has_ignored_ancestor: bool,
637 hg_path: &BorrowedPath<'_, 'on_disk>, 666 hg_path: &BorrowedPath<'_, 'on_disk>,
638 ) { 667 ) -> bool {
639 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path); 668 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
640 if is_ignored { 669 if is_ignored {
641 if self.options.list_ignored { 670 if self.options.list_ignored {
642 self.outcome 671 self.outcome
643 .lock() 672 .lock()
652 .unwrap() 681 .unwrap()
653 .unknown 682 .unknown
654 .push(hg_path.detach_from_tree()) 683 .push(hg_path.detach_from_tree())
655 } 684 }
656 } 685 }
686 is_ignored
657 } 687 }
658 } 688 }
659 689
660 #[cfg(unix)] // TODO 690 #[cfg(unix)] // TODO
661 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 { 691 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {