/**
@module ember-flexberry
*/
import { A } from '@ember/array';
import Service from '@ember/service';
import { schedule } from '@ember/runloop';
import Route from '@ember/routing/route';
import { subscribe, unsubscribe } from '@ember/instrumentation';
export default Service.extend({
/**
Flag indicates whether perf service is enabled or not.
@property enabled
@type Boolean
@default false
@example
```
// PerfServise 'enabled' setting could be also defined through application config/environment.js
module.exports = function(environment) {
let ENV = {
...
APP: {
...
perf: {
enabled: true
}
...
}
...
};
```
*/
enabled: false,
tagsHaveBeenPlaced: false,
perfObjects: undefined,
/**
Initializes perf service.
Ember services are singletons, so this code will be executed only once since application initialization.
*/
init() {
this._super(...arguments);
this.set('perfObjects', []);
let enabled = this.get('enabled');
if (!enabled) {
return;
} else {
this._init();
}
},
willDestroyElement() {
this._super(...arguments);
this._turnOff();
},
handleClick(e) {
let elem = e.target.tagName === 'PERF' ? e.target : e.target.parentElement;
if (elem.tagName === 'PERF') {
elem.style.top = elem.offsetTop + 13 + 'px';
elem.style.left = elem.offsetLeft + 20 + 'px';
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
}
},
_init() {
let PERF_SELECTORS = 'perf_selectors';
window.perf = {
runningTime: null,
selectionStartTime: null,
selectionEndTime: null,
selection: {},
results: [],
/**
accepts regExp or array of regExp e.g. `componentName` | `/componentName/` | `[/componentName/, /componentNamePartial/]`
@param selector filters on Component.className
// e.g. the following line is a wildcard pattern (default)
window.perf.setSectors(/./);
*/
setSelectors: (selectorArr) => {
selectorArr = [].concat(selectorArr);
selectorArr.forEach(selector => selector.toString());
localStorage.setItem(PERF_SELECTORS, selectorArr.toString());
},
getSelectors: () => {
if (!localStorage) {
return;
}
let selectors = localStorage.getItem(PERF_SELECTORS);
return selectors ? selectors.split(',').map((selector) => new RegExp(selector.replace(/(^\/)|(\/$)/g, ''))) : [/./];
},
clearSelectors() {
localStorage.setItem(PERF_SELECTORS, '');
}
};
let selectorArr = '/./';
localStorage.setItem(PERF_SELECTORS, selectorArr.toString());
this._turnOn();
},
_decorateElementWithPerf(perfObject) {
let element = perfObject.element;
if (!element) {
return;
}
let dataElement = document.createElement('perf');
let hasChildren = !![].concat.apply([], element.children).length;
let parentClasses = [].concat.apply([], element.classList);
let emberIndex = parentClasses.indexOf('ember-view');
let elementPosition = window.getComputedStyle(element).position;
if (!hasChildren || elementPosition === 'static') {
element.style.position = 'relative';
}
dataElement.classList.add('perf-data');
if (perfObject.totalRenderTime < 10) {
dataElement.classList.add('perf-fast');
} else if (perfObject.totalRenderTime < 20) {
dataElement.classList.add('perf-medium');
} else {
dataElement.classList.add('perf-slow');
}
dataElement.innerHTML = `<span class="perf-time">${perfObject.totalRenderTime + 'ms'}</span>`;
parentClasses.splice(emberIndex, 1);
parentClasses.unshift(`#${element.id}`);
dataElement.setAttribute('data-parent-classes', parentClasses.join('.'));
if (perfObject.isRerender) {
dataElement.classList.add('perf-rerendered');
}
schedule('afterRender', this, () => {
let nudgeCount = 0;
let isClosedTag = element.tagName === 'IMG' || element.tagName === 'INPUT';
let isNudger = false;
let perfElem;
element.classList.add('has-perf');
element.insertAdjacentElement(isClosedTag ? 'afterEnd' : 'beforeEnd', dataElement);
let dims = dataElement.getBoundingClientRect();
let sizeWidth = dims.left + (dims.width / 2);
let sizeHeight = dims.top + (dims.height / 2);
let perfElems = A(document.elementsFromPoint(sizeWidth, sizeHeight).filter((item) => item.tagName === 'PERF')).without(dataElement);
if (perfElems.length) {
perfElem = perfElems.find((item) => item.classList.includes('nudger')) || perfElems[0];
isNudger = perfElem.classList.includes('nudger');
if (isNudger) {
nudgeCount = perfElem.getAttribute('data-nudge-count');
} else {
perfElem.classList.add('nudger');
}
perfElem.setAttribute('data-nudge-count', ++nudgeCount);
dataElement.style.top = dataElement.offsetTop + (13 * nudgeCount) + 'px';
dataElement.classList.add('nudged');
}
});
},
_preDash(string, numberOfDashes) {
return new Array(numberOfDashes + 1).join('|––') + string;
},
_runOnTransitionEnd() {
let perfObjects = this.get('perfObjects');
if (perfObjects === undefined) {
return;
}
schedule('afterRender', () => {
for (let i = 0, len = perfObjects.length; i < len; i++) {
let obj = perfObjects[i];
let parent;
this._decorateElementWithPerf(obj);
if (obj.element) {
parent = obj.element.parentElement;
parent.removeEventListener('click', this.handleClick);
parent.addEventListener('click', this.handleClick);
}
}
this.set('tagsHaveBeenPlaced', true);
});
},
_turnOn() {
let isQueueRendering = false;
let selectors = window.perf.getSelectors();
let perfObjects = this.get('perfObjects');
let _this = this;
if (selectors) {
Route.reopen({
actions: {
willTransition: function() {
perfObjects = [];
this.set('tagsHaveBeenPlaced', false);
this._super(...arguments);
},
didTransition: function() {
this._super(...arguments);
_this._runOnTransitionEnd();
}
}
});
subscribe('render', {
before(name, timestamp, payload) {
let className = payload.containerKey;
if (className && selectors.filter(regEx => regEx.test(className)).length) {
if (!window.perf.runningTime) {
window.perf.runningTime = timestamp;
}
if (!isQueueRendering) {
isQueueRendering = true;
// console.profile();
// eslint-disable-next-line no-console
console.timeStamp('Render queue running.');
window.perf.startTime = timestamp;
}
// Rename if re-rendering, causes a new render object to be tracked.
if (window.perf.selection[payload.object]) {
payload.object = payload.object + '_+';
}
name = payload.object.replace(/(<voyager-web@)|(>)/g, '');
window.perf.selection[payload.object] = {
name: name,
beforeInsertTime: Math.round(timestamp * 100) / 100
};
// eslint-disable-next-line no-console
console.time(name);
}
},
after(name, timestamp, payload) {
let perfObject = window.perf.selection[payload.object];
if (perfObject) {
// eslint-disable-next-line no-console
console.timeEnd(perfObject.name);
perfObject.afterInsertTime = Math.round(timestamp * 100) / 100;
perfObject.totalRenderTime = Math.round((perfObject.afterInsertTime - perfObject.beforeInsertTime) * 100) / 100;
perfObject.renderIndex = Object.keys(window.perf.selection).filter(key => !window.perf.selection[key].afterInsertTime).length;
perfObject.dashedName = _this._preDash(perfObject.name, perfObject.renderIndex);
perfObject.element = payload.view.element;
perfObject.isReRender = /_\+/.test(payload.object);
perfObject.view = payload.view;
let displayObj = {
totalRenderTime: perfObject.totalRenderTime,
name: perfObject.dashedName,
element: perfObject.element,
isReRender: perfObject.isReRender,
};
perfObjects.unshift(perfObject);
window.perf.results.push(displayObj);
if (!perfObject.renderIndex) {
isQueueRendering = false;
// console.profileEnd();
window.perf.selectionEndTime = performance.now();
window.perf.selectionRenderTime = Math.round((window.perf.selectionEndTime - window.perf.runningTime) * 100) / 100;
window.perf.results.reverse();
// console.table(window.perf.results);
// eslint-disable-next-line no-console
console.log('Render queue is flushed: ', window.perf.selectionRenderTime + 'ms');
// eslint-disable-next-line no-console
console.timeStamp('Render queue is flushed: ', window.perf.selectionRenderTime + 'ms');
// window.perf.results.forEach(result => console.log(result.totalRenderTime + 'ms', result.element))
window.perf.results = [];
}
}
}
});
}
},
_turnOff() {
let elems = document.getElementsByClassName('has-perf');
for (let i = 0, len = elems.length; i < len; i++) {
elems[i].removeEventListener('click', this.handleClick);
}
unsubscribe('render');
}
});