Conditional suspension of Bindings

Hi, I have written and app for a small portable devices (watch) that requires very low background CPU and processing usage. I’ve been trying to optimise this and using the browser debug timeline I can see that the Aurelia binding engine is calling all subscribers every seconds or so when the screen is off as I think the browser is throttling the timers. Under predefined scenarios like document.visibility = “hidden” I want to suspend the binding engine, or at least throttle the binding engine/subscriptions to update every x seconds. Obviously I only want the last change to be called when the screen comes on. I’ve played around with throttling the flushTaskQueue and flushMicroTaskQueue (just a hack test) but this does not seem to be the way to go. Can anyone point me to a way of achieving this? could it be done with a plugin? Even some pointers on where I should look to inject some code as flushQueues does not seem to be the way forward here.

Oh, and i don’t want to add throttle/denounce to all bindings as it needs to be dynamic. Unless there is a way I can dynamically apply a throttle to all my bindings in code maybe…?

Cheers

2 Likes

By default, changes are only notified when there’s something that … has been changed. I guess there’s something in the app that triggers dirty checking, causing it to be constantly firing. Maybe have a check? Normally it can comes from either:

  • getter without @computedFrom
  • binding to an element property that Aurelia doesn’t know how to observe

You can also use this plugin https://github.com/jdanyow/aurelia-computed to automatically handle your computed property. Though I’d say it’s still better that you check it carefully.

There’s no dirty checking at all, I’ve made sure of this. And I am using @computedFrom appropriately. The problem is that the @computedFrom properties are being updated when the screen is off, and hence the binding in the dom. I don’t want the dom updates to run when the screen is off, and only the last property update to be applied when the screen comes on again.

Make sense?

1 Like

I get I could add a @computedFrom(‘signalSwicth’) and flip the switch only when I want updates to occur, but I’m looking for a way to do this globally.

1 Like

This is the part where it doesn’t make sense. Computed properties are constantly read whenever there’s something that constantly needs to read it. And if you’ve made sure that all getters are decorated with @computedFrom, then I think there’s some setInterval floating somewhere in your app, or the plugins that your app uses.

Can you put a breakpoint inside those getters, and see what’s calling it?

Ok, maybe I’m not explaining properly. I’ll try again. I have a background task that updates my model properties every 1 second. I also have a view bound to this model. So when the background task updates the model properties, Aurelia then updates the view where I’ve bound to these properties. I will continue to run my background task and update the model every 1 second, but I want to tell Aurelia to suspend all updates of the view and not reflect the model changes when the screen is off, or if say xx seconds elapsed since last update. I hope this makes sense?

1 Like

So, to be clear, you want to keep the model updates going, but changes that are supposed to go to view will be “suspended”?

Correct, conditionally suspended.

1 Like

If we have this template

<input value.bind="someProp">
<some-el value.two-way="someProp">
<some-el value.to-view="someProp">

There’ 3 types of flow here, despite all are in the “view”:
view model <-> view (<input/>)
view model <-> model (<some-el/>)
view model -> view model (one way with <some-el/>)

What will happen to the 2nd & 3rd one? Note that if it’s 2 way binding, suspended flow in one way may also stop the other.

In my case the condition will be (screen is off) so there’s no user input, I just want to suspend any view updates (to-view, ${prop}) from triggering.

or .bind, .two-way for that matter

1 Like

It’s not guaranteed that everything will work properly, but you can do it like this:

  • get the reference to the view of the current custom element
  • get the reference to all bindings and controllers (of custom elements/attributes) in side this view
  • for each binding, suspend update target
  • for each controller, get the view and recursively do this process, if wanted

So with the above process, you can suspend everything from the root of your application with a simple call.

The implementation will be something like this:

import { View } from 'aurelia-framework';
import { If, Repeat } from 'aurelia-templating-resources';

View.prototype.pauseUpdateTarget = function(recursive) {
  this.bindings.forEach(binding => {
    const shouldOvererideUpdateTarget = binding.updateTarget
      && binding.updateTarget !== pausableUpdateTarget;
    if (shouldOverrideUpdateTarget) {
      binding.originalUpdateTarget = binding.updateTarget;
      binding.updateTarget = pausableUpdateTarget;
    }
    binding.paused = true;
  });

  if (recursive) {
    this.controllers.forEach(controller => {
      if (controller.view) {
        controller.view.pauseUpdateTarget(true);
      }
    });
  }
};

View.prototype.resumeUpdateTarget = function(recursive) {
  this.bindings.forEach(binding => binding.paused = false);
  if (recursive) {
    this.controllers.forEach(controller =>
      controller.view && controller.view.resumeUpdateTarget(true)
    );
  }
}

function pausableUpdateTarget() {
  if (this.paused) {
    return;
  }
  return this.originalUpdateTarget.apply(this, arguments);
}

And in your app root

export class AppRoot {
  created(owningView, view) {
    this.view = view;
    document.on('visibilitychange', () => {
      if (document.hidden) {
        view.pauseUpdateTarget(true);
      } else {
        view.resumeUpdateTarget(true);
      }
    });
  }
}

cc @fkleuver for potential configuration in v2

2 Likes

We can go 1 step further and stop it from handling change notifications from source if needed, but that’s a bit more involved. Maybe try the above first and do some more monitoring?
Edit: Can also check if target is a DOM element then stop updating, else keep the value flow.

wow, thank you! Amazing work, it looks the goods… I’ll give this a go and let you know how it goes. I use Aurelia on several projects but don’t have this level of understanding of the inner workings as yet. I really appreciate the detail. Cheers

1 Like

wait, what is suspendableUpdateTarget and pausableUpdateTarget??

1 Like

Same, renaming without F2, use pausableUpdateTarget. Updated

1 Like

So far this seems to be working as expected but I have a little more testing to do as yet… here’s a question on this, when the screen visibility changes to visible is it possible to ‘flush’ the last update to the dom ? I’m looking to get the most recent property change to the dom, and not rely on it changing within the last second via my background task as not all properties will be updated every second.

1 Like

Probably it’s safe to do this:

View.prototype.flushChanges = function(recursive) {
  if (!this.isBound) {
    return;
  }
  this.bindings.forEach(binding => {
    if (binding.sourceExpression) {
      binding.originalUpdateTarget(binding.sourceExpression.evaluate(
        binding.source,
        binding.lookupFunctions
      ));
    }
  });

  if (recursive) {
    this.controllers.forEach(controller => {
      if (controller.view) controller.view.flushChanges(true);
    });
  }
}

Hi @bigopon,

I was able to reason about the code in your earlier post where you defined the pauseUpdateTarget and resumeUpdateTarget instance methods for the View class and used those in the AppRoot‘s created lifecycle hook. Very interesting. But I fail to see how your `flushChanges’ instance method in your latest post is intended to work.

Does it replace an existing implementation of a flushChanges instance method of the View class? Regrettably, I was unable to locate such a method in Aurelia’s source code for the View class (in /src/view.js in the aurelia-templating package). I did notice some flushChanges method implementations in some accessor and observer classes in the @aurelia/runtime-html package, but that package seems to be an Aurelia vNext package.

But if the flushChanges method does not already exist in the View class, I assume it could be considered to be an additional “custom” instance method of the View class? But how and when should it be used/called in that case? I currently do not understand how and when it gets called.

Or am I perhaps looking in the wrong places in the source code? Or do I need to investigate Aurelia’s internal mechanisms in some more detail to understand how this code is supposed to work?

Thanks in advance. :slight_smile:

Edit: Sorry… Just re-read @Devron’s latest post. So it is intended to be called when screen visibility is changed to visible. :grimacing: :sweat_smile:

2 Likes

Edit:…

Yes, so to clarify the whole picture with our future selves:

export class AppRoot {
  created(owningView, view) {
    this.view = view;
    document.on('visibilitychange', () => {
      if (document.hidden) {
        view.pauseUpdateTarget(true);
      } else {
        view.resumeUpdateTarget(true);
        view.flushChanges(true);
      }
    });
  }
}

@Devron use case is very interesting, and for the example code above, I think we can push it further, like this:

  • if binding target is not a DOM element, then proceed update
  • if binding is not paused, proceed update

This will make the data flow throw the entire app, without updating any UI pieces, and should be extremely performant if you want to propagate changes down to leaf components for some side effects, maybe :smile:

4 Likes

This is exactly how I’ve applied this, but I’ve been slightly distracted with other priorities. I’ll be doing more testing on this soon. Thanks again for all your help so far!

1 Like