Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Clone to SharedTree Revertible #23044

Open
wants to merge 47 commits into
base: main
Choose a base branch
from

Conversation

jikim-msft
Copy link
Contributor

@jikim-msft jikim-msft commented Nov 9, 2024

Description

13864

This PR adds forkable revertible feature to the Revertible object of SharedTree.

  • Removed DisposableRevertible and replaced by RevertibleAlpha.
  • Added clone() method to the new interface.
  • Uses TreeBranch (which is subset of TreeCheckout) to access data necessary for revert operation.

@github-actions github-actions bot added base: main PRs targeted against main branch area: dds Issues related to distributed data structures area: dds: tree labels Nov 9, 2024
@jikim-msft jikim-msft changed the title Revertible alpha Add clone to SharedTree Revertible Nov 9, 2024
@jikim-msft jikim-msft changed the title Add clone to SharedTree Revertible Add Clone to SharedTree Revertible Nov 9, 2024
@jikim-msft jikim-msft marked this pull request as ready for review November 9, 2024 02:36
@jikim-msft jikim-msft requested a review from a team as a code owner November 9, 2024 02:36
Copy link
Collaborator

@msfluid-bot msfluid-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Coverage Summary

↑ packages.dds.tree.src.simple-tree.api:
Line Coverage Change: 0.01%    Branch Coverage Change: No change
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 88.76% 88.76% → No change
Line Coverage 82.35% 82.36% ↑ 0.01%
↑ packages.dds.tree.src.shared-tree:
Line Coverage Change: 0.02%    Branch Coverage Change: 0.15%
Metric NameBaseline coveragePR coverageCoverage Diff
Branch Coverage 90.88% 91.03% ↑ 0.15%
Line Coverage 97.28% 97.30% ↑ 0.02%

Baseline commit: aa2718b
Baseline build: 306963
Happy Coding!!

Code coverage comparison check passed!!

@msfluid-bot
Copy link
Collaborator

msfluid-bot commented Nov 9, 2024

@fluid-example/bundle-size-tests: +919 Bytes
Metric NameBaseline SizeCompare SizeSize Diff
aqueduct.js 465.72 KB 465.75 KB +35 Bytes
azureClient.js 563.04 KB 563.09 KB +49 Bytes
connectionState.js 724 Bytes 724 Bytes No change
containerRuntime.js 262.34 KB 262.35 KB +14 Bytes
fluidFramework.js 426.99 KB 427.34 KB +351 Bytes
loader.js 134.18 KB 134.19 KB +14 Bytes
map.js 42.71 KB 42.71 KB +7 Bytes
matrix.js 149.84 KB 149.85 KB +7 Bytes
odspClient.js 528.83 KB 528.88 KB +49 Bytes
odspDriver.js 97.88 KB 97.9 KB +21 Bytes
odspPrefetchSnapshot.js 42.81 KB 42.83 KB +14 Bytes
sharedString.js 165.77 KB 165.78 KB +7 Bytes
sharedTree.js 417.45 KB 417.79 KB +344 Bytes
Total Size 3.37 MB 3.37 MB +919 Bytes

Baseline commit: afcf161

Generated by 🚫 dangerJS against 04fdc50

packages/dds/tree/src/core/revertible.ts Outdated Show resolved Hide resolved
packages/dds/tree/src/shared-tree/treeCheckout.ts Outdated Show resolved Hide resolved
packages/dds/tree/src/shared-tree/treeCheckout.ts Outdated Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 5 out of 6 changed files in this pull request and generated no suggestions.

Files not reviewed (1)
  • packages/dds/tree/src/core/index.ts: Evaluated as low risk
Comments skipped due to low confidence (2)

packages/dds/tree/src/shared-tree/treeCheckout.ts:634

  • [nitpick] The method name 'clone' might be ambiguous. Consider renaming it to 'createClone' or 'duplicate' for better clarity.
