diff --git a/Gemfile b/Gemfile index 715ce2bc6c228f5b78a9caa5031f14cf61d4c509..7a5b32a447d626e4fcd28efb7717cbc7b56a4e6c 100644 --- a/Gemfile +++ b/Gemfile @@ -264,6 +264,18 @@ gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development +# Perf bar +gem 'peek', '~> 1.0.1' +gem 'peek-gc', '~> 0.0.2' +gem 'peek-host', '~> 1.0.0' +gem 'peek-mysql2', '~> 1.1.0', group: :mysql +gem 'peek-performance_bar', '~> 1.2.1' +gem 'peek-pg', '~> 1.3.0' +gem 'peek-rblineprof', '~> 0.2.0' +gem 'pygments.rb', require: false +gem 'peek-redis', '~> 1.2.0' +gem 'peek-sidekiq', '~> 1.0.3' + # Metrics group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri diff --git a/Gemfile.lock b/Gemfile.lock index d34b84df5e62b6544132e6543390bd2835855502..4c5c3db40c90db8dcb8b43ad4f513bdbb16b29a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,6 +56,7 @@ GEM asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) ast (2.3.0) + atomic (1.1.99) attr_encrypted (3.0.3) encryptor (~> 3.0.0) attr_required (1.0.0) @@ -131,6 +132,8 @@ GEM coffee-script-source (1.10.0) colorize (0.7.7) concurrent-ruby (1.0.5) + concurrent-ruby-ext (1.0.5) + concurrent-ruby (= 1.0.5) connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -548,6 +551,36 @@ GEM parser (2.4.0.0) ast (~> 2.2) path_expander (1.0.1) + peek (1.0.1) + concurrent-ruby (>= 0.9.0) + concurrent-ruby-ext (>= 0.9.0) + railties (>= 4.0.0) + peek-gc (0.0.2) + peek + peek-host (1.0.0) + peek + peek-mysql2 (1.1.0) + atomic (>= 1.0.0) + mysql2 + peek + peek-performance_bar (1.2.1) + peek (>= 0.1.0) + peek-pg (1.3.0) + concurrent-ruby + concurrent-ruby-ext + peek + pg + peek-rblineprof (0.2.0) + peek + rblineprof + peek-redis (1.2.0) + atomic (>= 1.0.0) + peek + redis + peek-sidekiq (1.0.3) + atomic (>= 1.0.0) + peek + sidekiq pg (0.18.4) po_to_json (1.0.1) json (>= 1.6.0) @@ -575,6 +608,8 @@ GEM pry (~> 0.10) pry-rails (0.3.5) pry (>= 0.9.10) + pygments.rb (1.1.2) + multi_json (>= 1.0.0) pyu-ruby-sasl (0.0.3.3) rack (1.6.5) rack-accept (0.4.5) @@ -999,12 +1034,22 @@ DEPENDENCIES omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) paranoia (~> 2.2) + peek (~> 1.0.1) + peek-gc (~> 0.0.2) + peek-host (~> 1.0.0) + peek-mysql2 (~> 1.1.0) + peek-performance_bar (~> 1.2.1) + peek-pg (~> 1.3.0) + peek-rblineprof (~> 0.2.0) + peek-redis (~> 1.2.0) + peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) prometheus-client-mmap (~> 0.7.0.beta5) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) + pygments.rb rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) diff --git a/app/assets/javascripts/peek.js b/app/assets/javascripts/peek.js new file mode 100644 index 0000000000000000000000000000000000000000..4ba23ea1a09020f0c273d5d7a237e916ca99ed22 --- /dev/null +++ b/app/assets/javascripts/peek.js @@ -0,0 +1,4 @@ +import 'vendor/jquery.tipsy'; +import 'vendor/peek'; +import 'vendor/peek.performance_bar'; +import 'vendor/peek.rblineprof'; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 8ac71797c14c01cd478c9c339fe2b3927afeace4..2c7698eb17446c6c001c7ae1101dfda85adff11d 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ /* global findFileURL */ +import Cookies from 'js-cookie'; + import findAndFollowLink from './shortcuts_dashboard_navigation'; (function() { @@ -14,6 +16,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('f', (e => this.focusFilter(e))); + Mousetrap.bind('p b', this.onTogglePerfBar); const $globalDropdownMenu = $('.global-dropdown-menu'); const $globalDropdownToggle = $('.global-dropdown-toggle'); @@ -53,6 +56,17 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; return Shortcuts.toggleHelp(this.enabledHelp); }; + Shortcuts.prototype.onTogglePerfBar = function(e) { + e.preventDefault(); + if (Cookies.get('perf_bar_enabled') === 'true') { + Cookies.remove('perf_bar_enabled', { path: '/' }); + } + else { + Cookies.set('perf_bar_enabled', true, { path: '/' }); + } + return gl.utils.refreshCurrentPage(); + }; + Shortcuts.prototype.toggleMarkdownPreview = function(e) { // Check if short-cut was triggered while in Write Mode const $target = $(e.target); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 47ce21d238ba7e3d4cac21b49bb7b47e409492af..01b1462d5ec5335711fc62a787126377eb25ac45 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,7 +18,7 @@ class ApplicationController < ActionController::Base before_action :ldap_security_check before_action :sentry_context before_action :default_headers - before_action :add_gon_variables + before_action :add_gon_variables, unless: -> { request.path.start_with?('/peek') } before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? @@ -63,6 +63,19 @@ class ApplicationController < ActionController::Base end end + def peek_enabled? + return false unless Gitlab::PerformanceBar.enabled? + return false unless current_user + + if RequestStore.active? + if RequestStore.store.key?(:peek_enabled) + RequestStore.store[:peek_enabled] + else + RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present? + end + end + end + protected # This filter handles both private tokens and personal access tokens diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index ea8bbe92d865b20a8b5df29d1c103b426ccbaf64..420172289080a40e420ccecebdb8a32129536f05 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -27,6 +27,10 @@ %td.shortcut .key f %td Focus Filter + %tr + %td.shortcut + .key p b + %td Enable the Performance Bar %tr %td.shortcut .key ? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 1ef0d524dbb6908e6844ae1370b75be4039ab19a..eea33b5966f729bb3a81924f76596f24f1227511 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,6 +28,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "test", media: "all" if Rails.env.test? + = stylesheet_link_tag 'peek' if peek_enabled? = Gon::Base.render_data @@ -37,6 +38,7 @@ = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? + = webpack_bundle_tag 'peek' if peek_enabled? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 03688e9ff21dff600e7c31490d043bffaf08066d..2b07273a0a84debea7f08db8b7df92da2355ce0e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -3,6 +3,7 @@ = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = render "layouts/init_auto_complete" if @gfm_form + = render 'peek/bar' = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/config/application.rb b/config/application.rb index b0533759252e565833fa0f9ec657fb3733ebf1d6..8bbecf3ed0f5287cd2b45e62406b5ea7f27a6526 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" + config.assets.precompile << "peek.css" config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "test.css" diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb new file mode 100644 index 0000000000000000000000000000000000000000..73da7be788943b0c7a0ec89ed698955af5105493 --- /dev/null +++ b/config/initializers/peek.rb @@ -0,0 +1,9 @@ +Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis.params) } + +Peek.into Peek::Views::Host +Peek.into Peek::Views::PerformanceBar +Peek.into Gitlab::Database.mysql? ? Peek::Views::Mysql2 : Peek::Views::PG +Peek.into Peek::Views::Redis +Peek.into Peek::Views::Sidekiq +Peek.into Peek::Views::Rblineprof +Peek.into Peek::Views::GC diff --git a/config/routes.rb b/config/routes.rb index d909be38b42c441f7c5d291622ff53df01f39523..9a117711083467948f8567f8fc47227a33a09aaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,7 @@ Rails.application.routes.draw do post :toggle_award_emoji, on: :member end + mount Peek::Railtie => '/peek' draw :sherlock draw :development draw :ci diff --git a/config/webpack.config.js b/config/webpack.config.js index 7501acb763384fc676367d1eade4926beaf232bd..bb77c12f88a2c2d20545b831c3287ccef285109f 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -68,6 +68,7 @@ var config = { raven: './raven/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', + peek: './peek.js', }, output: { diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb new file mode 100644 index 0000000000000000000000000000000000000000..3324fec94d4c42ed47ef6c0b93dfdca38b8f7d7a --- /dev/null +++ b/lib/gitlab/performance_bar.rb @@ -0,0 +1,7 @@ +module Gitlab + module PerformanceBar + def self.enabled? + ENV["PERFORMANCE_BAR"] == '1' + end + end +end diff --git a/vendor/assets/javascripts/jquery.tipsy.js b/vendor/assets/javascripts/jquery.tipsy.js new file mode 100644 index 0000000000000000000000000000000000000000..d9fced24b609df9bbbe8e2fbc039fcdde83619e6 --- /dev/null +++ b/vendor/assets/javascripts/jquery.tipsy.js @@ -0,0 +1,258 @@ +// tipsy, facebook style tooltips for jquery +// version 1.0.0a +// (c) 2008-2010 jason frame [jason@onehackoranother.com] +// released under the MIT license + +(function($) { + + function maybeCall(thing, ctx) { + return (typeof thing == 'function') ? (thing.call(ctx)) : thing; + }; + + function isElementInDOM(ele) { + while (ele = ele.parentNode) { + if (ele == document) return true; + } + return false; + }; + + function Tipsy(element, options) { + this.$element = $(element); + this.options = options; + this.enabled = true; + this.fixTitle(); + }; + + Tipsy.prototype = { + show: function() { + var title = this.getTitle(); + if (title && this.enabled) { + var $tip = this.tip(); + + $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); + $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity + $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); + + var pos = $.extend({}, this.$element.offset(), { + width: this.$element[0].offsetWidth, + height: this.$element[0].offsetHeight + }); + + var actualWidth = $tip[0].offsetWidth, + actualHeight = $tip[0].offsetHeight, + gravity = maybeCall(this.options.gravity, this.$element[0]); + + var tp; + switch (gravity.charAt(0)) { + case 'n': + tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 's': + tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 'e': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; + break; + case 'w': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; + break; + } + + if (gravity.length == 2) { + if (gravity.charAt(1) == 'w') { + tp.left = pos.left + pos.width / 2 - 15; + } else { + tp.left = pos.left + pos.width / 2 - actualWidth + 15; + } + } + + $tip.css(tp).addClass('tipsy-' + gravity); + $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); + if (this.options.className) { + $tip.addClass(maybeCall(this.options.className, this.$element[0])); + } + + if (this.options.fade) { + $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); + } else { + $tip.css({visibility: 'visible', opacity: this.options.opacity}); + } + } + }, + + hide: function() { + if (this.options.fade) { + this.tip().stop().fadeOut(function() { $(this).remove(); }); + } else { + this.tip().remove(); + } + }, + + fixTitle: function() { + var $e = this.$element; + if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { + $e.attr('original-title', $e.attr('title') || '').removeAttr('title'); + } + }, + + getTitle: function() { + var title, $e = this.$element, o = this.options; + this.fixTitle(); + var title, o = this.options; + if (typeof o.title == 'string') { + title = $e.attr(o.title == 'title' ? 'original-title' : o.title); + } else if (typeof o.title == 'function') { + title = o.title.call($e[0]); + } + title = ('' + title).replace(/(^\s*|\s*$)/, ""); + return title || o.fallback; + }, + + tip: function() { + if (!this.$tip) { + this.$tip = $('
').html('
'); + this.$tip.data('tipsy-pointee', this.$element[0]); + } + return this.$tip; + }, + + validate: function() { + if (!this.$element[0].parentNode) { + this.hide(); + this.$element = null; + this.options = null; + } + }, + + enable: function() { this.enabled = true; }, + disable: function() { this.enabled = false; }, + toggleEnabled: function() { this.enabled = !this.enabled; } + }; + + $.fn.tipsy = function(options) { + + if (options === true) { + return this.data('tipsy'); + } else if (typeof options == 'string') { + var tipsy = this.data('tipsy'); + if (tipsy) tipsy[options](); + return this; + } + + options = $.extend({}, $.fn.tipsy.defaults, options); + + function get(ele) { + var tipsy = $.data(ele, 'tipsy'); + if (!tipsy) { + tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); + $.data(ele, 'tipsy', tipsy); + } + return tipsy; + } + + function enter() { + var tipsy = get(this); + tipsy.hoverState = 'in'; + if (options.delayIn == 0) { + tipsy.show(); + } else { + tipsy.fixTitle(); + setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); + } + }; + + function leave() { + var tipsy = get(this); + tipsy.hoverState = 'out'; + if (options.delayOut == 0) { + tipsy.hide(); + } else { + setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut); + } + }; + + if (!options.live) this.each(function() { get(this); }); + + if (options.trigger != 'manual') { + var binder = options.live ? 'live' : 'bind', + eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', + eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; + this[binder](eventIn, enter)[binder](eventOut, leave); + } + + return this; + + }; + + $.fn.tipsy.defaults = { + className: null, + delayIn: 0, + delayOut: 0, + fade: false, + fallback: '', + gravity: 'n', + html: false, + live: false, + offset: 0, + opacity: 0.8, + title: 'title', + trigger: 'hover' + }; + + $.fn.tipsy.revalidate = function() { + $('.tipsy').each(function() { + var pointee = $.data(this, 'tipsy-pointee'); + if (!pointee || !isElementInDOM(pointee)) { + $(this).remove(); + } + }); + }; + + // Overwrite this method to provide options on a per-element basis. + // For example, you could store the gravity in a 'tipsy-gravity' attribute: + // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); + // (remember - do not modify 'options' in place!) + $.fn.tipsy.elementOptions = function(ele, options) { + return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; + }; + + $.fn.tipsy.autoNS = function() { + return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; + }; + + $.fn.tipsy.autoWE = function() { + return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; + }; + + /** + * yields a closure of the supplied parameters, producing a function that takes + * no arguments and is suitable for use as an autogravity function like so: + * + * @param margin (int) - distance from the viewable region edge that an + * element should be before setting its tooltip's gravity to be away + * from that edge. + * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer + * if there are no viewable region edges effecting the tooltip's + * gravity. It will try to vary from this minimally, for example, + * if 'sw' is preferred and an element is near the right viewable + * region edge, but not the top edge, it will set the gravity for + * that element's tooltip to be 'se', preserving the southern + * component. + */ + $.fn.tipsy.autoBounds = function(margin, prefer) { + return function() { + var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, + boundTop = $(document).scrollTop() + margin, + boundLeft = $(document).scrollLeft() + margin, + $this = $(this); + + if ($this.offset().top < boundTop) dir.ns = 'n'; + if ($this.offset().left < boundLeft) dir.ew = 'w'; + if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; + if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; + + return dir.ns + (dir.ew ? dir.ew : ''); + } + }; + +})(jQuery); diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js new file mode 100644 index 0000000000000000000000000000000000000000..2d5d05ca8e64a44693b84e634d096d519efecea9 --- /dev/null +++ b/vendor/assets/javascripts/peek.js @@ -0,0 +1,84 @@ +var requestId; + +requestId = null; + +(function($) { + var fetchRequestResults, getRequestId, initializeTipsy, peekEnabled, toggleBar, updatePerformanceBar; + getRequestId = function() { + if (requestId != null) { + return requestId; + } else { + return $('#peek').data('request-id'); + } + }; + peekEnabled = function() { + return $('#peek').length; + }; + updatePerformanceBar = function(results) { + var key, label; + for (key in results.data) { + for (label in results.data[key]) { + $("[data-defer-to=" + key + "-" + label + "]").text(results.data[key][label]); + } + } + return $(document).trigger('peek:render', [getRequestId(), results]); + }; + initializeTipsy = function() { + return $('#peek .peek-tooltip, #peek .tooltip').each(function() { + var el, gravity; + el = $(this); + gravity = el.hasClass('rightwards') || el.hasClass('leftwards') ? $.fn.tipsy.autoWE : $.fn.tipsy.autoNS; + return el.tipsy({ + gravity: gravity + }); + }); + }; + toggleBar = function(event) { + var wrapper; + if ($(event.target).is(':input')) { + return; + } + if (event.which === 96 && !event.metaKey) { + wrapper = $('#peek'); + if (wrapper.hasClass('disabled')) { + wrapper.removeClass('disabled'); + return document.cookie = "peek=true; path=/"; + } else { + wrapper.addClass('disabled'); + return document.cookie = "peek=false; path=/"; + } + } + }; + fetchRequestResults = function() { + return $.ajax('/peek/results', { + data: { + request_id: getRequestId() + }, + success: function(data, textStatus, xhr) { + return updatePerformanceBar(data); + }, + error: function(xhr, textStatus, error) {} + }); + }; + $(document).on('keypress', toggleBar); + $(document).on('peek:update', initializeTipsy); + $(document).on('peek:update', fetchRequestResults); + $(document).on('pjax:end', function(event, xhr, options) { + if (xhr != null) { + requestId = xhr.getResponseHeader('X-Request-Id'); + } + if (peekEnabled()) { + return $(this).trigger('peek:update'); + } + }); + $(document).on('page:change turbolinks:load', function() { + if (peekEnabled()) { + return $(this).trigger('peek:update'); + } + }); + return $(function() { + if (peekEnabled()) { + return $(this).trigger('peek:update'); + } + }); +})(jQuery); diff --git a/vendor/assets/javascripts/peek.performance_bar.js b/vendor/assets/javascripts/peek.performance_bar.js new file mode 100644 index 0000000000000000000000000000000000000000..3318e21889003161ffa844195ddb5183737d43d2 --- /dev/null +++ b/vendor/assets/javascripts/peek.performance_bar.js @@ -0,0 +1,191 @@ +var PerformanceBar, ajaxStart, renderPerformanceBar, updateStatus; + +PerformanceBar = (function() { + PerformanceBar.prototype.appInfo = null; + + PerformanceBar.prototype.width = null; + + PerformanceBar.formatTime = function(value) { + if (value >= 1000) { + return ((value / 1000).toFixed(3)) + "s"; + } else { + return (value.toFixed(0)) + "ms"; + } + }; + + function PerformanceBar(options) { + var k, v; + if (options == null) { + options = {}; + } + this.el = $('#peek-view-performance-bar .performance-bar'); + for (k in options) { + v = options[k]; + this[k] = v; + } + if (this.width == null) { + this.width = this.el.width(); + } + if (this.timing == null) { + this.timing = window.performance.timing; + } + } + + PerformanceBar.prototype.render = function(serverTime) { + var networkTime, perfNetworkTime; + if (serverTime == null) { + serverTime = 0; + } + this.el.empty(); + this.addBar('frontend', '#90d35b', 'domLoading', 'domInteractive'); + perfNetworkTime = this.timing.responseEnd - this.timing.requestStart; + if (serverTime && serverTime <= perfNetworkTime) { + networkTime = perfNetworkTime - serverTime; + this.addBar('latency / receiving', '#f1faff', this.timing.requestStart + serverTime, this.timing.requestStart + serverTime + networkTime); + this.addBar('app', '#90afcf', this.timing.requestStart, this.timing.requestStart + serverTime, this.appInfo); + } else { + this.addBar('backend', '#c1d7ee', 'requestStart', 'responseEnd'); + } + this.addBar('tcp / ssl', '#45688e', 'connectStart', 'connectEnd'); + this.addBar('redirect', '#0c365e', 'redirectStart', 'redirectEnd'); + this.addBar('dns', '#082541', 'domainLookupStart', 'domainLookupEnd'); + return this.el; + }; + + PerformanceBar.prototype.isLoaded = function() { + return this.timing.domInteractive; + }; + + PerformanceBar.prototype.start = function() { + return this.timing.navigationStart; + }; + + PerformanceBar.prototype.end = function() { + return this.timing.domInteractive; + }; + + PerformanceBar.prototype.total = function() { + return this.end() - this.start(); + }; + + PerformanceBar.prototype.addBar = function(name, color, start, end, info) { + var bar, left, offset, time, title, width; + if (typeof start === 'string') { + start = this.timing[start]; + } + if (typeof end === 'string') { + end = this.timing[end]; + } + if (!((start != null) && (end != null))) { + return; + } + time = end - start; + offset = start - this.start(); + left = this.mapH(offset); + width = this.mapH(time); + title = name + ": " + (PerformanceBar.formatTime(time)); + bar = $('
  • ', { + title: title, + "class": 'peek-tooltip' + }); + bar.css({ + width: width + "px", + left: left + "px", + background: color + }); + bar.tipsy({ + gravity: $.fn.tipsy.autoNS + }); + return this.el.append(bar); + }; + + PerformanceBar.prototype.mapH = function(offset) { + return offset * (this.width / this.total()); + }; + + return PerformanceBar; + +})(); + +renderPerformanceBar = function() { + var bar, resp, span, time; + resp = $('#peek-server_response_time'); + time = Math.round(resp.data('time') * 1000); + bar = new PerformanceBar; + bar.render(time); + span = $('', { + 'class': 'peek-tooltip', + title: 'Total navigation time for this page.' + }).text(PerformanceBar.formatTime(bar.total())); + span.tipsy({ + gravity: $.fn.tipsy.autoNS + }); + return updateStatus(span); +}; + +updateStatus = function(html) { + return $('#serverstats').html(html); +}; + +ajaxStart = null; + +$(document).on('pjax:start page:fetch turbolinks:request-start', function(event) { + return ajaxStart = event.timeStamp; +}); + +$(document).on('pjax:end page:load turbolinks:load', function(event, xhr) { + var ajaxEnd, serverTime, total; + if (ajaxStart == null) { + return; + } + ajaxEnd = event.timeStamp; + total = ajaxEnd - ajaxStart; + serverTime = xhr ? parseInt(xhr.getResponseHeader('X-Runtime')) : 0; + return setTimeout(function() { + var bar, now, span, tech; + now = new Date().getTime(); + bar = new PerformanceBar({ + timing: { + requestStart: ajaxStart, + responseEnd: ajaxEnd, + domLoading: ajaxEnd, + domInteractive: now + }, + isLoaded: function() { + return true; + }, + start: function() { + return ajaxStart; + }, + end: function() { + return now; + } + }); + bar.render(serverTime); + if ($.fn.pjax != null) { + tech = 'PJAX'; + } else { + tech = 'Turbolinks'; + } + span = $('', { + 'class': 'peek-tooltip', + title: tech + " navigation time" + }).text(PerformanceBar.formatTime(total)); + span.tipsy({ + gravity: $.fn.tipsy.autoNS + }); + updateStatus(span); + return ajaxStart = null; + }, 0); +}); + +$(function() { + if (window.performance) { + return renderPerformanceBar(); + } else { + return $('#peek-view-performance-bar').remove(); + } +}); + +// --- +// generated by coffee-script 1.9.2 diff --git a/vendor/assets/javascripts/peek.rblineprof.js b/vendor/assets/javascripts/peek.rblineprof.js new file mode 100644 index 0000000000000000000000000000000000000000..cad6e24d40ee7fd3d41a2c9b00dba1f38d5cf4e9 --- /dev/null +++ b/vendor/assets/javascripts/peek.rblineprof.js @@ -0,0 +1,5 @@ +$(document).on('click', '.js-lineprof-file', function(e) { + $(this).parents('.heading').next('div').toggle(); + e.preventDefault(); + return false; +}); diff --git a/vendor/assets/stylesheets/peek.scss b/vendor/assets/stylesheets/peek.scss new file mode 100644 index 0000000000000000000000000000000000000000..4b2957c557502dfe75937f6a51d32854407a93b3 --- /dev/null +++ b/vendor/assets/stylesheets/peek.scss @@ -0,0 +1,138 @@ +//= require peek/views/performance_bar +//= require peek/views/rblineprof +//= require peek/views/rblineprof/pygments + +#peek { + background: #000; + height: 35px; + line-height: 35px; + color: #999; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.75); + + .hidden { + display: none; + visibility: visible; + } + + &.disabled { + display: none; + } + + &.production { + background-color: #222; + } + + &.staging { + background-color: #291430; + } + + &.development { + background-color: #4c1210; + } + + .wrapper { + width: 800px; + margin: 0 auto; + } + + // UI Elements + .bucket { + background: #111; + display: inline-block; + padding: 4px 6px; + font-family: Consolas, "Liberation Mono", Courier, monospace; + line-height: 1; + color: #ccc; + border-radius: 3px; + box-shadow: 0 1px 0 rgba(255,255,255,.2), inset 0 1px 2px rgba(0,0,0,.25); + + .hidden { + display: none; + } + + &:hover .hidden { + display: inline; + } + } + + strong { + color: #fff; + } + + .view { + margin-right: 15px; + float: left; + + &:last-child { + margin-right: 0; + } + } + + .css-truncate { + &.css-truncate-target, + .css-truncate-target { + display: inline-block; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + } + + &.expandable:hover .css-truncate-target, + &.expandable:hover.css-truncate-target { + max-width: 10000px !important; + } + } +} + +// .performance-bar { +// position: relative; +// top: 2px; +// display: inline-block; +// width: 75px; +// height: 10px; +// margin: 0 0 0 5px; +// list-style: none; +// background-color: rgba(0, 0, 0, .5); +// border: 1px solid rgba(0, 0, 0, .7); +// border-radius: 2px; +// box-shadow: 0 1px 0 rgba(255, 255, 255, .15); +// +// li { +// position: absolute; +// top: 0; +// bottom: 0; +// overflow: hidden; +// opacity: .8; +// color: transparent; +// +// &:hover { +// opacity: 1; +// cursor: default; +// } +// } +// } + +.tipsy { font-size: 10px; position: absolute; padding: 5px; z-index: 100000; } + .tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } + + /* Rounded corners */ + .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } + + .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; } + + /* Rules to colour arrows */ + .tipsy-arrow-n { border-bottom-color: #000; } + .tipsy-arrow-s { border-top-color: #000; } + .tipsy-arrow-e { border-left-color: #000; } + .tipsy-arrow-w { border-right-color: #000; } + + .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } + .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; }