diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 85a977f..a6361ac 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,6 +33,7 @@ jobs: POSTGRES_USERNAME: postgres POSTGRES_PASSWORD: postgres SCHEMA: structure.sql + PERFORMANCE_TESTS_DISABLED: 1 steps: - name: Checkout repository uses: actions/checkout@v3 diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f522fae..6712f67 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -58,6 +58,7 @@ jobs: POSTGRES_PASSWORD: postgres BUNDLE_GEMFILE: ${{ matrix.gemfile }} SPEC_DISABLE_DROP_DATABASE: 1 + PERFORMANCE_TESTS_DISABLED: 1 SCHEMA: structure.sql steps: - name: Checkout repository diff --git a/README.md b/README.md index 07819c3..1f44b14 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ photo.destroy() Photo.profile.ordered.pluck(:label, :position) # => [["A", 0], ["B", 1], ["C", 2]] Photo.cover.ordered.pluck(:label, :position) # => [["D", 0], ["E", 1]] ``` -#### Default push front +#### Auto set ```ruby class Image < ActiveRecord::Base @@ -216,7 +216,7 @@ Image.create(label: "B") Image.ordered.pluck(:label, :position) # => [["A", 10], ["B", 11]] ``` -### Decremental sequence +#### Decremental sequence ```ruby class Image < ActiveRecord::Base diff --git a/spec/performance/index_spec.rb b/spec/performance/index_spec.rb new file mode 100644 index 0000000..e730c0d --- /dev/null +++ b/spec/performance/index_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +RSpec.describe "Performance with index" do + before(:all) do + attrs = (0...100_000).map do |position| + "('#{SecureRandom.alphanumeric(8)}', #{position})" + end + + BasicModel.connection.execute(<<-SQL) + INSERT INTO basic_models (name, position) + VALUES + #{attrs.join(', ')}; + SQL + end + + after(:all) { BasicModel.delete_all } + + describe "model without scope" do + context "on #update" do + subject { create(:basic_model).update!(position: position) } + + context "with position set as first value" do + let(:position) { 0 } + + it "takes less than 1.25s" do + expect(elapsed_time { subject }).to be < 1.25 + end + end + + context "with position set as middle value" do + let(:position) { 50_000 } + + it "takes less than 0.75s" do + expect(elapsed_time { subject }).to be < 0.75 + end + end + + context "with position set as last value" do + let(:position) { 99_999 } + + it "takes less than 0.15s" do + expect(elapsed_time { subject }).to be < 0.15 + end + end + end + + context "on #create" do + subject { create(:basic_model, **attrs) } + let(:attrs) { {} } + + context "without position specified" do + it "takes less than 0.07s" do + expect(elapsed_time { subject }).to be < 0.07 + end + end + + context "with postion set as 3/4 of total count" do + let(:attrs) { { position: 75_000 } } + + it "takes less than 0.35s" do + expect(elapsed_time { subject }).to be < 0.35 + end + end + + context "with position set as the middle value" do + let(:attrs) { { position: 50_000 } } + + it "takes less than 0.8s" do + expect(elapsed_time { subject }).to be < 0.8 + end + end + + context "with position set as first value" do + let(:attrs) { { position: 0 } } + + it "takes less than 1.5s" do + expect(elapsed_time { subject }).to be < 1.5 + end + end + end + + context "on #destroy" do + subject { record.destroy! } + let!(:record) { create(:basic_model, **attrs) } + + context "with position set as last value" do + let(:attrs) { { position: 100_000 } } + + it "takes less than 0.01s" do + expect(elapsed_time { subject }).to be < 0.01 + end + end + + context "with position set as middle value" do + let(:attrs) { { position: 50_000 } } + + it "takes less than 0.85s" do + expect(elapsed_time { subject }).to be < 0.85 + end + end + + context "with position set as first value" do + let(:attrs) { { position: 0 } } + + it "takes less than 1.5s" do + expect(elapsed_time { subject }).to be < 1.5 + end + end + end + end +end diff --git a/spec/performance/no_index_spec.rb b/spec/performance/no_index_spec.rb new file mode 100644 index 0000000..c7c777f --- /dev/null +++ b/spec/performance/no_index_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +RSpec.describe "Performance no index" do + before(:all) do + attrs = (0...100_000).map do |position| + "('#{SecureRandom.alphanumeric(8)}', #{position})" + end + + BasicModel.connection.execute(<<-SQL) + ALTER TABLE basic_models + DROP CONSTRAINT basic_models_position_key; + + INSERT INTO basic_models (name, position) + VALUES + #{attrs.join(', ')}; + SQL + end + + after(:all) do + BasicModel.delete_all + BasicModel.connection.execute(<<-SQL) + ALTER TABLE basic_models + ADD UNIQUE(position) DEFERRABLE INITIALLY DEFERRED; + SQL + end + + describe "model without scope" do + context "on #update" do + subject { create(:basic_model).update!(position: position) } + + context "with position set as first value" do + let(:position) { 0 } + + it "takes less than 0.9s" do + expect(elapsed_time { subject }).to be < 0.9 + end + end + + context "with position set as middle value" do + let(:position) { 50_000 } + + it "takes less than 0.5s" do + expect(elapsed_time { subject }).to be < 0.5 + end + end + + context "with position set as last value" do + let(:position) { 99_999 } + + it "takes less than 0.09s" do + expect(elapsed_time { subject }).to be < 0.09 + end + end + end + + context "on #create" do + subject { create(:basic_model, **attrs) } + let(:attrs) { {} } + + context "without position specified" do + it "takes less than 0.04s" do + expect(elapsed_time { subject }).to be < 0.04 + end + end + + context "with postion set as 3/4 of total count" do + let(:attrs) { { position: 75_000 } } + + it "takes less than 0.25s" do + expect(elapsed_time { subject }).to be < 0.25 + end + end + + context "with position set as the middle value" do + let(:attrs) { { position: 50_000 } } + + it "takes less than 0.5s" do + expect(elapsed_time { subject }).to be < 0.5 + end + end + + context "with position set as first value" do + let(:attrs) { { position: 0 } } + + it "takes less than 0.9s" do + expect(elapsed_time { subject }).to be < 0.9 + end + end + end + + context "on #destroy" do + subject { record.destroy! } + let!(:record) { create(:basic_model, **attrs) } + + context "with position set as last value" do + let(:attrs) { { position: 100_000 } } + + it "takes less than 0.025s" do + expect(elapsed_time { subject }).to be < 0.025 + end + end + + context "with position set as middle value" do + let(:attrs) { { position: 50_000 } } + + it "takes less than 0.6s" do + expect(elapsed_time { subject }).to be < 0.6 + end + end + + context "with position set as first value" do + let(:attrs) { { position: 0 } } + + it "takes less than 1.15s" do + expect(elapsed_time { subject }).to be < 1.15 + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f0b5112..9c38a0d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,6 +36,8 @@ end end + config.exclude_pattern = "spec/performance/*.rb" if ENV["PERFORMANCE_TESTS_DISABLED"] == "1" + config.before(:suite) do Rake::Task["db:create"].invoke Rake::Task["db:migrate"].invoke @@ -62,3 +64,9 @@ with.library :active_model end end + +def elapsed_time(&block) + Benchmark.measure do + block.call + end.real +end