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

refactor: simplify JSV #109

Merged
merged 30 commits into from
Mar 17, 2021
Merged

refactor: simplify JSV #109

merged 30 commits into from
Mar 17, 2021

Conversation

marcelltoth
Copy link
Contributor

@marcelltoth marcelltoth commented Mar 4, 2021

Alright, so this is going to be opinionated. Prep your coffee, tea, monster (😉), whatever. Don't read in a bad mood please. 😇

Background

We've long had an issue, stoplightio/elements#667. If you read the description you can see that while it's a non-trivial problem, it feels reasonable, something that can be done.

Then we tried to implement it. We struggled. Then we invited @P0lip to help us. Then we tried again. Struggled still. Then we held a 4-hour long session together to make something useful. We still struggled, but we built some POC. I tried to build something complete myself based on our POC, but I failed. I was running into all sorts of hard-to-debug state-management-related issues, crashes, etc. Items missing from caches, TypeError-s, etc. I gave up.

TLDR

While the current state management approach JSV is using has some wonderful features, it is unfortunately borderline impossible to comprehend by basic human beings like myself. This poses a huge risk to the Elements project (and possibly the Stoplight ecosystem as a whole) because - as this oneOf/anyOf issue shows - Team Undefined seems to be simply incapable of working on it, in it's current form.

Proposal

I propose to remove most of the state-management complexity from JSV, and fall back to a basic, idiomatic react approach:

  • No mobx
  • No stores of any kind
  • No tree-list
  • No virtualization
  • Much less context usage, much more props passing

The foundation, json-schema-tree is kept as-is, JST is an amazing library for abstracting away the bulk of the logic. There wasn't much extra logic actually on top of what JST offers, the most notable one being array element type hoisting. (a.k.a. "flattening") That logic was moved from the SchemaTreeListTree class to custom hooks and functions, see utils.ts

The bulk of the presentation logic is kept intact, except it is now simpler as it:

  • only uses a single source of input: the JST node, no jumping between a tree list node and a JST node
  • uses only props for the most part, not a lot of context, which makes components easier to reason about
  • some typing improvements were made, see PrimitiveArrayNode, etc

Results

