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

Dirty Checking #1214

Closed
justinbmeyer opened this issue Aug 26, 2014 · 11 comments
Closed

Dirty Checking #1214

justinbmeyer opened this issue Aug 26, 2014 · 11 comments

Comments

@justinbmeyer
Copy link
Contributor

VOTE HERE

Dirty checking enables observation of plain old JavaScript objects. In CanJS, this will allow the creation of observable data from any source, bypassing the need to wrap data with a can.compute, can.Map, or can.List from the beginning.

By using existing observable interfaces and bindings, dirty checking will allow non-observable objects to be used anywhere a CanJS Observable would be used. This will make development more straightforward, lower the barrier to entry for new CanJS developers, and provide out-of-the-box compatibility with libraries that don't provide their own observable interfaces.

@justinbmeyer justinbmeyer added this to the 2.2.0 milestone Aug 26, 2014
@mjstahl
Copy link
Contributor

mjstahl commented Aug 27, 2014

Value, Object, and Array APIs

We should be able to use dirty checking relatively transparently with current CanJS APIs. The watch API will return an appropriate CanJS Observable (e.g. for use in a template, can.Control, or can.Component).

var foo = {bar: 'hi', baz: 'bye'};
// returns a can.compute that dirty checks the bar
// property of foo and includes normal compute
// bindings
var dirtyBar = can.compute.watch(foo, 'bar');
// returns a can.Map that dirty checks the entire foo object
// and fires all change events available with a can.Map
var dirtyFoo = can.Map.watch(foo);

var fooArray = [0, 2, 3, 4]
// returns a can.compute that dirty checks the first
// item in the array
can.compute.watch(fooArray, '0');
// returns a can.List that dirty checks the entire fooArray
// and fires all change events available with a can.List
can.List.watch(fooArray)

Secondary API

can.watch(value [, property_string]) // where value is a primitive, array, or object

can.watch is an convienence function allows the programmer to call can.watch on their desired value without having to decide which datastructure to use. It is a wrapper that ultimately ends up calling one of the Primary APIs.

The pseudo-code for can.watch is as follows:

can.watch = function(value, property) {
  if (alreadyObservable(value)) {
    return observable;
  }
  if (property) {
    return can.compute.watch(value, property);
  }
  if (can.isArray(value)) {
    return can.List.watch(observable);
  }
  if (can.isObject(value)) {
    return can.Map.watch(observable);
  }
  return can.compute.watch(value);
}

Template

Template can automatically convert a primitive object into a can.watched item. We are unsure if this is a good idea. _INPUT NEEDED_

For example:

<script id='template' type='text/mustache'>
    <h1>Welcome {{user}}</h1>
    <p>You have {{messages}} messages.</p>
</script>
var data = {
    user: 'Tina Fey',
    messages: 0 
};

var template = can.view('#template', data);
document.body.appendChild(template);

In the above example data is now an (dirty) observable, automatically converted via can.watch. Any changes to data will be reflected in the template.

Challenges

Many browsers do not have support for Object.observe. Thankfully Polymer’s observe-js provides us a means to “dirty-check” an object manually by calling performMicrotaskCheckpoint. The challenge is in the fact that we have to manage our own interval checking. One obvious question is how often we should call performMicrotaskCheckpoint.

Performance

Since performMicrotaskCheckpoint will need to be polled regularly, there are a few performance considerations. For one, we likely need to shim requestAnimationFrame to avoid CPU usage with an inactive app.

Second, can.__reading may be able to pause the polling procedure and then call it at the end of can.__reading. This may not be necessary if can.__reading ends up calling performMicrotaskCheckpoint anyway (the polling debounces itself).

@zkat
Copy link
Contributor

zkat commented Aug 28, 2014

I like this API a lot, specially can.watch. My minor comment on that is that calling it can.observe might make it feel more like the standard Object.observe, but I don't think it matters that much.

I also think it's good to have can.view automatically dirty check -- and I would argue that's one of the more important parts of all this, since so much of our object observation is for the sake of template rendering.

