Optimizing the issue growth graph. With thousands of issues, the old version ran quite slowly as it was grabbing information for each issue. This new implementation only selects what it needs to show the graph. Future versions might simplify it further by selecting issues created per week instead of per day, but I'll leave that for later.

This commit is contained in:
Brad Beattie 2009-03-12 10:45:55 -07:00
parent bd5a1567f8
commit f81d0d5160
4 changed files with 56 additions and 44 deletions

View File

@ -4,7 +4,7 @@ class GraphsController < ApplicationController
before_filter :find_version, :only => [:target_version_graph] before_filter :find_version, :only => [:target_version_graph]
before_filter :find_open_issues, :only => [:old_issues, :issue_age_graph] before_filter :find_open_issues, :only => [:old_issues, :issue_age_graph]
before_filter :find_all_issues, :only => [:issue_growth_graph, :issue_growth] before_filter :find_optional_project, :only => [:issue_growth_graph]
helper IssuesHelper helper IssuesHelper
@ -16,6 +16,7 @@ class GraphsController < ApplicationController
# Initialize the graph # Initialize the graph
graph = SVG::Graph::TimeSeries.new({ graph = SVG::Graph::TimeSeries.new({
:area_fill => true,
:height => 300, :height => 300,
:min_y_value => 0, :min_y_value => 0,
:no_css => true, :no_css => true,
@ -26,26 +27,40 @@ class GraphsController < ApplicationController
:show_data_values => false, :show_data_values => false,
:stagger_x_labels => true, :stagger_x_labels => true,
:style_sheet => "/plugin_assets/redmine_graphs/stylesheets/issue_growth.css", :style_sheet => "/plugin_assets/redmine_graphs/stylesheets/issue_growth.css",
:timescale_divisions => "1 weeks", :timescale_divisions => "1 months",
:width => 800, :width => 720,
:x_label_format => "%b %d" :x_label_format => "%b %y"
}) })
# Group issues # Get the top visible projects by issue count
issues_by_project = @issues.group_by {|issue| issue.project } sql = "SELECT project_id, COUNT(*) issue_count"
projects_by_size = issues_by_project.collect { |project, issues| [project, issues.size] }.sort { |a,b| b[1]<=>a[1] }[0..5] sql << " FROM issues"
sql << " LEFT JOIN #{Project.table_name} ON #{Issue.table_name}.project_id = #{Project.table_name}.id"
sql << " WHERE (%s)" % Project.allowed_to_condition(User.current, :view_issues)
sql << " GROUP BY project_id"
sql << " ORDER BY issue_count DESC"
sql << " LIMIT 6"
top_projects = ActiveRecord::Base.connection.select_all(sql).collect { |p| p["project_id"] }
# Generate the created_on line # Get the issues created per project, per day
projects_by_size.each do |project, size| sql = "SELECT project_id, date(#{Issue.table_name}.created_on) as date, COUNT(*) as issue_count"
issues_by_created_on = issues_by_project[project].group_by {|issue| issue.created_on.to_date }.sort sql << " FROM #{Issue.table_name}"
sql << " WHERE project_id IN (%s)" % top_projects.compact.join(',')
sql << " AND (project_id = #{@project.id})" unless @project.nil?
sql << " GROUP BY project_id, date"
issue_counts = ActiveRecord::Base.connection.select_all(sql).group_by { |c| c["project_id"] }
# Generate the created_on lines
top_projects.each do |project_id, total_count|
counts = issue_counts[project_id].sort { |a,b| a["date"]<=>b["date"] }
created_count = 0 created_count = 0
created_on_line = Hash.new created_on_line = Hash.new
created_on_line[(issues_by_created_on.first[0]-1).to_s] = 0 created_on_line[(Date.parse(counts.first["date"])-1).to_s] = 0
issues_by_created_on.each { |created_on, issues| created_count += issues.size; created_on_line[created_on.to_s] = created_count } counts.each { |count| created_count += count["issue_count"].to_i; created_on_line[count["date"]] = created_count }
created_on_line[Date.today.to_s] = created_count created_on_line[Date.today.to_s] = created_count
graph.add_data({ graph.add_data({
:data => created_on_line.sort.flatten, :data => created_on_line.sort.flatten,
:title => project.name :title => Project.find(project_id).to_s
}) })
end end
@ -71,12 +86,12 @@ class GraphsController < ApplicationController
:show_x_guidelines => true, :show_x_guidelines => true,
:scale_x_integers => true, :scale_x_integers => true,
:scale_y_integers => true, :scale_y_integers => true,
:show_data_points => true, :show_data_points => false,
:show_data_values => false, :show_data_values => false,
:stagger_x_labels => true, :stagger_x_labels => true,
:style_sheet => "/plugin_assets/redmine_graphs/stylesheets/issue_age.css", :style_sheet => "/plugin_assets/redmine_graphs/stylesheets/issue_age.css",
:timescale_divisions => "1 weeks", :timescale_divisions => "1 weeks",
:width => 800, :width => 720,
:x_label_format => "%b %d" :x_label_format => "%b %d"
}) })
@ -176,19 +191,16 @@ class GraphsController < ApplicationController
private private
def find_open_issues def find_open_issues
@project = Project.find(params[:project_id]) unless params[:project_id].blank? find_optional_project
deny_access unless User.current.allowed_to?(:view_issues, @project, :global => true)
@issues = Issue.visible.find(:all, :include => [:status], :conditions => ["#{IssueStatus.table_name}.is_closed=?", false]) if @project.nil? @issues = Issue.visible.find(:all, :include => [:status], :conditions => ["#{IssueStatus.table_name}.is_closed=?", false]) if @project.nil?
@issues = @project.issues.collect { |issue| issue unless issue.closed? }.compact unless @project.nil? @issues = @project.issues.collect { |issue| issue unless issue.closed? }.compact unless @project.nil?
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end
def find_all_issues def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank? @project = Project.find(params[:project_id]) unless params[:project_id].blank?
deny_access unless User.current.allowed_to?(:view_issues, @project, :global => true) if @project.nil? deny_access unless User.current.allowed_to?(:view_issues, @project, :global => true)
@issues = Issue.visible.find(:all, :include => [:project])
@issues = @project.issues unless @project.nil?
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
render_404 render_404
end end

