Replacing a vector of paths with PathMap in zipper excl. tracking#4
Conversation
|
Ugh... I manually merged this with all the latest changes, but it seems I don't have the permissions to push back to this branch. |
| /// A more efficient version of [read_zipper_at_path](Self::read_zipper_at_path), where the returned | ||
| /// zipper is constrained by the `'path` lifetime |
There was a problem hiding this comment.
We need to justify the failure mode of this function, i.e. "why could it be the case that I can't read at a path" in its docs.
| let zipper_tracker = ZipperTracker::new_read_tracker(self.tracker_paths.clone(), path); | ||
| ReadZipperUntracked::new_with_node_and_cloned_path(root.borrow(), path.as_ref(), Some(path.len()), Some(zipper_tracker)) | ||
| let zipper_tracker = | ||
| ZipperTracker::<TrackingRead>::new_no_check(self.tracker_paths.clone(), path); |
There was a problem hiding this comment.
read_zipper_at_path_unchecked is a very hot method, is it possible to avoid the clone here? How deep is this clone? Debug mode is close to uselessly slow as is, so we have to avoid any additional "order of magnitude" reduction in speed.
There was a problem hiding this comment.
I think it clones Arc - which results in just increasing ref count. https://doc.rust-lang.org/src/alloc/sync.rs.html#2090
| /// Creates a new [WriteZipper] with the specified path from the `ZipperHead` | ||
| pub fn write_zipper_at_exclusive_path<'k, K: AsRef<[u8]>>(&self, path: K) -> WriteZipperTracked<'a, 'k, V> { | ||
| pub fn write_zipper_at_exclusive_path<'k, K: AsRef<[u8]>>( | ||
| &self, | ||
| path: K, | ||
| ) -> Result<WriteZipperTracked<'a, 'k, V>, Conflict> { |
There was a problem hiding this comment.
With Trackers being more internalized now, perhaps we should rename this method to "write_zipper_at_path", and the result indicates whether or not the path was exclusive or not.
There was a problem hiding this comment.
@luketpeterson ⏫ I'm leaving it for you to decide
| //Make a ZipperHead for the whole map, and two child zippers | ||
| let map_head = map.zipper_head(); | ||
| let mut a_zipper = map_head.write_zipper_at_exclusive_path(b"a"); | ||
| let mut b_zipper = map_head.write_zipper_at_exclusive_path(b"b"); | ||
| let mut a_zipper = map_head.write_zipper_at_exclusive_path(b"a").unwrap(); | ||
| let mut b_zipper = map_head.write_zipper_at_exclusive_path(b"b").unwrap(); | ||
|
|
||
| //Make a separate ZipperHead on each WriteZipper | ||
| let a_head = a_zipper.zipper_head(); | ||
| let b_head = b_zipper.zipper_head(); | ||
|
|
||
| //Make some WriteZippers on each head | ||
| let mut a0_zipper = a_head.write_zipper_at_exclusive_path(b"0"); | ||
| let mut a1_zipper = a_head.write_zipper_at_exclusive_path(b"1"); | ||
| let mut b0_zipper = b_head.write_zipper_at_exclusive_path(b"0"); | ||
| let mut b1_zipper = b_head.write_zipper_at_exclusive_path(b"1"); | ||
| let mut a0_zipper = a_head.write_zipper_at_exclusive_path(b"0").unwrap(); | ||
| let mut a1_zipper = a_head.write_zipper_at_exclusive_path(b"1").unwrap(); | ||
| let mut b0_zipper = b_head.write_zipper_at_exclusive_path(b"0").unwrap(); | ||
| let mut b1_zipper = b_head.write_zipper_at_exclusive_path(b"1").unwrap(); |
There was a problem hiding this comment.
Seems like we should reduce the synthetic overhead here if possible. Is it possible for users to use the ? operator? As mentioned, the "exclusive" seems to be captured by the unwrap here.
There was a problem hiding this comment.
Makes perfect sense. Agreed
| #[derive(Debug)] | ||
| pub struct Conflict { | ||
| with: IsTracking, | ||
| at: Vec<u8>, |
There was a problem hiding this comment.
Seems like every context Conflicts occur in, you'd have access to the reference of the path? If people are expecting a Conflict, copying the path is a silly price to pay.
There was a problem hiding this comment.
Yes, perhaps, but the path does not have to be simply a clone of the original path, as conflicts can occur, depending on the usage pattern, below or above the passed path.
| self.modify_at(path, try_add_reader_internal) | ||
| } | ||
|
|
||
| fn add_reader_unchecked(&self, path: &[u8]) { |
There was a problem hiding this comment.
What's the use of this function, or in what notion is this unchecked? This object being used on a relevant path seems mutually exclusive to at_path_unchecked being used.
There was a problem hiding this comment.
It's only used for cloning tracked zippers right now. The difference is that it simply increments the counter, without checking for exclusivity. In this case it is actually safe, because during cloning you obviously do not thread on a writer.
| /// A shared registry of every outstanding zipper | ||
| #[derive(Clone, Default)] | ||
| pub(crate) struct SharedTrackerPaths(Arc<RwLock<ZipperPaths>>); | ||
| pub(crate) struct SharedTrackerPaths(Arc<RwLock<BytesTrieMap<IsTracking>>>); |
There was a problem hiding this comment.
Why is this Arc and RwLock, only one thread may have access to the tracking?
There was a problem hiding this comment.
Can drop happen on a different thread?
Plus a couple of related changes: - replacing panic with error, - more precise types
31e36f0 to
c947ca1
Compare
Cherry picked from 798815e
luketpeterson
left a comment
There was a problem hiding this comment.
Looks fine except for the build-breaking change in release mode. Which should simply be reverted.
|
I resolved the merge in my 8ceb571 commit. I also attached the diff. zipper_head_ext_merge_diff.txt |
…ipper::into_read_zipper
Thank you @luketpeterson . Cherry picked as 06477da |
Plus a couple of related changes:
Changes
ZipperTrackeris now parametrized byTrackingModeTrackingModeserves as a type-level indicator whether we're tracking writes or reads. In Rust you cannot do much with this - but at the very least we don't need to panic at runtime e.g. inClone. Also various constructors throughout the code are aware of what mode they expect, which may be helpful in further development as there is no way to mix read-tracking zippers with write-tracking zippers.BytesTrieMapis used instad ofVecto track locked paths (SharedTrackerPaths)This helps the complexity as checking no longer needs traversing all of the
npaths and checking their prefixes.The replacement uses
BytesTrieMapas a prefix tree annotated withIsTrackingvalues that designate the subtree as locked for writing, or reading (unsigned non-zero counter)Replacing panic with error
The
Conflicttype is implemented, serving as a structured error info, and methods that previously panicked (and were poisoning the locks) now returnResult<(), Conflict>allowing the caller to graciously handle read/write conflictsWhat does not work?
Tests - I have not finished adapting them - they don't eve compile currently. Will fix in a subsequent PR
Things to scrutinize
Usage of zippers may be imprecise. Please check methods that rely on certain zipper/trie behavior e.g.
Conflict::check_subtree_for_conflict,SharedTrackPaths::try_add_writer,SharedTrackPaths::try_add_readerI am sorry for
Reformatting a lot of code which makes the PR much bigger. This is my lack of tooling knowledge showing up (I don't know how to turn it off 😄 ). Perhaps we could have some kind of project-wide formatting config to avoid this stylistic inconsistency?