Repository navigation
Conversation
commit: |
| @@ -0,0 +1,14 @@ | |||
| export interface Node { | |||
| parent?: Node; | |||
| contexts: Record<string, unknown>; | |||
There was a problem hiding this comment.
FYI this pattern can get pretty slow since it means repeated property access in the hot path.
V8 shouldn't deopt it at least if every node has the same shape. But you are probably better off storing the state separately.
E.g. in a WeakMap<Node, Context>.
Similarly, it'll end up in dictionary mode in V8 given the context object will be inconsistently shaped. We really should use a Map instead of a plain object if we can.
There was a problem hiding this comment.
If we don't use an object here, then we'll have to implement the prototype chain ourselves in Context.get()
There was a problem hiding this comment.
I'm a bit lost, can you explain more what you mean?
This is just a properties bag attached to the node right now. I'm suggesting don't attach it to the node, just relate them through a weak map instead.
If this is actually meant to be context: Context right now, the same applies. WeakMap<Node, Context>
There was a problem hiding this comment.
It's so that contextual values are inherited from one node to the next in the tree.
We can use a Map as the underlying storage mechanism, but then we'd have to implement the lookup ourselves.
There was a problem hiding this comment.
it is clearer now that you moved to a class, you're talking about this inheritance:
https://github.com/bombshell-dev/ui/pull/17/changes#diff-3b2a6d756e64248f2ab7b374b867eadb4e3bae17231db7308752d43bd113372dR8
you can still have that without storing it on the node, though.
const contexts = new WeakMap<Node, Context>();
// elsewhere, in the Node constructor
const parentContext = parent ? contexts.get(parent) : null;
contexts.set(this, Object.create(parentContext, whatever));
// anywhere you need a context
contexts.get(node); // Context or undefinedit is more expensive to repeatedly be accessing properties of nodes during traversal than it is to look them up in a weak map, basically.
There was a problem hiding this comment.
i don't think the WeakMap buys us anything here. your idea still calls Object.create(parentContext). the inheritance mechanism is identical, it's just parked behind a WeakMap.get first. so it's the same prototype walk plus a hash lookup, not fewer property accesses.
same with Map it has no prototype chain, so Context.get would have to loop up parent by hand.
the one thing WeakMap does give us is that contexts stops being public on Node. if that's the concern I'm on board 👀
There was a problem hiding this comment.
You've misunderstood a bit because this wasn't about the inheritance at all. It was purely about the property access:
FYI this pattern can get pretty slow since it means repeated property access in the hot path
the WeakMap absolutely would have bought us a perf bump. however, since Node is now a class, i suspect it means v8 can optimise the shape into the same hidden class across the board.
happy to resolve this because of the move to using a class.
| return value; | ||
| } | ||
| }, | ||
| hasOwn: (node) => Object.hasOwn(node.contexts, name), |
There was a problem hiding this comment.
re my other comment, this is a slow path that would be made a lot faster if it was just a map. this.contexts.has(name) etc
There was a problem hiding this comment.
a Map has no prototype chain, so you'd hand-roll the parent walk in Context.get which is O(depth) loop instead of one engine-native lookup. i don't think is worth it now.
There was a problem hiding this comment.
why would we hand roll any kind of walk?
assuming every node has a context, and the context is a Map, its just a plain old .get(name) or .has(name) like in my comment above.
in the current code and my proposed code there's no concept of a prototype chain. its a flat hasOwn of a single object.
EDIT: ok i understand now. this manual inheritance stuff is a pain. contexts isn't a regular object, its the prototype chain from before. so we can't replace it with a Map elsewhere (this usage absolutely could use a map) thanks to that... i need to think about this as its forcing us into obvious perf bottlenecks.
|
@43081j We can definitely give all those a try. I also tried to make the middleware (#18 ) class-based as well and use For performance concerns, we should get a perf suite set up so that we don't introduce any regressions. I don't know how folks are feeling about CodSpeed which we are using for |
|
i wouldn't spend much time on setting up a benchmark suite yet as we're barely at the architecture level. we're better off tackling that when we've actually settled on a design. |
6152cfc to
a572b5c
Compare
|
i agree with james, we shouldn't be worrying about perf issues rn. |
|
tree-node...tree-node-jamesified here you go, also messaged it in discord 👍 |
Store contexts in lazily allocated, string-keyed maps and walk the node ancestry during lookup. Explicit undefined values continue to shadow inherited values. Co-authored-by: James Garbutt <43081j@users.noreply.github.com>
natemoo-re
left a comment
There was a problem hiding this comment.
Works for me! This seems to match @43081j's implementation quite literally. 💜
dreyfus92
left a comment
There was a problem hiding this comment.
let's keep this rollin' bois 🤘🏻
Elements participate in several overlapping scopes: host ownership, layout behavior, focus state, event handling, and rendering policy. Passing every capability through every operation would make element authors and framework adapters carry plumbing that has nothing to do with their component logic. Nodes with inherited contexts provide ambient, subtree-scoped capabilities while still allowing a descendant to override any individual concern.
The tree at this layer records semantic attachment, not visual order. That keeps the primitive small and allows the ordered host tree introduced later to define presentation without turning every middleware concern into a layout concern.
This PR adds the minimal
NodeandContextclasses, parent references, child collections, and prototype-inherited context storage inside the UI core.createContext()remains as the ergonomic context factory, with tests covering inheritance, overrides, local ownership, defaults, and null-terminated lookup.