DEV Community

Shrouk Abozeid
Shrouk Abozeid

Posted on

Snehal Ahire - Rails Under a Microscope: Summarized with AI

Sometimes I'm lazy to talk notes myself(which ofcourse is better) but instead of not talking notes at all i let ai summarize what i just watched to make sure i didn't drop anything.

in this video Snehal Talks about performance problems that developers can face in large scale application and what could be some of the causes.

So here is her talk in points :D

Rails Performance Challenges

  • Performance issues in Rails applications, such as slow listing pages, delayed search queries, and long-running Sidekiq batches, cause significant frustration for both users and developers.
  • Persistent performance bottlenecks lead to increased infrastructure costs and delays in feature development, making performance optimization a necessity for application survival.
  • While developers may be tempted to rewrite slow Rails applications in languages like Rust or Go, such rewrites are often risky and costly; instead, applications should be analyzed to identify specific bottlenecks.

Data Retrieval Optimization

  • Code that appears innocent in the early stages of an application can evolve into a performance bottleneck as the system scales over many years.
  • Common performance issues in mature applications include memory bloat, memory leaks, and inefficient database queries.
  • Fetching unnecessary data, such as loading all columns when only a few are needed, is inefficient and becomes increasingly problematic as the number of rows grows into the millions.
  • Developers should optimize data retrieval by selecting only the specific columns required for the task.

Pagination Strategies

  • Standard pagination using offset is suitable for small datasets but becomes inefficient for large databases because the system must scan and discard all preceding records to reach the requested page.
  • Keyset pagination is a more efficient alternative for large datasets because it fetches only the required data without scanning and discarding previous records.
  • Keyset pagination is index-friendly, leveraging indexed columns like ID or created_at to provide consistent performance regardless of how deep into the dataset the user navigates.
  • Keyset pagination is an effective strategy for features like "Load More" or infinite scrolls because it uses pointers from the last row of a previous page to start the next query, though it prevents direct navigation to specific page numbers.

Query and Caching Best Practices

  • To prevent N+1 queries, Rails 7 and later versions offer strict loading features that can be used during development to avoid accidental performance issues.
  • Caching should only be implemented when the value of recomputing data exceeds the costs associated with cache maintenance and invalidation, as improper caching can negatively impact performance.

Memory Management and Heap Fragmentation

  • Memory bloat occurs when there is a significant discrepancy between the actual size of live objects in an application and the Resident Set Size (RSS) reported by the system.
  • The Memory Profiler gem is a tool used to diagnose memory issues by identifying memory allocations and retained objects, categorized by gem, location, class, or specific line of code.
  • Memory consumption in Ruby is often driven by arrays, strings, and hashes, which are dynamic allocations stored in the heap, while static functions are managed in the stack.
  • Ruby manages heap memory by dividing pages into multiple slots; when a new object is created, Ruby searches for a free slot within these pages.
  • When a page is full, Ruby requests additional pages from the system allocator—such as Malloc or Jemalloc—rather than communicating directly with the operating system.
  • Heap fragmentation occurs when the garbage collector frees objects, leaving non-adjacent holes in memory that are too small or poorly positioned to accommodate new object allocations.
  • Because Ruby cannot utilize these non-adjacent free slots, it is forced to request more pages from the system allocator, leading to an increase in total memory usage despite the presence of free space.

Allocator and Garbage Collection Dynamics

  • Allocator fragmentation also occurs when the system allocator cannot merge non-adjacent memory blocks, a problem that can be mitigated in multi-threaded environments like Sidekiq by configuring Malloc Arena Max to two or four.
  • Increased numbers of arenas contribute to higher levels of memory fragmentation.
  • Locators retain memory and maintain caches of different block sizes to facilitate instant future allocations.
  • Garbage collectors do not immediately return memory to the system, instead retaining it for future use, which keeps process memory usage high even when objects are no longer needed.
  • Configuring the heap growth factor can help manage fragmentation, as a higher factor results in less fragmentation.
  • Ruby 3.2 introduced variable width allocation, which manages slots through size pools rather than statically to reduce memory bloat and fragmentation, though its effectiveness depends on code support.
  • Memory bloat is primarily caused by heap fragmentation, allocator fragmentation, allocators reserving extra memory, and the garbage collector holding onto memory.
  • Resident Set Size (RSS) refers to the amount of RAM a specific process uses.

