Design advice: class-level introspection (SC.Record attributes)

25 views
Skip to first unread message

Dave Porter

unread,
Sep 13, 2013, 3:16:20 PM9/13/13
to sproutc...@googlegroups.com
Hi all,

I have a challenge where I need the ability to know what attributes a model class has, without actually having a record instance on hand. I'm certainly not going to create an instance of the record just to poke at it.

My hope is to take the opportunity to add some class-level introspection to SC.Record which will expose a list of attributes (that is, properties that are an SC.RecordAttribute) on SC.Record subclasses.

At a high level, does anyone have any objections to this kind of thing? (Maurits? =) ) If so, why?

At a lower level, how should this look? The best idea I've come up with so far is to just slap a hash on the SC.Record subclasses called "recordAttributes" or something which has all the attributes listed. Keeping it fresh and updated is easy via a(nother) addition to SC.Record.extend. But having a raw hash hanging out seems unpleasant somehow. Does anyone have any experience with or advice on class introspection API design?

Thanks much,
Dave

Tyler Keating

unread,
Sep 13, 2013, 3:31:52 PM9/13/13
to sproutc...@googlegroups.com
I don't know what I think about making this part of an API since it seems to either involve more memory use or more CPU use. Here's a CPU intensive way to find attributes:

for (var propertyName in MyApp.Person.prototype) { if (MyApp.Person.prototype.hasOwnProperty(propertyName)) { var property = MyApp.Person.prototype[propertyName];
if (property && property.isRecordAttribute) { console.log('found attr: ' + propertyName); } } }

--
Tyler Keating



--
You received this message because you are subscribed to the Google Groups "SproutCore Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-de...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.

Dave Porter

unread,
Sep 13, 2013, 3:42:12 PM9/13/13
to sproutc...@googlegroups.com
Increased memory use should be nil: it's a single additional hash per model class. The attributes themselves are in the hash by ref of course, so no extra there.

I'm minimizing CPU use by having code which executes only once per SC.Record.extend call, and does stuff once per extended property, so again once per model class. Below is my current (fully armed and operational) implementation. There are some things here which would slow down if we were doing it recursively or extensively, but each line of code is being called a max of once per SC.Record.extend({...}) property over the lifetime of the app.



SC.Record.reopen({

  recordAttributes: {},

  extend: function() {
    var ret = SC.Object.extend.apply(this, arguments);
    if(SC.Query) SC.Query._scq_didDefineRecordType(ret);
    ret._screc_listAttrs.apply(ret, arguments); // <-- new call
    return ret;
  },

  /* @private - expose attributes at the class level for introspection. */
  _screc_listAttrs: function() {
    // Clone current list for the subclass.
    var recordAttributes = SC.clone(this.recordAttributes);

    var hash, property, value,
        i, len = arguments.length;
    for (i = 0; i < len; i++) {
      hash = arguments[i];
      if (SC.typeOf(hash) !== SC.T_HASH) continue;
      for (property in hash) {
        value = hash[property];
        if (SC.kindOf(value, SC.RecordAttribute)) {
          recordAttributes[property] = value;
        }
      }
    }

    this.recordAttributes = recordAttributes;
  }
})

Dave

unread,
Sep 13, 2013, 3:53:58 PM9/13/13
to sproutc...@googlegroups.com
(Good call with isRecordAttribute, that should be speedier than SC.kindOf.)

--
Tyler Keating



To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-dev+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/groups/opt_out.

--
You received this message because you are subscribed to the Google Groups "SproutCore Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-dev+unsubscribe@googlegroups.com.

Dave

unread,
Sep 13, 2013, 5:13:50 PM9/13/13
to sproutc...@googlegroups.com
Here's the baseline current-master performance of SC.Record.extend with three attributes (run in 10000x batches) from SC.Benchmark.

"BENCH 0.225 msec: without (30000x); latest: 0"

This is with my original code (sent out earlier):

"BENCH 0.262 msec: first try (30000x); latest: 0"

Consistently got the 15-20% higher numbers. That's not the worst for a rare run like this but it's not awesome. So I optimized a couple of places, including using Array.prototype.slice.apply(arguments) to remove passing the arguments object around:

"BENCH 0.235 msec: second try (30000x); latest: 0"

It held consistently in the 5-10% range. That held as more record attributes were added too: 4 attributes gave me 0.272 vs. 0.29, 5 was 0.285 vs. 0.317, and 10 gave me 0.414 vs. 0.479. Is that acceptable for a run-N-times function? What's the largest number of attributes and record subclasses you've ever seen?

-----

New code:

SC.Record.reopen({
  recordAttributes: {},

  extend: function() {
    var args = Array.prototype.slice.apply(arguments);
    var ret = SC.Object.extend.apply(this, args);
    if (SC.Query) SC.Query._scq_didDefineRecordType(ret);
    ret._screc_listAttrs(args);
    return ret;
  },

  /* @private - expose attributes at the class level for introspection. */
  _screc_listAttrs: function(hashes) {
    // Clone current list for the subclass.
    var recordAttributes = {};
    for (var key in this.recordAttributes) { recordAttributes[key] = this.recordAttributes[key]; }

    var hash, property, value,
        i, len = hashes.length;
    for (i = 0; i < len; i++) {
      hash = hashes[i];
      if (SC.typeOf(hash) !== SC.T_HASH) continue;
      for (property in hash) {
        value = hash[property];
        // If the property is an attribute, add it to recordAttributes.
        if (value.isRecordAttribute) {
          recordAttributes[property] = value;
        }
      }
    }

    this.recordAttributes = recordAttributes;
  }
})

Maurits Lamers

unread,
Sep 15, 2013, 4:07:37 AM9/15/13
to sproutc...@googlegroups.com
Sorry for responding a bit late. No, I have not problem whatsoever with adding this kind of functionality :)
What would the performance be if you would implement this as a computed property on the model?

cheers

Maurits


To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-de...@googlegroups.com.

Maurits Lamers

unread,
Sep 15, 2013, 4:11:53 AM9/15/13
to sproutc...@googlegroups.com
Hi all,

In the thread about record properties I noticed something strange.
In the SC.Record extend function there is a call to SC.Query to register the model. I find this peculiar, because it binds the querying and models way too tight. 
With this I mean that if you would want to exclude SC.Query from the build and only use the records and the store, you would never be able to.

I would suggest to move this registration to the store, and SC.Query to request model info from the store if necessary. 

Thoughts?

Maurits


Op 13 sep 2013, om 23:13 heeft Dave het volgende geschreven:

To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-de...@googlegroups.com.

Dave Porter

unread,
Sep 15, 2013, 8:46:13 AM9/15/13
to sproutc...@googlegroups.com
Sproutcore unfortunately doesn't allow computed properties on classes. If it was a method the performance would be worse I think, albeit in an opt-in way.

Dave Porter

unread,
Sep 16, 2013, 8:23:11 AM9/16/13
to sproutc...@googlegroups.com
I don't know enough about the functionality to know whether it would be okay to move into the store, but if it works and you prefer there, I'm all for it. However, note that the code in questions wrapped in an if block, and so allows for SC.Query to be removed without problem.


On Sunday, September 15, 2013, Maurits Lamers wrote:

Dave

unread,
Sep 16, 2013, 11:17:20 AM9/16/13
to sproutc...@googlegroups.com
I think it's pretty obvious in retrospect that this code should be a once-per-record performance hit that's triggered as needed, i.e. the first time anyone requests this information. The penalty will be notably larger but rarer and opt-in. I'll refactor.

Thanks all,
Dave
To unsubscribe from this group and stop receiving emails from it, send an email to sproutcore-dev+unsubscribe@googlegroups.com.

Tyler Keating

unread,
Oct 27, 2013, 3:18:58 PM10/27/13
to sproutc...@googlegroups.com
I’m looking at that pull request and it’s done very well, but I would like to better understand why it’s needed. How is it that you don’t know the attributes of your own models? Is this a problem that would be solved using SC.Record’s polymorphic support? Why do you need an additional hash of the record attributes, isn’t the names enough (i.e. with the name you can retrieve the attribute from the class directly)?

I’m also reminded of a discussion I had with someone else about introspection vs. declaration and the conclusion is that introspection code is cooler and less typing for the developer, but declaration is faster. For example, we could introspect the childViews array, but instead the developer has to declare it which is more upfront work, but ultimately faster. Would you be better served with (the decidedly uncool) declaration in your model classes like so?

MyApp.MyRec = SC.Record.extend({
  name: SC.Record.attr(String),
  date: SC.Record.attr(SC.DateTime)
});

MyApp.MyRec.mixin({
  attrNames: ['name', 'date']
});

--
Tyler Keating


On Sep 13, 2013, at 1:16 PM, Dave Porter <dcpo...@gmail.com> wrote:

Dave

unread,
Oct 29, 2013, 11:33:04 PM10/29/13
to sproutc...@googlegroups.com
We need the hash as well as the names since we may need more information about the attributes than just their names – most prominently their data type, but potentially other arbitrary things. We're operating at the class level – many model-layer processes pass a record type around (you definitely don't want to make the developer create a ghost instance of the record just to introspect) – and the goal is to avoid forcing the developer to understand or care about the prototype, which IMO should be an implementation detail.

You'll know your own attributes in any situation where you know what model you're working with; this change will help in any situation where you're writing generalized code that doesn't know what model it's handling – for convenience's sake (nicely generalized code), or because you're in a framework separate from your models (necessarily generalized code). One example is a view which takes a record class and builds a form with a field for each attribute, specialized based on the attribute's data type. Another example is from a project I'm working on: a query autocomplete-as-you-type feature, where given a root recordType, it suggests attributes that you might mean (and follows dots into toOne and toMany attributes, suggesting attributes for those record type as well).
Reply all
Reply to author
Forward
0 new messages