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

Plugin system #142

Merged
merged 8 commits into from
Jun 5, 2021
Merged

Plugin system #142

merged 8 commits into from
Jun 5, 2021

Conversation

6utt3rfly
Copy link
Collaborator

(after #141)
Splitting out part of #123 to start a plugin system

  • move utilities from tests.js to test_utils.js (so they can be used by plugin tests)
  • added hooks class + tests
  • Added hooks to JSEP + tests for their arbitrary usage
  • Moved Ternary to a plugin. Default JSEP still includes ternary support.

- move utilities from tests.js to test_utils.js
- added hooks class + tests
- Added hooks to JSEP + tests for their arbitrary usage
- Moved Ternary to a plugin. Default JSEP still includes ternary support.
src/hooks.js Outdated
run(name, env) {
this[name] = this[name] || [];
this[name].forEach(function (callback) {
callback.call(env && env.context ? env.context : env, env);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

with JSEP, we kind of need access to the Jsep instance, so I think it should always be bound to this from the caller? I went back and forth on how to pass in local information, but since the plugins need to be able to manipulate the results - or provide new results, without returning a value, I still ended up with this.node and this.nodes being passed in and then read back out. It allows reassigning a value, or even clearing a parsed tree. Any better ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The usual pattern is that the hook creation code just passes this instead of a custom env object.
Are there any local variables we want to be passing that are not part of the state on this?

Copy link
Collaborator

@LeaVerou LeaVerou May 4, 2021

Choose a reason for hiding this comment

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

Oh I see. Yeah, in that case the typical pattern is that you pass in {context: this, node, nodes} as the env. I don't think it's a good idea to have instance properties that are essentially local variables that don't make sense outside the execution context that created them. Are these properties more generally useful?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! I'll push a change that uses env.context and env.node. See if it's more what you had in mind?

src/jsep.js Outdated
const nodes = this.gobbleExpressions();
Jsep.hooks.run('before-all', this);
this.nodes = this.gobbleExpressions();
Jsep.hooks.run('after-all', this);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

could potentially pass in the nodes[] array. Hooks wouldn't be able to reassign to it (but would be able to push/pop to manipulate the array)

Copy link
Collaborator

@LeaVerou LeaVerou May 4, 2021

Choose a reason for hiding this comment

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

You're passing in this, so you're passing that as well. Not sure what change you're proposing?

Edit: nvm, got it. See comment above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I changed this to run with just env.node. So now all uses would just be accessing env.node.

src/jsep.js Outdated
* @returns {?jsep.Expression}
*/
gobbleExpression() {
const test = this.gobbleBinaryExpression();

this.node = false;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I know this pattern was commented on in #123 - it's basically trying to set an initial value, then it can check if it was set by the hook (without having a return value from multiple potential hooks). Any other ideas or suggestions?

Copy link
Collaborator

Choose a reason for hiding this comment

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

If you actually use an env object (with context: this) instead of this, you can just check for the presence of a given property. Not sure how generally useful node can be if read at random times and I can easily see bugs happening because some code forgot to reset it.

Copy link
Collaborator

@LeaVerou LeaVerou left a comment

Choose a reason for hiding this comment

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

As a general comment, we should have hooks and variables exposed to hooks be guided by the needs of actual plugins, not by the needs that we anticipate theoretical future plugins may have. Thanks to your fantastic work, we have a good starter set of plugins that can guide us in terms of what we need to do. Here, I see a lot of working around to cater to the use case of plugins modifying the current node before it's returned. Is this something the plugins we have actually require? (it may be -- I don't know).
Also, I think essentially you are using this.node to mean "return value of the current function" which is going to be quite confusing. Do we actually need to be able to override every possible return value?

src/jsep.js Outdated
* @returns {?jsep.Expression}
*/
gobbleExpression() {
const test = this.gobbleBinaryExpression();

this.node = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you actually use an env object (with context: this) instead of this, you can just check for the presence of a given property. Not sure how generally useful node can be if read at random times and I can easily see bugs happening because some code forgot to reset it.

src/jsep.js Outdated Show resolved Hide resolved
src/jsep.js Show resolved Hide resolved
- switch from `this.node` to local method object `env.node`
- make after-all hook use env.node instead of nodes
- Update readme
src/hooks.js Outdated
* @public
*/
add(name, callback, first) {
if (typeof arguments[0] != 'string') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick: spaces for indentation in this entire file (I assume it's the default setting in your editor, so it happened when you copy/pasted).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So sorry! I added .editorconfig and added the tab as a rule to eslint so it should be fixed :-)

@LeaVerou
Copy link
Collaborator

LeaVerou commented May 5, 2021

LGTM, I guess we should wait until the other PR is merged now.

add lint rule for tabs
update all spaces to tabs to pass lint
# Conflicts:
#	rollup.config.js
#	src/jsep.js
#	test/tests.js
@6utt3rfly
Copy link
Collaborator Author

I merged in the main branch to update. I checked the performance of this PR versus the current and I'm a little concerned that this seems to make jsep significantly slower in parsing expressions (~35% slower, from my tests so far). There are ways to make it less readable for some minor gains (i.e. trading off setup time vs parsing time, possibly removing some hooks that aren't used just yet). Any thoughts?

- add Jsep.hooksAdd() shortcut to Jsep.hooks.add()
- pre-compute the calls to all registered hooks, binding to this from the constructor. Wrap the object-handling of the node argument into the hooks and return the result. This changes `Jsep.hooks.run('name', { context: this, node: {} })` to `updatedNode = this['name'](node)'`
- remove `before-all` and `after-all` hooks
- updated tsd.d.ts (including spacing)
@6utt3rfly
Copy link
Collaborator Author

@LeaVerou - I made another update for performance optimization:
Current Main: 100%
Current release (before the recent PRs, switching to a class): 78.87%
Pre-optimization: 67.26%
Now: 84.04%

I can revert if you prefer, but I felt that the performance hit of the hook calls was worth the change (and it's easier to use within the code, after the constructor pre-computes the calls). I'm open to further feedback and thoughts if you have any :-)

@6utt3rfly 6utt3rfly requested a review from LeaVerou May 23, 2021 19:37
@LeaVerou
Copy link
Collaborator

It would be good to see some actual timings instead of deciding solely on the basis of percentages. A 35% increase in running time can be ok if it takes execution time from 2ms to 3ms, but not ok if it takes it from 400ms to 600ms (numbers are random to make a point).

In a few words, what do you think helped improve performance in the latest commit? Is it adding the hooksAdd() method? Is it adding the hook functions on this? It might be a good idea to tease those apart if they are actually separate and see if it's one of the two that makes the biggest improvement.

It would be good to find a way to improve performance that doesn't involve repeating hook names all around the codebase. E.g. if we know we always want to bind to this, maybe we can remove the context parameter entirely and just provide this to the function as an explicit parameter (with optional extra arguments). Then perhaps we could make this optimization behind the scenes, transparently?

Overall, I would have liked to merge this PR at the state it was before the last commit, and iterate on the performance improvement vs maintainability tradeoff of the last commit in a separate PR.

@6utt3rfly
Copy link
Collaborator Author

The biggest performance bottleneck seemed to be from calling the hooks.run() method with the created { context: this, node: xx } and then using env.node everywhere. With multiple hook calls for each part of the expression parsing, it adds up. Changing to separate arguments for binding and the arg didn't have too much impact. You're right that it's probably negligible overall if it's only used to parse a single expression (versus an entire code base, for example)...

Would you be okay with a local method like this?

  runHook(name, node) {
    if (Jsep.hooks[name]) {
      const env = { context: this, node };
      Jsep.hooks.run(name, env);
      return env.node;
    }
    return node;
  }

it still has the if-statement for every run call, but then no setup in the constructor. It simplifies the callers and keeps performance close to the original. Or I can just back out my change (or create a new PR from the earlier commit)? The hooksAdd was just a convenience and for adding to the types file, but can be removed too, if you prefer.

@LeaVerou
Copy link
Collaborator

LeaVerou commented May 24, 2021

Would you be okay with a local method like this?

I think so, but is this and node the only state we ever pass to a hook?
Though if this is faster, we could at least use it on the hooks that it covers.
But if the hooks only ever need this and node we could just pass node as a parameter and bind to this with a custom hook implementation, which would save us from creating env each time, surely that will improve performance a bit.

Though if we're down to object creation and binding making a difference, I suspect the gains or losses are too marginal to matter, even with parsing multiple expressions repeatedly.

- Remove pre-compute optimization, but add local method this.runHook(name, node). This will check if there is a hook to run before calling hooks.run() in order to save time. Performance is around 89% of the code without hooks (but without the optimization was around 67% - mostly due to making the { context: this, node } object)
@6utt3rfly
Copy link
Collaborator Author

Hopefully okay now @LeaVerou ? Or if it needs anything else changed just let me know! :-)

@6utt3rfly
Copy link
Collaborator Author

Any further thoughts on this @LeaVerou ?

Copy link
Collaborator

@LeaVerou LeaVerou left a comment

Choose a reason for hiding this comment

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

Sorry, I thought we had merged this! Approved.

@6utt3rfly 6utt3rfly merged commit f987680 into master Jun 5, 2021
@6utt3rfly 6utt3rfly deleted the plugin-system branch June 5, 2021 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants