From 31389b99747a531094a733679533a8bd20c08b9a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 22 Apr 2015 13:16:10 -0400 Subject: [PATCH] Fix CPU issues by removing constant digesting. We do so by: - Fixing the title and description (meta) to only respond to a rootScope watch, rather than using a timer - Change the tabs listening code to be completely self contained --- static/js/app.js | 107 ++++---------------------- static/js/core-ui.js | 12 +-- static/js/services/meta-service.js | 70 +++++------------ static/js/services/ui-service.js | 118 ++++++++++++++++++++++++++++- static/js/services/util-service.js | 13 ---- 5 files changed, 151 insertions(+), 169 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 47d38eecc..6881be2c0 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -232,10 +232,10 @@ if (window.__config && window.__config.SENTRY_PUBLIC_DSN) { } // Run the application. -quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService', - function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService) { +quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout', 'CookieService', 'Features', '$anchorScroll', 'UtilService', 'MetaService', 'UIService', + function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout, CookieService, Features, $anchorScroll, UtilService, MetaService, UIService) { - var title = window.__config['REGISTRY_TITLE'] || 'Quay.io'; + var defaultTitle = window.__config['REGISTRY_TITLE'] || 'Quay.io'; // Handle session security. Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], {'_csrf_token': window.__token || ''}); @@ -273,38 +273,6 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi return; } - var changeTab = function(activeTab, opt_timeout) { - var checkCount = 0; - $timeout(function() { - if (checkCount > 5) { return; } - checkCount++; - - $('a[data-toggle="tab"]').each(function(index) { - var tabName = this.getAttribute('data-target').substr(1); - if (tabName != activeTab) { - return; - } - - if (this.clientWidth == 0) { - changeTab(activeTab, 500); - return; - } - - UtilService.clickElement(this); - }); - }, opt_timeout); - }; - - var resetDefaultTab = function() { - $timeout(function() { - $('a[data-toggle="tab"]').each(function(index) { - if (index == 0) { - UtilService.clickElement(this); - } - }); - }); - }; - $rootScope.$watch('description', function(description) { if (!description) { description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; @@ -316,11 +284,16 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi $('#descriptionTag').attr('content', description); }); - $rootScope.$on('$routeUpdate', function(){ - if ($location.search()['tab']) { - changeTab($location.search()['tab']); - } else { - resetDefaultTab(); + // Listen for scope changes and update the title and description accordingly. + $rootScope.$watch(function() { + var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; + if ($rootScope.title != title) { + $rootScope.title = title; + } + + var description = MetaService.getDescription($rootScope.currentPage) || ''; + if ($rootScope.description != description) { + $rootScope.description = description; } }); @@ -336,63 +309,9 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi $rootScope.newLayout = !!current.$$route.newLayout; $rootScope.fixFooter = !!current.$$route.fixFooter; - MetaService.getInitialTitle(current, function(title) { - $rootScope.title = title; - }); - - MetaService.getInitialDescription(current, function(description) { - $rootScope.description = description - }); - $anchorScroll(); }); - $rootScope.$on('$viewContentLoaded', function(event) { - var current = $rootScope.currentPage; - - MetaService.getTitle(current, function(title) { - $rootScope.title = title; - }); - - MetaService.getDescription(current, function(description) { - $rootScope.description = description; - }); - - var activeTab = $location.search()['tab']; - var checkTabs = function() { - var tabs = $('a[data-toggle="tab"]'); - if (tabs.length == 0) { - $timeout(checkTabs, 50); - return; - } - - tabs.on('shown.bs.tab', function (e) { - var tabName = e.target.getAttribute('data-target').substr(1); - $rootScope.$apply(function() { - var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target; - var newSearch = $.extend($location.search(), {}); - if (isDefaultTab) { - delete newSearch['tab']; - } else { - newSearch['tab'] = tabName; - } - - $location.search(newSearch); - }); - - e.preventDefault(); - }); - - if (activeTab) { - changeTab(activeTab); - } - }; - - // Setup deep linking of tabs. This will change the search field of the URL whenever a tab - // is changed in the UI. - $timeout(checkTabs, 50); - }); - var initallyChecked = false; window.__isLoading = function() { if (!initallyChecked) { diff --git a/static/js/core-ui.js b/static/js/core-ui.js index 0f27bdd7a..f23e18c50 100644 --- a/static/js/core-ui.js +++ b/static/js/core-ui.js @@ -243,7 +243,7 @@ angular.module("core-ui", []) transclude: true, restrict: 'C', scope: {}, - controller: function($rootScope, $scope, $element, $timeout) { + controller: function($rootScope, $scope, $element, $timeout, $location, UIService) { $scope.isClosed = true; $scope.toggleClosed = function(e) { @@ -252,13 +252,9 @@ angular.module("core-ui", []) e.preventDefault(); }; - // Add a listener to auto-close the tabs when a new tab is clicked. - $timeout(function() { - var tabs = $element.find('a[data-toggle="tab"]'); - tabs.on('shown.bs.tab', function (e) { - $scope.isClosed = true; - }); - }, 100); + UIService.initializeTabs($scope, $element, function() { + $scope.isClosed = true; + }); } }; return directiveDefinitionObject; diff --git a/static/js/services/meta-service.js b/static/js/services/meta-service.js index 515370e20..157a86c24 100644 --- a/static/js/services/meta-service.js +++ b/static/js/services/meta-service.js @@ -4,72 +4,36 @@ angular.module('quay').factory('MetaService', ['$interpolate', 'Config', '$rootScope', '$interval', function($interpolate, Config, $rootScope, $interval) { var metaService = {}; - var intervals = []; - var interpolate = function(page, expr, callback) { - var previous = ''; + var interpolate = function(page, expr) { + if (!expr) { + return null; + } - var currentInterval = $interval(function() { - var inter = $interpolate(expr, true, null, true); - var result = inter(page.scope) + var inter = $interpolate(expr, true, null, true); + if (!inter) { + return null; + } - if (previous && result != previous) { - $interval.cancel(currentInterval); - } - - previous = result; - callback(result); - }, 500); - - intervals.push(currentInterval); + return inter(page.scope); }; - var initial = function(value, default_value, callback) { - for (var i = 0; i < intervals.length; ++i) { - $interval.cancel(intervals[i]); + metaService.getTitle = function(page) { + if (!page || !page.$$route) { + return null; } - intervals = []; - - if (!value) { - callback(default_value); - return; - } - - if (value.indexOf('{{') < 0) { - callback(default_value); - return; - } - - callback('Loading...'); - }; - - metaService.getInitialTitle = function(page, callback) { var route = page.$$route; - initial(route && route.title, Config.REGISTRY_TITLE_SHORT, callback); + return interpolate(page, route && route.title); }; - metaService.getInitialDescription = function(page, callback) { - var route = page.$$route; - initial(route && route.description, Config.REGISTRY_TITLE_SHORT, callback); - }; - - metaService.getTitle = function(page, callback) { - var route = page.$$route; - if (!route || !route.title || route.title.indexOf('{{') < 0) { - return; + metaService.getDescription = function(page) { + if (!page || !page.$$route) { + return null; } - interpolate(page, route.title, callback); - }; - - metaService.getDescription = function(page, callback) { var route = page.$$route; - if (!route || !route.description || route.description.indexOf('{{') < 0) { - return; - } - - interpolate(page, route.description, callback); + return interpolate(route && route.description); }; return metaService; diff --git a/static/js/services/ui-service.js b/static/js/services/ui-service.js index 2a5250ca6..809f59eae 100644 --- a/static/js/services/ui-service.js +++ b/static/js/services/ui-service.js @@ -1,7 +1,7 @@ /** * Service which provides helper methods for performing some simple UI operations. */ -angular.module('quay').factory('UIService', [function() { +angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', function($timeout, $rootScope, $location) { var CheckStateController = function(items, opt_checked) { this.items = items; this.checked = opt_checked || []; @@ -134,5 +134,121 @@ angular.module('quay').factory('UIService', [function() { }); }; + uiService.clickElement = function(el) { + // From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements + var ev = document.createEvent("MouseEvent"); + ev.initMouseEvent( + "click", + true /* bubble */, true /* cancelable */, + window, null, + 0, 0, 0, 0, /* coordinates */ + false, false, false, false, /* modifier keys */ + 0 /*left*/, null); + el.dispatchEvent(ev); + }; + + uiService.initializeTabs = function(scope, element, opt_clickCallback) { + var locationListener = null; + var disposed = false; + + var changeTab = function(activeTab) { + if (disposed) { return; } + + $('a[data-toggle="tab"]').each(function(index) { + var tabName = this.getAttribute('data-target').substr(1); + if (tabName != activeTab) { + return; + } + + if ($(this).parent().hasClass('active')) { + return; + } + + if (this.clientWidth == 0) { + setTimeout(function() { + changeTab(activeTab); + }, 100); + return; + } + + uiService.clickElement(this); + }); + }; + + var resetDefaultTab = function() { + if (disposed) { return; } + + $timeout(function() { + element.find('a[data-toggle="tab"]').each(function(index) { + if (index == 0) { + uiService.clickElement(this); + } + }); + }); + }; + + var checkTabs = function() { + if (disposed) { return; } + + // Poll until we find the tabs. + var tabs = element.find('a[data-toggle="tab"]'); + if (tabs.length == 0) { + $timeout(checkTabs, 50); + return; + } + + // Register listeners. + registerListeners(tabs); + + // Set the active tab (if any). + var activeTab = $location.search()['tab']; + if (activeTab) { + changeTab(activeTab); + } + }; + + var registerListeners = function(tabs) { + // Listen for scope destruction. + scope.$on('$destroy', function() { + dispoed = true; + locationListener && locationListener(); + }); + + // Listen for route changes and update the tabs accordingly. + locationListener = $rootScope.$on('$routeUpdate', function(){ + if ($location.search()['tab']) { + changeTab($location.search()['tab']); + } else { + resetDefaultTab(); + } + }); + + // Listen for tab changes. + tabs.on('shown.bs.tab', function (e) { + // Invoke the callback, if any. + opt_clickCallback && opt_clickCallback(); + + // Update the search location. + var tabName = e.target.getAttribute('data-target').substr(1); + $rootScope.$apply(function() { + var isDefaultTab = tabs[0] == e.target; + var newSearch = $.extend($location.search(), {}); + if (isDefaultTab) { + delete newSearch['tab']; + } else { + newSearch['tab'] = tabName; + } + + $location.search(newSearch); + }); + + e.preventDefault(); + }); + }; + + // Start the checkTabs timer. + checkTabs(); + }; + return uiService; }]); diff --git a/static/js/services/util-service.js b/static/js/services/util-service.js index 64956f1b8..bf9edbbc4 100644 --- a/static/js/services/util-service.js +++ b/static/js/services/util-service.js @@ -76,18 +76,5 @@ angular.module('quay').factory('UtilService', ['$sanitize', function($sanitize) return $sanitize(utilService.escapeHtmlString(text)); }; - utilService.clickElement = function(el) { - // From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements - var ev = document.createEvent("MouseEvent"); - ev.initMouseEvent( - "click", - true /* bubble */, true /* cancelable */, - window, null, - 0, 0, 0, 0, /* coordinates */ - false, false, false, false, /* modifier keys */ - 0 /*left*/, null); - el.dispatchEvent(ev); - }; - return utilService; }]);