From d39ecf1ca7e9455abcdeb17c251a2d248a47d471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 17 May 2017 13:20:55 +0200 Subject: New performance bar that can be enabled with the `p b` shortcut MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- vendor/assets/javascripts/jquery.tipsy.js | 258 ++++++++++++++++++++++ vendor/assets/javascripts/peek.js | 84 +++++++ vendor/assets/javascripts/peek.performance_bar.js | 191 ++++++++++++++++ vendor/assets/javascripts/peek.rblineprof.js | 5 + vendor/assets/stylesheets/peek.scss | 138 ++++++++++++ 5 files changed, 676 insertions(+) create mode 100644 vendor/assets/javascripts/jquery.tipsy.js create mode 100644 vendor/assets/javascripts/peek.js create mode 100644 vendor/assets/javascripts/peek.performance_bar.js create mode 100644 vendor/assets/javascripts/peek.rblineprof.js create mode 100644 vendor/assets/stylesheets/peek.scss (limited to 'vendor') diff --git a/vendor/assets/javascripts/jquery.tipsy.js b/vendor/assets/javascripts/jquery.tipsy.js new file mode 100644 index 00000000000..d9fced24b60 --- /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 00000000000..2d5d05ca8e6 --- /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 00000000000..3318e218890 --- /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 00000000000..cad6e24d40e --- /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 00000000000..4b2957c5575 --- /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; } -- cgit v1.2.3