Project

General

Profile

Patch #43946 » benchmark_group_user_added.rb

[Agileware]Kota Uchino, 2026-04-10 13:40

 
1
# frozen_string_literal: true
2

    
3
# Benchmark script for Group#user_added performance.
4
#
5
# Measures SQL query count and elapsed time when adding a user to a group
6
# that has memberships in multiple projects with inheriting subprojects.
7
#
8
# This script has no dependency on its file location.
9
# It can be run from anywhere:
10
#
11
#   bin/rails runner /path/to/benchmark_group_user_added.rb
12
#
13
# Configuration:
14
#   Adjust NUM_PROJECTS, NUM_ROLES, SUBPROJECT_DEPTH below.
15

    
16
require 'benchmark'
17

    
18
NUM_PROJECTS     = 50
19
NUM_ROLES        = 3
20
SUBPROJECT_DEPTH = 5
21

    
22
puts "=== Group#user_added benchmark ==="
23
puts "Projects: #{NUM_PROJECTS}, Roles: #{NUM_ROLES}, Subproject depth: #{SUBPROJECT_DEPTH}"
24
puts
25

    
26
seq = SecureRandom.hex(4)
27

    
28
# -- Setup --
29
group = Group.create!(:lastname => "BenchGroup_#{seq}")
30

    
31
user = User.new(
32
  :login => "benchuser_#{seq}",
33
  :firstname => 'Bench',
34
  :lastname => 'User',
35
  :mail => "benchuser_#{seq}@example.com"
36
)
37
user.password = 'password'
38
user.save!
39

    
40
roles = Role.where(:builtin => 0).limit(NUM_ROLES).to_a
41
raise "Need at least #{NUM_ROLES} non-builtin roles" if roles.size < NUM_ROLES
42

    
43
role_ids = roles.map(&:id)
44

    
45
projects = NUM_PROJECTS.times.map do |i|
46
  root = Project.create!(
47
    :name => "bench_#{seq}_#{i}",
48
    :identifier => "bench-#{seq}-#{i}",
49
    :is_public => false
50
  )
51
  Member.create!(:principal => group, :project => root, :role_ids => role_ids)
52

    
53
  parent = root
54
  SUBPROJECT_DEPTH.times do |d|
55
    child = Project.new(
56
      :name => "bench_#{seq}_#{i}_d#{d}",
57
      :identifier => "bench-#{seq}-#{i}-d#{d}",
58
      :is_public => false,
59
      :inherit_members => true
60
    )
61
    child.parent = parent
62
    child.save!
63
    parent = child
64
  end
65
  root
66
end
67

    
68
total_projects = NUM_PROJECTS * (1 + SUBPROJECT_DEPTH)
69
puts "Setup complete: #{NUM_PROJECTS} root projects, each with #{SUBPROJECT_DEPTH} levels of inheriting subprojects"
70
puts "Total projects: #{total_projects}"
71
puts
72

    
73
# -- Count queries --
74
query_count = 0
75
query_details = Hash.new(0)
76

    
77
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |*, payload|
78
  unless payload[:name] == 'SCHEMA' || payload[:sql].start_with?('PRAGMA')
79
    query_count += 1
80
    op = payload[:sql].split(' ', 2).first.upcase
81
    query_details[op] += 1
82
  end
83
end
84

    
85
elapsed = Benchmark.realtime do
86
  group.users << user
87
end
88

    
89
ActiveSupport::Notifications.unsubscribe(subscriber)
90

    
91
puts "--- Results ---"
92
puts "Time:    #{(elapsed * 1000).round(1)} ms"
93
puts "Queries: #{query_count}"
94
puts
95
puts "Query breakdown:"
96
query_details.sort_by { |_, v| -v }.each do |op, count|
97
  puts "  #{op}: #{count}"
98
end
99
puts
100

    
101
# -- Verify correctness --
102
errors = []
103
projects.each do |root|
104
  root.reload
105
  Project.where("lft >= ? AND rgt <= ?", root.lft, root.rgt).each do |proj|
106
    unless user.member_of?(proj)
107
      errors << "User NOT a member of project #{proj.id} (#{proj.identifier})"
108
    end
109
  end
110
end
111

    
112
if errors.empty?
113
  puts "Correctness: OK (user is a member of all #{total_projects} projects)"
114
else
115
  puts "Correctness: FAILED"
116
  errors.first(10).each { |e| puts "  #{e}" }
117
  puts "  ... (#{errors.size} total)" if errors.size > 10
118
end
119

    
120
# -- Cleanup --
121
projects.each do |root|
122
  root.reload
123
  Project.where("lft >= ? AND rgt <= ?", root.lft, root.rgt).order(lft: :desc).each(&:destroy)
124
end
125
group.destroy
126
user.destroy
127

    
128
puts
129
puts "Cleanup complete."
(2-2/2)