-
Notifications
You must be signed in to change notification settings - Fork 12k
Option resolution with proxies #8260
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
Conversation
| } | ||
| } | ||
|
|
||
| const notIndexable = ['borderDash', 'fill', 'events']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would there ever be a need for plugins to mark fields as not indexable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. Extensions too. So this needs to be designed so it can be expanded (on register I'd imagine)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if not hardcoded (which should definitely not be the case), determining if an option is indexable (or scriptable) should not rely on the option name. For example, a plugin could use one of these names as option (e.g. fill) while still expecting it to be indexable/scriptable. This information should be pass to the resolver (as it is with the existing resolve() method).
| const cache = Object.create(null); | ||
| return new Proxy(cache, { | ||
| get(target, prop) { | ||
| let value = Reflect.get(target, prop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first thought on this was performance and needing to look up the loop over and over again. Reflect is a nice solution to this!
| const defaults = { | ||
| color: 'red', | ||
| backgroundColor: 'green', | ||
| hoverColor: (ctx, options) => getHoverColor(options.color) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty powerful and would enable pulling the hover settings out into defaults
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, but it will need to support throwing an error in case of cyclic dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all platforms throw at some depth, which is quickly reached on a cyclic dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be interesting to test this case on a few browsers. It should not freeze the browser but also be easy to debug such case. Ideally, we should throw an error with the dependency loop explained.
// should throw at the "third" recursion when trying to resolve either foo, bar or xyz
const options = {
foo(ctx, opts) => opts.bar,
bar(ctx, opts) => opts.xyx,
xyz(ctx, opts) => opts.foo,
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is how it goes with the POC:
https://codepen.io/kurkle/pen/LYRdrrQ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's indeed fast to fail, though hard to debug but maybe that's because it's a codepen.
|
made some progress with performance testing: master:
resolver:
so not quite there, but not bad either. most test still fail, only go the line controller functioning. |
|
Made some more progress, got tests passing. This branch is POC: https://github.com/chartjs/Chart.js/compare/master...kurkle:resolver-wip?expand=1 Things achieved:
Couple of issues encountered:
|
|
I think I like to general idea of this PR (if I understand it correctly, which I'm still unsure), but there is a few things that should not be implicit for the caller of this method (i.e. we should not guess if an option is supposed to be scriptable / indexable). However, I would not expect this logic to replace the simple Some additional thoughts: few years ago, I was thinking to introduce option descriptors that would describe the way options are shaped and resolved. I don't remember exactly of the syntax, but conceptually it was something like this: const descriptors = {
color: {
indexable: true,
scriptable: true,
default: '#123456',
values: [
(chart, ctx) => chart.data.datasets[ctx.datasetIndex].color,
'options',
{ source: 'options', key: 'baseColor' },
'defaults'
],
},
borderDash: {
indexable: false,
scriptable: true,
default: false,
values: [
(chart, ctx) => chart.data.datasets[ctx.datasetIndex].borderDash,
'defaults'
]
},
events: {
indexable: false,
scriptable: false,
default: {},
values: [
'options',
'defaults'
]
}
}const options = helpers.options.describe(descriptors, {
sources: {
'defaults': Chart.defaults,
'options': chart.options,
}
});
/*
const resolvedColor = options.color(context, index);
const resolvedBorderDash = options.borderDash(context);
const resolvedEvents = options.events;
*/Having descriptors also allows to pre-optimize option resolution by creating accessors (proxies?) that skip some checks when not indexable, scriptable or if a source is null. |
|
Maybe it's a bit out of the scope of this PR but something else that could simplify the option resolution logic would be to fully drop support for indexable options in v3. IMO, they are cumbersome, limited and not anymore useful since scriptable options have been introduced: const colors: ['blue', 'red', 'yellow'];
const options = {
colors: colors, // don't support index > 2,
// eq.
colors: ({ index }) => colors[index],
// or better:
colors: ({ index }) => colors[index] || 'grey',
// or a different use case:
colors: ({ index }) => colors[index % 3]
}But additionally, we could add an const options = {
color: helpers.indexable(colors),
// or
color: helpers.indexable(colors, {
key: 'index' | 'dataIndex' | 'datasetIndex' | '...',
loop: false | 'repeat' | 'alternate',
reverse: true,
default: 'grey'
}),
}So controllers don't have to care anymore about how to "interpret" this option array. |
|
I'd be supportive of dropping support for indexable options (probably first as a separate PR to keep this one from getting too large). I didn't look at #8008 too closely so I'm curious what it was that you considered too complicated with it |
|
I think dropping indexable options might be quite disruptive so I'd want to avoid that. The descriptors idea looks promising in that it would be allow simplifying the lookup code (plus it would address the concerns above which options are indexable) and we could allow plugins to provide descriptors. I think for legacy reasons we'd need to support no descriptors, but that could follow the existing rules. |
|
Thanks all, good feedback! IndexableI agree with @etimberg it would be quite disruptive to stop supporting that. I had thought of it, because it can not be used on all options - but now that I tested making everything scriptable, I noticed we have quite a few callbacks (that can not be scriptable). So that same/similar issue remain even if indexable was removed. The resolution would be simpler without indexable options, but not by much. DescriptorsInteresting idea, combine all the configurable things about an option to single entity. As a DBA I'd say its almost like flat vs normalized. Descriptors being flat and defaults, scopes, indexability and scriptability separated being normalized. In a DB that means more space used. Performance depends on many things. I'm not sure how we would describe the different resolution paths we currently have, depending on the scope a option is resolved to. Eg, It would not solve "determining if an option is indexable (or scriptable) should not rely on the option name." (I'm not sure that can be solved). We would need to register the descriptors (instead of the plain defaults we not register). Actually that is just the thing we really need. Registering the indexability / scriptability of options with defaults. Those can be either flat or separate, and we can even flatten or normalize those at registration, depending on what turns out to be the best solution. Some of the things I'm trying to consider/address in this pr
8008 complexityI realized we'd need to list every single option that is usable in each scope. Also really hard to get working after more than 100 commits to master after initial PR (not quite sure why). |
I'm curious to know if indexable options are used a lot and how. I'm fine with keeping support for indexable options as long as we don't introduce extra resolve option to support looping in the array. For example, |
It would because a descriptor describes a specific option. A A set of descriptors is indeed flat but there is as many sets as there is locations/scopes where we need to read options (plugins, controllers, elements, core, etc.). For example, I would generate one set of descriptors for the datalabels plugin. In my 2017 POC, for the line controller, there was a set of descriptors for the dataset elements and another one for the data elements. Descriptors (and resolved options) are internal and should not be exposed to the public API. |
|
Agreed, the way its currently in this PR is not the way to go. (Or could be expect for the implementation of
Wouldn't the descriptors (or the spec of a descriptor) be public API so extension/plugins can provide those? |
|
Ok, that's now the default behavior which is probably better than resolving to
I'm sure users will ask for them and my point is that we should not introduce new options to support these extra use cases. Instead, we should ask users to rely on scriptable options and maybe provide an
I'm not sure I understand what you mean by "provide those". Descriptors are internal logic to describe how to resolve an option "required by a specific piece of logic". There is no dependency between descriptors, so I'm not sure why they would be public. |
|
Closing in favor of #8374 |
As #8008 gets overly complicated, I thought of an (yet another) alternative way for resolving the options.
Using proxies, and split in 2 parts.
_resolverresolves the raw values on property access (and caches that)._withContextapplies an context on top of that.One really nice advantage of this method is, that we can pass the
_withContextobject to the scriptable function, so all other values at that context are available. See the tests for example.Hoping for comments on this :)