View File

@ -1,7 +1,5 @@
.fill1 { fill: #666 !important; } .line1 { stroke: #666666 !important; } .fill1 { fill: #666666 !important; } .key1 { fill: #666666 !important; }
.line1 { stroke: #666 !important; } .line2 { stroke: #507AAA !important; } .fill2 { fill: #BACCE0 !important; } .key2 { fill: #507AAA !important; }
.dataPoint1, .key1 { fill: #666 !important; }
.fill2 { fill: #bacce0 !important; fill-opacity: 0.8 !important; } .fill1, .fill2 { fill-opacity: 0.6 !important; }
.line2 { stroke: #507AAA !important; } .line1, .line2 { stroke-width: 2px !important; }
.dataPoint2, .key2 { fill: #507AAA !important; }

View File

@ -1,6 +1,9 @@
.line1 { stroke: #C00 !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key1 { fill: #C00 !important; } .line1 { stroke: #C00 !important; } .fill1 { fill: #C00 !important; } .key1 { fill: #C00 !important; }
.line2 { stroke: #CC0 !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key2 { fill: #CC0 !important; } .line2 { stroke: #CC0 !important; } .fill2 { fill: #CC0 !important; } .key2 { fill: #CC0 !important; }
.line3 { stroke: #0C0 !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key3 { fill: #0C0 !important; } .line3 { stroke: #0C0 !important; } .fill3 { fill: #0C0 !important; } .key3 { fill: #0C0 !important; }
.line4 { stroke: #0CC !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key4 { fill: #0CC !important; } .line4 { stroke: #0CC !important; } .fill4 { fill: #0CC !important; } .key4 { fill: #0CC !important; }
.line5 { stroke: #00C !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key5 { fill: #00C !important; } .line5 { stroke: #00C !important; } .fill5 { fill: #00C !important; } .key5 { fill: #00C !important; }
.line6 { stroke: #C0C !important; stroke-width: 4px !important; stroke-opacity: 0.8 !important; } .key6 { fill: #C0C !important; } .line6 { stroke: #C0C !important; } .fill6 { fill: #C0C !important; } .key6 { fill: #C0C !important; }
.fill1, .fill2, .fill3, .fill4, .fill5, .fill6 { fill-opacity: 0.6 !important; }
.line1, .line2, .line3, .line4, .line5, .line6 { stroke-width: 2px !important; }

View File

@ -1,12 +1,11 @@
#target_version_graph { clear: both; } #target_version_graph { clear: both; }
.fill1 { fill: #666 !important; } .line1 { stroke: #666666 !important; } .fill1 { fill: #666666 !important; } .key1 { fill: #666666 !important; }
.line1 { stroke: #666 !important; } .line2 { stroke: #009900 !important; } .fill2 { fill: #BAE0BA !important; } .key2 { fill: #009900 !important; }
.key1, .dataPoint1 { fill: #666 !important; }
.fill2 { fill: #BAE0BA !important; fill-opacity: 0.8 !important; } .fill1, .fill2 { fill-opacity: 0.6 !important; }
.line2 { stroke: #090 !important; } .line1, .line2 { stroke-width: 2px !important; }
.dataPoint2, .key2 { fill: #090 !important; }
.key3 { fill: #95964e !important; } .dataPoint1, .dataPoint2 { display: none !important; }
.dataPoint3 { stroke: #95964e !important; stroke-width: 10px !important; } .dataPoint3 { stroke: #AA5050 !important; stroke-width: 10px !important; }
.key3 { fill: #AA5050 !important; }