{"id":17558159,"url":"https://github.com/fractaledmind/railsconf-2024","last_synced_at":"2026-01-20T04:06:22.549Z","repository":{"id":234774277,"uuid":"789491771","full_name":"fractaledmind/railsconf-2024","owner":"fractaledmind","description":null,"archived":false,"fork":false,"pushed_at":"2024-05-01T11:32:04.000Z","size":1662,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-01T13:28:26.035Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fractaledmind.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-20T17:45:48.000Z","updated_at":"2024-05-03T12:32:37.887Z","dependencies_parsed_at":"2024-04-22T13:26:19.425Z","dependency_job_id":"0d914dad-ffbf-4e6e-a560-228eddf80191","html_url":"https://github.com/fractaledmind/railsconf-2024","commit_stats":null,"previous_names":["fractaledmind/railsconf-2024"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractaledmind%2Frailsconf-2024","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractaledmind%2Frailsconf-2024/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractaledmind%2Frailsconf-2024/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fractaledmind%2Frailsconf-2024/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fractaledmind","download_url":"https://codeload.github.com/fractaledmind/railsconf-2024/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247557749,"owners_count":20958047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-21T09:43:25.818Z","updated_at":"2026-01-20T04:06:17.531Z","avatar_url":"https://github.com/fractaledmind.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# README\n\nThis is an app built for demonstration purposes for the [RailsConf 2024 conference](https://railsconf.org) held in Detroit, Michigan on May 7–9, 2024.\n\nThe application is a basic \"Hacker News\" style app with `User`s, `Post`s, and `Comment`s. The seeds file will create ~100 users, ~1,000 posts, and ~10 comments per post. Every user has the same password: `password`, so you can sign in as any user to test the app.\n\n## Setup\n\nFirst you need to clone the repository to your local machine:\n\n```sh\ngit clone git@github.com:fractaledmind/railsconf-2024.git\ncd railsconf-2024\n```\n\nAfter cloning the repository, run the `bin/setup` command to install the dependencies and set up the database:\n\n```sh\nbin/setup\n```\n\n## Details\n\nThis application runs on Ruby 3.2.4, Rails `main`, and SQLite 3.45.3 (gem version 2.0.1).\n\nIt was created using the following command:\n\n```\nrails new railsconf-2024 \\\n  --main \\\n  --database=sqlite3 \\\n  --asset-pipeline=propshaft \\\n  --javascript=esbuild \\\n  --css=tailwind \\\n  --skip-jbuilder \\\n  --skip-action-mailbox \\\n  --skip-spring\n```\n\nSo it uses [`propshaft`](https://github.com/rails/propshaft) for asset compilation, [`esbuild`](https://esbuild.github.io) for JavaScript bundling, and [`tailwind`](https://tailwindcss.com) for CSS.\n\n## Setup Load Testing\n\nLoad testing can be done using the [`oha` CLI utility](https://github.com/hatoo/oha), which can be installed on MacOS via [homebrew](https://brew.sh):\n\n```sh\nbrew install oha\n```\n\nand on Windows via [winget](https://github.com/microsoft/winget-cli):\n\n```sh\nwinget install hatoo.oha\n```\n\nor using their [precompiled binaries](https://github.com/hatoo/oha?tab=readme-ov-file#installation) on other platforms.\n\nIn order to perform the load testing, you will need to run the web server in the `production` environment. To do this from your laptop, there are a few environment variables you will need to set:\n\n```sh\nRELAX_SSL=true RAILS_LOG_LEVEL=warn RAILS_ENV=production WEB_CONCURRENCY=10 RAILS_MAX_THREADS=5 bin/rails server\n```\n\nThe `RELAX_SSL` environment variable is necessary to allow you to use `http://localhost`. The `RAILS_LOG_LEVEL` is set to `warn` to reduce the amount of logging output. Set `WEB_CONCURRENCY` to the number of cores you have on your laptop. I am on an M1 Macbook Pro with 10 cores, and thus I set the value to 10. The `RAILS_MAX_THREADS` controls the number of threads per worker. I left it at the default of 5, but you can tweak it to see how it affects performance.\n\nWith your server running in one terminal window, you can use the load testing utility to test the app in another terminal window. Here is the shape of the command you will use to test the app:\n\n```sh\noha -c N -z 10s -m POST http://localhost:3000/benchmarking/PATH\n```\n\n`N` is the number of concurrent requests that `oha` will make. I recommend running a large variety of different scenarios with different values of `N`. Personally, I scale up from 1 to 256 concurrent requests, doubling the number of concurrent requests each time. In general, when `N` matches your `WEB_CONCURRENCY` number, this is mostly likely the sweet spot for this app.\n\n`PATH` can be any of the benchmarking paths defined in the app. The app has a few different paths that you can test. From the `routes.rb` file:\n\n```ruby\nnamespace :benchmarking do\n  post \"read_heavy\"\n  post \"write_heavy\"\n  post \"balanced\"\n  post \"post_create\"\n  post \"comment_create\"\n  post \"post_destroy\"\n  post \"comment_destroy\"\n  post \"post_show\"\n  post \"posts_index\"\n  post \"user_show\"\nend\n```\n\nThe `read_heavy`, `write_heavy`, and `balanced` paths are designed to test the performance of the app under a mix of scenarios. Each of those paths will randomly run one of the more precise actions, with the overall distribution defined in the controller to match the name. The rest of the paths are specific actions, which you can use if you want to see how a particular action handles concurrent load.\n\n## Run Baseline Load Tests\n\nBefore we start, let's establish a baseline. This is the starting point from which we will measure our progress. It's important to have a clear understanding of where we are now, so we can see how far we've come.\n\nWe will run two load tests to assess the current state of the application's performance; one for the `post_create` action and one for the `posts_index` action. We will run each test with 20 concurrent requests for 10 seconds.\n\nWe will run the read operation first since it can't have any effect on the write operation performance (while the inverse cannot be said). But first, it is often worth checking that the endpoint is responding as expected _before_ running a load test. So, let's make a single `curl` request first.\n\nIn one terminal window, start the Rails server:\n\n```sh\nRELAX_SSL=true RAILS_LOG_LEVEL=warn RAILS_ENV=production WEB_CONCURRENCY=10 RAILS_MAX_THREADS=5 bin/rails server\n```\n\nIn another, make a single `curl` request to the `posts_index` endpoint:\n\n```sh\ncurl -X POST http://localhost:3000/benchmarking/posts_index\n```\n\nYou should see an HTML response with a footer near the bottom of the page:\n\n```\n\u003cfooter class=\"mt-auto text-sm text-center\"\u003e\n  \u003cp class=\"py-4\"\u003e\n    Made with \u0026heartsuit; by \u003ca href=\"https://twitter.com/fractaledmind\" class=\"underline focus:outline-none focus:ring focus:ring-offset-2 focus:ring-blue-500\"\u003e@fractaledmind\u003c/a\u003e for \u003ca href=\"https://railsconf.org\" class=\"underline focus:outline-none focus:ring focus:ring-offset-2 focus:ring-blue-500\"\u003eRailsConf 2024\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/footer\u003e\n```\n\nIf you see that response, everything is working as expected. If you don't, you may need to troubleshoot the issue before proceeding.\n\nOnce we have verified that our Rails application is responding to the `benchmarking/posts_index` route as expected, we can run the load test and record the results.\n\nAs stated earlier, we will use the `oha` tool to run the load test. We will send waves of 20 concurrent requests, which is twice the number of Puma workers that our application has spun up. We will run the test for 10 seconds. The command to run the load test is as follows:\n\n```sh\noha -c 20 -z 10s -m POST http://localhost:3000/benchmarking/posts_index\n```\n\nRunning this on my 2021 M1 MacBook Pro (32 GB of RAM running MacOS 12.5.1), I get the following results:\n\n```\nSummary:\n  Success rate:\t100.00%\n  Total:\t10.0063 secs\n  Slowest:\t5.2124 secs\n  Fastest:\t0.0224 secs\n  Average:\t0.1081 secs\n  Requests/sec:\t40.8744\n\n  Total data:\t22.08 MiB\n  Size/request:\t58.13 KiB\n  Size/sec:\t2.21 MiB\n\nResponse time histogram:\n  0.022 [1]   |\n  0.541 [387] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  1.060 [0]   |\n  1.579 [0]   |\n  2.098 [0]   |\n  2.617 [0]   |\n  3.136 [0]   |\n  3.655 [0]   |\n  4.174 [0]   |\n  4.693 [0]   |\n  5.212 [1]   |\n\nResponse time distribution:\n  10.00% in 0.0446 secs\n  25.00% in 0.0697 secs\n  50.00% in 0.0875 secs\n  75.00% in 0.1035 secs\n  90.00% in 0.1463 secs\n  95.00% in 0.1963 secs\n  99.00% in 0.2991 secs\n  99.90% in 5.2124 secs\n  99.99% in 5.2124 secs\n\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t0.0018 secs, 0.0012 secs, 0.0022 secs\n  DNS-lookup:\t0.0002 secs, 0.0000 secs, 0.0006 secs\n\nStatus code distribution:\n  [200] 379 responses\n  [500] 10 responses\n\nError distribution:\n  [20] aborted due to deadline\n```\n\nA quick analysis of the results shows that the average response time is 108 ms, with the slowest response taking **over 5 seconds**! This means that the slowest request is _~50× slower_ than the average. Then, even on my high-powered laptop over localhost, our server can only support ~40 requests per second; this is a low number, and should be higher. Plus, we see 7 responses returning a 500 status code, which is not what we want.\n\nNow that we have the baseline for the `posts_index` action, we can move on to the `post_create` action. We will follow the same steps as above, but this time we will run the load test on the `post_create` endpoint.\n\nWith the Rails server still running in one terminal window, we can make a single `curl` request to the `post_create` endpoint in another:\n\n```sh\ncurl -X POST http://localhost:3000/benchmarking/post_create\n```\n\nAgain, you should see the `\u003cfooter\u003e` in the response. If you don't, you may need to troubleshoot the issue before proceeding.\n\nOnce we have verified that our Rails application is responding to the `benchmarking/post_create` route as expected, we can run the load test and record the results.\n\n```sh\noha -c 20 -z 10s -m POST http://localhost:3000/benchmarking/post_create\n```\n\nRunning this on my 2021 M1 MacBook Pro (32 GB of RAM running MacOS 12.5.1), I get the following results:\n\n```\nSummary:\n  Success rate:\t100.00%\n  Total:\t10.0051 secs\n  Slowest:\t5.4778 secs\n  Fastest:\t0.0033 secs\n  Average:\t0.0468 secs\n  Requests/sec:\t379.2079\n\n  Total data:\t9.92 MiB\n  Size/request:\t2.69 KiB\n  Size/sec:\t1015.39 KiB\n\nResponse time histogram:\n  0.003 [1]    |\n  0.551 [3747] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  1.098 [6]    |\n  1.646 [0]    |\n  2.193 [0]    |\n  2.741 [0]    |\n  3.288 [0]    |\n  3.835 [0]    |\n  4.383 [0]    |\n  4.930 [0]    |\n  5.478 [20]   |\n\nResponse time distribution:\n  10.00% in 0.0068 secs\n  25.00% in 0.0091 secs\n  50.00% in 0.0124 secs\n  75.00% in 0.0189 secs\n  90.00% in 0.0312 secs\n  95.00% in 0.0501 secs\n  99.00% in 0.1784 secs\n  99.90% in 5.3393 secs\n  99.99% in 5.4778 secs\n\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t0.0016 secs, 0.0013 secs, 0.0021 secs\n  DNS-lookup:\t0.0001 secs, 0.0000 secs, 0.0004 secs\n\nStatus code distribution:\n  [500] 2925 responses\n  [200] 849 responses\n\nError distribution:\n  [20] aborted due to deadline\n```\n\nImmediately, it should jump out just how many `500` responses we are seeing. **77%** of the responses are returning an error status code. Suffice it to say, this is not at all what we want from our application. We still see some requests taking over 5 seconds to complete, which is aweful. But at least for a single resource write request we are seeing a healthier ~380 requests per second.\n\nOur first challenge is to fix these performance issues.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffractaledmind%2Frailsconf-2024","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffractaledmind%2Frailsconf-2024","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffractaledmind%2Frailsconf-2024/lists"}