Browse Source

Maintenance: Create basic performance tests in CI

(cherry picked from commit fa925a4a799e2316357401f073bd98dcd0c1b7ce)

c004f1f3 Maintenance: Create basic performance tests in CI
58df87a4 Run in push pipelines for now
1b903920 Add missing service
afa9440d Typo
4b5b70bc Fill database
fd12f3cb Change invocation
150dcfd1 Test from ME
8fb81835 Refactor
9bbbe487 Add test for assets:precompile
c69bfb99 Clear activerecord cache
dbd1614e Improved output
2e615e93 Add branch switching
a94fa15b Fix gitlab syntax
24beed7a Rails 6.1 compat
789989f5 Save to file for later comparison
68e18cf7 Fix compatibilty to Rails 6.1
378e84d0 Drop file handling code again, and switch to versin based limits instead
21d7234e Improved pipeline configuration and import speed
a1b0abdc Improve output and expectations
3b0b2784 Simplified code, add performance fixes.
99a26b76 Decrease the limit
52768dc9 Switch test stage
06dd026f Improve comment
796f45ca Revert branch trigger change for packager.io.

Co-authored-by: Martin Gruner <mg@zammad.com>
Martin Gruner 4 months ago
parent
commit
84133a875f

+ 1 - 3
.gitlab/ci/__includes__/scripts.yml

@@ -2,10 +2,8 @@
   source_rvm:
     - source /etc/profile.d/rvm.sh # ensure RVM is loaded
   bundle_install:
-    - echo -e "\\e[0Ksection_start:`date +%s`:bundle_install[collapsed=true]\\r\\e[0Kbundle install"
     - bundle config set --local deployment 'true'
-    - bundle install -j $(nproc)
-    - echo -e "\\e[0Ksection_end:`date +%s`:bundle_install\\r\\e[0K"
+    - bundle install -j $(nproc) > /dev/null
   pnpm_init: &pnpm_init
     - pnpm config set store-dir ${CI_PROJECT_DIR}/tmp/cache/pnpm-store
   pnpm_install: &pnpm_install

+ 1 - 1
.gitlab/ci/test/packager.io.yml

@@ -8,7 +8,7 @@
   cache: []
   before_script: []
   script:
-    - .gitlab/packager.io/run-scenarios.sh
+    - .gitlab/packager.io/run_scenarios.sh
   after_script:
     - .gitlab/packager.io/cleanup.sh
 

+ 40 - 0
.gitlab/ci/test/performance.yml

@@ -0,0 +1,40 @@
+performance:
+  stage: test
+  rules:
+  - if: $CI_PIPELINE_SOURCE == "schedule" || $CI_PIPELINE_SOURCE == "web"
+  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
+    changes:
+      - .gitlab/performance/**/*
+  # Make sure this gets executed exclusively on a runner.
+  tags: ['no_concurrency']
+  services:
+    - !reference [.services, postgresql]
+    - !reference [.services, redis]
+  cache: []
+  before_script:
+    # Copy performance checking code to tmp/ so that it survives branch switching
+    - cp -rv .gitlab/performance tmp/
+
+    # First, checkout stable and set it up.
+    - git fetch --unshallow >/dev/null 2>&1
+    - git checkout stable-6.0
+    - !reference [.scripts, source_rvm]
+    - rvm use 3.1.3
+    - !reference [.scripts, bundle_install]
+    - !reference [.scripts, configure_environment]
+    - !reference [.scripts, zammad_db_init]
+    - yarn install --silent # Required for assets:precompile test
+  script:
+    # Run performance tests for stable.
+    - bundle exec rails r tmp/performance/run_tests.rb
+    # Then, switch to the current commit, migrate to it and run a few selected tests.
+    - git checkout $CI_COMMIT_SHA
+    - rvm use 3.2.4
+    - !reference [.scripts, bundle_install]
+    # Force redis usage, even if it was disabled by the initial configure_environment script of stable.
+    - export REDIS_URL=redis://redis
+    - bundle exec rails db:migrate > /dev/null
+    - rm -rf node_modules && pnpm install > /dev/null # Required for assets:precompile test
+    # Run performance tests for current branch.
+    - bundle exec rails r .gitlab/performance/run_tests.rb
+  after_script: []