clone: (forkedBranch?: TreeBranch) => {

packages/dds/tree/src/shared-tree/treeCheckout.ts:639

  • [nitpick] The error message 'Unable to dispose a revertible that has already been disposed.' could be more descriptive. Consider providing more context about why this error might occur.
throw new UsageError("Unable to dispose a revertible that has already been disposed.");

Tip: Copilot code review supports C#, Go, Java, JavaScript, Markdown, Python, Ruby and TypeScript, with more languages coming soon. Learn more

@github-actions github-actions bot added the public api change Changes to a public API label Nov 12, 2024
@github-actions github-actions bot added the area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct label Nov 13, 2024

it("clone revertible fails if trees are different", () => {
const viewA = asTreeViewAlpha(createLocalSharedTree("testSharedTreeOne"));
const viewB = asTreeViewAlpha(createLocalSharedTreeTwo("testSharedTreeTwo"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this test case, it's not important that the two trees have different schema - the part that matters is that they are different instances of a SharedTree. So, it should be perfectly fine to initialize viewB using a second call to createLocalSharedTree() rather than createLocalSharedTreeTwo(). And then you can delete createLocalSharedTreeTwo().

@@ -453,6 +464,137 @@ describe("Undo and redo", () => {
revertible.revert();
assert.equal(view.root.foo, 1);
});

it("reverts original & forked revertibles after making change to the original view", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you write these tests within the loop that all of the other tests (except 1 - "can undo while detached") in this file are in? If you look at the describe block at the top of the file, you will see that it runs all the tests in "attached" and "detached" modes. If you adapt your tests here to use the getCheckout helper, it should be easy to have them do the same thing.

Copy link
Contributor

@noencke noencke Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, actually, it's a little weird. You have a couple of options.

  1. Write your tests in the pattern already established in this file. So, you don't define a schema, and instead you do what all the other tests do and use createCheckout and tree.editor to do edits. This isn't the best way to edit the tree, but it's consistent with how the rest of the file is doing it. So when somebody goes to convert this file later to the "better way", it won't really add any more work to change your tests since they'll all be updated in the same way.
  2. Write your tests in a newer, better pattern - kind of what you are already doing, although I'd recommend you use something like TestTreeProviderLite rather than writing the factory/runtime code yourself. This is nice because it uses a real schema, and the better APIs for editing. However, then we end up with two different patterns in this file - the existing ones, and your new ones. And somebody coming along later to fix this file will have to deal with both patterns.

My vote would be for (1). We should either convert this whole file to a better test pattern first, and then do this PR. Or, we should write new tests in the same way as the current pattern, and then convert them all at once.

Curious what you think. Either way, having your tests run against both "attached" and "detached" scenarios is valuable - and doing that and also (2) will mean some duplicated/divergent code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I can go along with case (1) and I've noticed that there are skipped cases with // TODO: unskip once forking revertibles is supported comment.

I can un-skip these pre-written test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noencke

I've tried to use createCheckout() and tree.editor but realized clone: (branch?: TreeBranch), thus I can't (and shouldn't) pass TreeCheckout. Unless there is a way to convert TreeCheckout to TreeBranch prior to calling clone(), I think I should rewrite the skipped tests with my test design.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just update createCheckout() to also return the SharedTree so that you can make a view from it via viewWith(). You can change createCheckout() however you want, really - since as we discussed above, we don't particularly like how any of this is currently operating.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it were me, I'd probably:

  1. Have createTree() which does pretty much everything that the current createCheckout() does, except that it returns a SharedTree
  2. Change createCheckout() to just do return createTree().checkout

/**
* TODO: Add description
*
* @param forkedBranch - A target branch to apply the revertible to. If not given, spawns a cloned revertible of the original branch.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should prob add additional details about what we exactly expect of this branch, mainly that it has the same commit that the revertible is meant to revert. also, what happens if we have a cloned revertible of the original branch? are the lifetimes of the clone and the original tied together?

packages/dds/tree/src/core/revertible.ts Outdated Show resolved Hide resolved
packages/dds/tree/src/core/revertible.ts Outdated Show resolved Hide resolved
revertible.dispose();
}
},
clone: (forkedBranch?: TreeBranch) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what was the reasoning behind allowing forkedBranch to be optional? why not just make it required and not allow clone to be called without a fork? the behavior for cloning a revertible onto the same checkout doesn't seem obvious to me and i'm not sure why we'd want to do so

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cloning a revertible onto the same branch is useful for an application that wants to provide ownership of a revertible to multiple components. Suppose I have two components in my application that:

  1. Don't know about each other
  2. Are operating on the same branch (e.g. the main branch)
  3. Both want to be able to revert (e.g. they both have undo buttons)

You'd want to clone the revertible, giving one to the first component and the clone to the second. Or, perhaps the parent component retains ownership of the original revertible and gives a new clone to each subcomponent. If you can't clone the revertible for the same branch, then you have to share the same revertible between the parent and both components, and now they do need to know about each other so that they know when to (or when not to) dispose the revertible.

Regarding the parameter being optional, it's just a convenience. For an application like the above, which operates solely on the main branch, it would be annoying at best and confusing at worst to be forced to pass in the main branch every time it wants to do a clone. With it being optional, the user doesn't need to understand the branching feature at all in order to clone revertibles.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it would be useful, but I'm not entirely convinced that the changes here sufficiently support it. At the very least, I think it needs to be documented better.

some thoughts:

  • if one of the components does a revert, how does it get communicated to the other? do we just fail whenever the other ones tries to do a revert and we can't find the repair data anymore?
  • it seems like the status is currently cloned as well, which is fine since revertibles can be disposed without being reverted, but I think we'd need some way to properly communicate between clones whether the repair data they share can still be used
  • do we maybe want a different api for cloning with the same branch? it kinda sounds like the meaning changes depending on if we're cloning with a different branch or the same branch i.e. with a different branch, nothing you do with the cloned revertible will affect the original but with the same branch, they still have the same dependencies even though they can be owned by different components and disposed separately as well although i guess it would be somewhat similar to the case where a user explicitly does not dispose after revert and we call revert again on the same revertible and it just noops. which I still think it kinda strange.
  • I think having the parameter be optional is fine if the behavior around operating on the same branch is more clear

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a set of clones of the same revertible associated with the same branch, the repair data will not be collected until all clones have called dispose(). This is the intended behavior, and I believe is the currently behavior, but if it's not covered by tests then we should add more tests.

So, yes, if component A and component B each have a clone of a revertible, and are on the same branch, then when A does a revert it will do something, and when B does a revert it will (probably) no-op. This is what I would expect - the reason you cloned in the first place is so that A and B don't have to know what each other are doing, therefore, if B does an undo but it was already undone by A, nothing will happen (because it "already happened"). The other option which you alluded to in your first bullet is for B's undo stack to be changed along with A's undo stack. But if that's what the user wants, then A and B are not isolated and the user shouldn't have done a clone in the first place. Undo-stacks are probably 1:1 with branches most of the time, is my guess. Being able to clone a revertible on the same branch is more of a ref-counting thing. Like you have some code in your application that wants to ensure it can revert some specific data at some point in the future, so it holds on to a particular revertible - regardless of whether or not a clone of that revertible might get reverted anyway by some other means. I think it's likely to be a pretty uncommon scenario, but it might be useful to have as an option, it's easy for us to implement, and it's the natural behavior (IMO) of a parameterless call to clone.

A review of potential scenarios (e.g. with Nick) to inform the documentation would probably not be a bad idea. If we find that it's just too hard to explain why you would do this, then that could be a sign that we should just leave it out.

packages/dds/tree/src/shared-tree/treeCheckout.ts Outdated Show resolved Hide resolved
packages/dds/tree/src/test/shared-tree/undo.spec.ts Outdated Show resolved Hide resolved
Copy link
Contributor

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> [email protected] ci:linkcheck /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test ci:start 1313 linkcheck:full

1: starting server using command "npm run ci:start"
and when url "[ 'http://127.0.0.1:1313' ]" is responding with HTTP status code 200
running tests using command "npm run linkcheck:full"


> [email protected] ci:start
> http-server ./public --port 1313 --silent


> [email protected] linkcheck:full
> npm run linkcheck:fast -- --external


> [email protected] linkcheck:fast
> linkcheck http://localhost:1313 --skip-file skipped-urls.txt --external

Crawling...

Stats:
  443760 links
    3414 destination URLs
       2 URLs ignored
       0 warnings
       0 errors


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: dds: tree area: dds Issues related to distributed data structures area: framework Framework is a tag for issues involving the developer framework. Eg Aqueduct base: main PRs targeted against main branch public api change Changes to a public API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants