1

For my angular application, i'm trying to track how many promises have been processed and how many still need processing.

My code decorates $q with wrapper methods that update a simple counter whenever an operation is started and whenever one is finished, seems simple:

  angular.module('DuckieTV',[])
  .config(function($provide) {
  var count = window.promiseStats = { 
    open: 0,
    done: 0 
  };
  $provide.decorator('$q', function($delegate) {

    function decorate(p) {
      p._then = p.then;
      p.then = function(thenFn, errFn, notifyFn) {
       count.open++;
       return p._then(function() { 
            count.done++;
            if(thenFn) return thenFn.apply(this,arguments)
        }, function() {
          count.done++;
          if(errFn) return errFn.apply(this,arguments)
        }, notifyFn);
      };

      p._finally = p.finally;
      p.finally = function(callback) {
        count.done++;
        p._finally(callback)
      }

      p._catch = p.catch;
      p.catch = function(callback) {
        count.done++;
        p._catch(callback)
      }
      return p;
    }

    var d = $delegate.defer;
    $delegate.defer = function() {
      var deferred = d();
      decorate(deferred.promise);
      return deferred;
    };

    return $delegate;
  });
})

The fun begins when I'm noticing a discrepancy between started/finished promises. After just a few minutes of performing lots of deferred operations it can become a > 15% percentage overall.

Example console output:

promiseStats
Object {open: 99, done: 95}
Math.floor(promiseStats.done / promiseStats.open * 100);
94

after some work (import operations)

promiseStats;
Object {open: 185, done: 172}
Math.floor(promiseStats.done / promiseStats.open * 100);
92

My actual question: Can anyone tell me if i'm missing something with this implementation?

As far as I know, I haven't missed optional implementations and all the promises that i'm firing via .then are properly coded

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
SchizoDuckie
  • 9,353
  • 6
  • 33
  • 40

2 Answers2

2

Can anyone tell me if i'm missing something with this implementation?

I can see two issues with your implementation:

  • Your decorated catch and invoke methods only ever increase the done count, never the open one. Since you not seeing this, I'd guess that you haven't used either in your code.
  • By introducing an onfail handler to every .then() call, you implicitly catch all errors and severely hurt control flow. You might be able to fix this by appending the following line to your handler code:

     …
     else throw arguments[0];
    

I'm noticing a discrepancy between started/finished promises

I don't think this is caused by your implementation of the counters. Rather you really have some ever-pending promises in your code; i.e. deferreds that are never resolved. This for example might be caused by mistakes in code that uses the deferred antipattern.

My code decorates $q with wrapper methods that update a simple counter whenever an operation is started and whenever one is finished, seems simple:

In fact, it updates a counter whenever a listener is attached, i.e. a then/catch/finally method is called. I'd propose a simpler implementation, that counts on creation and doesn't even need to overwrite the methods:

$provide.decorator('$q', function($delegate) {
    var defer = $delegate.defer;
    $delegate.defer = function() {
        var deferred = defer();
        count.open++;
        deferred.promise.finally(function() {
            count.done++;
        });
        return deferred;
    };
    return $delegate;
});
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for your excellent answer. This is what I came to StackOverflow for. I'm definitely guilty of abusing the deferred antipattern and will fix this asap :) – SchizoDuckie Oct 15 '14 at 13:29
  • So it was really responsible for the pending promises? Thanks for the confirmation, it was only a suspicion of mine that such could happen :-) – Bergi Oct 15 '14 at 14:25
1

The plot thickens.

I've been profiling and tracing code based upon the excellent code by @Bergi and noticed that consistently promise #31 was left open, so I added a debug statement right there:

.config(function($provide) {
    var count = window.promiseStats = {
        open: 0,
        done: 0,
        promises: {}
    };
    $provide.decorator('$q', function($delegate) {


    var defer = $delegate.defer;
        $delegate.defer = function() {

            count.open++;
            var traceId = count.open;
            if(traceId == 31) { 
                debugger;
            }
            var deferred = count.promises[traceId] = defer();
            console.timeline('promise ' +traceId);
            console.profile('promise '+traceId);

            deferred.promise.finally(function() {
                count.done++;
                console.timelineEnd('promise ' +traceId);
                console.profileEnd('promise '+traceId);
                delete count.promises[traceId];    
            });
            return deferred;
        };
        return $delegate;
    });
})

This dropped me directly inside an angular-core template request that seems to have a different kind of promise handling.

enter image description here

I'm still trying to assess wether or not this is a problem. as it does seem to unregister with some specialized code.

SchizoDuckie
  • 9,353
  • 6
  • 33
  • 40
  • Well, [there we've got them](https://github.com/angular/angular.js/blob/9ba24c54d60e643b1450cc5cfa8f990bd524c130/src/ng/http.js#L965-989)! In the case that `cachedResp` is `return`ed, there is indeed a `deferred` constructed that is never resolved. It's not really a problem (since no one *expects* the `deferred.promise` to resolve in that case), but it looks like unclean coding. – Bergi Oct 15 '14 at 23:03