From 94631e4c6ac157d0c9ada18e6000ab859613f112 Mon Sep 17 00:00:00 2001 From: Simon Tomlinson <stomlinson@gitlab.com> Date: Fri, 4 Oct 2024 21:17:56 +0000 Subject: [PATCH] Memoize table information in schema_spec.rb This improves the runtime of the test dramatically. --- spec/db/schema_spec.rb | 15 ++++--- spec/support/helpers/dns_helpers.rb | 17 ++++++-- spec/support/stack_prof.rb | 68 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 spec/support/stack_prof.rb diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 4b3de618e555..3ec0b7ef8ce0 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -3,7 +3,12 @@ require 'spec_helper' require Rails.root.join('ee', 'spec', 'db', 'schema_support') if Gitlab.ee? -RSpec.describe 'Database schema', feature_category: :database do +RSpec.describe 'Database schema', + # These skip a bit of unnecessary setup for each spec invocation, + # and there are thousands of specs in this file. In total, this improves runtime by roughly 30% + :do_not_mock_admin_mode_setting, :do_not_stub_snowplow_by_default, + stackprof: { interval: 101000 }, + feature_category: :database do prepend_mod_with('DB::SchemaSupport') let(:tables) { connection.tables } @@ -396,7 +401,7 @@ end context 'existence of Postgres schemas' do - def get_schemas + let_it_be(:schemas) do sql = <<~SQL SELECT schema_name FROM information_schema.schemata @@ -411,17 +416,17 @@ def get_schemas end it 'we have a public schema' do - expect(get_schemas).to include('public') + expect(schemas).to include('public') end Gitlab::Database::EXTRA_SCHEMAS.each do |schema| it "we have a '#{schema}' schema'" do - expect(get_schemas).to include(schema.to_s) + expect(schemas).to include(schema.to_s) end end it 'we do not have unexpected schemas' do - expect(get_schemas.size).to eq(Gitlab::Database::EXTRA_SCHEMAS.size + 1) + expect(schemas.size).to eq(Gitlab::Database::EXTRA_SCHEMAS.size + 1) end end diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb index 27310c7e04c9..d96571f392f5 100644 --- a/spec/support/helpers/dns_helpers.rb +++ b/spec/support/helpers/dns_helpers.rb @@ -2,6 +2,8 @@ module DnsHelpers include ViteHelper + # strong_memoize is used in a class method, so we extend it instead of including it. + extend Gitlab::Utils::StrongMemoize def block_dns! stub_all_dns! @@ -57,14 +59,21 @@ def db_hosts ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:host).compact.uniq end + def self.redis_hosts + # This needs to be a class method so that the memoization sticks across different specs. + # Without memoization, this adds a considerable amount of time to each spec execution. + strong_memoize(:redis_hosts) do + Gitlab::Redis::ALL_CLASSES.flat_map do |redis_instance| + redis_instance.params[:host] || redis_instance.params[:nodes]&.map { |n| n[:host] } + end.uniq.compact + end + end + def permit_redis! # https://github.com/redis-rb/redis-client/blob/v0.11.2/lib/redis_client/ruby_connection.rb#L51 uses Socket.tcp that # calls Addrinfo.getaddrinfo internally. - hosts = Gitlab::Redis::ALL_CLASSES.flat_map do |redis_instance| - redis_instance.params[:host] || redis_instance.params[:nodes]&.map { |n| n[:host] } - end.uniq.compact - hosts.each do |host| + DnsHelpers.redis_hosts.each do |host| allow(Addrinfo).to receive(:getaddrinfo).with(host, anything, nil, :STREAM, anything, anything, any_args).and_call_original end end diff --git a/spec/support/stack_prof.rb b/spec/support/stack_prof.rb new file mode 100644 index 000000000000..1f0e5cfaebc9 --- /dev/null +++ b/spec/support/stack_prof.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'stackprof' + +# Applies stackprof instrumentation to a given rspec context +# Use as context "your context", :stackprof do +# or as context "your context", stackprof: { mode: wall, interval: 100000 } do +# to change arguments to stackprof. +# +# Results will be gzipped and placed in tmp/ if running locally, or rspec/ if running in CI, and can be viewed +# with any json-compatible flamegraph viewer, such as speedscope. + +module Support + module StackProf + def self.start(example) + puts "Starting stackprof" + raise "Cannot nest stackprof calls!" if ::StackProf.running? + + ::StackProf.start(**stackprof_args_for(example)) + end + + def self.finish(example) + raise "Stackprof was not running!" unless ::StackProf.running? + + ::StackProf.stop + puts 'finishing stackprof' + location = example.class.metadata[:location] + # Turn a file path like ./spec/path/to/spec.rb:123 into spec_path_to_spec_123 + location_sanitized = location.gsub('./', '').tr('/', '_').gsub('.rb', '').tr(':', '_') + + out_filepath = if ENV['CI'] + "rspec/stackprof_#{location_sanitized}-#{ENV['CI_JOB_NAME_SLUG']}.json.gz" + else + "tmp/stackprof_#{location_sanitized}.json.gz" + end + + start_time = ::Gitlab::Metrics::System.monotonic_time + Zlib::GzipWriter.open(out_filepath) do |gz| + gz.puts Gitlab::Json.generate(::StackProf.results) + end + end_time = ::Gitlab::Metrics::System.monotonic_time + puts "Wrote stackprof dump to #{out_filepath} in #{(end_time - start_time).round(2)}s" + end + + def self.stackprof_args_for(example) + caller_config = example.class.metadata[:stackprof] + default = { + mode: :wall, + interval: 10100, # in us, 99hz + raw: true # Needed for flamegraphs + } + # If called as `:stackprof`, the value will be a literal `true` + return default unless caller_config.is_a?(Hash) + + default.merge(caller_config) + end + end +end + +RSpec.configure do |config| + config.before(:all, :stackprof) do |example| + Support::StackProf.start(example) + end + + config.after(:all, :stackprof) do |example| + Support::StackProf.finish(example) + end +end -- GitLab