Conditional suspension of Bindings

Unless I’m not understanding the issue…

Can you not just suspend updating the UI by… not pushing any new data to the UI state?

So you keep some of your app state separate from the UI state and only push changes between them when the page is visible?

4 Likes

FYI, we do have a baked-in mechanism for this sort of thing in v2 since all DOM updates go via the scheduler which is “throttled” by a rAF loop. Post-alpha we’ll be adding a timeslicing api that provides more fine-grained control over how frequently and in what priority updates are propagated.

6 Likes

Hi @bigopon, I’m finally back into this. I have some really odd memory growth in the chromium GPU when the screen is off only, and I’ve narrowed this down to my component page. If I run my services without the view bound there is no memory growth with the screen off. So I’m thinking that the browser is rendering and buffering (or something) when the screen is off based in DOM changes, and when the screen comes on it draws/paints (whatever) but does not clear it’s bitmap buffers. To be honest I don’t really know what I’m talking about… it’s all just guesses at the moment.

Anyhow, to eliminate the DOM updates I really need to suspend all binding when the screen is off. The code above this that you provided previously seems to work well for normal bindings, but I also need to suspend InterpolationBinding/ChildInterpolationBinding as most of my UI is dynamic and using compose with view.bind=“viewStrategy” and Interpolation. How would I modify this code to also suspend (and resume/flush) ChildInterpolationBinding. I’ve explored the view tree and can see the compose bindings but can’t seem to find the ChildInterpolationBindings??

Any help appreciated.

Cheers

1 Like

I guess i can do something like this:

ChildInterpolationBinding.prototype.originalUpdateTarget = ChildInterpolationBinding.prototype.updateTarget
ChildInterpolationBinding.prototype.updateTarget = function (value) {
if (document.visibilityState == “hidden”) return
this.originalUpdateTarget(value)
}

Which seems to work, but I don’t know how to access these bindings from the view to flushChanges when the the document.visibilityState == “visible”…?

1 Like

The code for constructing interpolation bindings is here templating-binding/interpolation-binding-expression.js at f865e5934546536629aae75d1b0c9e56e953bc26 · aurelia/templating-binding · GitHub

ChildInterpolationBinding is an optimization done when the interpolation has only 1 dynamic parts.

For your use case, you can do it like this, probably:

if (binding instanceof ChildInterpolationBinding) {
  binding.updateTarget = ... // here
} else if (binding instanceof InterpolationBinding) {
    let parts = binding.parts;
    for (let i = 1, ii = parts.length; i < ii; i += 2) {
      let childBinding = binding[`childBinding${i}`];
      childBinding.updateTarget = ... // here
    }
}
2 Likes

thank you @bigopon, this helps clarify. But, these binding don’t seem to be accessible via the chain above being controller.view.bindings[]. How would I access these bindings from the view (or elsewhere) so I can flush when the screen comes on?

1 Like

with this template
image
Doing this
image
Gives me this
image

It seems to be accessible. Can you check again?

2 Likes

I’m using compose to pull in a dynamic view strategy like this:

<template ref="myElement">
  <slot></slot>
  <compose view.bind="view" containerless></compose>
</template>

would this make a difference? maybe I need a model on this compose and access the view via model.created? could I just do view-model.bind="$this".

Or is there a better way to attach a dynamic template? I tried getViewStrategy returning an inlineViewStrategy but the viewstrategy is determined by some bindings.

1 Like

For compose, it’s stored internally of the compose view model.
With this template
image
Doing this
image
Gives me this
image

I guess it’s the timing that is a bit unfortunate, as compose itself delays things on its own, hence the use of the timeout.

3 Likes

Excellent, I can work with this. I’ll see how I go. Thanks again.

1 Like

@bigopon, I came up with this. I struggled to get to all the bindings I needed to pause, so I started from the other end (Binding prototypes) and worked back to the DOM. It seems to work well but I’m sure there are probably all sorts of anti-patterns going on here. What do you think? Any feedback on gotchas, making it more efficient, simpler etc etc.?

// @ts-ignore
import { Binding } from 'aurelia-binding';
import { ChildInterpolationBinding } from 'aurelia-templating-binding';
import { isTizen } from 'core/utils';
import { remove } from 'lodash';

declare var tizen;

export function auShim() {

    let isPaused = false
    const containedInSelecor = 'layout-block'
    const managedBindings: any[] = []
    const cibPrototype = ((<any>ChildInterpolationBinding.prototype))
    const bPrototype = ((<any>Binding.prototype))

    //modify binding prototypes to make "pausable" on dom elements
    function setupBindingPrototype(bProto) {

        bProto.findDOMTarget = function (target = this) {
            while (target && typeof target.closest !== "function") {
                target = target.target ?? target.parent ?? target.parentElement
                if (target) {
                    target = this.findDOMTarget(target)
                }
            }
            return target
        }

        bProto.getDOMTarget = function () {
            //some doc frags seem to be loaded late so recheck fragments incase they've since been attached
            if (!this.DOMTarget && this.source?.fragment instanceof DocumentFragment) {
                this.DOMTarget = this.findDOMTarget()
            }
            return this.DOMTarget
        }

        bProto.originalUpdateTarget = bProto.updateTarget
        bProto.updateTarget = function (value) {
            const DOMTarget = this.getDOMTarget()
            if (!this.isPausable) {
                this.isPausable = DOMTarget?.closest(containedInSelecor) ? true : false
            }

            if (isPaused && this.isPausable && !this.isManaged && this.isBound) {
                this.isManaged = true
                managedBindings.push(this)
                console.log('managedBindings add:', managedBindings)
            }

            if (this.isManaged && isPaused) {
                this.lastSupressedValue = value
            } else {
                this.lastSupressedValue = undefined
                return this.originalUpdateTarget.apply(this, arguments);
            }
        }

        bProto.flushChanges = function () {
            if (this.isBound && this.lastSupressedValue) {
                this.originalUpdateTarget(this.lastSupressedValue)
            }
        }
    }

    setupBindingPrototype(cibPrototype)
    setupBindingPrototype(bPrototype)

    function flushAllChanges() {
        managedBindings.forEach(b => {
            b.flushChanges()
        })
        let removed = remove(managedBindings, b => {
            if (!b.isBound) {
                b.isManaged = false
                return true
            } else {
                return false
            }
        })

        console.log('managedBindings del:', removed.length, 'remain:', managedBindings)
    }

    tizen.power.setScreenStateChangeListener((prevState: string, currState: string) => {
        if (currState === 'SCREEN_NORMAL' && prevState === 'SCREEN_OFF') {
            flushAllChanges();
            isPaused = false
        } else if (currState === 'SCREEN_OFF' && prevState === 'SCREEN_NORMAL') {
            isPaused = true
        }
    });

    //for debug testing purposes only
    (window as any).togglePause = function togglePause() {
        if (isPaused) {
            flushAllChanges();
            isPaused = false
        } else {
            isPaused = true
        }
    }
}
1 Like