The good

  1. It's hopefully much simpler to understand and work on. There is evidence of this as I have built a rudimentary version of the oneOf/anyOf selector in only about 70 LOC (and in about 2 hours) which already works much more reliably than our old POC.
  2. It unknowingly fixed some bugs. Come here open Permissions then ids. It's broken, I found it when comparing the new with the old. The new version just works. It's much more primitive logic, but because of that, there's likely less edge cases and random bugs like this.
  3. Removed ~400 LOC, improving readability and bundle size.
  4. Bundle size is much better. Own size is 12KB vs 20KB prior, in addition to losing two big dependencies:
  5. Not having virtualization soles a lot of our current and future problems, such as:
    • Struggling to instantiate JSV into a non-fixed-height container.
    • Not being able to show the entire description if the user asks for it. (Word wrapping and virtualization don't play well together.)
    • Being able to render and assert against JSV in a JSDOM environment without mocking either JSV or autosizer or both.
  6. stoplightio/platform-internal#5476 should be implicitly resolved by this change.

The bad

  1. Initial rendering performance is worse for large schemas, especially if defaultExpandedDepth > 1.

The stress test which renders a 15-thousand-line json-schema twice with defaultExpandedDepth: 2 takes a little more than 1 second on my machine vs the virtualized version, which takes about 300ms.

I would say this is still definitely the "worth it" category. The results above are without any attempt at performance optimization, but even if we can't improve it much: 30k lines worth of content loading in 1 sec should be acceptable IMO. We'll throw a loading-spinner, animation or anything and it will feel butter-smooth. (Or a progress-bar, right, @wmhilton? 😉)

TODO

  1. While working on the POC I removed some props, some of which may be needed, I need to add them back.
  2. As tests were largely relying on Snapshots and props passing, I need to update and/or rewrite them.

@marcelltoth marcelltoth self-assigned this Mar 4, 2021
@marcelltoth marcelltoth marked this pull request as ready for review March 8, 2021 10:35
@marcelltoth marcelltoth requested a review from a team March 8, 2021 10:35
@marcelltoth marcelltoth requested a review from a team March 8, 2021 11:19
Copy link
Contributor

@mpodlasin mpodlasin left a comment

Choose a reason for hiding this comment

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

For me this is insanely impressive.

However I am obviously waiting for the passing tests - my main gripe would be catching any regressions, so final opinion on this PR would largely depend for me on quality of test refactor.

@mallachari
Copy link
Contributor

I'm surprised that removing tree list went so smoothly. I was expecting much bigger refactor. At this point the code is way more accessible and considering current requirements that change might be good to have. I got used to tree list usage, also got familiar with it to some extent, but trying to add recent changes made it too painful and unnecessarily complex. In the end it's a matter of picking between virtualization and simplicity.
I didn't notice anything breaking with this approach so far, though porting tests might encounter something that was missed.
Generally I like this approach as it solves problems that we are struggling with for some time, though I'm curious of the opinion from the void side.

@P0lip
Copy link
Contributor

P0lip commented Mar 9, 2021

I'm surprised that removing tree list went so smoothly. I was expecting much bigger refactor. At this point the code is way more accessible and considering current requirements that change might be good to have. I got used to tree list usage, also got familiar with it to some extent, but trying to add recent changes made it too painful and unnecessarily complex. In the end it's a matter of picking between virtualization and simplicity.

@mallachari yeah, it's probably because the last time you looked at the component, JSV was strictly tied to tree-list.
Plenty of UI components accessed tree-list nodes directly (its metadata), and the whole tree was somewhat built on top of tree-list's tree (we simply created tree-list tree nodes when iterating over schemas, etc.)
It was painful as hell to get rid of that tree-list usage. I had to spend 2-3 days to do that, but had to do it, so that you all don't hate me :trollface:
Once JSV was set to use JST, the tree-list was actually used in one spot, that was still quite tangled, but there was not much I could do about it, cause we still had to map JST node with a tree-list node and so on.


Speaking of the PR, I'm going to review it soon, probably today or tomorrow.

yarn.lock Show resolved Hide resolved
@marcelltoth
Copy link
Contributor Author

marcelltoth commented Mar 9, 2021

@mallachari yeah, it's probably because the last time you looked at the component, JSV was strictly tied to tree-list.
Plenty of UI components accessed tree-list nodes directly (its metadata), and the whole tree was somewhat built on top of tree-list's tree (we simply created tree-list tree nodes when iterating over schemas, etc.)
It was painful as hell to get rid of that tree-list usage. I had to spend 2-3 days to do that, but had to do it, so that you all don't hate me :trollface:
Once JSV was set to use JST, the tree-list was actually used in one spot, that was still quite tangled, but there was not much I could do about it, cause we still had to map JST node with a tree-list node and so on.

Oh yeah, I wouldn't have attempted this a few months ago 😂 . The JST-extraction was amazing, it took out most of the core logic without having any UI-related dependencies. This is philosophically meant to be a follow-up to that PR, completing what you started there.

@P0lip
Copy link
Contributor

P0lip commented Mar 9, 2021

Yeah, to be frank, when working on #91 and related PR, I had a moment when I thought it might be easier to simply write a new component.


Speaking of dropping virtualization, I'm totally down for it, as long as it doesn't meaningfully impact performance.
I know very well how difficult it was to deal with it, therefore I totally understand your motivation.
In any case, I'll take a look at it soon, but I doubt I'll have any major objections unless any of the test models I use for testing turns out to be slow to render, yet hopefully that's not the case.

@domagojk
Copy link
Contributor

The PR looks great! So much more deletions than insertions 🤩 But I haven't look into detail, I just wanted to say that this PR will automatically close this one:

Screen Shot 2021-03-10 at 16 46 18

And I guess we shouldn't do this because we still need to update it in studio 🤷‍♂️

@marcelltoth
Copy link
Contributor Author

Oh, these magic words always get me, even though I know them 😄 Thanks for the heads-up @domagojk, I'll update it.

@marcelltoth
Copy link
Contributor Author

marcelltoth commented Mar 10, 2021

I'm okay with dropping tree list, therefore I'm also fine with the chosen approach
Ping me when this PR is ready and I'll be happy to have a final pass.

@P0lip I cleaned up the TODOs, addressed your comments and reimplemented the required props with one change: I swapped the custom rowRenderer feature for a rowAddon that does pretty much the same and fits the masking use-case just as well. Reasoning being that with SchemaRow now rendering its own children, writing a correct and good looking rowRenderer would be tricky I found out when trying it out in the storybook. This narrowed feature-set still serves the same purpose but should be much easier to use.

The only TODO left is the tests, which are being handled by #111 first, otherwise this is ready for a next pass.

Copy link
Contributor

@P0lip P0lip left a comment

Choose a reason for hiding this comment

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

Looks good with the exception of stepIn being gone in the new code.
We should try to add it back. LMK if you have any concerns
Apologies I didn't spot this during the first pass.

});
}
jsonSchemaTree.walker.hookInto('filter', node => {
Copy link
Contributor

Choose a reason for hiding this comment

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

You lost stepIn in the transition.
This is quite important, otherwise really large schemas with a ton of $refs will be much slower to process than needed.
stepIn is needed, so that JST tree is not built upfront for the whole schema, but for actual parts that are currently rendered.
This is also one of the more notable factors contributing to the entire complexity of the former solution.
While, getting rid of virtualization is fine by me, we certainly should at least try to preserve that optimization.
By adding it back, we should be more or less on par with current v3.

@P0lip
Copy link
Contributor

P0lip commented Mar 15, 2021

Hmm, or maybe not.
This is what happens when I try to open one of our reference models. (scroll to the bottom of my comment, it's not an issue caused by your PR, but a relatively minor issue in JST)
image
image

Chromium is no different. 😢
image

Note - I modified the story to load only a single instance of JSV instead of two.

 .add('stress-test schema', () => (
    <>
      <div style={{ height: '100vh', overflowY: 'scroll' }}>
        <JsonSchemaViewer
          schema={stressSchema as JSONSchema4}
          defaultExpandedDepth={number('defaultExpandedDepth', 2)}
          onGoToRef={action('onGoToRef')}
        />
      </div>
    </>
  ))

It's likely to be something off with JST meeting some very recursive data, as the model I test it against has plenty of such.
I'll look into it now.


I got the fix for that JST issue, but even with that, it's still quite slow.
With profiling on and no CPU throttling it takes over a second for render, which is probably too much.
With CPU throttling set to 4x, it's over 7s.
Do note that I still test it having the above storybook tweak applied.

Now, in the case of current origin/beta tip the results are ~250ms and ~750ms respectively for CPU 4x throttling.

Whether it's acceptable or not, it's up to Product team, I suppose.

Copy link
Contributor

@P0lip P0lip left a comment

Choose a reason for hiding this comment

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

Aside from that perf issue, the code is good 👍

@@ -37,7 +37,6 @@
"peerDependencies": {
"@stoplight/markdown-viewer": "^4",
"@stoplight/ui-kit": "^3",
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still rely on UI-Kit in JSV?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately it's still a peer of MDV :\

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 removed the include from the storybook _styles.scss. That at least make Storybook rebuilds faster.... which is a win I guess... 😅

@marcelltoth marcelltoth merged commit 512383d into beta Mar 17, 2021
@marcelltoth marcelltoth deleted the refactor/jsv branch March 17, 2021 16:06
@stoplight-bot
Copy link
Collaborator

🎉 This PR is included in version 4.0.0-beta.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

@stoplight-bot
Copy link
Collaborator

🎉 This PR is included in version 4.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants