Refactor: Lazy Loading and Per-Type Subclasses for oNode Performance#83
Refactor: Lazy Loading and Per-Type Subclasses for oNode Performance#83Tilix4 wants to merge 5 commits intocfourney:require-refactorfrom
Conversation
There was a problem hiding this comment.
Lots of interesting things, but I do worry about some of the patterns and the compatibility with some edge cases.
I'm not sure if you know, but because of the cyclic nature of the access to the $ object embedded in each prototype (even though the $ object is created last), which was setup to fix the issue of lack of access of the global scope in some scenarios, we do this thing in base.js where we create the $ link on all the classes that were loaded from the lib. I think because of this it would be better to make oNodeType a class with a prototype and intitialize it during load instead of creating a simple dictionary type struct. It would also ensure that this.$ is properly accessible in the methods and not pointing to the global scope
Also in terms of philosophy of design I'm not sure about the return undefined that we have here, they could hide a lot of errors until later... I would rather we throw the errors when we reach them; also would be more representative for the testing.
And last thing, I think the current getter setter logic is flawed as it returns the attribute instead of getting/setting its value... Maybe there's a way to reuse the old logic here to avoid code duplication and inconsistent behavior?
I'm also not a fan of having to create dummy nodes to read the attributes. We can assume that oNode objects will be created as a wrapper around an existing node so we can simply use that I think?
openHarmony/classes/oNode.js
Outdated
| } | ||
|
|
||
| // Base initializer (shared by per-type subclasses) | ||
| oNode._init = function(path, oSceneObject){ |
There was a problem hiding this comment.
Don't we have to add this on the oNode prototype instead? I think the jerry rigging of attaching $ to the object will only work if this is an instance method (otherwise you will lose access to this.$ inside the function body)
openHarmony/classes/oNode.js
Outdated
| * Each type is scanned once (first use) to define shorthand getters | ||
| * on a shared prototype. Subsequent instances of that type reuse the subclass. | ||
| */ | ||
| var oNodeTypes = { |
There was a problem hiding this comment.
I think also this might break the attaching of $ to the prototype, which might make it an issue when the code is run from a package, dialog or Qtimer. I would make this a proper "class" (as proper as Qtscript has anyway) and intitialize it like a singleton during the lib init, then access it with this.$ inside the oNode constructor.
This will also make it stylistically closer to the other classes of the lib and ensure we control what 'this' points to in these methods.
openHarmony/classes/oNode.js
Outdated
| _classes: {}, | ||
| _prototypeGettersAdded: {}, // Track which prototypes have had getters added for which types | ||
|
|
||
| getKnownTypes: function(){ |
There was a problem hiding this comment.
Maybe that can just be a property like oNodeTypes.types that returns the object with keys as types and values as constructors? Then it will be easy for people to enumerate over them?
openHarmony/classes/oNode.js
Outdated
| * This is called from oNode._init to handle named subclasses that inherit from oNode.prototype. | ||
| * @private | ||
| */ | ||
| _ensurePrototypeGetters: function(ctor, type){ |
There was a problem hiding this comment.
this is just a stylistic nitpick but I'm not a big fan of contracted/abreviated variable names for readability. I think maybe we could spell it constructor here to make it obvious?
But also, I'm thinking that there is something about the architecture that we can improve so we don't have to create the node. Why don't we pass the node path instead of type? Typically, oNodes are created to reflect existing nodes so it's redundant to create one for the type when we are currently wrapping an existing one. I agree it's less cleanly separated but asking the app to do something like adding a node has significant overhead compared to just running some js
openHarmony/classes/oNode.js
Outdated
| if (!type || !ctor || !ctor.prototype) return; | ||
|
|
||
| // Create a unique key for this constructor+type combination | ||
| var ctorName = ctor.name || ctor.toString().substring(0, 50); |
There was a problem hiding this comment.
not super clear here why we need the substring 50 ?
openHarmony/classes/oNode.js
Outdated
| get: function(){ | ||
| // Trigger lazy loading; attributes getter will install instance getters | ||
| var attrs = this.attributes; | ||
| if (!attrs) return undefined; |
There was a problem hiding this comment.
again here if attributes is null we have an issue, throwing an error would be better
openHarmony/classes/oNode.js
Outdated
|
|
||
| // Fallback: if attribute exists in cache, return it directly | ||
| // This handles edge cases where setAttrGetterSetter didn't create a matching getter | ||
| if (attrs[kw]) return attrs[kw]; |
There was a problem hiding this comment.
getters setters are never full fledged attributes. They get set differently, and accept different values. Here we should return the value not the attribute I think
openHarmony/classes/oNode.js
Outdated
| if (attrs[kw]) return attrs[kw]; | ||
|
|
||
| // Attribute doesn't exist on this node type | ||
| return undefined; |
There was a problem hiding this comment.
again, here I would prefer if we throw an error
openHarmony/classes/oNode.js
Outdated
|
|
||
| // After lazy loading, instance getter should exist (created by setAttrGetterSetter) | ||
| // Check and call it directly to get proper value with sub-attribute handling | ||
| var desc = Object.getOwnPropertyDescriptor(this, kw); |
There was a problem hiding this comment.
should it not be
var desc = Object.getOwnPropertyDescriptor(attrs, kw);here?
I'm a little confused about why we're not just accessing the attribute from the attribute object using the kw.
openHarmony/classes/oNode.js
Outdated
| // Clean up temp node first, then group (order matters) | ||
| if (tempNodePath) { | ||
| try { | ||
| node.deleteNode(tempNodePath); |
There was a problem hiding this comment.
yeah I would really prefer not having to do this. Also not sure deleteNode needs a try catch?
… mechanisms Co-Authored-By: Félix David <felixg.david@gmail.com>
Two optimizations:
1. Lazy Attribute Loading
attributesBuildCache()andsetAttrGetterSetter()run on first access to.attributesor shorthand properties2. Per-Type Subclass Factory with Dynamic Inheritance
oNodeTypesfactory that creates and caches per-node-type subclassesnode.getAttrList()position,offset) that trigger lazy loadingoNodeconstructor delegates to the appropriate per-type subclassoDrawingNode,oPegNode, etc.) inherit shorthand getters via_ensurePrototypeGetters()3. Optimized
getNodesByType()subNodes()traversal with nativenode.getNodes([typeName])APIKey Changes
oNodeconstructor: Removed eagerrefreshAttributes()call; added lazy loading flagsoNode.prototype.attributesgetter: TriggersattributesBuildCache()andsetAttrGetterSetter()on first accessoNodeTypesfactory: New system for creating per-type subclasses with prototype shorthand gettersoGroupNode.prototype.getNodesByType(): Uses native Harmony API instead of recursive traversalfinallyblocksBenefits
node.position.x = 5works immediately via prototype gettersTesting