+ 0 - 0
.gitlab/packager.io/run-scenarios.sh → .gitlab/packager.io/run_scenarios.sh


+ 110 - 0
.gitlab/performance/run_tests.rb

@@ -0,0 +1,110 @@
+#!/usr/bin/env ruby
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails'
+
+def run
+  puts "# Zammad Performance Tests\n\n"
+  ensure_test_data_present
+  run_tests!
+end
+
+def run_tests!
+  puts 'Run test scenarios…'
+  [].tap do |results|
+    run_test_scenarios(results:)
+
+    if results.any? { |r| r[:failed] }
+      puts 'Tests failed.'
+      exit 1
+    end
+
+    puts 'All tests were successful.'
+  end
+end
+
+def run_test_scenarios(results:)
+  agent = User.with_permissions('ticket.agent').first
+
+  expect(title: 'assets:precompile', max_time: 120, max_sql_queries: nil, results:) do
+    system('RAILS_ENV=production bundle exec rails assets:precompile > /dev/null 2>&1', exception: true)
+  end
+
+  max_index_queries = version_oldstable? ? 70 : 35
+  expect(title: 'Ticket::Overviews.index', max_time: 1, max_sql_queries: max_index_queries, results:) do
+    Ticket::Overviews.index(agent)
+  end
+
+  expect(title: 'Ticket::Overviews.all', max_time: 0.05, max_sql_queries: 5, results:) do
+    Ticket::Overviews.all(current_user: agent)
+  end
+
+  max_overview_list_time    = version_oldstable? ? 15     : 20
+  max_overview_list_queries = version_oldstable? ? 16_000 : 25_000
+  expect(title: 'Sessions::Backend::TicketOverviewList#push', max_time: max_overview_list_time, max_sql_queries: max_overview_list_queries, results:) do
+    Sessions::Backend::TicketOverviewList.new(agent, {}).push
+  end
+end
+
+def version_oldstable?
+  Version.get.match?(%r{^6\.0\.})
+end
+
+def expect(title:, max_time:, max_sql_queries:, results:, &block)
+
+  ActiveRecord::Base.connection.query_cache.clear
+  Rails.cache.clear
+
+  sql_queries = 0
+  failed      = false
+  callback = ->(_name, _start, _finish, _id, payload) { sql_queries += 1 if !payload[:cached] }
+  time = Benchmark.measure do
+    ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)
+  end
+  puts "  #{title}: #{time.real}s (#{sql_queries} queries)"
+
+  if max_time && time.real > max_time
+    puts "    ERROR: took #{time.real}s, rather than expected maximum of #{max_time}s"
+    failed = true
+  end
+
+  if max_sql_queries && sql_queries > max_sql_queries
+    puts "    ERROR: caused #{sql_queries} SQL queries, rather than expected maximum of #{max_sql_queries}"
+    failed = true
+  end
+
+  results.push({ title:, max_time:, time: time.real, max_sql_queries:, sql_queries:, failed: })
+end
+
+def ensure_test_data_present
+  puts 'Ensuring test data with 15k tickets is present…'
+
+  return if Ticket.count >= 15_000
+
+  # Speed up the import
+  Setting.set('import_mode', true)
+
+  suppress_output do
+    FillDb.load(
+      agents:        100,
+      customers:     4000,
+      groups:        80,
+      organizations: 400,
+      overviews:     4,
+      tickets:       15_000,
+      nice:          0,
+    )
+  end
+
+  Setting.set('import_mode', false)
+end
+
+def suppress_output
+  original_stdout = $stdout.clone
+  $stdout.reopen(File.new('/dev/null', 'w'))
+  yield
+ensure
+  $stdout.reopen(original_stdout)
+end
+
+run