mike-north/ember-perf

View on GitHub
addon/services/ember-perf.js

Summary

Maintainability
C
1 day
Test Coverage
/* eslint no-console: 0 */
import Service from '@ember/service';

import { assert } from '@ember/debug';
import { classify } from '@ember/string';
import { oneWay } from '@ember/object/computed';
import { defer } from 'rsvp';
import { schedule, scheduleOnce } from '@ember/runloop';
import EObj, { set, get, getWithDefault } from '@ember/object';
import Evented, { on } from '@ember/object/evented';
import Ember from 'ember';
import TransitionData from '../core/transition-data';
import RenderData from '../core/render-data';

const {
  Logger
} = Ember;
const Base = Service || EObj;
const {
  keys
} = Object;

// jscs:enable disallowDirectPropertyAccess

let transitionCounter = 0;

export default Base.extend(Evented, {
  transitionData: null,

  debugMode: oneWay('defaultDebugMode'),

  debugLog() {
    if (this.get('debugMode')) {
      Logger.log(...arguments);
    }
  },

  init() {
    this._super(...arguments);
    this._setDefaults();
  },

  _setDefaults() {
    let defaults = getWithDefault(this, 'defaults', {});
    keys(defaults).map((key) => {
      let classifiedKey = classify(key);
      let defaultKey = `default${classifiedKey}`;
      return set(this, defaultKey, defaults[key]);
    });
  },

  /**
   * Measure a transition (promise)
   * @param  {Promise} transitionInfo - promise associated with the transition
   * @private
   */
  _measureTransition(transitionInfo) {
    if (transitionInfo.promise._emberPerfTransitionId) {
      return;
    }
    transitionInfo.promise._emberPerfTransitionId = transitionCounter++;
    let transitionRoute = transitionInfo.promise.targetName || get(transitionInfo.promise, 'intent.name');
    let transitionCtxt = get(transitionInfo.promise, 'intent.contexts');
    let hasTransitionCtxt = transitionCtxt && transitionCtxt[0];
    let transitionUrl = get(transitionInfo.promise, 'intent.url');
    assert('Must have at least a route name', transitionRoute);

    if (!transitionUrl) {
      if (hasTransitionCtxt) {
        transitionUrl = transitionInfo.promise.router.generate(transitionRoute, ...transitionCtxt);
      } else {
        transitionUrl = transitionInfo.promise.router.generate(transitionRoute);
      }
    }
    this.renderData = this.transitionData = new TransitionData({
      destURL: transitionUrl,
      destRoute: transitionRoute
    });
    transitionInfo.promise.then(() => {
      this.transitionData.finish();
      let event = this.transitionData;
      scheduleOnce('afterRender', () => {
        this.trigger('transitionComplete', event);
      });
    });
  },

  /**
   * Method to be called to measure one full pass of rendering.
   *
   * @returns {Promise} Returns a promise that resolves with the render data.
   * @public
   */
  measureRender() {
    this.transitionData = null;

    let deferred = defer('measureRender');

    this.renderData = new RenderData();

    schedule('afterRender', () => {
      let event = this.renderData;
      event.finish();

      this.trigger('renderComplete', event);
      deferred.resolve(event);
    });

    return deferred.promise;
  },

  /**
   * Hook that's called whenever a route is activated
   * @param  {Ember.Route} route
   * @public
   */
  routeActivated(route) {
    assert('Expected non-empty transitionData', this.transitionData);
    this.transitionData.activateRoute(route);
    this.debugLog(`route activated - ${route.get('routeName')}`);
  },

  /**
   * Hook that's called whenever a route is beginning to render (after all setup has completed).
   * @param  {Ember.Route} route
   * @public
   */
  routeWillRender(route) {
    assert('Expected non-empty transitionData', this.transitionData);
    this.transitionData.routeFinishedSetup(route);
    this.debugLog(`route will render - ${route.get('routeName')}`);
  },

  /**
   * Hook that's called before a view starts rendering
   * @param  {String} name      The name of the view that's about to render
   * @param  {int}    timestamp The time at which this event was fired
   * @param  {Object} payload   More information about the view/template
   * @public
   */
  renderBefore(name, timestamp, payload) {
    assert('Expected non-empty renderData', this.renderData);
    this.renderData.willRender(name, timestamp, payload);
    this.debugLog(`view will render - ${(payload.view || {})._debugContainerKey}`);
  },

  renderAfter(name, timestamp, payload) {
    assert('Expected non-empty renderData', this.renderData);
    this.renderData.didRender(name, timestamp, payload);
    this.debugLog(`view did render - ${(payload.view || {})._debugContainerKey}`);
  },

  transitionLogger: on('transitionComplete', function(data) {
    if (this.get('debugMode')) {
      console.group(`Top-Level Transition to ${data.destRoute} (${data.destURL}): ${data.elapsedTime}ms`);
      for (let i = 0; i < data.routes.length; i++) {
        console.group(`${data.routes[i].name} ${data.routes[i].elapsedTime}ms`);
        if (data.routes[i].views) {
          for (let j = 0; j < (data.routes[i].views || []).length; j++) {
            let v = data.viewData[data.routes[i].views[j]];
            console.log(`${v.containerKey} (${v.id}): ${v.elapsedTime}ms`);
          }
        }
        console.groupEnd();
      }
      console.groupEnd();
    }
  }),

  renderLogger: on('renderComplete', function(data) {
    if (this.get('debugMode')) {
      console.group(`Render Completed: ${data.elapsedTime}ms`);
      for (let i = 0; i < data.viewData.length; i++) {
        let v = data.viewData[i];
        console.log(`${v.containerKey} (${v.id}): ${v.elapsedTime}ms`);
      }
      console.groupEnd();
    }
  })
});