|
1 |
# frozen_string_literal: true
|
|
2 |
|
|
3 |
# Benchmark of the journal queries affected by the Journal/Issue decoupling (#44274).
|
|
4 |
#
|
|
5 |
# Runs with the same data on both variants, eg.:
|
|
6 |
#
|
|
7 |
# git checkout 7.0-stable && bin/rails r extra/benchmarks/bench_journals.rb
|
|
8 |
# git checkout journal-decoupling && bin/rails r extra/benchmarks/bench_journals.rb
|
|
9 |
# CLEANUP=1 bin/rails r extra/benchmarks/bench_journals.rb
|
|
10 |
#
|
|
11 |
# The benchmark data is created on the first run and kept (and analyzed),
|
|
12 |
# so that both variants run against the same data and statistics.
|
|
13 |
# Use CLEANUP=1 to remove it. Do not run it on a production database.
|
|
14 |
#
|
|
15 |
# Environment variables:
|
|
16 |
# * PROJECTS (default 20), ISSUES (default 20000), JOURNALS_PER_ISSUE (default 10):
|
|
17 |
# size of the data, used when the data is created
|
|
18 |
# * ITER (default 10): iterations of each benchmark, the median is reported
|
|
19 |
# * ONLY: comma separated names of the benchmarks to run (default all)
|
|
20 |
# * EXPLAIN=1: prints the query plans of the visible journals query
|
|
21 |
|
|
22 |
require 'benchmark'
|
|
23 |
|
|
24 |
# Do not measure the SQL logging (eg. verbose query logs in development)
|
|
25 |
ActiveRecord::Base.logger = nil
|
|
26 |
|
|
27 |
PREFIX = 'bench-journals'
|
|
28 |
projects_count = (ENV['PROJECTS'] || '20').to_i
|
|
29 |
issues_count = (ENV['ISSUES'] || '20000').to_i
|
|
30 |
journals_per_issue = (ENV['JOURNALS_PER_ISSUE'] || '10').to_i
|
|
31 |
iterations = (ENV['ITER'] || '10').to_i
|
|
32 |
|
|
33 |
def bench_projects
|
|
34 |
Project.where("identifier LIKE ?", "#{PREFIX}-%").order(:id)
|
|
35 |
end
|
|
36 |
|
|
37 |
def bench_users
|
|
38 |
User.where("login LIKE ?", "#{PREFIX.tr('-', '_')}_%").order(:id)
|
|
39 |
end
|
|
40 |
|
|
41 |
def cleanup
|
|
42 |
projects = bench_projects.to_a
|
|
43 |
project_ids = projects.map(&:id)
|
|
44 |
issue_ids = Issue.where(:project_id => project_ids).pluck(:id)
|
|
45 |
ActiveRecord::Base.transaction do
|
|
46 |
journal_ids = Journal.where(:journalized_type => 'Issue', :journalized_id => issue_ids).pluck(:id)
|
|
47 |
journal_ids.each_slice(10_000) do |ids|
|
|
48 |
JournalDetail.where(:journal_id => ids).delete_all
|
|
49 |
Journal.where(:id => ids).delete_all
|
|
50 |
end
|
|
51 |
Issue.where(:id => issue_ids).delete_all
|
|
52 |
member_ids = Member.where(:project_id => project_ids).pluck(:id)
|
|
53 |
MemberRole.where(:member_id => member_ids).delete_all
|
|
54 |
Member.where(:id => member_ids).delete_all
|
|
55 |
EnabledModule.where(:project_id => project_ids).delete_all
|
|
56 |
Project.connection.execute("DELETE FROM projects_trackers WHERE project_id IN (#{project_ids.join(',')})") if project_ids.any?
|
|
57 |
Project.where(:id => project_ids).delete_all
|
|
58 |
bench_users.each(&:destroy)
|
|
59 |
Role.where("name LIKE ?", "#{PREFIX} %").delete_all
|
|
60 |
end
|
|
61 |
Project.rebuild_tree! if Project.respond_to?(:rebuild_tree!)
|
|
62 |
puts "Benchmark data removed (#{issue_ids.size} issues)."
|
|
63 |
end
|
|
64 |
|
|
65 |
def create_data(projects_count, issues_count, journals_per_issue)
|
|
66 |
now = Time.now.utc
|
|
67 |
rand = Random.new(44274)
|
|
68 |
tracker = Tracker.order(:position).first || raise('No tracker found, load the default data')
|
|
69 |
statuses = IssueStatus.order(:position).to_a
|
|
70 |
priority = IssuePriority.default || IssuePriority.first || raise('No issue priority found')
|
|
71 |
admin = User.where(:admin => true).active.first || raise('No active admin found')
|
|
72 |
|
|
73 |
ActiveRecord::Base.transaction do
|
|
74 |
manager = Role.create!(:name => "#{PREFIX} manager", :permissions => [:view_issues, :view_private_notes])
|
|
75 |
reporter = Role.create!(:name => "#{PREFIX} reporter", :permissions => [:view_issues])
|
|
76 |
member = User.new(:firstname => 'Bench', :lastname => 'Member', :mail => "#{PREFIX}-member@example.net")
|
|
77 |
member.login = "#{PREFIX.tr('-', '_')}_member"
|
|
78 |
member.save!
|
|
79 |
|
|
80 |
projects = Array.new(projects_count) do |i|
|
|
81 |
project = Project.new(:name => "Bench journals #{i}", :identifier => "#{PREFIX}-#{i}",
|
|
82 |
:is_public => i % 4 != 0)
|
|
83 |
project.enabled_module_names = ['issue_tracking']
|
|
84 |
project.trackers = [tracker]
|
|
85 |
project.save!
|
|
86 |
# The member is manager of a quarter of the projects, reporter of another quarter
|
|
87 |
if i % 4 == 1
|
|
88 |
Member.create!(:project => project, :principal => member, :roles => [manager])
|
|
89 |
elsif i % 4 == 2
|
|
90 |
Member.create!(:project => project, :principal => member, :roles => [reporter])
|
|
91 |
end
|
|
92 |
project
|
|
93 |
end
|
|
94 |
print "Projects: #{projects.size}\nInserting issues"
|
|
95 |
|
|
96 |
issues_count.times.each_slice(5_000) do |slice|
|
|
97 |
rows = slice.map do |i|
|
|
98 |
created_on = now - rand.rand(365 * 24 * 3600)
|
|
99 |
{
|
|
100 |
:project_id => projects[i % projects.size].id, :tracker_id => tracker.id,
|
|
101 |
:subject => "Bench issue #{i}", :author_id => admin.id,
|
|
102 |
:status_id => statuses[i % statuses.size].id, :priority_id => priority.id,
|
|
103 |
:is_private => (i % 10 == 0), :lft => 1, :rgt => 2, :lock_version => 0, :done_ratio => 0,
|
|
104 |
:created_on => created_on, :updated_on => created_on
|
|
105 |
}
|
|
106 |
end
|
|
107 |
Issue.insert_all(rows)
|
|
108 |
print '.'
|
|
109 |
end
|
|
110 |
Issue.where(:project_id => projects.map(&:id)).update_all("root_id = id")
|
|
111 |
puts
|
|
112 |
|
|
113 |
print "Inserting journals"
|
|
114 |
users = [admin.id, member.id]
|
|
115 |
Issue.where(:project_id => projects.map(&:id)).in_batches(:of => 1_000) do |batch|
|
|
116 |
journal_rows = []
|
|
117 |
batch.pluck(:id, :created_on).each do |issue_id, issue_created_on|
|
|
118 |
journals_per_issue.times do |j|
|
|
119 |
kind = rand.rand(10)
|
|
120 |
journal_rows << {
|
|
121 |
:journalized_type => 'Issue', :journalized_id => issue_id, :user_id => users[j % 2],
|
|
122 |
:notes => (kind < 6 ? "Note #{j} on issue #{issue_id}" : ''),
|
|
123 |
:private_notes => (kind == 0),
|
|
124 |
:created_on => issue_created_on + ((j + 1) * 3600), :updated_on => issue_created_on + ((j + 1) * 3600)
|
|
125 |
}
|
|
126 |
end
|
|
127 |
end
|
|
128 |
Journal.insert_all(journal_rows)
|
|
129 |
# :returning is not supported by all databases
|
|
130 |
ids = Journal.where(:journalized_type => 'Issue', :journalized_id => batch.select(:id)).order(:id).pluck(:id)
|
|
131 |
# A third of the journals without notes is a status change
|
|
132 |
detail_rows = ids.each_with_index.filter_map do |id, index|
|
|
133 |
next unless index % 3 == 0
|
|
134 |
|
|
135 |
{:journal_id => id, :property => 'attr', :prop_key => 'status_id',
|
|
136 |
:old_value => statuses.first.id.to_s, :value => statuses.last.id.to_s}
|
|
137 |
end
|
|
138 |
JournalDetail.insert_all(detail_rows) if detail_rows.any?
|
|
139 |
print '.'
|
|
140 |
end
|
|
141 |
puts
|
|
142 |
end
|
|
143 |
end
|
|
144 |
|
|
145 |
if ENV['CLEANUP']
|
|
146 |
cleanup
|
|
147 |
exit
|
|
148 |
end
|
|
149 |
|
|
150 |
if bench_projects.empty?
|
|
151 |
create_data(projects_count, issues_count, journals_per_issue)
|
|
152 |
conn = ActiveRecord::Base.connection
|
|
153 |
%w(issues journals journal_details projects members member_roles).each do |table|
|
|
154 |
case conn.adapter_name
|
|
155 |
when /postgres/i then conn.execute("ANALYZE #{table}")
|
|
156 |
when /mysql|trilogy/i then conn.execute("ANALYZE TABLE #{table}")
|
|
157 |
end
|
|
158 |
end
|
|
159 |
end
|
|
160 |
|
|
161 |
projects = bench_projects.to_a
|
|
162 |
project_ids = projects.map(&:id)
|
|
163 |
member = bench_users.first
|
|
164 |
admin = User.where(:admin => true).active.first
|
|
165 |
anonymous = User.anonymous
|
|
166 |
bench_issue_ids = Issue.where(:project_id => project_ids).order(:id).pluck(:id)
|
|
167 |
bench_journals = Journal.where(:journalized_type => 'Issue', :journalized_id => bench_issue_ids)
|
|
168 |
latest = bench_journals.maximum(:created_on)
|
|
169 |
sample_journal_id = bench_journals.where(:private_notes => false).order(:id).offset(bench_journals.count / 2).pick(:id)
|
|
170 |
|
|
171 |
variant = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
172 |
variant = Journal.respond_to?(:joins_journalized) ? 'journal-decoupling' : 'legacy' if variant.empty?
|
|
173 |
puts "variant=#{variant} ruby=#{RUBY_VERSION} rails=#{Rails.version} db=#{ActiveRecord::Base.connection.adapter_name} " \
|
|
174 |
"projects=#{projects.size} issues=#{bench_issue_ids.size} journals=#{bench_journals.count} iter=#{iterations}"
|
|
175 |
|
|
176 |
if ENV['EXPLAIN']
|
|
177 |
[admin, member].each do |user|
|
|
178 |
puts "\nEXPLAIN Journal.visible(#{user.login}).order(id DESC).limit(25):"
|
|
179 |
puts Journal.visible(user).order(:id => :desc).limit(25).explain.inspect
|
|
180 |
end
|
|
181 |
end
|
|
182 |
|
|
183 |
benchmarks = {
|
|
184 |
'visible_count' => lambda {|user| Journal.visible(user).count},
|
|
185 |
'visible_latest_25' => lambda {|user| Journal.visible(user).reorder(:id => :desc).limit(25).ids.sum},
|
|
186 |
'visible_find' => lambda {|user| Journal.visible(user).where(:id => sample_journal_id).ids.sum},
|
|
187 |
'activity_project' => lambda do |user|
|
|
188 |
Redmine::Activity::Fetcher.new(user, :project => projects[1], :with_subprojects => true).
|
|
189 |
events(latest - 30.days, latest).size
|
|
190 |
end,
|
|
191 |
'activity_all_30d' => lambda {|user| Redmine::Activity::Fetcher.new(user).events(latest - 30.days, latest).size},
|
|
192 |
'activity_last_by_project' => lambda {|user| Redmine::Activity::Fetcher.new(user).events(nil, nil, :last_by_project => true).size},
|
|
193 |
'issue_query_journals' => lambda do |user|
|
|
194 |
User.current = user
|
|
195 |
IssueQuery.new(:name => '_').journals(:order => "#{Journal.table_name}.created_on DESC", :limit => 25).sum(&:id)
|
|
196 |
end,
|
|
197 |
'issue_last_notes_25' => lambda do |user|
|
|
198 |
issues = Issue.where(:id => bench_issue_ids.last(25)).to_a
|
|
199 |
Issue.load_visible_last_notes(issues, user)
|
|
200 |
Issue.load_visible_last_updated_by(issues, user)
|
|
201 |
issues.sum {|issue| issue.last_notes.to_s.size}
|
|
202 |
end
|
|
203 |
}
|
|
204 |
|
|
205 |
puts
|
|
206 |
puts format('%-26s %-10s %10s %10s %10s %14s', 'benchmark', 'user', 'median_ms', 'min_ms', 'max_ms', 'result')
|
|
207 |
only = ENV['ONLY'].to_s.split(',').map(&:strip).reject(&:empty?)
|
|
208 |
benchmarks.slice!(*only) if only.any?
|
|
209 |
benchmarks.each do |name, block|
|
|
210 |
{'admin' => admin, 'member' => member, 'anonymous' => anonymous}.each do |label, user|
|
|
211 |
result = block.call(user) # warm up
|
|
212 |
times = ApplicationRecord.uncached do
|
|
213 |
Array.new(iterations) { Benchmark.realtime { block.call(user) } * 1000 }
|
|
214 |
end.sort
|
|
215 |
median = times.size.odd? ? times[times.size / 2] : (times[(times.size / 2) - 1] + times[times.size / 2]) / 2
|
|
216 |
puts format('%-26s %-10s %10.2f %10.2f %10.2f %14s', name, label, median, times.first, times.last, result)
|
|
217 |
end
|
|
218 |
end
|
|
219 |
User.current = nil
|