diff --git a/Gemfile b/Gemfile index 2de00bfc0a1..2c200f2fa7a 100644 --- a/Gemfile +++ b/Gemfile @@ -264,6 +264,17 @@ 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', group: :postgres +gem 'peek-rblineprof', '~> 0.2.0' +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 a898c12485e..6755c75e331 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) @@ -999,6 +1032,15 @@ 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) diff --git a/app/assets/javascripts/peek.js b/app/assets/javascripts/peek.js new file mode 100644 index 00000000000..de1a99fa3bd --- /dev/null +++ b/app/assets/javascripts/peek.js @@ -0,0 +1,16 @@ +import 'vendor/peek'; +import 'vendor/peek.performance_bar'; + +$(document).on('click', '#peek-show-queries', (e) => { + e.preventDefault(); + $('.peek-rblineprof-modal').hide(); + const $modal = $('#modal-peek-pg-queries'); + if ($modal.length) { + $modal.modal('toggle'); + } +}); + +$(document).on('click', '.js-lineprof-file', (e) => { + e.preventDefault(); + $(e.target).parents('.peek-rblineprof-file').find('.data').toggle(); +}); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 8ac71797c14..a4a7f3fa944 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(); + const performanceBarCookieName = 'perf_bar_enabled'; + if (Cookies.get(performanceBarCookieName) === 'true') { + Cookies.remove(performanceBarCookieName, { path: '/' }); + } else { + Cookies.set(performanceBarCookieName, true, { path: '/' }); + } + 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 47ce21d238b..91694ebcd1d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,6 +9,7 @@ class ApplicationController < ActionController::Base include SentryHelper include WorkhorseHelper include EnforcesTwoFactorAuthentication + include Peek::Rblineprof::CustomControllerHelpers before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_rss_token! @@ -18,7 +19,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 +64,21 @@ 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 + else + cookies[:perf_bar_enabled].present? + end + end + protected # This filter handles both private tokens and personal access tokens diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 88dfe78c90c..833d3c36b28 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -27,6 +27,7 @@ module NavHelper def nav_header_class class_name = '' class_name << " with-horizontal-nav" if defined?(nav) && nav + class_name << " with-peek" if peek_enabled? class_name end diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index ea8bbe92d86..331d1181220 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 Show/hide the Performance Bar %tr %td.shortcut .key ? diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 1ef0d524dbb..eea33b5966f 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 03688e9ff21..2b07273a0a8 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/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml new file mode 100644 index 00000000000..ac811a10ef5 --- /dev/null +++ b/app/views/peek/views/_mysql2.html.haml @@ -0,0 +1,4 @@ +- local_assigns.fetch(:view) + += render 'peek/views/sql', view: view +mysql diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml new file mode 100644 index 00000000000..ee94c2f3274 --- /dev/null +++ b/app/views/peek/views/_pg.html.haml @@ -0,0 +1,4 @@ +- local_assigns.fetch(:view) + += render 'peek/views/sql', view: view +pg diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml new file mode 100644 index 00000000000..16fc010f66f --- /dev/null +++ b/app/views/peek/views/_sql.html.haml @@ -0,0 +1,13 @@ +%strong + %a#peek-show-queries{ href: '#' } + %span{ data: { defer_to: "#{view.defer_key}-duration" } }... + \/ + %span{ data: { defer_to: "#{view.defer_key}-calls" } }... +#modal-peek-pg-queries.modal{ tabindex: -1 } + .modal-dialog + #modal-peek-pg-queries-content.modal-content + .modal-header + %a.close{ href: "#", "data-dismiss" => "modal" } × + %h4 + SQL queries + .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }... diff --git a/changelogs/unreleased/29010-perf-bar.yml b/changelogs/unreleased/29010-perf-bar.yml new file mode 100644 index 00000000000..f4167e5562f --- /dev/null +++ b/changelogs/unreleased/29010-perf-bar.yml @@ -0,0 +1,4 @@ +--- +title: Add an optional performance bar to view performance metrics for the current page +merge_request: 11439 +author: diff --git a/config/application.rb b/config/application.rb index b0533759252..8bbecf3ed0f 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 00000000000..65432caac2a --- /dev/null +++ b/config/initializers/peek.rb @@ -0,0 +1,32 @@ +Rails.application.config.peek.adapter = :redis, { client: ::Redis.new(Gitlab::Redis.params) } + +Peek.into Peek::Views::Host +Peek.into Peek::Views::PerformanceBar +if Gitlab::Database.mysql? + require 'peek-mysql2' + PEEK_DB_CLIENT = ::Mysql2::Client + PEEK_DB_VIEW = Peek::Views::Mysql2 +else + require 'peek-pg' + PEEK_DB_CLIENT = ::PG::Connection + PEEK_DB_VIEW = Peek::Views::PG +end +Peek.into PEEK_DB_VIEW +Peek.into Peek::Views::Redis +Peek.into Peek::Views::Sidekiq +Peek.into Peek::Views::Rblineprof +Peek.into Peek::Views::GC + +# rubocop:disable Style/ClassAndModuleCamelCase +class PEEK_DB_CLIENT + class << self + attr_accessor :query_details + end + self.query_details = Concurrent::Array.new +end + +PEEK_DB_VIEW.prepend ::Gitlab::PerformanceBar::PeekQueryTracker + +class Peek::Views::PerformanceBar::ProcessUtilization + prepend ::Gitlab::PerformanceBar::PeekPerformanceBarWithRackBody +end diff --git a/config/routes.rb b/config/routes.rb index d909be38b42..4fd6cb5d439 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,7 @@ Rails.application.routes.draw do get 'liveness' => 'health#liveness' get 'readiness' => 'health#readiness' resources :metrics, only: [:index] + mount Peek::Railtie => '/peek' end # Koding route diff --git a/config/webpack.config.js b/config/webpack.config.js index c5060669da1..120f9d3193d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -78,6 +78,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/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md index c5b7488be69..87416008e98 100644 --- a/doc/workflow/shortcuts.md +++ b/doc/workflow/shortcuts.md @@ -6,7 +6,10 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?' | Keyboard Shortcut | Description | | ----------------- | ----------- | +| n | Main navigation | | s | Focus search | +| f | Focus filter | +| p b | Show/hide the Performance Bar | | ? | Show/hide this dialog | | + shift + p | Toggle markdown preview | | | Edit last comment (when focused on an empty textarea) | diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb new file mode 100644 index 00000000000..163a40ad306 --- /dev/null +++ b/lib/gitlab/performance_bar.rb @@ -0,0 +1,7 @@ +module Gitlab + module PerformanceBar + def self.enabled? + Feature.enabled?('gitlab_performance_bar') + end + end +end diff --git a/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb new file mode 100644 index 00000000000..d939a6ea18d --- /dev/null +++ b/lib/gitlab/performance_bar/peek_performance_bar_with_rack_body.rb @@ -0,0 +1,22 @@ +# This solves a bug with a X-Senfile header that wouldn't be set properly, see +# https://github.com/peek/peek-performance_bar/pull/27 +module Gitlab + module PerformanceBar + module PeekPerformanceBarWithRackBody + def call(env) + @env = env + reset_stats + + @total_requests += 1 + first_request if @total_requests == 1 + + env['process.request_start'] = @start.to_f + env['process.total_requests'] = total_requests + + status, headers, body = @app.call(env) + body = Rack::BodyProxy.new(body) { record_request } + [status, headers, body] + end + end + end +end diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb new file mode 100644 index 00000000000..7ab80f5ee0f --- /dev/null +++ b/lib/gitlab/performance_bar/peek_query_tracker.rb @@ -0,0 +1,39 @@ +# Inspired by https://github.com/peek/peek-pg/blob/master/lib/peek/views/pg.rb +module Gitlab + module PerformanceBar + module PeekQueryTracker + def sorted_queries + PEEK_DB_CLIENT.query_details. + sort { |a, b| b[:duration] <=> a[:duration] } + end + + def results + super.merge(queries: sorted_queries) + end + + private + + def setup_subscribers + super + + # Reset each counter when a new request starts + before_request do + PEEK_DB_CLIENT.query_details = [] + end + + subscribe('sql.active_record') do |_, start, finish, _, data| + if RequestStore.active? && RequestStore.store[:peek_enabled] + track_query(data[:sql].strip, data[:binds], start, finish) + end + end + end + + def track_query(raw_query, bindings, start, finish) + query = Gitlab::Sherlock::Query.new(raw_query, start, finish) + query_info = { duration: '%.3f' % query.duration, sql: query.formatted_query } + + PEEK_DB_CLIENT.query_details << query_info + end + end + end +end diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb new file mode 100644 index 00000000000..99f9c2c9b04 --- /dev/null +++ b/lib/peek/rblineprof/custom_controller_helpers.rb @@ -0,0 +1,96 @@ +module Peek + module Rblineprof + module CustomControllerHelpers + extend ActiveSupport::Concern + + # This will become useless once https://github.com/peek/peek-rblineprof/pull/5 + # is merged + def pygmentize(file_name, code, lexer = nil) + if lexer.present? + Gitlab::Highlight.highlight(file_name, code) + else + "
#{Rack::Utils.escape_html(code)}
" + end + end + + # rubocop:disable all + def inject_rblineprof + ret = nil + profile = lineprof(rblineprof_profiler_regex) do + ret = yield + end + + if response.content_type =~ %r|text/html| + sort = params[:lineprofiler_sort] + mode = params[:lineprofiler_mode] || 'cpu' + min = (params[:lineprofiler_min] || 5).to_i * 1000 + summary = params[:lineprofiler_summary] + + # Sort each file by the longest calculated time + per_file = profile.map do |file, lines| + total, child, excl, total_cpu, child_cpu, excl_cpu = lines[0] + + wall = summary == 'exclusive' ? excl : total + cpu = summary == 'exclusive' ? excl_cpu : total_cpu + idle = summary == 'exclusive' ? (excl - excl_cpu) : (total - total_cpu) + + [ + file, lines, + wall, cpu, idle, + sort == 'idle' ? idle : sort == 'cpu' ? cpu : wall + ] + end.sort_by{ |a,b,c,d,e,f| -f } + + output = '' + per_file.each do |file_name, lines, file_wall, file_cpu, file_idle, file_sort| + + output << "
" + + show_src = file_sort > min + tmpl = show_src ? "%s" : "%s" + + if mode == 'cpu' + output << sprintf("% 8.1fms + % 8.1fms #{tmpl}", file_cpu / 1000.0, file_idle / 1000.0, file_name.sub(Rails.root.to_s + '/', '')) + else + output << sprintf("% 8.1fms #{tmpl}", file_wall/1000.0, file_name.sub(Rails.root.to_s + '/', '')) + end + + output << "
" # .heading + + next unless show_src + + output << "
" + code = [] + times = [] + File.readlines(file_name).each_with_index do |line, i| + code << line + wall, cpu, calls = lines[i + 1] + + if calls && calls > 0 + if mode == 'cpu' + idle = wall - cpu + times << sprintf("% 8.1fms + % 8.1fms (% 5d)", cpu / 1000.0, idle / 1000.0, calls) + else + times << sprintf("% 8.1fms (% 5d)", wall / 1000.0, calls) + end + else + times << ' ' + end + end + output << "
#{times.join("\n")}
" + # The following line was changed from + # https://github.com/peek/peek-rblineprof/blob/8d3b7a283a27de2f40abda45974516693d882258/lib/peek/rblineprof/controller_helpers.rb#L125 + # This will become useless once https://github.com/peek/peek-rblineprof/pull/16 + # is merged and is implemented. + output << "
#{pygmentize(file_name, code.join, 'ruby')}
" + output << "
" # .data then .peek-rblineprof-file + end + + response.body += "
#{output}
".html_safe + end + + ret + end + end + end +end diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb new file mode 100644 index 00000000000..c2842255b86 --- /dev/null +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe 'User can display performacne bar', :js do + shared_examples 'performance bar is disabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + shared_examples 'performance bar is enabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + context 'when user is logged-out' do + before do + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + end + + context 'when user is logged-in' do + before do + login_as :user + + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is enabled' + end + end +end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 140fd2720bf..ba247dcc5cf 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -20,8 +20,8 @@ describe Commit, models: true do end it 'caches the author' do + allow(RequestStore).to receive(:active?).and_return(true) user = create(:user, email: commit.author_email) - expect(RequestStore).to receive(:active?).twice.and_return(true) expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original expect(commit.author).to eq(user) diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js new file mode 100644 index 00000000000..f7e77de34ff --- /dev/null +++ b/vendor/assets/javascripts/peek.js @@ -0,0 +1,78 @@ +(function($) { + var fetchRequestResults, getRequestId, peekEnabled, toggleBar, updatePerformanceBar; + getRequestId = function() { + return $('#peek').data('request-id'); + }; + peekEnabled = function() { + return $('#peek').length; + }; + updatePerformanceBar = function(results) { + var key, label, data, table, html, tr, duration_td, sql_td, strong; + + Object.keys(results.data).forEach(function(key) { + Object.keys(results.data[key]).forEach(function(label) { + data = results.data[key][label]; + + if (label == 'queries') { + table = document.createElement('table'); + + for (var i = 0; i < data.length; i += 1) { + tr = document.createElement('tr'); + duration_td = document.createElement('td'); + sql_td = document.createElement('td'); + strong = document.createElement('strong'); + + strong.append(data[i]['duration'] + 'ms'); + duration_td.appendChild(strong); + tr.appendChild(duration_td); + + sql_td.appendChild(document.createTextNode(data[i]['sql'])); + tr.appendChild(sql_td); + + table.appendChild(tr); + } + + table.className = 'table'; + $("[data-defer-to=" + key + "-" + label + "]").html(table); + } else { + $("[data-defer-to=" + key + "-" + label + "]").text(results.data[key][label]); + } + }); + }); + return $(document).trigger('peek:render', [getRequestId(), results]); + }; + 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', fetchRequestResults); + 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..6ed86dce2f2 --- /dev/null +++ b/vendor/assets/javascripts/peek.performance_bar.js @@ -0,0 +1,182 @@ +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 = $('
  • ', { + 'data-title': title, + 'data-toggle': 'tooltip', + 'data-container': 'body' + }); + bar.css({ + width: width + "px", + left: left + "px", + background: color + }); + 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 = $('', { + 'data-toggle': 'tooltip', + 'data-title': 'Total navigation time for this page.', + 'data-container': 'body' + }).text(PerformanceBar.formatTime(bar.total())); + 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 = $('', { + 'data-toggle': 'tooltip', + 'data-title': tech + " navigation time", + 'data-container': 'body' + }).text(PerformanceBar.formatTime(total)); + updateStatus(span); + return ajaxStart = null; + }, 0); +}); + +$(function() { + if (window.performance) { + return renderPerformanceBar(); + } else { + return $('#peek-view-performance-bar').remove(); + } +}); diff --git a/vendor/assets/stylesheets/peek.scss b/vendor/assets/stylesheets/peek.scss new file mode 100644 index 00000000000..f1845fb9044 --- /dev/null +++ b/vendor/assets/stylesheets/peek.scss @@ -0,0 +1,94 @@ +//= require peek/views/performance_bar +//= require peek/views/rblineprof + +header.navbar-gitlab.with-peek { + top: 35px; +} + +#peek { + height: 35px; + background: #000; + line-height: 35px; + color: #999; + + &.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; + } + + table { + strong { + color: #000; + } + } + + .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; + } + } +} + +#modal-peek-pg-queries-content { + color: #000; +}