-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
220: Improve Rails perf 5-10x #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,3 +31,5 @@ | |
|
|
||
| # Ignore master key for decrypting credentials and more. | ||
| /config/master.key | ||
|
|
||
| /.idea | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,12 @@ | ||
| source "https://rubygems.org" | ||
|
|
||
| gem 'pg' | ||
| gem "falcon" | ||
| gem "oj" | ||
| gem "pg" | ||
| gem "prometheus-client" | ||
| gem "rage-rb" | ||
|
|
||
| gem 'prometheus-client' | ||
|
|
||
| # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" | ||
| gem "rails", "~> 7.2.1", ">= 7.2.1.2" | ||
| # Use sqlite3 as the database for Active Record | ||
| gem "sqlite3", ">= 1.4" | ||
| # Use the Puma web server [https://github.com/puma/puma] | ||
| gem "puma", ">= 5.0" | ||
| # Build JSON APIs with ease [https://github.com/rails/jbuilder] | ||
| # gem "jbuilder" | ||
| # Use Redis adapter to run Action Cable in production | ||
| # gem "redis", ">= 4.0.1" | ||
|
|
||
| # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] | ||
| # gem "kredis" | ||
|
|
||
| # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] | ||
| # gem "bcrypt", "~> 3.1.7" | ||
|
|
||
| # Windows does not include zoneinfo files, so bundle the tzinfo-data gem | ||
| gem "tzinfo-data", platforms: %i[ windows jruby ] | ||
| gem "rails" | ||
|
|
||
| # Reduces boot times through caching; required in config/boot.rb | ||
| gem "bootsnap", require: false | ||
|
|
||
| # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] | ||
| # gem "image_processing", "~> 1.2" | ||
|
|
||
| # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible | ||
| # gem "rack-cors" | ||
|
|
||
| group :development, :test do | ||
| # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem | ||
| gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" | ||
|
|
||
| # Static analysis for security vulnerabilities [https://brakemanscanner.org/] | ||
| gem "brakeman", require: false | ||
|
|
||
| # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] | ||
| gem "rubocop-rails-omakase", require: false | ||
| end | ||
|
|
||
|
|
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| class ApplicationController < ActionController::API | ||
| class ApplicationController < ActionController::Metal | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,56 +1,34 @@ | ||
| class DevicesController < ApplicationController | ||
| before_action :set_device, only: %i[ show update destroy ] | ||
| # frozen_string_literal: true | ||
|
|
||
| class DevicesController < ApplicationController | ||
| # GET /devices | ||
| def index | ||
| devices = [ | ||
| Device.new(id: 1, uuid: "9add349c-c35c-4d32-ab0f-53da1ba40a2a", mac: "5F-33-CC-1F-43-82", firmware: "2.1.6", created_at: "2024-5-28T15:21:51.137Z", updated_at: "2024-5-28T15:21:51.137Z"), | ||
| Device.new(id: 2, uuid: "d2293412-36eb-46e7-9231-af7e9249fffe", mac: "E7-34-96-33-0C-4C", firmware: "1.0.3", created_at: "2024-01-28T15:20:51.137Z", updated_at: "2024-01-28T15:20:51.137Z"), | ||
| Device.new(id: 3, uuid: "eee58ca8-ca51-47a5-ab48-163fd0e44b77", mac: "68-93-9B-B5-33-B9", firmware: "4.3.1", created_at: "2024-8-28T15:18:21.137Z", updated_at: "2024-8-28T15:18:21.137Z") | ||
| Device.new(id: 1, uuid: "9add349c-c35c-4d32-ab0f-53da1ba40a2a", mac: "5F-33-CC-1F-43-82", firmware: "2.1.6", created_at: "2024-5-28T15:21:51.137Z", updated_at: "2024-5-28T15:21:51.137Z").attributes, | ||
| Device.new(id: 2, uuid: "d2293412-36eb-46e7-9231-af7e9249fffe", mac: "E7-34-96-33-0C-4C", firmware: "1.0.3", created_at: "2024-01-28T15:20:51.137Z", updated_at: "2024-01-28T15:20:51.137Z").attributes, | ||
| Device.new(id: 3, uuid: "eee58ca8-ca51-47a5-ab48-163fd0e44b77", mac: "68-93-9B-B5-33-B9", firmware: "4.3.1", created_at: "2024-8-28T15:18:21.137Z", updated_at: "2024-8-28T15:18:21.137Z").attributes | ||
| ] | ||
|
|
||
| render json: devices | ||
| end | ||
|
|
||
| # GET /devices/1 | ||
| def show | ||
| render json: @device | ||
| build_response(devices, 200) | ||
| end | ||
|
|
||
| # POST /devices | ||
| def create | ||
| @device = Device.new(device_params) | ||
| @device.uuid = SecureRandom.uuid | ||
|
|
||
| if @device.save | ||
| render json: @device, status: :created, location: @device | ||
| else | ||
| render json: @device.errors, status: :unprocessable_entity | ||
| end | ||
| end | ||
| device = Device.new(params.slice(:mac, :firmware)) | ||
| device.uuid = SecureRandom.uuid | ||
|
|
||
| # PATCH/PUT /devices/1 | ||
| def update | ||
| if @device.update(device_params) | ||
| render json: @device | ||
| if device.save | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when rails saving, query wrapped in a transaction, so here we have 3 queries to database TRANSACTION (1.2ms) BEGIN
Device Create (1.7ms) INSERT INTO "devices" ("uuid", "mac", "firmware", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["uuid", nil], ["mac", "dfdf"], ["firmware", nil], ["created_at", "2024-11-06 00:53:09.435080"], ["updated_at", "2024-11-06 00:53:09.435080"]]
TRANSACTION (0.6ms) COMMITthis code is sensitive latency to database I do not know how other frameworks (django,laravel) works, but they can only insertion (1 query) we can write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @simpl1g @antonputra any ideas about this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ermolaev yeah, it makes sense to disable transactions if Django also is not using them. It is better to check Django before I guess. But you can at least create PR
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep it in place for now. |
||
| build_response(device, 201) | ||
| else | ||
| render json: @device.errors, status: :unprocessable_entity | ||
| build_response(device.errors, 422) | ||
| end | ||
| end | ||
|
|
||
| # DELETE /devices/1 | ||
| def destroy | ||
| @device.destroy! | ||
| end | ||
|
|
||
| private | ||
| # Use callbacks to share common setup or constraints between actions. | ||
| def set_device | ||
| @device = Device.find(params[:id]) | ||
| end | ||
|
|
||
| # Only allow a list of trusted parameters through. | ||
| def device_params | ||
| params.require(:device).permit(:uuid, :mac, :firmware) | ||
| end | ||
| def build_response(body, status_code) | ||
| self.content_type = "application/json" | ||
| self.status = status_code | ||
| self.response_body = body.to_json | ||
| end | ||
| end | ||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,9 @@ | ||
| class Device < ApplicationRecord | ||
| def save | ||
| result = nil | ||
|
|
||
| PrometheusClient.db_histogram.observe(Benchmark.realtime { result = super }) | ||
|
|
||
| result | ||
| end | ||
| end |
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| #!/usr/bin/env ruby | ||
| # frozen_string_literal: true | ||
|
|
||
| # | ||
| # This file was generated by Bundler. | ||
| # | ||
| # The application 'iodine' is installed as part of a gem, and | ||
| # this file is here to facilitate running it. | ||
| # | ||
|
|
||
| ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) | ||
|
|
||
| bundle_binstub = File.expand_path("bundle", __dir__) | ||
|
|
||
| if File.file?(bundle_binstub) | ||
| if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") | ||
| load(bundle_binstub) | ||
| else | ||
| abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. | ||
| Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") | ||
| end | ||
| end | ||
|
|
||
| require "rubygems" | ||
| require "bundler/setup" | ||
|
|
||
| load Gem.bin_path("rage-iodine", "iodine") |
This file was deleted.
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@antonputra @simpl1g what is the practical meaning of this benchmark?
for example, in node.js just creates hashes, we can do the same in ruby, and we don't need ActiveRecord objects, hash is much lighter and faster
I think it's more correct to load records from postgresql here, it is close to real use
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it is just CPU bound benchmark, to test how well it handles serialisation. 2nd test is for IO.
But I agree that comparing objects with plain hash is not ideal in this case, that's why I added pure ruby example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ermolaev well i'm going to test Rails with Django, and pure Ruby implementation from this PR with Node.js - #330