Commit 4fcbcce3 authored by Tiago Botelho's avatar Tiago Botelho

Add BatchLoader as a way to refactor the base stage code

parent 7c109c57
...@@ -6,9 +6,9 @@ class CycleAnalytics ...@@ -6,9 +6,9 @@ class CycleAnalytics
@options = options @options = options
end end
def self.all_medians_per_stage(projects, options) def all_medians_per_stage
STAGES.each_with_object({}) do |stage_name, hsh| STAGES.each_with_object({}) do |stage_name, hsh|
hsh[stage_name] = Gitlab::CycleAnalytics::Stage[stage_name].new(projects: projects, options: options).medians&.values || [] hsh[stage_name] = self[stage_name].median
end end
end end
...@@ -31,7 +31,7 @@ class CycleAnalytics ...@@ -31,7 +31,7 @@ class CycleAnalytics
end end
def [](stage_name) def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(projects: [@project], options: @options) Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end end
private private
......
...@@ -6,13 +6,7 @@ class AnalyticsStageEntity < Grape::Entity ...@@ -6,13 +6,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :legend expose :legend
expose :description expose :description
expose :medians, as: :values do |stage| expose :median, as: :value do |stage|
medians = stage.medians stage.median && !stage.median.blank? ? distance_of_time_in_words(stage.median) : nil
unless medians.blank?
medians.each do |id, median|
medians[id] = distance_of_time_in_words(median)
end
end
end end
end end
...@@ -7,14 +7,14 @@ module Gitlab ...@@ -7,14 +7,14 @@ module Gitlab
private private
def base_query def base_query(project_ids = nil)
@base_query ||= stage_query stage_query(project_ids)
end end
def stage_query def stage_query(project_ids = nil)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.where(issue_table[:project_id].in(Arel.sql(@projects.select(:id).to_sql))) # rubocop:disable Gitlab/ModuleWithInstanceVariables .where(issue_table[:project_id].in(project_ids || @project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests # Load merge_requests
......
...@@ -3,8 +3,8 @@ module Gitlab ...@@ -3,8 +3,8 @@ module Gitlab
class BaseStage class BaseStage
include BaseQuery include BaseQuery
def initialize(projects:, options:) def initialize(project:, options:)
@projects = projects @project = project
@options = options @options = options
end end
...@@ -20,18 +20,21 @@ module Gitlab ...@@ -20,18 +20,21 @@ module Gitlab
raise NotImplementedError.new("Expected #{self.name} to implement title") raise NotImplementedError.new("Expected #{self.name} to implement title")
end end
def medians def median
BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
cte_table = Arel::Table.new("cte_table_for_#{name}") cte_table = Arel::Table.new("cte_table_for_#{name}")
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current # We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage. # cycle analytics stage.
interval_query = Arel::Nodes::As.new( interval_query = Arel::Nodes::As.new(cte_table,
cte_table, subtract_datetimes(base_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
median_datetimes(cte_table, interval_query, name) median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
end
end end
def name def name
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
module ProductionHelper module ProductionHelper
def stage_query def stage_query(project_ids = nil)
super super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at] .where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
......
...@@ -25,11 +25,11 @@ module Gitlab ...@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges") _("Total test time for all commits/merges")
end end
def stage_query def stage_query(project_ids = nil)
if @options[:branch] if @options[:branch]
super.where(build_table[:ref].eq(@options[:branch])) super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else else
super super(project_ids)
end end
end end
end end
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
values = {} values = {}
medians_per_stage.each do |stage_name, medians| medians_per_stage.each do |stage_name, medians|
medians = medians.compact medians = medians.map(&:presence).compact
stage_values = { stage_values = {
average: calc_average(medians), average: calc_average(medians),
...@@ -35,7 +35,12 @@ module Gitlab ...@@ -35,7 +35,12 @@ module Gitlab
private private
def medians_per_stage def medians_per_stage
@medians_per_stage ||= ::CycleAnalytics.all_medians_per_stage(projects, options) projects.each_with_object({}) do |project, hsh|
::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
end
end end
def calc_average(values) def calc_average(values)
...@@ -61,4 +66,3 @@ module Gitlab ...@@ -61,4 +66,3 @@ module Gitlab
end end
end end
end end
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
module Gitlab module Gitlab
module Database module Database
module Median module Median
def median_datetimes(arel_table, query_so_far, column_sym) def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
median_queries = median_queries =
if Gitlab::Database.postgresql? if Gitlab::Database.postgresql?
pg_median_datetime_sql(arel_table, query_so_far, column_sym) pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
elsif Gitlab::Database.mysql? elsif Gitlab::Database.mysql?
mysql_median_datetime_sql(arel_table, query_so_far, column_sym) mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
end end
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
if Gitlab::Database.postgresql? if Gitlab::Database.postgresql?
result.values.map do |id, median| result.values.map do |id, median|
[id, median&.to_f] [id.to_i, median&.to_f]
end.to_h end.to_h
elsif Gitlab::Database.mysql? elsif Gitlab::Database.mysql?
result.to_a.flatten.first result.to_a.flatten.first
...@@ -53,7 +53,7 @@ module Gitlab ...@@ -53,7 +53,7 @@ module Gitlab
] ]
end end
def pg_median_datetime_sql(arel_table, query_so_far, column_sym) def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
# Create a CTE with the column we're operating on, row number (after sorting by the column # Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows # we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count` # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
...@@ -69,19 +69,22 @@ module Gitlab ...@@ -69,19 +69,22 @@ module Gitlab
cte_table, cte_table,
arel_table arel_table
.project( .project(
arel_table[:project_id], arel_table[partition_column],
arel_table[column_sym].as(column_sym.to_s), arel_table[column_sym].as(column_sym.to_s),
Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("rank", []), Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("rank", []),
Arel::Nodes::Window.new.partition(arel_table[:project_id]) Arel::Nodes::Window.new.partition(arel_table[partition_column])
.order(arel_table[column_sym])).as('row_id'), .order(arel_table[column_sym])).as('row_id'),
arel_table.from(arel_table.alias).project("COUNT(*)").where(arel_table[:project_id].eq(arel_table.alias[:project_id])).as('ct')). arel_table.from(arel_table.alias)
.project("COUNT(*)")
.where(arel_table[partition_column].eq(arel_table.alias[partition_column])).as('ct')).
# Disallow negative values # Disallow negative values
where(arel_table[column_sym].gteq(zero_interval))) where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished # From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value. # selected rows, and this is the median value.
cte_table.project(cte_table[:project_id]) cte_table
.project(cte_table[partition_column])
.project(average([extract_epoch(cte_table[column_sym])], "median")) .project(average([extract_epoch(cte_table[column_sym])], "median"))
.where( .where(
Arel::Nodes::Between.new( Arel::Nodes::Between.new(
...@@ -93,8 +96,8 @@ module Gitlab ...@@ -93,8 +96,8 @@ module Gitlab
) )
) )
.with(query_so_far, cte) .with(query_so_far, cte)
.group(cte_table[:project_id]) .group(cte_table[partition_column])
.order(cte_table[:project_id]) .order(cte_table[partition_column])
.to_sql .to_sql
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment