From deca31c5a4cc30423ab7fe4d517007275979f232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sat, 16 Aug 2025 10:05:54 +0300 Subject: [PATCH 01/11] start --- .gitignore | 1 + .ruby-gemset | 1 + 2 files changed, 2 insertions(+) create mode 100644 .ruby-gemset diff --git a/.gitignore b/.gitignore index 59c74047..2eec9120 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /tmp /log /public +/.idea \ No newline at end of file diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 00000000..823585cd --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +optimization \ No newline at end of file From 4ba68ba942682d8823a8bef17f0be81c8b48ec28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sat, 16 Aug 2025 13:39:51 +0300 Subject: [PATCH 02/11] feat(config): add gems --- Gemfile | 24 ++++++++- Gemfile.lock | 49 ++++++++++++++++++- config/environments/development.rb | 40 +++++++++++++++ config/initializers/rack_mini_profiler.rb | 17 +++++++ config/initializers/strong_migrations.rb | 29 +++++++++++ ...0250816103837_create_pghero_space_stats.rb | 13 +++++ ...0250816103847_create_pghero_query_stats.rb | 15 ++++++ db/schema.rb | 36 ++++++++++---- 8 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 config/initializers/rack_mini_profiler.rb create mode 100644 config/initializers/strong_migrations.rb create mode 100644 db/migrate/20250816103837_create_pghero_space_stats.rb create mode 100644 db/migrate/20250816103847_create_pghero_query_stats.rb diff --git a/Gemfile b/Gemfile index 34074dfd..e2f526d1 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,27 @@ gem 'pg' gem 'puma' gem 'listen' gem 'bootsnap' -gem 'rack-mini-profiler' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'bullet' + +gem 'pghero' +gem 'pg_query', '>= 2' + +group :development, :test do + gem 'strong_migrations' + + # Profiling tools + gem 'memory_profiler', require: false + gem 'ruby-prof', require: false + gem 'stackprof', require: false + gem 'rack-mini-profiler', require: false + gem 'test-prof', "1.4.4" + gem 'vernier', "~> 1.7", require: false +end + +group :test do + gem 'rspec-sqlimit' +end diff --git a/Gemfile.lock b/Gemfile.lock index a9ddd818..de989027 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,15 +78,22 @@ GEM bootsnap (1.18.4) msgpack (~> 1.2) builder (3.3.0) + bullet (8.0.8) + activesupport (>= 3.0.0) + uniform_notifier (~> 1.11) concurrent-ruby (1.3.5) connection_pool (2.5.0) crass (1.0.6) date (3.4.1) + diff-lcs (1.6.2) drb (2.2.1) erubi (1.13.1) ffi (1.17.1-arm64-darwin) globalid (1.2.1) activesupport (>= 6.1) + google-protobuf (4.32.0-arm64-darwin) + bigdecimal + rake (>= 13) i18n (1.14.7) concurrent-ruby (~> 1.0) io-console (0.8.0) @@ -107,6 +114,7 @@ GEM net-pop net-smtp marcel (1.0.4) + memory_profiler (1.1.0) mini_mime (1.1.5) minitest (5.25.4) msgpack (1.8.0) @@ -123,6 +131,10 @@ GEM nokogiri (1.18.2-arm64-darwin) racc (~> 1.4) pg (1.5.9) + pg_query (6.1.0) + google-protobuf (>= 3.25.3) + pghero (3.7.0) + activerecord (>= 7.1) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -133,7 +145,7 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.1.9) - rack-mini-profiler (3.3.1) + rack-mini-profiler (4.0.1) rack (>= 1.2.0) rack-session (2.1.0) base64 (>= 0.1.0) @@ -179,14 +191,38 @@ GEM psych (>= 4.0.0) reline (0.6.0) io-console (~> 0.5) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.5) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-sqlimit (1.0.0) + activerecord (>= 4.2.0) + rspec (~> 3.0) + rspec-support (3.13.4) + ruby-prof (1.7.2) + base64 securerandom (0.4.1) + stackprof (0.2.27) stringio (3.1.2) + strong_migrations (2.5.0) + activerecord (>= 7.1) + test-prof (1.4.4) thor (1.3.2) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + uniform_notifier (1.17.0) uri (1.0.2) useragent (0.16.11) + vernier (1.8.0) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) @@ -198,12 +234,21 @@ PLATFORMS DEPENDENCIES bootsnap + bullet listen + memory_profiler pg + pg_query (>= 2) + pghero puma rack-mini-profiler rails (~> 8.0.1) - tzinfo-data + rspec-sqlimit + ruby-prof + stackprof + strong_migrations + test-prof (= 1.4.4) + vernier (~> 1.7) RUBY VERSION ruby 3.4.1p0 diff --git a/config/environments/development.rb b/config/environments/development.rb index bc3f8142..99fef80d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -58,4 +58,44 @@ # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + config.after_initialize do + Bullet.enable = true # Включает работу Bullet + Bullet.alert = false # Показывает всплывающее JS-окно в браузере при обнаружении проблемы + Bullet.bullet_logger = true # Записывает предупреждения в log/bullet.log + Bullet.console = true # Выводит предупреждения в консоль браузера (console.log) + Bullet.rails_logger = true # Записывает предупреждения в Rails логи + Bullet.add_footer = true # Добавляет предупреждение внизу HTML-страницы + + # Bullet.sentry = true # Отправка уведомлений в Sentry + # Bullet.xmpp = { # Отправка уведомлений через XMPP (Jabber) + # :account => 'bullets_account@jabber.org', # Аккаунт для отправки + # :password => 'bullets_password_for_jabber', # Пароль аккаунта + # :receiver => 'your_account@jabber.org', # Кому отправлять + # :show_online_status => true # Показывать статус онлайн + # } + # Bullet.honeybadger = true # Отправка уведомлений в Honeybadger + # Bullet.bugsnag = true # Отправка уведомлений в Bugsnag + # Bullet.appsignal = true # Отправка уведомлений в AppSignal + # Bullet.airbrake = true # Отправка уведомлений в Airbrake + # Bullet.rollbar = true # Отправка уведомлений в Rollbar + + Bullet.skip_html_injection = false # Если true — не будет встраивать HTML-предупреждения в ответ + Bullet.skip_http_headers = false # Если true — не будет добавлять HTTP-заголовки с предупреждениями + + # Bullet.stacktrace_includes = [ 'your_gem', 'your_middleware' ] + # Отображать только указанные библиотеки/модули в stacktrace + + # Bullet.stacktrace_excludes = [ 'their_gem', 'their_middleware', ['my_file.rb', 'my_method'], ['my_file.rb', 16..20] ] + # Исключать указанные файлы, методы или диапазоны строк из stacktrace + + # Bullet.slack = { webhook_url: 'http://some.slack.url', channel: '#default', username: 'notifier' } + # Отправка уведомлений в Slack через Webhook + + # Bullet.opentelemetry = true # Отправка уведомлений в OpenTelemetry + + Bullet.raise = false # Если true — будет выбрасывать исключение при проблемах (удобно в тестах) + Bullet.always_append_html_body = false # Если true — всегда вставлять HTML с предупреждениями даже без + Bullet.skip_user_in_notification = false # Если true — не добавлять инфо о пользователе в уведомления (актуально в multi-user) + end end diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb new file mode 100644 index 00000000..9b794335 --- /dev/null +++ b/config/initializers/rack_mini_profiler.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +unless Rails.env.test? + require "memory_profiler" + require "rack-mini-profiler" + require "stackprof" + + Rack::MiniProfiler.config.authorization_mode = :allow_all + Rack::MiniProfiler.config.position = "left" + + # Do not let rack-mini-profiler disable caching + Rack::MiniProfiler.config.disable_caching = false + Rack::MiniProfiler.config.show_total_sql_count = true + + # Если initializer грузится поздно — инициализируем вручную + Rack::MiniProfilerRails.initialize!(Rails.application) +end diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb new file mode 100644 index 00000000..d405c6ed --- /dev/null +++ b/config/initializers/strong_migrations.rb @@ -0,0 +1,29 @@ +# Mark existing migrations as safe +StrongMigrations.start_after = 20250816103323 + +# Set timeouts for migrations +# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user +StrongMigrations.lock_timeout = 10.seconds +StrongMigrations.statement_timeout = 1.hour + +# Analyze tables after indexes are added +# Outdated statistics can sometimes hurt performance +StrongMigrations.auto_analyze = true + +# Set the version of the production database +# so the right checks are run in development +# StrongMigrations.target_version = 10 + +# Add custom checks +# StrongMigrations.add_check do |method, args| +# if method == :add_index && args[0].to_s == "users" +# stop! "No more indexes on the users table" +# end +# end + +# Remove invalid indexes when rerunning migrations +# StrongMigrations.remove_invalid_indexes = true + +# Make some operations safe by default +# See https://github.com/ankane/strong_migrations#safe-by-default +# StrongMigrations.safe_by_default = true diff --git a/db/migrate/20250816103837_create_pghero_space_stats.rb b/db/migrate/20250816103837_create_pghero_space_stats.rb new file mode 100644 index 00000000..5592db37 --- /dev/null +++ b/db/migrate/20250816103837_create_pghero_space_stats.rb @@ -0,0 +1,13 @@ +class CreatePgheroSpaceStats < ActiveRecord::Migration[8.0] + def change + create_table :pghero_space_stats do |t| + t.text :database + t.text :schema + t.text :relation + t.integer :size, limit: 8 + t.timestamp :captured_at + end + + add_index :pghero_space_stats, [:database, :captured_at] + end +end diff --git a/db/migrate/20250816103847_create_pghero_query_stats.rb b/db/migrate/20250816103847_create_pghero_query_stats.rb new file mode 100644 index 00000000..74aaaa9a --- /dev/null +++ b/db/migrate/20250816103847_create_pghero_query_stats.rb @@ -0,0 +1,15 @@ +class CreatePgheroQueryStats < ActiveRecord::Migration[8.0] + def change + create_table :pghero_query_stats do |t| + t.text :database + t.text :user + t.text :query + t.integer :query_hash, limit: 8 + t.float :total_time + t.integer :calls, limit: 8 + t.timestamp :captured_at + end + + add_index :pghero_query_stats, [:database, :captured_at] + end +end diff --git a/db/schema.rb b/db/schema.rb index f6921e45..9c86f5eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -2,18 +2,17 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_30_193044) do - +ActiveRecord::Schema[8.0].define(version: 2025_08_16_103847) do # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" + enable_extension "pg_catalog.plpgsql" create_table "buses", force: :cascade do |t| t.string "number" @@ -29,6 +28,26 @@ t.string "name" end + create_table "pghero_query_stats", force: :cascade do |t| + t.text "database" + t.text "user" + t.text "query" + t.bigint "query_hash" + t.float "total_time" + t.bigint "calls" + t.datetime "captured_at", precision: nil + t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at" + end + + create_table "pghero_space_stats", force: :cascade do |t| + t.text "database" + t.text "schema" + t.text "relation" + t.bigint "size" + t.datetime "captured_at", precision: nil + t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at" + end + create_table "services", force: :cascade do |t| t.string "name" end @@ -41,5 +60,4 @@ t.integer "price_cents" t.integer "bus_id" end - end From 70b5e771ce32d7b40d3e8af8332d438ff9b7fdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sat, 16 Aug 2025 18:35:51 +0300 Subject: [PATCH 03/11] feat(test): add rspec --- .rspec | 1 + Gemfile | 5 ++ Gemfile.lock | 27 +++++++ app/services/json_trip_loader.rb | 64 ++++++++++++++++ case-study.md | 55 ++++++++++++++ config/routes.rb | 2 + db/migrate/20250816140257_add_indexes.rb | 11 +++ db/schema.rb | 3 +- lib/tasks/utils.rake | 33 +-------- script/stackprof/flamegraph.rb | 12 +++ script/stackprof/table.rb | 8 ++ script/vernier.rb | 9 +++ spec/rails_helper.rb | 72 ++++++++++++++++++ spec/services/json_trip_loader_spec.rb | 28 +++++++ spec/spec_helper.rb | 94 ++++++++++++++++++++++++ 15 files changed, 392 insertions(+), 32 deletions(-) create mode 100644 .rspec create mode 100644 app/services/json_trip_loader.rb create mode 100644 case-study.md create mode 100644 db/migrate/20250816140257_add_indexes.rb create mode 100644 script/stackprof/flamegraph.rb create mode 100644 script/stackprof/table.rb create mode 100644 script/vernier.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/services/json_trip_loader_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index e2f526d1..66a60e05 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'pg' gem 'puma' gem 'listen' gem 'bootsnap' +gem "sprockets-rails" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem # gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] @@ -27,8 +28,12 @@ group :development, :test do gem 'rack-mini-profiler', require: false gem 'test-prof', "1.4.4" gem 'vernier', "~> 1.7", require: false + gem 'profile-viewer' end group :test do + gem "rspec-rails" gem 'rspec-sqlimit' end + +gem "activerecord-import" diff --git a/Gemfile.lock b/Gemfile.lock index de989027..f196a0e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,6 +53,8 @@ GEM activemodel (= 8.0.1) activesupport (= 8.0.1) timeout (>= 0.4.0) + activerecord-import (2.2.0) + activerecord (>= 4.2) activestorage (8.0.1) actionpack (= 8.0.1) activejob (= 8.0.1) @@ -130,6 +132,7 @@ GEM nio4r (2.7.4) nokogiri (1.18.2-arm64-darwin) racc (~> 1.4) + optparse (0.6.0) pg (1.5.9) pg_query (6.1.0) google-protobuf (>= 3.25.3) @@ -138,6 +141,9 @@ GEM pp (0.6.2) prettyprint prettyprint (0.2.0) + profile-viewer (0.0.5) + optparse + webrick psych (5.2.3) date stringio @@ -203,6 +209,14 @@ GEM rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) + rspec-rails (8.0.2) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-sqlimit (1.0.0) activerecord (>= 4.2.0) rspec (~> 3.0) @@ -210,6 +224,14 @@ GEM ruby-prof (1.7.2) base64 securerandom (0.4.1) + sprockets (4.2.2) + concurrent-ruby (~> 1.0) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) stackprof (0.2.27) stringio (3.1.2) strong_migrations (2.5.0) @@ -223,6 +245,7 @@ GEM uri (1.0.2) useragent (0.16.11) vernier (1.8.0) + webrick (1.9.1) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) @@ -233,6 +256,7 @@ PLATFORMS arm64-darwin-24 DEPENDENCIES + activerecord-import bootsnap bullet listen @@ -240,11 +264,14 @@ DEPENDENCIES pg pg_query (>= 2) pghero + profile-viewer puma rack-mini-profiler rails (~> 8.0.1) + rspec-rails rspec-sqlimit ruby-prof + sprockets-rails stackprof strong_migrations test-prof (= 1.4.4) diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb new file mode 100644 index 00000000..5c409b7d --- /dev/null +++ b/app/services/json_trip_loader.rb @@ -0,0 +1,64 @@ +require "benchmark" + +class JsonTripLoader + BATCH_SIZE = 5_000 + + def load!(path) + json = JSON.parse(File.read(path)) + + old_logger = ActiveRecord::Base.logger + old_verbose = + ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil + + time = Benchmark.realtime do + begin + # тихий режим + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + + ActiveRecord::Base.transaction do + ActiveRecord::Base.connection.execute <<~SQL + TRUNCATE TABLE + buses_services, + trips, + buses, + services, + cities + RESTART IDENTITY + CASCADE; + SQL + + json.each do |trip| + from = City.find_or_create_by(name: trip['from']) + to = City.find_or_create_by(name: trip['to']) + + services = [] + trip['bus']['services'].each do |service| + s = Service.find_or_create_by(name: service) + services << s + end + + bus = Bus.find_or_create_by(number: trip['bus']['number']) + bus.update(model: trip['bus']['model'], services: services) + + Trip.create!( + from: from, + to: to, + bus: bus, + start_time: trip['start_time'], + duration_minutes: trip['duration_minutes'], + price_cents: trip['price_cents'] + ) + end + end + ensure + ActiveRecord::Base.logger = old_logger + if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + ActiveRecord::Base.verbose_query_logs = old_verbose + end + end + end + + puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" + end +end diff --git a/case-study.md b/case-study.md new file mode 100644 index 00000000..fb01c28d --- /dev/null +++ b/case-study.md @@ -0,0 +1,55 @@ +## Шаги выстраивания оптимизации +### Начало +- установил гемы с настройками +- запустил импорт на разных объемах, убедился в том, что все работает медленно +- посмотрел на код и хотел на всикду порефакторить, но решил искать гемами точки роста + +### pghero +- зашел в него и увидел сообщения о нехватке индексов +- увидел много транзакций `SELECT`, `INSERT` c большим количеством запусков(Calls) + +### rspec-sqlimit +- вынес импорт в отдельный сервис, чтобы проще было тестировать +- создал тест для сервиса и увидел 14777 запроса при импорте файла `small.json` + +### stackprof +- установил гем и написал скрипт для профилирования +``` +================================== + Mode: wall(1000) + Samples: 4075 (0.02% miss rate) + GC: 271 (6.65%) +================================== + TOTAL (pct) SAMPLES (pct) FRAME + 693 (17.0%) 693 (17.0%) String#sub + 493 (12.1%) 493 (12.1%) PG::Connection#exec_params + 462 (11.3%) 462 (11.3%) PG::Connection#exec_prepared + 360 (8.8%) 360 (8.8%) Regexp#match? + 305 (7.5%) 305 (7.5%) Thread::Backtrace::Location#to_s + 199 (4.9%) 199 (4.9%) (sweeping) + 130 (3.2%) 130 (3.2%) PG::Connection#exec + 113 (2.8%) 113 (2.8%) String#include? + 1627 (39.9%) 76 (1.9%) ActiveSupport::BacktraceCleaner#clean_frame + 72 (1.8%) 72 (1.8%) (marking) + 66 (1.6%) 66 (1.6%) #.[] + 58 (1.4%) 58 (1.4%) IO#write + 3744 (91.9%) 48 (1.2%) Array#each + 33 (0.8%) 33 (0.8%) IO#stat + 1745 (42.8%) 29 (0.7%) Thread.each_caller_location + 301 (7.4%) 27 (0.7%) Array#any? + 168 (4.1%) 25 (0.6%) Class#new + 1962 (48.1%) 18 (0.4%) ActiveRecord::LogSubscriber#sql + 18 (0.4%) 18 (0.4%) Process.clock_gettime + 14 (0.3%) 14 (0.3%) Regexp#=== + 260 (6.4%) 14 (0.3%) Rails::BacktraceCleaner#initialize + 35 (0.9%) 11 (0.3%) Hash#fetch + 39 (1.0%) 10 (0.2%) Arel::Visitors::Visitor#visit + 17 (0.4%) 10 (0.2%) Array#inspect + 9 (0.2%) 9 (0.2%) block in redefine + 10 (0.2%) 8 (0.2%) ActiveSupport::IsolatedExecutionState.state + 18 (0.4%) 8 (0.2%) ActiveSupport::IsolatedExecutionState.[] + 700 (17.2%) 7 (0.2%) ActiveSupport::Callbacks::CallTemplate::ObjectCall#make_lambda + 7 (0.2%) 7 (0.2%) String#start_with? + 14 (0.3%) 7 (0.2%) ActiveSupport::LogSubscriber#color +``` + diff --git a/config/routes.rb b/config/routes.rb index 0bbefa7a..0fe2bd78 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,6 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html get "автобусы/:from/:to" => "trips#index" + + mount PgHero::Engine, at: 'pghero' end diff --git a/db/migrate/20250816140257_add_indexes.rb b/db/migrate/20250816140257_add_indexes.rb new file mode 100644 index 00000000..4431553b --- /dev/null +++ b/db/migrate/20250816140257_add_indexes.rb @@ -0,0 +1,11 @@ +class AddIndexes < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + # add_index :buses, :number, + # algorithm: :concurrently + # + # add_index :trips, [:from_id, :to_id], + # algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 9c86f5eb..6a52dfa7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_16_103847) do +ActiveRecord::Schema[8.0].define(version: 2025_08_16_140257) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" + enable_extension "pg_stat_statements" create_table "buses", force: :cascade do |t| t.string "number" diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 540fe871..c48f19f8 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,34 +1,5 @@ # Наивная загрузка данных из json-файла в БД -# rake reload_json[fixtures/small.json] +# rake 'reload_json[fixtures/small.json]' task :reload_json, [:file_name] => :environment do |_task, args| - json = JSON.parse(File.read(args.file_name)) - - ActiveRecord::Base.transaction do - City.delete_all - Bus.delete_all - Service.delete_all - Trip.delete_all - ActiveRecord::Base.connection.execute('delete from buses_services;') - - json.each do |trip| - from = City.find_or_create_by(name: trip['from']) - to = City.find_or_create_by(name: trip['to']) - services = [] - trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s - end - bus = Bus.find_or_create_by(number: trip['bus']['number']) - bus.update(model: trip['bus']['model'], services: services) - - Trip.create!( - from: from, - to: to, - bus: bus, - start_time: trip['start_time'], - duration_minutes: trip['duration_minutes'], - price_cents: trip['price_cents'], - ) - end - end + JsonTripLoader.new.load!(args.file_name) end diff --git a/script/stackprof/flamegraph.rb b/script/stackprof/flamegraph.rb new file mode 100644 index 00000000..dc02e53a --- /dev/null +++ b/script/stackprof/flamegraph.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "json" +require "stackprof" +require_relative "../../config/environment" + +profile = StackProf.run(mode: :wall, raw: true) do + JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") +end + +File.write("tmp/reports/stackprof/flamegraph.json", JSON.generate(profile)) + diff --git a/script/stackprof/table.rb b/script/stackprof/table.rb new file mode 100644 index 00000000..13c6f09c --- /dev/null +++ b/script/stackprof/table.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "stackprof" +require_relative "../../config/environment" + +StackProf.run(mode: :wall, out: "tmp/reports/stackprof/table.dump") do + JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") +end diff --git a/script/vernier.rb b/script/vernier.rb new file mode 100644 index 00000000..a7ec5350 --- /dev/null +++ b/script/vernier.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "vernier" +require_relative "../config/environment" + + +Vernier.run(out: "tmp/reports/vernier.json") do + JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..79811b8f --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,72 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +# Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file +# that will avoid rails generators crashing because migrations haven't been run yet +# return unless Rails.env.test? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Ensures that the test database schema matches the current schema file. +# If there are pending migrations it will invoke `db:test:prepare` to +# recreate the test database by loading the schema. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails uses metadata to mix in different behaviours to your tests, + # for example enabling you to call `get` and `post` in request specs. e.g.: + # + # RSpec.describe UsersController, type: :request do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/8-0/rspec-rails + # + # You can also this infer these behaviours automatically by location, e.g. + # /spec/models would pull in the same behaviour as `type: :model` but this + # behaviour is considered legacy and will be removed in a future version. + # + # To enable this behaviour uncomment the line below. + # config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/services/json_trip_loader_spec.rb b/spec/services/json_trip_loader_spec.rb new file mode 100644 index 00000000..6bf6769b --- /dev/null +++ b/spec/services/json_trip_loader_spec.rb @@ -0,0 +1,28 @@ +require "rails_helper" +require "rspec/sqlimit" + +RSpec.describe JsonTripLoader do + let(:path) { Rails.root.join("fixtures/small.json") } + + it "limits SELECT/INSERT/DELETE/UPDATE separately" do + # SELECT + expect { + described_class.new.load!(path.to_s) + }.not_to exceed_query_limit(1).with(/\bSELECT\b/i) + + # INSERT (включая UPSERT — это INSERT ... ON CONFLICT) + expect { + described_class.new.load!(path.to_s) + }.not_to exceed_query_limit(10).with(/\bINSERT\b/i) + + # DELETE + expect { + described_class.new.load!(path.to_s) + }.not_to exceed_query_limit(1).with(/\bDELETE\b/i) + + # UPDATE — должно быть 0, если всё через upsert_all и нет отдельных update + expect { + described_class.new.load!(path.to_s) + }.not_to exceed_query_limit(0).with(/\bUPDATE\b/i) + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..327b58ea --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end From 31d7d998ae55b121c2f610b29bc69aee44f98303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sat, 16 Aug 2025 20:44:44 +0300 Subject: [PATCH 04/11] feat(service): add import --- app/services/json_trip_loader.rb | 37 +++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb index 5c409b7d..cb8038c6 100644 --- a/app/services/json_trip_loader.rb +++ b/app/services/json_trip_loader.rb @@ -28,28 +28,41 @@ def load!(path) CASCADE; SQL + city_id_cache = {} + service_id_cache = {} + bus_cache = {} + trips = [] json.each do |trip| - from = City.find_or_create_by(name: trip['from']) - to = City.find_or_create_by(name: trip['to']) + from_name = trip['from'] + to_name = trip['to'] - services = [] - trip['bus']['services'].each do |service| - s = Service.find_or_create_by(name: service) - services << s + # Кешируем ID городов + from_id = city_id_cache[from_name] ||= City.find_or_create_by(name: from_name).id + to_id = city_id_cache[to_name] ||= City.find_or_create_by(name: to_name).id + + # Кешируем ID сервисов + service_ids = trip['bus']['services'].map do |name| + service_id_cache[name] ||= Service.find_or_create_by(name:).id end - bus = Bus.find_or_create_by(number: trip['bus']['number']) - bus.update(model: trip['bus']['model'], services: services) + # Кеш автобусов (объект нужен для присвоения сервисов и обновления модели) + number = trip['bus']['number'] + model = trip['bus']['model'] + bus = bus_cache[number] ||= Bus.find_or_create_by(number:) + bus.model = model if bus.model != model + bus.service_ids = service_ids if bus.service_ids != service_ids + bus.save if bus.changed? - Trip.create!( - from: from, - to: to, - bus: bus, + trips << Trip.new( + from_id: from_id, + to_id: to_id, + bus_id: bus.id, start_time: trip['start_time'], duration_minutes: trip['duration_minutes'], price_cents: trip['price_cents'] ) end + Trip.import(trips, validate: true) end ensure ActiveRecord::Base.logger = old_logger From 613fc92acd4398781767958884077d584bfdeb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sat, 16 Aug 2025 21:52:31 +0300 Subject: [PATCH 05/11] feat(ai): update --- Gemfile | 8 +- Gemfile.lock | 2 + app/services/json_trip_loader.rb | 19 ++- app/services/json_trip_loader_ai.rb | 161 +++++++++++++++++++++++++ spec/services/json_trip_loader_spec.rb | 2 +- 5 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 app/services/json_trip_loader_ai.rb diff --git a/Gemfile b/Gemfile index 66a60e05..9d08fe9b 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'pg' gem 'puma' gem 'listen' gem 'bootsnap' -gem "sprockets-rails" +gem 'sprockets-rails' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem # gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] @@ -29,11 +29,11 @@ group :development, :test do gem 'test-prof', "1.4.4" gem 'vernier', "~> 1.7", require: false gem 'profile-viewer' + gem 'ruby-progressbar' + gem 'activerecord-import' end group :test do - gem "rspec-rails" + gem 'rspec-rails' gem 'rspec-sqlimit' end - -gem "activerecord-import" diff --git a/Gemfile.lock b/Gemfile.lock index f196a0e7..c4b04065 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,6 +223,7 @@ GEM rspec-support (3.13.4) ruby-prof (1.7.2) base64 + ruby-progressbar (1.13.0) securerandom (0.4.1) sprockets (4.2.2) concurrent-ruby (~> 1.0) @@ -271,6 +272,7 @@ DEPENDENCIES rspec-rails rspec-sqlimit ruby-prof + ruby-progressbar sprockets-rails stackprof strong_migrations diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb index cb8038c6..3957f591 100644 --- a/app/services/json_trip_loader.rb +++ b/app/services/json_trip_loader.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require "benchmark" +require "ruby-progressbar" class JsonTripLoader - BATCH_SIZE = 5_000 - def load!(path) json = JSON.parse(File.read(path)) @@ -32,20 +33,25 @@ def load!(path) service_id_cache = {} bus_cache = {} trips = [] + + # прогрессбар на количество записей + progress_bar = ProgressBar.create( + title: "Loading trips", + total: json.size, + format: "%t: |%B| %p%% %e" + ) + json.each do |trip| from_name = trip['from'] to_name = trip['to'] - # Кешируем ID городов from_id = city_id_cache[from_name] ||= City.find_or_create_by(name: from_name).id to_id = city_id_cache[to_name] ||= City.find_or_create_by(name: to_name).id - # Кешируем ID сервисов service_ids = trip['bus']['services'].map do |name| service_id_cache[name] ||= Service.find_or_create_by(name:).id end - # Кеш автобусов (объект нужен для присвоения сервисов и обновления модели) number = trip['bus']['number'] model = trip['bus']['model'] bus = bus_cache[number] ||= Bus.find_or_create_by(number:) @@ -61,7 +67,10 @@ def load!(path) duration_minutes: trip['duration_minutes'], price_cents: trip['price_cents'] ) + + progress_bar.increment end + Trip.import(trips, validate: true) end ensure diff --git a/app/services/json_trip_loader_ai.rb b/app/services/json_trip_loader_ai.rb new file mode 100644 index 00000000..d3218515 --- /dev/null +++ b/app/services/json_trip_loader_ai.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require "benchmark" +require "ruby-progressbar" +require "set" + +class JsonTripLoaderAi + BATCH_SIZE = 5_000 + + # Если нет модели для join-таблицы — объявим локально. + unless defined?(BusService) + class BusService < ApplicationRecord + self.table_name = "buses_services" + end + end + + def load!(path) + json = JSON.parse(File.read(path)) + + old_logger = ActiveRecord::Base.logger + old_verbose = + ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil + + time = Benchmark.realtime do + begin + # Тихий режим логгера, чтобы не тратить время на форматирование логов + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + + ActiveRecord::Base.transaction do + # Быстрый сброс + ActiveRecord::Base.connection.execute <<~SQL + TRUNCATE TABLE + buses_services, + trips, + buses, + services, + cities + RESTART IDENTITY + CASCADE; + SQL + + # 1) Собираем уникальные сущности из JSON + city_names = Set.new + service_names = Set.new + bus_number_to_model = {} # number => model + bus_number_to_service_names = Hash.new { |h, k| h[k] = Set.new } + + json.each do |trip| + from_name = trip["from"] + to_name = trip["to"] + city_names << from_name + city_names << to_name + + number = trip["bus"]["number"] + model = trip["bus"]["model"] + bus_number_to_model[number] = model + + (trip["bus"]["services"] || []).each do |sname| + service_names << sname + bus_number_to_service_names[number] << sname + end + end + + # 2) Вставляем города/сервисы одним махом (ignore duplicates) + if city_names.any? + City.import( + city_names.map { |n| City.new(name: n) }, + on_duplicate_key_ignore: true, + validate: false + ) + end + + if service_names.any? + Service.import( + service_names.map { |n| Service.new(name: n) }, + on_duplicate_key_ignore: true, + validate: false + ) + end + + # 3) Получаем маппинги name -> id одним запросом на каждую таблицу + city_id_by_name = City.where(name: city_names.to_a).pluck(:name, :id).to_h + service_id_by_name = Service.where(name: service_names.to_a).pluck(:name, :id).to_h + + # 4) Upsert автобусов (обновляем model при конфликте по number) + if bus_number_to_model.any? + # Через activerecord-import с on_duplicate_key_update + Bus.import( + bus_number_to_model.map { |number, model| Bus.new(number: number, model: model) }, + on_duplicate_key_update: [:model], + validate: false + ) + end + + # 5) Получаем маппинг автобусов number -> id одним запросом + bus_id_by_number = Bus.where(number: bus_number_to_model.keys).pluck(:number, :id).to_h + + # 6) Пакетно вставляем связи bus↔service в join-таблицу (ignore duplicates) + join_rows = [] + bus_number_to_service_names.each do |number, sset| + bus_id = bus_id_by_number[number] + sset.each do |sname| + sid = service_id_by_name[sname] + next unless bus_id && sid + join_rows << { bus_id: bus_id, service_id: sid } + end + end + + if join_rows.any? + # Через модель BusService — activerecord-import игнорирует дубликаты + BusService.import( + join_rows, + on_duplicate_key_ignore: true, + validate: false + ) + end + + # 7) Формируем и заливаем trips батчами + trips_buffer = [] + trips_buffer.reserve([json.size, BATCH_SIZE].min) if trips_buffer.respond_to?(:reserve) + + flush_trips = lambda do + next if trips_buffer.empty? + Trip.import(trips_buffer, validate: true, batch_size: BATCH_SIZE) + trips_buffer.clear + end + + # прогрессбар на количество записей + progress_bar = ProgressBar.create( + title: "Loading trips", + total: json.size, + format: "%t: |%B| %p%% %e" + ) + + json.each do |t| + trips_buffer << Trip.new( + from_id: city_id_by_name[t["from"]], + to_id: city_id_by_name[t["to"]], + bus_id: bus_id_by_number[t["bus"]["number"]], + start_time: t["start_time"], + duration_minutes: t["duration_minutes"], + price_cents: t["price_cents"] + ) + flush_trips.call if trips_buffer.size >= BATCH_SIZE + + progress_bar.increment + end + flush_trips.call + end + ensure + ActiveRecord::Base.logger = old_logger + if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + ActiveRecord::Base.verbose_query_logs = old_verbose + end + end + end + + puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" + end +end diff --git a/spec/services/json_trip_loader_spec.rb b/spec/services/json_trip_loader_spec.rb index 6bf6769b..9314138a 100644 --- a/spec/services/json_trip_loader_spec.rb +++ b/spec/services/json_trip_loader_spec.rb @@ -2,7 +2,7 @@ require "rspec/sqlimit" RSpec.describe JsonTripLoader do - let(:path) { Rails.root.join("fixtures/small.json") } + let(:path) { Rails.root.join("fixtures/large.json") } it "limits SELECT/INSERT/DELETE/UPDATE separately" do # SELECT From a5489135a6a26a8e51a06ebae4fce08a0c3f049d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sun, 17 Aug 2025 08:39:03 +0300 Subject: [PATCH 06/11] refactor(service): speed up --- Gemfile | 5 +- Gemfile.lock | 6 + app/services/json_trip_loader.rb | 25 ++-- app/services/json_trip_loader_stream.rb | 182 ++++++++++++++++++++++++ script/stackprof/flamegraph.rb | 6 +- script/stackprof/table.rb | 2 +- script/vernier.rb | 3 +- 7 files changed, 210 insertions(+), 19 deletions(-) create mode 100644 app/services/json_trip_loader_stream.rb diff --git a/Gemfile b/Gemfile index 9d08fe9b..b9c59059 100644 --- a/Gemfile +++ b/Gemfile @@ -11,10 +11,9 @@ gem 'bootsnap' gem 'sprockets-rails' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'bullet' - gem 'pghero' gem 'pg_query', '>= 2' @@ -37,3 +36,5 @@ group :test do gem 'rspec-rails' gem 'rspec-sqlimit' end + +gem 'oj' diff --git a/Gemfile.lock b/Gemfile.lock index c4b04065..995e35e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,7 +132,11 @@ GEM nio4r (2.7.4) nokogiri (1.18.2-arm64-darwin) racc (~> 1.4) + oj (3.16.11) + bigdecimal (>= 3.0) + ostruct (>= 0.2) optparse (0.6.0) + ostruct (0.6.3) pg (1.5.9) pg_query (6.1.0) google-protobuf (>= 3.25.3) @@ -262,6 +266,7 @@ DEPENDENCIES bullet listen memory_profiler + oj pg pg_query (>= 2) pghero @@ -277,6 +282,7 @@ DEPENDENCIES stackprof strong_migrations test-prof (= 1.4.4) + tzinfo-data vernier (~> 1.7) RUBY VERSION diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb index 3957f591..abd79756 100644 --- a/app/services/json_trip_loader.rb +++ b/app/services/json_trip_loader.rb @@ -34,7 +34,6 @@ def load!(path) bus_cache = {} trips = [] - # прогрессбар на количество записей progress_bar = ProgressBar.create( title: "Loading trips", total: json.size, @@ -59,19 +58,25 @@ def load!(path) bus.service_ids = service_ids if bus.service_ids != service_ids bus.save if bus.changed? - trips << Trip.new( - from_id: from_id, - to_id: to_id, - bus_id: bus.id, - start_time: trip['start_time'], - duration_minutes: trip['duration_minutes'], - price_cents: trip['price_cents'] - ) + # Используем массив - самый быстрый вариант + trips << [ + from_id, + to_id, + bus.id, + trip['start_time'], + trip['duration_minutes'], + trip['price_cents'] + ] progress_bar.increment end - Trip.import(trips, validate: true) + # Указываем колонки явно при импорте массивов + Trip.import( + [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents], + trips, + validate: false + ) end ensure ActiveRecord::Base.logger = old_logger diff --git a/app/services/json_trip_loader_stream.rb b/app/services/json_trip_loader_stream.rb new file mode 100644 index 00000000..36095241 --- /dev/null +++ b/app/services/json_trip_loader_stream.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require "benchmark" +require "ruby-progressbar" +require "oj" # Быстрый JSON парсер + +class JsonTripLoaderStream + BATCH_SIZE = 5000 + + def load!(path) + old_logger = ActiveRecord::Base.logger + old_verbose = ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil + + time = Benchmark.realtime do + begin + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + + ActiveRecord::Base.transaction do + clear_database + + # Кэши для уникальных значений + @city_cache = {} + @service_cache = {} + @bus_cache = {} + @trips_batch = [] + + # Получаем размер файла для прогресс-бара (приблизительно) + file_size = File.size(path) + progress_bar = ProgressBar.create( + title: "Loading trips", + total: file_size, + format: "%t: |%B| %p%% %e" + ) + + # Потоковое чтение и парсинг + stream_parse_file(path, progress_bar) + + # Импортируем последний батч + import_trips_batch if @trips_batch.any? + end + ensure + ActiveRecord::Base.logger = old_logger + if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + ActiveRecord::Base.verbose_query_logs = old_verbose + end + end + end + + puts "JsonTripLoader.load!: #{time.round(3)}s" + end + + private + + def clear_database + ActiveRecord::Base.connection.execute <<~SQL + TRUNCATE TABLE + buses_services, + trips, + buses, + services, + cities + RESTART IDENTITY + CASCADE; + SQL + end + + def stream_parse_file(path, progress_bar) + File.open(path, 'r') do |file| + nesting = 0 + str = +"" + bytes_read = 0 + in_array = false + + until file.eof? + ch = file.read(1) + bytes_read += 1 + + # Обновляем прогресс каждые 10KB + if bytes_read % 10_000 == 0 + progress_bar.progress = bytes_read + end + + case ch + when '[' + in_array = true if nesting == 0 + str << ch if nesting > 0 + when ']' + # Конец корневого массива + if nesting == 0 + in_array = false + next + end + str << ch + when '{' + nesting += 1 + str << ch + when '}' + str << ch + nesting -= 1 + + # Если закончился объект верхнего уровня (trip) + if nesting == 0 && in_array + begin + trip = Oj.load(str) + process_trip(trip) + rescue Oj::ParseError => e + puts "Parse error: #{e.message}" + puts "String: #{str[0..100]}..." if str.length > 100 + raise + end + str = +"" + end + when ',' + # Запятая между объектами - игнорируем если на верхнем уровне + next if nesting == 0 + str << ch + when /\s/ + # Пробелы - игнорируем если на верхнем уровне + next if nesting == 0 + str << ch + else + str << ch if nesting > 0 + end + end + + progress_bar.finish + end + end + + def process_trip(trip) + from_id = get_or_create_city_id(trip['from']) + to_id = get_or_create_city_id(trip['to']) + bus_id = get_or_create_bus_id(trip['bus']) + + @trips_batch << [ + from_id, + to_id, + bus_id, + trip['start_time'], + trip['duration_minutes'], + trip['price_cents'] + ] + + # Импортируем батч когда набрался нужный размер + if @trips_batch.size >= BATCH_SIZE + import_trips_batch + @trips_batch.clear + end + end + + def get_or_create_city_id(name) + @city_cache[name] ||= City.find_or_create_by(name: name).id + end + + def get_or_create_bus_id(bus_data) + number = bus_data['number'] + + @bus_cache[number] ||= begin + bus = Bus.find_or_create_by(number: number) + bus.model = bus_data['model'] if bus.model != bus_data['model'] + + service_ids = bus_data['services'].map { |name| get_or_create_service_id(name) } + bus.service_ids = service_ids if bus.service_ids != service_ids + + bus.save if bus.changed? + bus.id + end + end + + def get_or_create_service_id(name) + @service_cache[name] ||= Service.find_or_create_by(name: name).id + end + + def import_trips_batch + Trip.import( + [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents], + @trips_batch, + validate: false + ) + end +end diff --git a/script/stackprof/flamegraph.rb b/script/stackprof/flamegraph.rb index dc02e53a..8fbac676 100644 --- a/script/stackprof/flamegraph.rb +++ b/script/stackprof/flamegraph.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -require "json" require "stackprof" require_relative "../../config/environment" profile = StackProf.run(mode: :wall, raw: true) do - JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") + Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json") end -File.write("tmp/reports/stackprof/flamegraph.json", JSON.generate(profile)) - +File.write("tmp/reports/stackprof/flamegraph.json", Oj.dump(profile)) diff --git a/script/stackprof/table.rb b/script/stackprof/table.rb index 13c6f09c..8c44f9c8 100644 --- a/script/stackprof/table.rb +++ b/script/stackprof/table.rb @@ -4,5 +4,5 @@ require_relative "../../config/environment" StackProf.run(mode: :wall, out: "tmp/reports/stackprof/table.dump") do - JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") + Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json") end diff --git a/script/vernier.rb b/script/vernier.rb index a7ec5350..b964eb18 100644 --- a/script/vernier.rb +++ b/script/vernier.rb @@ -3,7 +3,6 @@ require "vernier" require_relative "../config/environment" - Vernier.run(out: "tmp/reports/vernier.json") do - JsonTripLoader.new.load!(ARGV[0] || "fixtures/small.json") + Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json") end From 79b7221f99a689ba73527d760ef6c11ee11fac07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sun, 17 Aug 2025 08:48:53 +0300 Subject: [PATCH 07/11] feat(gems): add rubocop --- .rubocop.yml | 19 ++++++++++++++ Gemfile | 42 +++++++++++++++++++------------ Gemfile.lock | 53 +++++++++++++++++++++++++++++++++++++-- script/stackprof/table.rb | 8 +++--- 4 files changed, 100 insertions(+), 22 deletions(-) create mode 100644 .rubocop.yml diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..f19b848f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,19 @@ +require: + - rubocop-performance +AllCops: + SuggestExtensions: true + NewCops: enable +Style/Documentation: + Enabled: false +Style/AsciiComments: + Enabled: false +Layout/LineLength: + Max: 200 +Metrics/MethodLength: + Enabled: false +Metrics/ClassLength: + Max: 140 +Metrics/AbcSize: + Enabled: false +Metrics/CyclomaticComplexity: + Max: 20 diff --git a/Gemfile b/Gemfile index b9c59059..250854d0 100644 --- a/Gemfile +++ b/Gemfile @@ -3,38 +3,48 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby file: '.ruby-version' -gem 'rails', '~> 8.0.1' +# Core Rails gems +gem 'bootsnap' +gem 'listen' gem 'pg' gem 'puma' -gem 'listen' -gem 'bootsnap' +gem 'rails', '~> 8.0.1' gem 'sprockets-rails' +# Core application dependencies +gem 'activerecord-import' +gem 'oj' +gem 'ruby-progressbar' + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -gem 'bullet' -gem 'pghero' -gem 'pg_query', '>= 2' - -group :development, :test do +group :development do + # Database monitoring and optimization + gem 'bullet' + gem 'pghero' + gem 'pg_query', '>= 2' gem 'strong_migrations' +end +group :development, :test do # Profiling tools gem 'memory_profiler', require: false + gem 'pry' + gem 'rack-mini-profiler', require: false gem 'ruby-prof', require: false gem 'stackprof', require: false - gem 'rack-mini-profiler', require: false - gem 'test-prof', "1.4.4" - gem 'vernier', "~> 1.7", require: false - gem 'profile-viewer' - gem 'ruby-progressbar' - gem 'activerecord-import' + gem 'test-prof', '~> 1.4.4' + gem 'vernier', '~> 1.7', require: false end group :test do + gem 'profile-viewer', require: false gem 'rspec-rails' gem 'rspec-sqlimit' -end -gem 'oj' + gem 'rubocop' + gem 'rubocop-minitest' + gem 'rubocop-performance' + gem 'rubocop-rspec' +end diff --git a/Gemfile.lock b/Gemfile.lock index 995e35e6..bbcf0a9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) + ast (2.4.3) base64 (0.2.0) benchmark (0.4.0) bigdecimal (3.1.9) @@ -83,6 +84,7 @@ GEM bullet (8.0.8) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) + coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.0) crass (1.0.6) @@ -90,7 +92,7 @@ GEM diff-lcs (1.6.2) drb (2.2.1) erubi (1.13.1) - ffi (1.17.1-arm64-darwin) + ffi (1.17.2-arm64-darwin) globalid (1.2.1) activesupport (>= 6.1) google-protobuf (4.32.0-arm64-darwin) @@ -103,6 +105,9 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + json (2.13.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) @@ -117,6 +122,7 @@ GEM net-smtp marcel (1.0.4) memory_profiler (1.1.0) + method_source (1.1.0) mini_mime (1.1.5) minitest (5.25.4) msgpack (1.8.0) @@ -137,6 +143,10 @@ GEM ostruct (>= 0.2) optparse (0.6.0) ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.9.0) + ast (~> 2.4.1) + racc pg (1.5.9) pg_query (6.1.0) google-protobuf (>= 3.25.3) @@ -145,9 +155,13 @@ GEM pp (0.6.2) prettyprint prettyprint (0.2.0) + prism (1.4.0) profile-viewer (0.0.5) optparse webrick + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) psych (5.2.3) date stringio @@ -193,12 +207,14 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) + rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) rdoc (6.12.0) psych (>= 4.0.0) + regexp_parser (2.11.2) reline (0.6.0) io-console (~> 0.5) rspec (3.13.1) @@ -225,6 +241,31 @@ GEM activerecord (>= 4.2.0) rspec (~> 3.0) rspec-support (3.13.4) + rubocop (1.79.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.46.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.46.0) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-minitest (0.38.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rspec (3.6.0) + lint_roller (~> 1.1) + rubocop (~> 1.72, >= 1.72.1) ruby-prof (1.7.2) base64 ruby-progressbar (1.13.0) @@ -246,6 +287,9 @@ GEM timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (3.1.5) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) uniform_notifier (1.17.0) uri (1.0.2) useragent (0.16.11) @@ -271,17 +315,22 @@ DEPENDENCIES pg_query (>= 2) pghero profile-viewer + pry puma rack-mini-profiler rails (~> 8.0.1) rspec-rails rspec-sqlimit + rubocop + rubocop-minitest + rubocop-performance + rubocop-rspec ruby-prof ruby-progressbar sprockets-rails stackprof strong_migrations - test-prof (= 1.4.4) + test-prof (~> 1.4.4) tzinfo-data vernier (~> 1.7) diff --git a/script/stackprof/table.rb b/script/stackprof/table.rb index 8c44f9c8..fe389abd 100644 --- a/script/stackprof/table.rb +++ b/script/stackprof/table.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "stackprof" -require_relative "../../config/environment" +require 'stackprof' +require_relative '../../config/environment' -StackProf.run(mode: :wall, out: "tmp/reports/stackprof/table.dump") do - Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json") +StackProf.run(mode: :wall, out: 'tmp/reports/stackprof/table.dump') do + Object.const_get(ARGV[0] || 'JsonTripLoader').new.load!(ARGV[1] || 'fixtures/small.json') end From bffcee7f4dfc7fde8fab7da39c81858566a51539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sun, 17 Aug 2025 08:59:54 +0300 Subject: [PATCH 08/11] fix(rubocop): refactor --- .rubocop.yml | 171 ++++++++++++- .rubocop_todo.yml | 7 + .vscode/settings.json | 25 ++ Gemfile | 64 ++--- Gemfile.lock | 7 + Rakefile | 2 +- app/mailers/application_mailer.rb | 4 +- app/models/bus.rb | 13 +- app/models/city.rb | 2 +- app/models/service.rb | 20 +- app/models/trip.rb | 6 +- app/services/json_trip_loader.rb | 118 +++++---- app/services/json_trip_loader_ai.rb | 235 +++++++++--------- app/services/json_trip_loader_stream.rb | 118 +++++---- bin/rails | 2 +- bin/rake | 2 +- bin/spring | 2 +- bin/yarn | 12 +- config.ru | 2 +- config/application.rb | 4 +- config/boot.rb | 6 +- config/environment.rb | 2 +- config/environments/development.rb | 12 +- config/environments/production.rb | 6 +- config/environments/test.rb | 2 +- config/initializers/strong_migrations.rb | 2 +- config/routes.rb | 2 +- config/spring.rb | 10 +- ...0250816103837_create_pghero_space_stats.rb | 2 +- ...0250816103847_create_pghero_query_stats.rb | 2 +- db/schema.rb | 72 +++--- script/stackprof/table.rb | 8 +- spec/rails_helper.rb | 10 +- spec/services/json_trip_loader_spec.rb | 18 +- spec/spec_helper.rb | 92 ++++--- test/test_helper.rb | 16 +- 36 files changed, 628 insertions(+), 450 deletions(-) create mode 100644 .rubocop_todo.yml create mode 100644 .vscode/settings.json diff --git a/.rubocop.yml b/.rubocop.yml index f19b848f..99935ea2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,19 +1,180 @@ -require: +plugins: + - rubocop-minitest - rubocop-performance + - rubocop-rspec + - rubocop-rails + AllCops: - SuggestExtensions: true + SuggestExtensions: false NewCops: enable + TargetRubyVersion: 3.4 + Exclude: + - 'vendor/**/*' + - 'db/schema.rb' + - 'db/migrate/*' + - 'bin/*' + - 'node_modules/**/*' + - 'tmp/**/*' + - 'log/**/*' + +# ОСНОВНЫЕ СТИЛЕВЫЕ ПРАВИЛА + +# Предпочитаем ДВОЙНЫЕ кавычки +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + Enabled: true + EnforcedStyle: double_quotes + +# Отключаем требование документации Style/Documentation: Enabled: false + +# Разрешаем не-ASCII комментарии (русские комментарии) Style/AsciiComments: Enabled: false + +# МЕТРИКИ И ДЛИНЫ + Layout/LineLength: Max: 200 + AllowURI: true + AllowHeredoc: true + Metrics/MethodLength: - Enabled: false + Max: 30 + CountAsOne: ['array', 'hash', 'heredoc'] + Metrics/ClassLength: - Max: 140 + Max: 200 + CountAsOne: ['array', 'hash', 'heredoc'] + +Metrics/ModuleLength: + Max: 200 + +Metrics/BlockLength: + Max: 50 + CountAsOne: ['array', 'hash', 'heredoc'] + Exclude: + - 'spec/**/*' + - 'config/routes.rb' + Metrics/AbcSize: - Enabled: false + Max: 20 + Metrics/CyclomaticComplexity: + Max: 15 + +Metrics/PerceivedComplexity: + Max: 15 + +# LAYOUT И ФОРМАТИРОВАНИЕ + +Layout/EmptyLinesAroundAttributeAccessor: + Enabled: true + +Layout/SpaceAroundMethodCallOperator: + Enabled: true + +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented + +# RAILS-СПЕЦИФИЧНЫЕ ПРАВИЛА + +Rails/FilePath: + Enabled: false + +Rails/UnknownEnv: + Environments: + - production + - development + - test + +Rails/SkipsModelValidations: + Enabled: false # Для bulk import операций + +# RSPEC-СПЕЦИФИЧНЫЕ ПРАВИЛА + +RSpec/ExampleLength: Max: 20 + +RSpec/MultipleExpectations: + Max: 5 + +RSpec/NestedGroups: + Max: 4 + +RSpec/DescribeClass: + Enabled: false + +# PERFORMANCE ПРАВИЛА + +Performance/StringReplacement: + Enabled: true + +Performance/RedundantBlockCall: + Enabled: true + +Performance/StringInclude: + Enabled: true + +# БЕЗОПАСНОСТЬ + +Security/Open: + Enabled: true + +Security/YAMLLoad: + Enabled: true + +# ИМЕНОВАНИЕ + +Naming/PredicatePrefix: + Enabled: true + AllowedMethods: + - is_a? + - has_key? + +Naming/MemoizedInstanceVariableName: + Enabled: false + +# STYLE ПРАВИЛА + +Style/HashEachMethods: + Enabled: true + +Style/HashTransformKeys: + Enabled: true + +Style/HashTransformValues: + Enabled: true + +Style/RedundantRegexpEscape: + Enabled: false # Может конфликтовать с некоторыми регексами + +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + +Style/SymbolArray: + Enabled: true + EnforcedStyle: brackets + +Style/WordArray: + Enabled: true + EnforcedStyle: brackets + +# Разрешаем trailing запятые +Style/TrailingCommaInArguments: + Enabled: true + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInArrayLiteral: + Enabled: true + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + Enabled: true + EnforcedStyleForMultiline: comma diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..7f609f6d --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,7 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# and is meant to be a temporary file to ease migration to stricter rules. +# Remove or comment out rules as you fix the violations. + +# Uncomment below if you have many violations and want to fix them gradually +# inherit_from: .rubocop_todo.yml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..3e1ce0e7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,25 @@ +{ + "ruby.useLanguageServer": true, + "ruby.format": "rubocop", + "ruby.lint": { + "rubocop": true, + "ruby": false + }, + "ruby.rubocop.executePath": "bundle exec rubocop", + "ruby.rubocop.configFilePath": ".rubocop.yml", + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "[ruby]": { + "editor.defaultFormatter": "rebornix.Ruby", + "editor.formatOnSave": true, + "editor.rulers": [200], + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "emmet.includeLanguages": { + "erb": "html" + } +} diff --git a/Gemfile b/Gemfile index 250854d0..78ed876a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,50 +1,52 @@ -source 'https://rubygems.org' +source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby file: '.ruby-version' +ruby file: ".ruby-version" # Core Rails gems -gem 'bootsnap' -gem 'listen' -gem 'pg' -gem 'puma' -gem 'rails', '~> 8.0.1' -gem 'sprockets-rails' +gem "bootsnap" +gem "listen" +gem "pg" +gem "puma" +gem "rails", "~> 8.0.1" +gem "sprockets-rails" # Core application dependencies -gem 'activerecord-import' -gem 'oj' -gem 'ruby-progressbar' +gem "activerecord-import" +gem "oj" +gem "ruby-progressbar" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] group :development do # Database monitoring and optimization - gem 'bullet' - gem 'pghero' - gem 'pg_query', '>= 2' - gem 'strong_migrations' + gem "bullet" + gem "pghero" + gem "pg_query", ">= 2" + gem "strong_migrations" end group :development, :test do + # Code quality and linting + gem "rubocop", require: false + gem "rubocop-minitest", require: false + gem "rubocop-performance", require: false + gem "rubocop-rails", require: false + gem "rubocop-rspec", require: false + # Profiling tools - gem 'memory_profiler', require: false - gem 'pry' - gem 'rack-mini-profiler', require: false - gem 'ruby-prof', require: false - gem 'stackprof', require: false - gem 'test-prof', '~> 1.4.4' - gem 'vernier', '~> 1.7', require: false + gem "memory_profiler", require: false + gem "pry" + gem "rack-mini-profiler", require: false + gem "ruby-prof", require: false + gem "stackprof", require: false + gem "test-prof", "~> 1.4.4" + gem "vernier", "~> 1.7", require: false end group :test do - gem 'profile-viewer', require: false - gem 'rspec-rails' - gem 'rspec-sqlimit' - - gem 'rubocop' - gem 'rubocop-minitest' - gem 'rubocop-performance' - gem 'rubocop-rspec' + gem "profile-viewer", require: false + gem "rspec-rails" + gem "rspec-sqlimit" end diff --git a/Gemfile.lock b/Gemfile.lock index bbcf0a9f..239ad5dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -263,6 +263,12 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) + rubocop-rails (2.33.3) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) rubocop-rspec (3.6.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) @@ -324,6 +330,7 @@ DEPENDENCIES rubocop rubocop-minitest rubocop-performance + rubocop-rails rubocop-rspec ruby-prof ruby-progressbar diff --git a/Rakefile b/Rakefile index e85f9139..9a5ea738 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require_relative 'config/application' +require_relative "config/application" Rails.application.load_tasks diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 286b2239..3c34c814 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' - layout 'mailer' + default from: "from@example.com" + layout "mailer" end diff --git a/app/models/bus.rb b/app/models/bus.rb index 1dcc54cb..c3ff49c6 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -1,16 +1,5 @@ class Bus < ApplicationRecord - MODELS = [ - 'Икарус', - 'Мерседес', - 'Сканиа', - 'Буханка', - 'УАЗ', - 'Спринтер', - 'ГАЗ', - 'ПАЗ', - 'Вольво', - 'Газель', - ].freeze + MODELS = ["Икарус", "Мерседес", "Сканиа", "Буханка", "УАЗ", "Спринтер", "ГАЗ", "ПАЗ", "Вольво", "Газель"].freeze has_many :trips has_and_belongs_to_many :services, join_table: :buses_services diff --git a/app/models/city.rb b/app/models/city.rb index 19ec7f36..53c2e295 100644 --- a/app/models/city.rb +++ b/app/models/city.rb @@ -3,6 +3,6 @@ class City < ApplicationRecord validate :name_has_no_spaces def name_has_no_spaces - errors.add(:name, "has spaces") if name.include?(' ') + errors.add(:name, "has spaces") if name.include?(" ") end end diff --git a/app/models/service.rb b/app/models/service.rb index 9cbb2a32..06e925a5 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,15 +1,15 @@ class Service < ApplicationRecord SERVICES = [ - 'WiFi', - 'Туалет', - 'Работающий туалет', - 'Ремни безопасности', - 'Кондиционер общий', - 'Кондиционер Индивидуальный', - 'Телевизор общий', - 'Телевизор индивидуальный', - 'Стюардесса', - 'Можно не печатать билет', + "WiFi", + "Туалет", + "Работающий туалет", + "Ремни безопасности", + "Кондиционер общий", + "Кондиционер Индивидуальный", + "Телевизор общий", + "Телевизор индивидуальный", + "Стюардесса", + "Можно не печатать билет", ].freeze has_and_belongs_to_many :buses, join_table: :buses_services diff --git a/app/models/trip.rb b/app/models/trip.rb index 9d63dfff..5bee8f26 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -1,15 +1,15 @@ class Trip < ApplicationRecord HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/ - belongs_to :from, class_name: 'City' - belongs_to :to, class_name: 'City' + belongs_to :from, class_name: "City" + belongs_to :to, class_name: "City" belongs_to :bus validates :from, presence: true validates :to, presence: true validates :bus, presence: true - validates :start_time, format: { with: HHMM_REGEXP, message: 'Invalid time' } + validates :start_time, format: { with: HHMM_REGEXP, message: "Invalid time" } validates :duration_minutes, presence: true validates :duration_minutes, numericality: { greater_than: 0 } validates :price_cents, presence: true diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb index abd79756..65c4704d 100644 --- a/app/services/json_trip_loader.rb +++ b/app/services/json_trip_loader.rb @@ -12,78 +12,74 @@ def load!(path) ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil time = Benchmark.realtime do - begin - # тихий режим - ActiveRecord::Base.logger = nil - ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + # тихий режим + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - ActiveRecord::Base.transaction do - ActiveRecord::Base.connection.execute <<~SQL - TRUNCATE TABLE - buses_services, - trips, - buses, - services, - cities - RESTART IDENTITY - CASCADE; - SQL + ActiveRecord::Base.transaction do + ActiveRecord::Base.connection.execute <<~SQL + TRUNCATE TABLE + buses_services, + trips, + buses, + services, + cities + RESTART IDENTITY + CASCADE; + SQL - city_id_cache = {} - service_id_cache = {} - bus_cache = {} - trips = [] + city_id_cache = {} + service_id_cache = {} + bus_cache = {} + trips = [] - progress_bar = ProgressBar.create( - title: "Loading trips", - total: json.size, - format: "%t: |%B| %p%% %e" - ) + progress_bar = ProgressBar.create( + title: "Loading trips", + total: json.size, + format: "%t: |%B| %p%% %e", + ) - json.each do |trip| - from_name = trip['from'] - to_name = trip['to'] + json.each do |trip| + from_name = trip["from"] + to_name = trip["to"] - from_id = city_id_cache[from_name] ||= City.find_or_create_by(name: from_name).id - to_id = city_id_cache[to_name] ||= City.find_or_create_by(name: to_name).id + from_id = city_id_cache[from_name] ||= City.find_or_create_by(name: from_name).id + to_id = city_id_cache[to_name] ||= City.find_or_create_by(name: to_name).id - service_ids = trip['bus']['services'].map do |name| - service_id_cache[name] ||= Service.find_or_create_by(name:).id - end - - number = trip['bus']['number'] - model = trip['bus']['model'] - bus = bus_cache[number] ||= Bus.find_or_create_by(number:) - bus.model = model if bus.model != model - bus.service_ids = service_ids if bus.service_ids != service_ids - bus.save if bus.changed? + service_ids = trip["bus"]["services"].map do |name| + service_id_cache[name] ||= Service.find_or_create_by(name:).id + end - # Используем массив - самый быстрый вариант - trips << [ - from_id, - to_id, - bus.id, - trip['start_time'], - trip['duration_minutes'], - trip['price_cents'] - ] + number = trip["bus"]["number"] + model = trip["bus"]["model"] + bus = bus_cache[number] ||= Bus.find_or_create_by(number:) + bus.model = model if bus.model != model + bus.service_ids = service_ids if bus.service_ids != service_ids + bus.save if bus.changed? - progress_bar.increment - end + # Используем массив - самый быстрый вариант + trips << [ + from_id, + to_id, + bus.id, + trip["start_time"], + trip["duration_minutes"], + trip["price_cents"], + ] - # Указываем колонки явно при импорте массивов - Trip.import( - [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents], - trips, - validate: false - ) - end - ensure - ActiveRecord::Base.logger = old_logger - if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - ActiveRecord::Base.verbose_query_logs = old_verbose + progress_bar.increment end + + # Указываем колонки явно при импорте массивов + Trip.import( + [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents], + trips, + validate: false, + ) end + ensure + ActiveRecord::Base.logger = old_logger + ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" diff --git a/app/services/json_trip_loader_ai.rb b/app/services/json_trip_loader_ai.rb index d3218515..d6376a7b 100644 --- a/app/services/json_trip_loader_ai.rb +++ b/app/services/json_trip_loader_ai.rb @@ -2,7 +2,6 @@ require "benchmark" require "ruby-progressbar" -require "set" class JsonTripLoaderAi BATCH_SIZE = 5_000 @@ -22,138 +21,136 @@ def load!(path) ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil time = Benchmark.realtime do - begin - # Тихий режим логгера, чтобы не тратить время на форматирование логов - ActiveRecord::Base.logger = nil - ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - - ActiveRecord::Base.transaction do - # Быстрый сброс - ActiveRecord::Base.connection.execute <<~SQL - TRUNCATE TABLE - buses_services, - trips, - buses, - services, - cities - RESTART IDENTITY - CASCADE; - SQL - - # 1) Собираем уникальные сущности из JSON - city_names = Set.new - service_names = Set.new - bus_number_to_model = {} # number => model - bus_number_to_service_names = Hash.new { |h, k| h[k] = Set.new } - - json.each do |trip| - from_name = trip["from"] - to_name = trip["to"] - city_names << from_name - city_names << to_name - - number = trip["bus"]["number"] - model = trip["bus"]["model"] - bus_number_to_model[number] = model - - (trip["bus"]["services"] || []).each do |sname| - service_names << sname - bus_number_to_service_names[number] << sname - end - end - - # 2) Вставляем города/сервисы одним махом (ignore duplicates) - if city_names.any? - City.import( - city_names.map { |n| City.new(name: n) }, - on_duplicate_key_ignore: true, - validate: false - ) + # Тихий режим логгера, чтобы не тратить время на форматирование логов + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + + ActiveRecord::Base.transaction do + # Быстрый сброс + ActiveRecord::Base.connection.execute <<~SQL + TRUNCATE TABLE + buses_services, + trips, + buses, + services, + cities + RESTART IDENTITY + CASCADE; + SQL + + # 1) Собираем уникальные сущности из JSON + city_names = Set.new + service_names = Set.new + bus_number_to_model = {} # number => model + bus_number_to_service_names = Hash.new { |h, k| h[k] = Set.new } + + json.each do |trip| + from_name = trip["from"] + to_name = trip["to"] + city_names << from_name + city_names << to_name + + number = trip["bus"]["number"] + model = trip["bus"]["model"] + bus_number_to_model[number] = model + + (trip["bus"]["services"] || []).each do |sname| + service_names << sname + bus_number_to_service_names[number] << sname end + end - if service_names.any? - Service.import( - service_names.map { |n| Service.new(name: n) }, - on_duplicate_key_ignore: true, - validate: false - ) - end + # 2) Вставляем города/сервисы одним махом (ignore duplicates) + if city_names.any? + City.import( + city_names.map { |n| City.new(name: n) }, + on_duplicate_key_ignore: true, + validate: false, + ) + end - # 3) Получаем маппинги name -> id одним запросом на каждую таблицу - city_id_by_name = City.where(name: city_names.to_a).pluck(:name, :id).to_h - service_id_by_name = Service.where(name: service_names.to_a).pluck(:name, :id).to_h - - # 4) Upsert автобусов (обновляем model при конфликте по number) - if bus_number_to_model.any? - # Через activerecord-import с on_duplicate_key_update - Bus.import( - bus_number_to_model.map { |number, model| Bus.new(number: number, model: model) }, - on_duplicate_key_update: [:model], - validate: false - ) - end + if service_names.any? + Service.import( + service_names.map { |n| Service.new(name: n) }, + on_duplicate_key_ignore: true, + validate: false, + ) + end - # 5) Получаем маппинг автобусов number -> id одним запросом - bus_id_by_number = Bus.where(number: bus_number_to_model.keys).pluck(:number, :id).to_h - - # 6) Пакетно вставляем связи bus↔service в join-таблицу (ignore duplicates) - join_rows = [] - bus_number_to_service_names.each do |number, sset| - bus_id = bus_id_by_number[number] - sset.each do |sname| - sid = service_id_by_name[sname] - next unless bus_id && sid - join_rows << { bus_id: bus_id, service_id: sid } - end - end + # 3) Получаем маппинги name -> id одним запросом на каждую таблицу + city_id_by_name = City.where(name: city_names.to_a).pluck(:name, :id).to_h + service_id_by_name = Service.where(name: service_names.to_a).pluck(:name, :id).to_h + + # 4) Upsert автобусов (обновляем model при конфликте по number) + if bus_number_to_model.any? + # Через activerecord-import с on_duplicate_key_update + Bus.import( + bus_number_to_model.map { |number, model| Bus.new(number: number, model: model) }, + on_duplicate_key_update: [:model], + validate: false, + ) + end - if join_rows.any? - # Через модель BusService — activerecord-import игнорирует дубликаты - BusService.import( - join_rows, - on_duplicate_key_ignore: true, - validate: false - ) - end + # 5) Получаем маппинг автобусов number -> id одним запросом + bus_id_by_number = Bus.where(number: bus_number_to_model.keys).pluck(:number, :id).to_h - # 7) Формируем и заливаем trips батчами - trips_buffer = [] - trips_buffer.reserve([json.size, BATCH_SIZE].min) if trips_buffer.respond_to?(:reserve) + # 6) Пакетно вставляем связи bus↔service в join-таблицу (ignore duplicates) + join_rows = [] + bus_number_to_service_names.each do |number, sset| + bus_id = bus_id_by_number[number] + sset.each do |sname| + sid = service_id_by_name[sname] + next unless bus_id && sid - flush_trips = lambda do - next if trips_buffer.empty? - Trip.import(trips_buffer, validate: true, batch_size: BATCH_SIZE) - trips_buffer.clear + join_rows << { bus_id: bus_id, service_id: sid } end + end - # прогрессбар на количество записей - progress_bar = ProgressBar.create( - title: "Loading trips", - total: json.size, - format: "%t: |%B| %p%% %e" + if join_rows.any? + # Через модель BusService — activerecord-import игнорирует дубликаты + BusService.import( + join_rows, + on_duplicate_key_ignore: true, + validate: false, ) + end - json.each do |t| - trips_buffer << Trip.new( - from_id: city_id_by_name[t["from"]], - to_id: city_id_by_name[t["to"]], - bus_id: bus_id_by_number[t["bus"]["number"]], - start_time: t["start_time"], - duration_minutes: t["duration_minutes"], - price_cents: t["price_cents"] - ) - flush_trips.call if trips_buffer.size >= BATCH_SIZE - - progress_bar.increment - end - flush_trips.call + # 7) Формируем и заливаем trips батчами + trips_buffer = [] + trips_buffer.reserve([json.size, BATCH_SIZE].min) if trips_buffer.respond_to?(:reserve) + + flush_trips = lambda do + next if trips_buffer.empty? + + Trip.import(trips_buffer, validate: true, batch_size: BATCH_SIZE) + trips_buffer.clear end - ensure - ActiveRecord::Base.logger = old_logger - if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - ActiveRecord::Base.verbose_query_logs = old_verbose + + # прогрессбар на количество записей + progress_bar = ProgressBar.create( + title: "Loading trips", + total: json.size, + format: "%t: |%B| %p%% %e", + ) + + json.each do |t| + trips_buffer << Trip.new( + from_id: city_id_by_name[t["from"]], + to_id: city_id_by_name[t["to"]], + bus_id: bus_id_by_number[t["bus"]["number"]], + start_time: t["start_time"], + duration_minutes: t["duration_minutes"], + price_cents: t["price_cents"], + ) + flush_trips.call if trips_buffer.size >= BATCH_SIZE + + progress_bar.increment end + flush_trips.call end + ensure + ActiveRecord::Base.logger = old_logger + ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" diff --git a/app/services/json_trip_loader_stream.rb b/app/services/json_trip_loader_stream.rb index 36095241..aac01672 100644 --- a/app/services/json_trip_loader_stream.rb +++ b/app/services/json_trip_loader_stream.rb @@ -12,39 +12,35 @@ def load!(path) old_verbose = ActiveRecord::Base.respond_to?(:verbose_query_logs) ? ActiveRecord::Base.verbose_query_logs : nil time = Benchmark.realtime do - begin - ActiveRecord::Base.logger = nil - ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - - ActiveRecord::Base.transaction do - clear_database - - # Кэши для уникальных значений - @city_cache = {} - @service_cache = {} - @bus_cache = {} - @trips_batch = [] - - # Получаем размер файла для прогресс-бара (приблизительно) - file_size = File.size(path) - progress_bar = ProgressBar.create( - title: "Loading trips", - total: file_size, - format: "%t: |%B| %p%% %e" - ) - - # Потоковое чтение и парсинг - stream_parse_file(path, progress_bar) - - # Импортируем последний батч - import_trips_batch if @trips_batch.any? - end - ensure - ActiveRecord::Base.logger = old_logger - if ActiveRecord::Base.respond_to?(:verbose_query_logs=) - ActiveRecord::Base.verbose_query_logs = old_verbose - end + ActiveRecord::Base.logger = nil + ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) + + ActiveRecord::Base.transaction do + clear_database + + # Кэши для уникальных значений + @city_cache = {} + @service_cache = {} + @bus_cache = {} + @trips_batch = [] + + # Получаем размер файла для прогресс-бара (приблизительно) + file_size = File.size(path) + progress_bar = ProgressBar.create( + title: "Loading trips", + total: file_size, + format: "%t: |%B| %p%% %e", + ) + + # Потоковое чтение и парсинг + stream_parse_file(path, progress_bar) + + # Импортируем последний батч + import_trips_batch if @trips_batch.any? end + ensure + ActiveRecord::Base.logger = old_logger + ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end puts "JsonTripLoader.load!: #{time.round(3)}s" @@ -66,7 +62,7 @@ def clear_database end def stream_parse_file(path, progress_bar) - File.open(path, 'r') do |file| + File.open(path, "r") do |file| nesting = 0 str = +"" bytes_read = 0 @@ -77,28 +73,26 @@ def stream_parse_file(path, progress_bar) bytes_read += 1 # Обновляем прогресс каждые 10KB - if bytes_read % 10_000 == 0 - progress_bar.progress = bytes_read - end + progress_bar.progress = bytes_read if bytes_read % 10_000 == 0 case ch - when '[' + when "[" in_array = true if nesting == 0 str << ch if nesting > 0 - when ']' + when "]" # Конец корневого массива if nesting == 0 in_array = false next end str << ch - when '{' + when "{" nesting += 1 str << ch - when '}' + when "}" str << ch nesting -= 1 - + # Если закончился объект верхнего уровня (trip) if nesting == 0 && in_array begin @@ -111,42 +105,44 @@ def stream_parse_file(path, progress_bar) end str = +"" end - when ',' + when "," # Запятая между объектами - игнорируем если на верхнем уровне next if nesting == 0 + str << ch when /\s/ - # Пробелы - игнорируем если на верхнем уровне + # Пробелы - игнорируем если на верхнем уровне next if nesting == 0 + str << ch else str << ch if nesting > 0 end end - + progress_bar.finish end end def process_trip(trip) - from_id = get_or_create_city_id(trip['from']) - to_id = get_or_create_city_id(trip['to']) - bus_id = get_or_create_bus_id(trip['bus']) + from_id = get_or_create_city_id(trip["from"]) + to_id = get_or_create_city_id(trip["to"]) + bus_id = get_or_create_bus_id(trip["bus"]) @trips_batch << [ from_id, to_id, bus_id, - trip['start_time'], - trip['duration_minutes'], - trip['price_cents'] + trip["start_time"], + trip["duration_minutes"], + trip["price_cents"], ] # Импортируем батч когда набрался нужный размер - if @trips_batch.size >= BATCH_SIZE - import_trips_batch - @trips_batch.clear - end + return unless @trips_batch.size >= BATCH_SIZE + + import_trips_batch + @trips_batch.clear end def get_or_create_city_id(name) @@ -154,15 +150,15 @@ def get_or_create_city_id(name) end def get_or_create_bus_id(bus_data) - number = bus_data['number'] - + number = bus_data["number"] + @bus_cache[number] ||= begin bus = Bus.find_or_create_by(number: number) - bus.model = bus_data['model'] if bus.model != bus_data['model'] - - service_ids = bus_data['services'].map { |name| get_or_create_service_id(name) } + bus.model = bus_data["model"] if bus.model != bus_data["model"] + + service_ids = bus_data["services"].map { |name| get_or_create_service_id(name) } bus.service_ids = service_ids if bus.service_ids != service_ids - + bus.save if bus.changed? bus.id end @@ -176,7 +172,7 @@ def import_trips_batch Trip.import( [:from_id, :to_id, :bus_id, :start_time, :duration_minutes, :price_cents], @trips_batch, - validate: false + validate: false, ) end end diff --git a/bin/rails b/bin/rails index 5badb2fd..7a8ff81e 100755 --- a/bin/rails +++ b/bin/rails @@ -1,6 +1,6 @@ #!/usr/bin/env ruby begin - load File.expand_path('../spring', __FILE__) + load File.expand_path('spring', __dir__) rescue LoadError => e raise unless e.message.include?('spring') end diff --git a/bin/rake b/bin/rake index d87d5f57..0ba8c48c 100755 --- a/bin/rake +++ b/bin/rake @@ -1,6 +1,6 @@ #!/usr/bin/env ruby begin - load File.expand_path('../spring', __FILE__) + load File.expand_path('spring', __dir__) rescue LoadError => e raise unless e.message.include?('spring') end diff --git a/bin/spring b/bin/spring index fb2ec2eb..991bd4ef 100755 --- a/bin/spring +++ b/bin/spring @@ -8,7 +8,7 @@ unless defined?(Spring) require 'bundler' lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) - spring = lockfile.specs.detect { |spec| spec.name == "spring" } + spring = lockfile.specs.detect { |spec| spec.name == 'spring' } if spring Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path gem 'spring', spring.version diff --git a/bin/yarn b/bin/yarn index 460dd565..d3627c34 100755 --- a/bin/yarn +++ b/bin/yarn @@ -1,11 +1,9 @@ #!/usr/bin/env ruby APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do - begin - exec "yarnpkg", *ARGV - rescue Errno::ENOENT - $stderr.puts "Yarn executable was not detected in the system." - $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" - exit 1 - end + exec 'yarnpkg', *ARGV +rescue Errno::ENOENT + warn 'Yarn executable was not detected in the system.' + warn 'Download Yarn at https://yarnpkg.com/en/docs/install' + exit 1 end diff --git a/config.ru b/config.ru index f7ba0b52..441e6ff0 100644 --- a/config.ru +++ b/config.ru @@ -1,5 +1,5 @@ # This file is used by Rack-based servers to start the application. -require_relative 'config/environment' +require_relative "config/environment" run Rails.application diff --git a/config/application.rb b/config/application.rb index f714e62d..da809c4b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,6 +1,6 @@ -require_relative 'boot' +require_relative "boot" -require 'rails/all' +require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. diff --git a/config/boot.rb b/config/boot.rb index b9e460ce..988a5ddc 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environment.rb b/config/environment.rb index 426333bb..cac53157 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require_relative 'application' +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 99fef80d..8637586b 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,12 +14,12 @@ # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + if Rails.root.join("tmp", "caching-dev.txt").exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.to_i}" + "Cache-Control" => "public, max-age=#{2.days.to_i}", } else config.action_controller.perform_caching = false @@ -60,8 +60,8 @@ config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.after_initialize do - Bullet.enable = true # Включает работу Bullet - Bullet.alert = false # Показывает всплывающее JS-окно в браузере при обнаружении проблемы + Bullet.enable = true # Включает работу Bullet + Bullet.alert = false # Показывает всплывающее JS-окно в браузере при обнаружении проблемы Bullet.bullet_logger = true # Записывает предупреждения в log/bullet.log Bullet.console = true # Выводит предупреждения в консоль браузера (console.log) Bullet.rails_logger = true # Записывает предупреждения в Rails логи @@ -94,8 +94,8 @@ # Bullet.opentelemetry = true # Отправка уведомлений в OpenTelemetry - Bullet.raise = false # Если true — будет выбрасывать исключение при проблемах (удобно в тестах) - Bullet.always_append_html_body = false # Если true — всегда вставлять HTML с предупреждениями даже без + Bullet.raise = false # Если true — будет выбрасывать исключение при проблемах (удобно в тестах) + Bullet.always_append_html_body = false # Если true — всегда вставлять HTML с предупреждениями даже без Bullet.skip_user_in_notification = false # Если true — не добавлять инфо о пользователе в уведомления (актуально в multi-user) end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 613d8289..2e585da0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -20,7 +20,7 @@ # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier @@ -54,7 +54,7 @@ config.log_level = :debug # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -77,7 +77,7 @@ config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' diff --git a/config/environments/test.rb b/config/environments/test.rb index 0a38fd3c..b48e5699 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -15,7 +15,7 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + "Cache-Control" => "public, max-age=#{1.hour.to_i}", } # Show full error reports and disable caching. diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb index d405c6ed..51660d38 100644 --- a/config/initializers/strong_migrations.rb +++ b/config/initializers/strong_migrations.rb @@ -1,5 +1,5 @@ # Mark existing migrations as safe -StrongMigrations.start_after = 20250816103323 +StrongMigrations.start_after = 20_250_816_103_323 # Set timeouts for migrations # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user diff --git a/config/routes.rb b/config/routes.rb index 0fe2bd78..f2784ed4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,5 +2,5 @@ # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html get "автобусы/:from/:to" => "trips#index" - mount PgHero::Engine, at: 'pghero' + mount PgHero::Engine, at: "pghero" end diff --git a/config/spring.rb b/config/spring.rb index 9fa7863f..bb9dd999 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,6 @@ -%w[ - .ruby-version - .rbenv-vars - tmp/restart.txt - tmp/caching-dev.txt +[ + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt", ].each { |path| Spring.watch(path) } diff --git a/db/migrate/20250816103837_create_pghero_space_stats.rb b/db/migrate/20250816103837_create_pghero_space_stats.rb index 5592db37..a9c10a3a 100644 --- a/db/migrate/20250816103837_create_pghero_space_stats.rb +++ b/db/migrate/20250816103837_create_pghero_space_stats.rb @@ -8,6 +8,6 @@ def change t.timestamp :captured_at end - add_index :pghero_space_stats, [:database, :captured_at] + add_index :pghero_space_stats, %i[database captured_at] end end diff --git a/db/migrate/20250816103847_create_pghero_query_stats.rb b/db/migrate/20250816103847_create_pghero_query_stats.rb index 74aaaa9a..5881a639 100644 --- a/db/migrate/20250816103847_create_pghero_query_stats.rb +++ b/db/migrate/20250816103847_create_pghero_query_stats.rb @@ -10,6 +10,6 @@ def change t.timestamp :captured_at end - add_index :pghero_query_stats, [:database, :captured_at] + add_index :pghero_query_stats, %i[database captured_at] end end diff --git a/db/schema.rb b/db/schema.rb index 6a52dfa7..cea04d83 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,55 +10,55 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_08_16_140257) do +ActiveRecord::Schema[8.0].define(version: 20_250_816_140_257) do # These are extensions that must be enabled in order to support this database - enable_extension "pg_catalog.plpgsql" - enable_extension "pg_stat_statements" + enable_extension 'pg_catalog.plpgsql' + enable_extension 'pg_stat_statements' - create_table "buses", force: :cascade do |t| - t.string "number" - t.string "model" + create_table 'buses', force: :cascade do |t| + t.string 'number' + t.string 'model' end - create_table "buses_services", force: :cascade do |t| - t.integer "bus_id" - t.integer "service_id" + create_table 'buses_services', force: :cascade do |t| + t.integer 'bus_id' + t.integer 'service_id' end - create_table "cities", force: :cascade do |t| - t.string "name" + create_table 'cities', force: :cascade do |t| + t.string 'name' end - create_table "pghero_query_stats", force: :cascade do |t| - t.text "database" - t.text "user" - t.text "query" - t.bigint "query_hash" - t.float "total_time" - t.bigint "calls" - t.datetime "captured_at", precision: nil - t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at" + create_table 'pghero_query_stats', force: :cascade do |t| + t.text 'database' + t.text 'user' + t.text 'query' + t.bigint 'query_hash' + t.float 'total_time' + t.bigint 'calls' + t.datetime 'captured_at', precision: nil + t.index %w[database captured_at], name: 'index_pghero_query_stats_on_database_and_captured_at' end - create_table "pghero_space_stats", force: :cascade do |t| - t.text "database" - t.text "schema" - t.text "relation" - t.bigint "size" - t.datetime "captured_at", precision: nil - t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at" + create_table 'pghero_space_stats', force: :cascade do |t| + t.text 'database' + t.text 'schema' + t.text 'relation' + t.bigint 'size' + t.datetime 'captured_at', precision: nil + t.index %w[database captured_at], name: 'index_pghero_space_stats_on_database_and_captured_at' end - create_table "services", force: :cascade do |t| - t.string "name" + create_table 'services', force: :cascade do |t| + t.string 'name' end - create_table "trips", force: :cascade do |t| - t.integer "from_id" - t.integer "to_id" - t.string "start_time" - t.integer "duration_minutes" - t.integer "price_cents" - t.integer "bus_id" + create_table 'trips', force: :cascade do |t| + t.integer 'from_id' + t.integer 'to_id' + t.string 'start_time' + t.integer 'duration_minutes' + t.integer 'price_cents' + t.integer 'bus_id' end end diff --git a/script/stackprof/table.rb b/script/stackprof/table.rb index fe389abd..8c44f9c8 100644 --- a/script/stackprof/table.rb +++ b/script/stackprof/table.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'stackprof' -require_relative '../../config/environment' +require "stackprof" +require_relative "../../config/environment" -StackProf.run(mode: :wall, out: 'tmp/reports/stackprof/table.dump') do - Object.const_get(ARGV[0] || 'JsonTripLoader').new.load!(ARGV[1] || 'fixtures/small.json') +StackProf.run(mode: :wall, out: "tmp/reports/stackprof/table.dump") do + Object.const_get(ARGV[0] || "JsonTripLoader").new.load!(ARGV[1] || "fixtures/small.json") end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 79811b8f..e644fc78 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,13 +1,13 @@ # This file is copied to spec/ when you run 'rails generate rspec:install' -require 'spec_helper' -ENV['RAILS_ENV'] ||= 'test' -require_relative '../config/environment' +require "spec_helper" +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? # Uncomment the line below in case you have `--require rails_helper` in the `.rspec` file # that will avoid rails generators crashing because migrations haven't been run yet # return unless Rails.env.test? -require 'rspec/rails' +require "rspec/rails" # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in @@ -37,7 +37,7 @@ RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_paths = [ - Rails.root.join('spec/fixtures') + Rails.root.join("spec/fixtures"), ] # If you're not using ActiveRecord, or you'd prefer not to run each of your diff --git a/spec/services/json_trip_loader_spec.rb b/spec/services/json_trip_loader_spec.rb index 9314138a..32847786 100644 --- a/spec/services/json_trip_loader_spec.rb +++ b/spec/services/json_trip_loader_spec.rb @@ -6,23 +6,23 @@ it "limits SELECT/INSERT/DELETE/UPDATE separately" do # SELECT - expect { + expect do described_class.new.load!(path.to_s) - }.not_to exceed_query_limit(1).with(/\bSELECT\b/i) + end.not_to exceed_query_limit(1).with(/\bSELECT\b/i) # INSERT (включая UPSERT — это INSERT ... ON CONFLICT) - expect { + expect do described_class.new.load!(path.to_s) - }.not_to exceed_query_limit(10).with(/\bINSERT\b/i) + end.not_to exceed_query_limit(10).with(/\bINSERT\b/i) # DELETE - expect { + expect do described_class.new.load!(path.to_s) - }.not_to exceed_query_limit(1).with(/\bDELETE\b/i) + end.not_to exceed_query_limit(1).with(/\bDELETE\b/i) # UPDATE — должно быть 0, если всё через upsert_all и нет отдельных update - expect { + expect do described_class.new.load!(path.to_s) - }.not_to exceed_query_limit(0).with(/\bUPDATE\b/i) + end.not_to exceed_query_limit(0).with(/\bUPDATE\b/i) end -end \ No newline at end of file +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 327b58ea..9c96a9b6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,51 +44,49 @@ # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ - config.disable_monkey_patching! - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = "doc" - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3ab84e3d..e8dc819d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,10 +1,12 @@ -ENV['RAILS_ENV'] ||= 'test' -require_relative '../config/environment' -require 'rails/test_help' +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" -class ActiveSupport::TestCase - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all +module ActiveSupport + class TestCase + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all - # Add more helper methods to be used by all tests here... + # Add more helper methods to be used by all tests here... + end end From 6171184935f506a59deecf275d2647fa0c254c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sun, 17 Aug 2025 09:14:59 +0300 Subject: [PATCH 09/11] fix(rubocop): refactor 2 --- .rubocop.yml | 2 ++ .ruby-version | 2 +- Gemfile | 2 ++ Gemfile.lock | 2 +- Rakefile | 2 ++ app/channels/application_cable/channel.rb | 2 ++ app/channels/application_cable/connection.rb | 2 ++ app/controllers/application_controller.rb | 2 ++ app/controllers/trips_controller.rb | 6 +++-- app/helpers/application_helper.rb | 2 ++ app/jobs/application_job.rb | 2 ++ app/mailers/application_mailer.rb | 2 ++ app/models/application_record.rb | 2 ++ app/models/bus.rb | 4 +++- app/models/city.rb | 2 ++ app/models/service.rb | 2 ++ app/models/trip.rb | 6 ++--- app/services/json_trip_loader.rb | 4 ++-- app/services/json_trip_loader_ai.rb | 4 ++-- app/services/json_trip_loader_stream.rb | 24 +++++++++---------- config.ru | 2 ++ config/application.rb | 2 ++ config/boot.rb | 2 ++ config/environment.rb | 2 ++ config/environments/development.rb | 2 ++ config/environments/production.rb | 4 +++- config/environments/test.rb | 2 ++ .../application_controller_renderer.rb | 2 ++ config/initializers/assets.rb | 2 ++ config/initializers/backtrace_silencers.rb | 2 ++ .../initializers/content_security_policy.rb | 2 ++ config/initializers/cookies_serializer.rb | 2 ++ .../initializers/filter_parameter_logging.rb | 2 ++ config/initializers/inflections.rb | 2 ++ config/initializers/mime_types.rb | 2 ++ config/initializers/strong_migrations.rb | 2 ++ config/initializers/wrap_parameters.rb | 2 ++ config/puma.rb | 8 ++++--- config/routes.rb | 2 ++ config/spring.rb | 2 ++ db/seeds.rb | 2 ++ lib/tasks/utils.rake | 2 ++ spec/rails_helper.rb | 2 ++ spec/services/json_trip_loader_spec.rb | 2 ++ spec/spec_helper.rb | 2 ++ test/application_system_test_case.rb | 2 ++ test/test_helper.rb | 2 ++ 47 files changed, 109 insertions(+), 29 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 99935ea2..a8bf122a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -165,6 +165,8 @@ Style/SymbolArray: Style/WordArray: Enabled: true EnforcedStyle: brackets + Exclude: + - 'app/models/**/*' # Разрешаем trailing запятые Style/TrailingCommaInArguments: diff --git a/.ruby-version b/.ruby-version index 47b322c9..f9892605 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.1 +3.4.4 diff --git a/Gemfile b/Gemfile index 78ed876a..35f7f110 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } diff --git a/Gemfile.lock b/Gemfile.lock index 239ad5dd..17a97d29 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -342,7 +342,7 @@ DEPENDENCIES vernier (~> 1.7) RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.4p34 BUNDLED WITH 2.6.2 diff --git a/Rakefile b/Rakefile index 9a5ea738..d2a78aa2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb index d6726972..9aec2305 100644 --- a/app/channels/application_cable/channel.rb +++ b/app/channels/application_cable/channel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f..8d6c2a1b 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12..7944f9f9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base end diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb index acb38be2..1490d713 100644 --- a/app/controllers/trips_controller.rb +++ b/app/controllers/trips_controller.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class TripsController < ApplicationController def index - @from = City.find_by_name!(params[:from]) - @to = City.find_by_name!(params[:to]) + @from = City.find_by!(name: params[:from]) + @to = City.find_by!(name: params[:to]) @trips = Trip.where(from: @from, to: @to).order(:start_time) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de6be794..15b06f0f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index a009ace5..d92ffddc 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c814..5cc63a0c 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..71fbba5b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/bus.rb b/app/models/bus.rb index c3ff49c6..7b96d9b9 100644 --- a/app/models/bus.rb +++ b/app/models/bus.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class Bus < ApplicationRecord - MODELS = ["Икарус", "Мерседес", "Сканиа", "Буханка", "УАЗ", "Спринтер", "ГАЗ", "ПАЗ", "Вольво", "Газель"].freeze + MODELS = %w[Икарус Мерседес Сканиа Буханка УАЗ Спринтер ГАЗ ПАЗ Вольво Газель].freeze has_many :trips has_and_belongs_to_many :services, join_table: :buses_services diff --git a/app/models/city.rb b/app/models/city.rb index 53c2e295..fd2ef235 100644 --- a/app/models/city.rb +++ b/app/models/city.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class City < ApplicationRecord validates :name, presence: true, uniqueness: true validate :name_has_no_spaces diff --git a/app/models/service.rb b/app/models/service.rb index 06e925a5..707b4993 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Service < ApplicationRecord SERVICES = [ "WiFi", diff --git a/app/models/trip.rb b/app/models/trip.rb index 5bee8f26..d9369f9b 100644 --- a/app/models/trip.rb +++ b/app/models/trip.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Trip < ApplicationRecord HHMM_REGEXP = /([0-1][0-9]|[2][0-3]):[0-5][0-9]/ @@ -5,10 +7,6 @@ class Trip < ApplicationRecord belongs_to :to, class_name: "City" belongs_to :bus - validates :from, presence: true - validates :to, presence: true - validates :bus, presence: true - validates :start_time, format: { with: HHMM_REGEXP, message: "Invalid time" } validates :duration_minutes, presence: true validates :duration_minutes, numericality: { greater_than: 0 } diff --git a/app/services/json_trip_loader.rb b/app/services/json_trip_loader.rb index 65c4704d..6ce55453 100644 --- a/app/services/json_trip_loader.rb +++ b/app/services/json_trip_loader.rb @@ -17,7 +17,7 @@ def load!(path) ActiveRecord::Base.verbose_query_logs = false if ActiveRecord::Base.respond_to?(:verbose_query_logs=) ActiveRecord::Base.transaction do - ActiveRecord::Base.connection.execute <<~SQL + ActiveRecord::Base.connection.execute <<~SQL.squish TRUNCATE TABLE buses_services, trips, @@ -82,6 +82,6 @@ def load!(path) ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end - puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" + Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" } end end diff --git a/app/services/json_trip_loader_ai.rb b/app/services/json_trip_loader_ai.rb index d6376a7b..43811240 100644 --- a/app/services/json_trip_loader_ai.rb +++ b/app/services/json_trip_loader_ai.rb @@ -27,7 +27,7 @@ def load!(path) ActiveRecord::Base.transaction do # Быстрый сброс - ActiveRecord::Base.connection.execute <<~SQL + ActiveRecord::Base.connection.execute <<~SQL.squish TRUNCATE TABLE buses_services, trips, @@ -153,6 +153,6 @@ def load!(path) ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end - puts "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" + Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s (#{json.size} trips)" } end end diff --git a/app/services/json_trip_loader_stream.rb b/app/services/json_trip_loader_stream.rb index aac01672..f652abe2 100644 --- a/app/services/json_trip_loader_stream.rb +++ b/app/services/json_trip_loader_stream.rb @@ -43,13 +43,13 @@ def load!(path) ActiveRecord::Base.verbose_query_logs = old_verbose if ActiveRecord::Base.respond_to?(:verbose_query_logs=) end - puts "JsonTripLoader.load!: #{time.round(3)}s" + Rails.logger.debug { "JsonTripLoader.load!: #{time.round(3)}s" } end private def clear_database - ActiveRecord::Base.connection.execute <<~SQL + ActiveRecord::Base.connection.execute <<~SQL.squish TRUNCATE TABLE buses_services, trips, @@ -73,15 +73,15 @@ def stream_parse_file(path, progress_bar) bytes_read += 1 # Обновляем прогресс каждые 10KB - progress_bar.progress = bytes_read if bytes_read % 10_000 == 0 + progress_bar.progress = bytes_read if (bytes_read % 10_000).zero? case ch when "[" - in_array = true if nesting == 0 - str << ch if nesting > 0 + in_array = true if nesting.zero? + str << ch if nesting.positive? when "]" # Конец корневого массива - if nesting == 0 + if nesting.zero? in_array = false next end @@ -94,29 +94,29 @@ def stream_parse_file(path, progress_bar) nesting -= 1 # Если закончился объект верхнего уровня (trip) - if nesting == 0 && in_array + if nesting.zero? && in_array begin trip = Oj.load(str) process_trip(trip) rescue Oj::ParseError => e - puts "Parse error: #{e.message}" - puts "String: #{str[0..100]}..." if str.length > 100 + Rails.logger.debug { "Parse error: #{e.message}" } + Rails.logger.debug { "String: #{str[0..100]}..." } if str.length > 100 raise end str = +"" end when "," # Запятая между объектами - игнорируем если на верхнем уровне - next if nesting == 0 + next if nesting.zero? str << ch when /\s/ # Пробелы - игнорируем если на верхнем уровне - next if nesting == 0 + next if nesting.zero? str << ch else - str << ch if nesting > 0 + str << ch if nesting.positive? end end diff --git a/config.ru b/config.ru index 441e6ff0..bff88d60 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. require_relative "config/environment" diff --git a/config/application.rb b/config/application.rb index da809c4b..50f4cf7d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "boot" require "rails/all" diff --git a/config/boot.rb b/config/boot.rb index 988a5ddc..aef6d031 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/config/environment.rb b/config/environment.rb index cac53157..7df99e89 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Load the Rails application. require_relative "application" diff --git a/config/environments/development.rb b/config/environments/development.rb index 8637586b..24e46a99 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/config/environments/production.rb b/config/environments/production.rb index 2e585da0..39f3b0e0 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -84,7 +86,7 @@ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV["RAILS_LOG_TO_STDOUT"].present? - logger = ActiveSupport::Logger.new(STDOUT) + logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end diff --git a/config/environments/test.rb b/config/environments/test.rb index b48e5699..f98f7779 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 89d2efab..6d56e439 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 49a8c192..833a302e 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf..4b63f289 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index d3bcaa5e..e3c96496 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Define an application-wide content security policy diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 5a6a32d3..ee8dff9c 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..7a4f47b4 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9..dc847422 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc189968..be6fedc5 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/strong_migrations.rb b/config/initializers/strong_migrations.rb index 51660d38..0b4b2dc4 100644 --- a/config/initializers/strong_migrations.rb +++ b/config/initializers/strong_migrations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Mark existing migrations as safe StrongMigrations.start_after = 20_250_816_103_323 diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index bbfc3961..2f3c0db4 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which diff --git a/config/puma.rb b/config/puma.rb index a5eccf81..c938cdc3 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch("PORT", 3000) # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch("RAILS_ENV", "development") # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index f2784ed4..3c406493 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html get "автобусы/:from/:to" => "trips#index" diff --git a/config/spring.rb b/config/spring.rb index bb9dd999..12efcb38 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + [ ".ruby-version", ".rbenv-vars", diff --git a/db/seeds.rb b/db/seeds.rb index 1beea2ac..ebd18895 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index c48f19f8..8e6ee12a 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Наивная загрузка данных из json-файла в БД # rake 'reload_json[fixtures/small.json]' task :reload_json, [:file_name] => :environment do |_task, args| diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index e644fc78..1e899ef3 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file is copied to spec/ when you run 'rails generate rspec:install' require "spec_helper" ENV["RAILS_ENV"] ||= "test" diff --git a/spec/services/json_trip_loader_spec.rb b/spec/services/json_trip_loader_spec.rb index 32847786..67426f12 100644 --- a/spec/services/json_trip_loader_spec.rb +++ b/spec/services/json_trip_loader_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails_helper" require "rspec/sqlimit" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9c96a9b6..409c64b6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file was generated by the `rails generate rspec:install` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d19212ab..c05709af 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase diff --git a/test/test_helper.rb b/test/test_helper.rb index e8dc819d..47447b6f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" From f0a72af87c088d67ce9688b6c4e5c096e120c4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Sun, 17 Aug 2025 09:18:22 +0300 Subject: [PATCH 10/11] feat(ui): add `preload` --- .rubocop.yml | 4 +- app/views/trips/index.html.erb | 2 +- db/migrate/20250816140257_add_indexes.rb | 13 ++-- db/schema.rb | 75 ++++++++++++------------ lib/tasks/utils.rake | 7 +-- 5 files changed, 53 insertions(+), 48 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a8bf122a..89c7595f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -44,7 +44,7 @@ Layout/LineLength: AllowHeredoc: true Metrics/MethodLength: - Max: 30 + Max: 100 CountAsOne: ['array', 'hash', 'heredoc'] Metrics/ClassLength: @@ -55,7 +55,7 @@ Metrics/ModuleLength: Max: 200 Metrics/BlockLength: - Max: 50 + Max: 100 CountAsOne: ['array', 'hash', 'heredoc'] Exclude: - 'spec/**/*' diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb index a60bce41..79bc535a 100644 --- a/app/views/trips/index.html.erb +++ b/app/views/trips/index.html.erb @@ -5,7 +5,7 @@ <%= "В расписании #{@trips.count} рейсов" %> -<% @trips.each do |trip| %> +<% @trips.preload([bus: [:services]]).each do |trip| %>
    <%= render "trip", trip: trip %> <% if trip.bus.services.present? %> diff --git a/db/migrate/20250816140257_add_indexes.rb b/db/migrate/20250816140257_add_indexes.rb index 4431553b..962c8aa8 100644 --- a/db/migrate/20250816140257_add_indexes.rb +++ b/db/migrate/20250816140257_add_indexes.rb @@ -2,10 +2,13 @@ class AddIndexes < ActiveRecord::Migration[8.0] disable_ddl_transaction! def change - # add_index :buses, :number, - # algorithm: :concurrently - # - # add_index :trips, [:from_id, :to_id], - # algorithm: :concurrently + add_index :buses, :number, + algorithm: :concurrently + + add_index :trips, [:from_id, :to_id], + algorithm: :concurrently + + add_index :buses_services, :bus_id, + algorithm: :concurrently end end diff --git a/db/schema.rb b/db/schema.rb index cea04d83..296f5c67 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,55 +10,58 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 20_250_816_140_257) do +ActiveRecord::Schema[8.0].define(version: 2025_08_16_140257) do # These are extensions that must be enabled in order to support this database - enable_extension 'pg_catalog.plpgsql' - enable_extension 'pg_stat_statements' + enable_extension "pg_catalog.plpgsql" + enable_extension "pg_stat_statements" - create_table 'buses', force: :cascade do |t| - t.string 'number' - t.string 'model' + create_table "buses", force: :cascade do |t| + t.string "number" + t.string "model" + t.index ["number"], name: "index_buses_on_number" end - create_table 'buses_services', force: :cascade do |t| - t.integer 'bus_id' - t.integer 'service_id' + create_table "buses_services", force: :cascade do |t| + t.integer "bus_id" + t.integer "service_id" + t.index ["bus_id"], name: "index_buses_services_on_bus_id" end - create_table 'cities', force: :cascade do |t| - t.string 'name' + create_table "cities", force: :cascade do |t| + t.string "name" end - create_table 'pghero_query_stats', force: :cascade do |t| - t.text 'database' - t.text 'user' - t.text 'query' - t.bigint 'query_hash' - t.float 'total_time' - t.bigint 'calls' - t.datetime 'captured_at', precision: nil - t.index %w[database captured_at], name: 'index_pghero_query_stats_on_database_and_captured_at' + create_table "pghero_query_stats", force: :cascade do |t| + t.text "database" + t.text "user" + t.text "query" + t.bigint "query_hash" + t.float "total_time" + t.bigint "calls" + t.datetime "captured_at", precision: nil + t.index ["database", "captured_at"], name: "index_pghero_query_stats_on_database_and_captured_at" end - create_table 'pghero_space_stats', force: :cascade do |t| - t.text 'database' - t.text 'schema' - t.text 'relation' - t.bigint 'size' - t.datetime 'captured_at', precision: nil - t.index %w[database captured_at], name: 'index_pghero_space_stats_on_database_and_captured_at' + create_table "pghero_space_stats", force: :cascade do |t| + t.text "database" + t.text "schema" + t.text "relation" + t.bigint "size" + t.datetime "captured_at", precision: nil + t.index ["database", "captured_at"], name: "index_pghero_space_stats_on_database_and_captured_at" end - create_table 'services', force: :cascade do |t| - t.string 'name' + create_table "services", force: :cascade do |t| + t.string "name" end - create_table 'trips', force: :cascade do |t| - t.integer 'from_id' - t.integer 'to_id' - t.string 'start_time' - t.integer 'duration_minutes' - t.integer 'price_cents' - t.integer 'bus_id' + create_table "trips", force: :cascade do |t| + t.integer "from_id" + t.integer "to_id" + t.string "start_time" + t.integer "duration_minutes" + t.integer "price_cents" + t.integer "bus_id" + t.index ["from_id", "to_id"], name: "index_trips_on_from_id_and_to_id" end end diff --git a/lib/tasks/utils.rake b/lib/tasks/utils.rake index 8e6ee12a..9a1e7acb 100644 --- a/lib/tasks/utils.rake +++ b/lib/tasks/utils.rake @@ -1,7 +1,6 @@ # frozen_string_literal: true -# Наивная загрузка данных из json-файла в БД -# rake 'reload_json[fixtures/small.json]' -task :reload_json, [:file_name] => :environment do |_task, args| - JsonTripLoader.new.load!(args.file_name) +# rake 'reload_json[JsonTripLoader, fixtures/small.json]' +task :reload_json, [:loader, :file_name] => :environment do |_task, args| + Object.const_get(args.loader || "JsonTripLoader").new.load!(args.file_name) end From dc1c84c9688029983f83607109642484116d1ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=CA=9F=E1=B4=8F=E1=B4=A0=20K=E1=B4=8F=C9=B4s?= =?UTF-8?q?=E1=B4=9B=E1=B4=80=C9=B4=E1=B4=9B=C9=AA=C9=B4?= Date: Mon, 18 Aug 2025 20:45:13 +0300 Subject: [PATCH 11/11] Homework 3: commit local changes --- config/initializers/rack_mini_profiler.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb index 9b794335..27aa0b36 100644 --- a/config/initializers/rack_mini_profiler.rb +++ b/config/initializers/rack_mini_profiler.rb @@ -14,4 +14,8 @@ # Если initializer грузится поздно — инициализируем вручную Rack::MiniProfilerRails.initialize!(Rails.application) + + Rack::MiniProfiler.config.enable_advanced_debugging_tools = true + Rack::MiniProfiler.config.start_hidden = false + Rack::MiniProfiler.config.backtrace_threshold_ms = 2 # захватывать бэктрейсы у медленных блоков end