Memory Optimization Techniques

  • Loading large datasets into an array can be optimized by using batches with configurable batch sizes instead of loading all records at once.
  • Converting large datasets to JSON within Ruby consumes significant memory; offloading this process to the database using functions like row_to_json or json_agg is more efficient.
  • Reading large CSV files should be done line-by-line or using foreach rather than loading millions of rows into memory at once.
  • Creating temporary objects inside loops, such as date objects, leads to excessive memory usage over millions of iterations; these objects should be created once before the loop and reused, ideally using freeze.
  • Using include? on arrays inside loops creates new arrays in every iteration, which can be avoided by using a Set for faster retrieval and freezing the collection.

Bulk Operations and Database Efficiency

  • The destroy method loads all associations into memory, whereas delete_all performs a single operation without callbacks or associations, though it requires manual handling of child records to avoid orphaned objects.
  • Per-record updates can trigger N+1 query issues and high memory usage; bulk operations like upsert, update_all, or upsert_all are more efficient alternatives.
  • Inserting rows one by one for associations, such as orders and line items, creates memory overhead when processing multiple rows.
  • Bulk queries can be optimized by using insert or upsert operations instead of individual queries.
  • Memory bloat issues can be addressed by utilizing batches, reusing data, and freezing objects to prevent redundant object creation.

Identifying and Resolving Memory Leaks

  • A gradual, steady increase in memory usage over time often indicates a memory leak, even if restarting the process temporarily resolves the symptoms.
  • Symptoms of a memory leak include a steadily growing Resident Set Size (RSS) that does not decrease after Sidekiq jobs, a heap of objects that are never deallocated, and frequent garbage collection (GC) pauses as the system struggles to clean up unreleased objects.
  • Unbounded in-memory objects, such as those stored in class or global variables, can cause memory leaks because the garbage collector cannot release them as long as they remain referenced.
  • Replacing class or global variables with local methods allows the garbage collector to automatically free memory after the data is used.
  • Closures can lead to memory leaks if they capture large objects rather than specific values, as the captured variables are not released.
  • Passing only the specific data required, such as a calculated size rather than an entire array, prevents unnecessary memory retention.
  • Implementing these memory optimizations resulted in a 42% reduction in memory usage (from 788 MB to 459 MB) and an 80% reduction in the number of objects (from 11 million to 2 million), while performance time improved from 2 minutes to between 10 and 20 seconds.

Database Query Tuning and Indexing

  • Database query performance can be analyzed and tuned using the EXPLAIN ANALYZE command in PostgreSQL.
  • Adding specific conditions to a query can shift the execution plan from a slow parallel sequential scan to a significantly faster index scan, as demonstrated by a reduction in execution time from 2,800 milliseconds to 0.7 milliseconds.
  • Partial indexes are effective only when queries include the specific index conditions, and neglecting these details can lead to high execution times.
  • Small, neglected features or individual application problems can cause significant performance degradation across an entire system.
  • Materialized views in Postgres serve as physical tables that store computed data, offering an alternative to complex joins across multiple tables or the complications of using read replicas.
  • Postgres features such as TS vectors combined with GIN indexes allow for efficient searching across multiple columns by creating a vector for direct querying.

Scaling and Infrastructure Management

  • Sidekiq can be autoscaled using latency-based queuing, which organizes tasks into multiple queues based on their latency requirements.
  • Specialized GIN indexes, which support various operators, can be utilized to achieve faster retrieval speeds for JSON columns.

Performance Culture and Workflow Integration

  • Performance optimization should begin with **profiling and measurement **rather than relying on intuition to identify bottlenecks.
  • Database capabilities, including indexes, aggregations, and bulk operations, should be leveraged for heavy tasks rather than attempting to reinvent these processes.
  • Identifying data patterns and understanding how data is derived and processed is essential, as even minor inefficiencies accumulate at scale.
  • Performance management is a collaborative responsibility that should be shared across the entire team.
  • P*erformance should be integrated into the development workflow* by setting budgets and monitoring metrics throughout the design, build, and shipping phases, rather than treating it as an afterthought.
  • Addressing performance issues requires a continuous, multi-faceted approach rather than a one-time fix or a complete rewrite of the application in a different language.

Thanks Snehal for an interesting talk and a great presentation, if you know her profile tag her for credits

Top comments (0)