As far as template rendering goes, it might be nice to have an event hook into rendering completion. CanJS is currently synchronous, and folks might still expect that. Perhaps telling users to call performMicrotaskCheckpoint themselves will work, but that leaves us SOL for browsers that support observation natively.

Finally, observe-js supports several kinds of object observation. How are we handling those different kinds? Some of them are fairly interesting (such as the path observers).

@rjgotten
Copy link

👎
While dirty checking enables you to write directly against raw objects even for browsers that don't support Object.observe, it is a major, major resource hog.

I'd encourage you to read the Change Detection document describing how change detection is going to be refactored in AngularJS 2.0 just to come to grips with the expense and the effort involved in trying to keep a handle on the performance.

@justinbmeyer
Copy link
Contributor Author

Read it when it was first released. can.Map will still be around for the foreseeable future.

Sent from my iPhone

On Sep 12, 2014, at 6:14 AM, rjgotten [email protected] wrote:

While dirty checking enables you to write directly against raw objects even for browsers that don't support Object.observe, it is a major, major resource hog.

I'd encourage you to read the Change Detection document describing how change detection is going to be refactored in AngularJS 2.0 just to come to grips with the expense and the effort involved in trying to keep a handle on the performance.


Reply to this email directly or view it on GitHub.

@justinbmeyer
Copy link
Contributor Author

Instead of returning a can.Map or a can.List and trying to keep them up to date (expensive), I think this should just return a "shim" object with a bind method like

var listLike = can.observe(["a",1]);
listLike.bind("add", function(ev, added, index){})

Q: What to do if someone listens to change?

@stevenvachon
Copy link
Contributor

This one hasn't been mentioned yet: Object.prototype.watch

Its polyfill uses Object.defineProperty (IE9+), so it's probably decently performant. Considering that dirty checking is currently a bad practice, such a browser limitation is probably not market-damaging. Though, a can.Map or can.List might still be faster.

@rjgotten
Copy link

@stevenvachon
Object.prototype.watch is detrimental to performance as it clearly states in red warning atop the MDN page and was only ever implemented in Firefox.

As for Object.defineProperty: cite a better jsperf test, please. The one you are citing now is fundamentally flawed as it counts the construction time of the getter/setter as part of the test body rather than the test setup. With only a small 1000 item loop, it contributes far too much weight and skews the run time, essentially making all of its test results worthless.

(I wonder how big a percentage of the web developer community just takes jsPerf tests at face value, rather than having a look for themselves if tests are correctly implemented to begin with.)

Also, the watch polyfill cited on MDN is essentially flawed in that it does not work when the property in question is already a getter/setter property. It clobbers the existing property without providing a proper inherited call chain into the original getter/setter pair.

@stevenvachon
Copy link
Contributor

Running a loop more than 1000 times is not necessary with benchmark.js (what jsperf runs on) as it already runs each test upwards of 100,000 times.

Defining a getter/setter is part of the dirty checking process per object, so testing its performance is still valid.

Here is the same jsperf with construction time excluded: http://jsperf.com/object-defineproperty-vs-definegetter-vs-normal/67 http://jsperf.com/object-defineproperty-vs-definegetter-vs-normal/63

@rjgotten
Copy link

Here is the same jsperf with construction time excluded

With construction time excluded and purely checking usage, you find out that anything not running on Blink & the V8 JS engine is up to 270 times slower in using a getter/setter property than a normal setFoo() function. Whether a property is defined using the deprecated __defineGetter__ and __defineSetter__ way or is defined using the standardized Object.defineProperty way makes no difference.

@stevenvachon
Copy link
Contributor

Yes, as stated before, dirty checking is currently a bad practice. However, it's likely the future technique with Object.observe, so getting a working implementation is a good headstart. I just hope that it's only a plugin and not added to core.

@daffl daffl modified the milestones: 2.3.0, 2.2.0 Jan 10, 2015
@daffl daffl removed this from the 2.3.0 milestone Oct 22, 2015
@justinbmeyer
Copy link
Contributor Author

Closing. We're moving to can-define and hopefully going to use proxies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants