rubino-agent 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +115 -0
- data/.rubocop_todo.yml +955 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +97 -0
- data/CHANGELOG.md +344 -0
- data/CONTRIBUTING.md +69 -0
- data/LICENSE +21 -0
- data/README.md +200 -0
- data/Rakefile +8 -0
- data/docs/agents.md +190 -0
- data/docs/api/v1.md +414 -0
- data/docs/architecture.md +177 -0
- data/docs/commands.md +375 -0
- data/docs/configuration.md +590 -0
- data/docs/getting-started.md +143 -0
- data/docs/jobs.md +332 -0
- data/docs/mcp.md +128 -0
- data/docs/memory.md +98 -0
- data/docs/models-and-keys.md +173 -0
- data/docs/oauth-providers.md +145 -0
- data/docs/plugins.md +195 -0
- data/docs/security.md +145 -0
- data/docs/skills.md +322 -0
- data/docs/tools.md +395 -0
- data/docs/troubleshooting.md +73 -0
- data/exe/rubino +9 -0
- data/install.sh +275 -0
- data/lib/rubino/active_skill.rb +50 -0
- data/lib/rubino/agent/agent_registry.rb +120 -0
- data/lib/rubino/agent/backoff_policy.rb +116 -0
- data/lib/rubino/agent/definition.rb +128 -0
- data/lib/rubino/agent/degenerate_recovery.rb +271 -0
- data/lib/rubino/agent/fallback_chain.rb +194 -0
- data/lib/rubino/agent/iteration_budget.rb +50 -0
- data/lib/rubino/agent/loop.rb +617 -0
- data/lib/rubino/agent/model_call_runner.rb +383 -0
- data/lib/rubino/agent/prompts/build.txt +69 -0
- data/lib/rubino/agent/prompts/compaction.txt +20 -0
- data/lib/rubino/agent/prompts/explore.txt +19 -0
- data/lib/rubino/agent/prompts/general.txt +20 -0
- data/lib/rubino/agent/prompts/plan.txt +31 -0
- data/lib/rubino/agent/response_validator.rb +70 -0
- data/lib/rubino/agent/router.rb +65 -0
- data/lib/rubino/agent/runner.rb +195 -0
- data/lib/rubino/agent/tool_executor.rb +402 -0
- data/lib/rubino/agent/truncation_continuation.rb +137 -0
- data/lib/rubino/api/middleware/auth.rb +43 -0
- data/lib/rubino/api/middleware/error_handler.rb +65 -0
- data/lib/rubino/api/middleware/json_parser.rb +100 -0
- data/lib/rubino/api/middleware/observability.rb +59 -0
- data/lib/rubino/api/middleware/rate_limit.rb +136 -0
- data/lib/rubino/api/operations/approvals/decide_operation.rb +49 -0
- data/lib/rubino/api/operations/clarifications/decide_operation.rb +44 -0
- data/lib/rubino/api/operations/cron_jobs/create_operation.rb +46 -0
- data/lib/rubino/api/operations/cron_jobs/delete_operation.rb +36 -0
- data/lib/rubino/api/operations/cron_jobs/list_operation.rb +55 -0
- data/lib/rubino/api/operations/cron_jobs/pause_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/resume_operation.rb +34 -0
- data/lib/rubino/api/operations/cron_jobs/schedule_validation.rb +30 -0
- data/lib/rubino/api/operations/cron_jobs/show_operation.rb +32 -0
- data/lib/rubino/api/operations/cron_jobs/trigger_operation.rb +38 -0
- data/lib/rubino/api/operations/cron_jobs/update_operation.rb +42 -0
- data/lib/rubino/api/operations/files/read_operation.rb +40 -0
- data/lib/rubino/api/operations/files/upload_operation.rb +175 -0
- data/lib/rubino/api/operations/health_operation.rb +46 -0
- data/lib/rubino/api/operations/memory/delete_operation.rb +32 -0
- data/lib/rubino/api/operations/memory/index_operation.rb +80 -0
- data/lib/rubino/api/operations/memory/stats_operation.rb +28 -0
- data/lib/rubino/api/operations/metrics_operation.rb +18 -0
- data/lib/rubino/api/operations/mode/show_operation.rb +29 -0
- data/lib/rubino/api/operations/mode/update_operation.rb +42 -0
- data/lib/rubino/api/operations/models/list_operation.rb +45 -0
- data/lib/rubino/api/operations/oauth/connections/disconnect_operation.rb +77 -0
- data/lib/rubino/api/operations/oauth/connections/list_operation.rb +36 -0
- data/lib/rubino/api/operations/oauth/providers/callback_operation.rb +82 -0
- data/lib/rubino/api/operations/oauth/providers/connect_operation.rb +44 -0
- data/lib/rubino/api/operations/oauth/providers/list_operation.rb +35 -0
- data/lib/rubino/api/operations/oauth/serializer.rb +21 -0
- data/lib/rubino/api/operations/runs/create_operation.rb +77 -0
- data/lib/rubino/api/operations/runs/events_operation.rb +195 -0
- data/lib/rubino/api/operations/runs/stop_operation.rb +34 -0
- data/lib/rubino/api/operations/sessions/create_operation.rb +46 -0
- data/lib/rubino/api/operations/sessions/delete_operation.rb +33 -0
- data/lib/rubino/api/operations/sessions/index_operation.rb +82 -0
- data/lib/rubino/api/operations/sessions/retry_operation.rb +45 -0
- data/lib/rubino/api/operations/sessions/show_operation.rb +59 -0
- data/lib/rubino/api/operations/sessions/undo_operation.rb +38 -0
- data/lib/rubino/api/operations/skills/list_operation.rb +34 -0
- data/lib/rubino/api/operations/skills/toggle_operation.rb +40 -0
- data/lib/rubino/api/operations/tasks/index_operation.rb +30 -0
- data/lib/rubino/api/operations/tasks/serializer.rb +60 -0
- data/lib/rubino/api/operations/tasks/show_operation.rb +33 -0
- data/lib/rubino/api/operations/tasks/stop_operation.rb +47 -0
- data/lib/rubino/api/request.rb +54 -0
- data/lib/rubino/api/responses.rb +64 -0
- data/lib/rubino/api/router.rb +72 -0
- data/lib/rubino/api/schemas.rb +103 -0
- data/lib/rubino/api/server.rb +102 -0
- data/lib/rubino/api/tls.rb +108 -0
- data/lib/rubino/attachments/classification.rb +16 -0
- data/lib/rubino/attachments/classify.rb +171 -0
- data/lib/rubino/attachments/defang.rb +47 -0
- data/lib/rubino/attachments/policy.rb +36 -0
- data/lib/rubino/attachments/preamble.rb +120 -0
- data/lib/rubino/boot/encryption_key.rb +32 -0
- data/lib/rubino/cli/chat/bang_shell.rb +257 -0
- data/lib/rubino/cli/chat/completion_builder.rb +290 -0
- data/lib/rubino/cli/chat/idle_card_host.rb +69 -0
- data/lib/rubino/cli/chat/image_inbox.rb +168 -0
- data/lib/rubino/cli/chat/session_resolver.rb +176 -0
- data/lib/rubino/cli/chat_command.rb +1674 -0
- data/lib/rubino/cli/commands.rb +250 -0
- data/lib/rubino/cli/config_command.rb +96 -0
- data/lib/rubino/cli/doctor_command.rb +251 -0
- data/lib/rubino/cli/jobs_command.rb +60 -0
- data/lib/rubino/cli/memory_command.rb +135 -0
- data/lib/rubino/cli/onboarding_wizard.rb +207 -0
- data/lib/rubino/cli/server_command.rb +139 -0
- data/lib/rubino/cli/session_command.rb +125 -0
- data/lib/rubino/cli/setup_command.rb +107 -0
- data/lib/rubino/cli/skills_command.rb +85 -0
- data/lib/rubino/cli/tools_command.rb +81 -0
- data/lib/rubino/cli/trust_gate.rb +71 -0
- data/lib/rubino/commands/built_ins.rb +46 -0
- data/lib/rubino/commands/command.rb +116 -0
- data/lib/rubino/commands/executor.rb +550 -0
- data/lib/rubino/commands/handlers/agents.rb +510 -0
- data/lib/rubino/commands/handlers/config.rb +88 -0
- data/lib/rubino/commands/handlers/help.rb +148 -0
- data/lib/rubino/commands/handlers/jobs.rb +71 -0
- data/lib/rubino/commands/handlers/mcp.rb +229 -0
- data/lib/rubino/commands/handlers/memory.rb +200 -0
- data/lib/rubino/commands/handlers/sessions.rb +207 -0
- data/lib/rubino/commands/handlers/skills.rb +195 -0
- data/lib/rubino/commands/handlers/status.rb +211 -0
- data/lib/rubino/commands/loader.rb +90 -0
- data/lib/rubino/config/configuration.rb +455 -0
- data/lib/rubino/config/defaults.rb +569 -0
- data/lib/rubino/config/loader.rb +115 -0
- data/lib/rubino/config/reasoning_prefs.rb +67 -0
- data/lib/rubino/config/writer.rb +72 -0
- data/lib/rubino/context/compressor.rb +149 -0
- data/lib/rubino/context/environment_inspector.rb +176 -0
- data/lib/rubino/context/file_discovery.rb +45 -0
- data/lib/rubino/context/message_boundary.rb +39 -0
- data/lib/rubino/context/prompt_assembler.rb +382 -0
- data/lib/rubino/context/summary_builder.rb +159 -0
- data/lib/rubino/context/token_budget.rb +68 -0
- data/lib/rubino/context/tool_pair_sanitizer.rb +70 -0
- data/lib/rubino/database/connection.rb +77 -0
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +156 -0
- data/lib/rubino/database/migrations/002_create_runs.rb +45 -0
- data/lib/rubino/database/migrations/003_create_skill_states.rb +15 -0
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +36 -0
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +27 -0
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +34 -0
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +59 -0
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +75 -0
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +55 -0
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +20 -0
- data/lib/rubino/database/migrator.rb +48 -0
- data/lib/rubino/documents/converters/csv.rb +79 -0
- data/lib/rubino/documents/converters/docx.rb +129 -0
- data/lib/rubino/documents/converters/html.rb +28 -0
- data/lib/rubino/documents/converters/json.rb +35 -0
- data/lib/rubino/documents/converters/pdf.rb +59 -0
- data/lib/rubino/documents/converters/plain.rb +68 -0
- data/lib/rubino/documents/converters/pptx.rb +64 -0
- data/lib/rubino/documents/converters/xlsx.rb +62 -0
- data/lib/rubino/documents/converters/xml.rb +45 -0
- data/lib/rubino/documents/html.rb +71 -0
- data/lib/rubino/documents/registry.rb +68 -0
- data/lib/rubino/documents/table.rb +63 -0
- data/lib/rubino/documents.rb +50 -0
- data/lib/rubino/errors.rb +119 -0
- data/lib/rubino/files/workspace.rb +93 -0
- data/lib/rubino/interaction/cancel_token.rb +43 -0
- data/lib/rubino/interaction/clipboard_image.rb +84 -0
- data/lib/rubino/interaction/event_bus.rb +48 -0
- data/lib/rubino/interaction/events.rb +101 -0
- data/lib/rubino/interaction/image_input.rb +127 -0
- data/lib/rubino/interaction/input_queue.rb +117 -0
- data/lib/rubino/interaction/lifecycle.rb +299 -0
- data/lib/rubino/interaction/probe.rb +65 -0
- data/lib/rubino/interaction/state.rb +56 -0
- data/lib/rubino/jobs/cron_job_repository.rb +75 -0
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +32 -0
- data/lib/rubino/jobs/handlers/compact_session_job.rb +21 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +186 -0
- data/lib/rubino/jobs/handlers/extract_memory_job.rb +37 -0
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +21 -0
- data/lib/rubino/jobs/queue.rb +184 -0
- data/lib/rubino/jobs/registry.rb +45 -0
- data/lib/rubino/jobs/runner.rb +79 -0
- data/lib/rubino/jobs/scheduler.rb +138 -0
- data/lib/rubino/jobs/webhook_delivery.rb +225 -0
- data/lib/rubino/jobs/worker.rb +59 -0
- data/lib/rubino/llm/adapter_factory.rb +47 -0
- data/lib/rubino/llm/adapter_response.rb +65 -0
- data/lib/rubino/llm/auxiliary_client.rb +61 -0
- data/lib/rubino/llm/bedrock_bearer_client.rb +235 -0
- data/lib/rubino/llm/content_builder.rb +55 -0
- data/lib/rubino/llm/credential_check.rb +93 -0
- data/lib/rubino/llm/error_classifier.rb +364 -0
- data/lib/rubino/llm/fake_provider.rb +292 -0
- data/lib/rubino/llm/inline_think_filter.rb +58 -0
- data/lib/rubino/llm/model_catalog.rb +29 -0
- data/lib/rubino/llm/provider_resolver.rb +48 -0
- data/lib/rubino/llm/reasoning_manager.rb +100 -0
- data/lib/rubino/llm/request.rb +56 -0
- data/lib/rubino/llm/ruby_llm_adapter.rb +794 -0
- data/lib/rubino/llm/scenario_loader.rb +68 -0
- data/lib/rubino/llm/scenario_selector.rb +80 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron-failure.yml +29 -0
- data/lib/rubino/llm/scenarios/agent-creates-cron.yml +36 -0
- data/lib/rubino/llm/scenarios/analysis.yml +501 -0
- data/lib/rubino/llm/scenarios/complex-analysis.yml +598 -0
- data/lib/rubino/llm/scenarios/failure.yml +65 -0
- data/lib/rubino/llm/scenarios/happy-path.yml +24 -0
- data/lib/rubino/llm/scenarios/provider-quota-completed.yml +14 -0
- data/lib/rubino/llm/scenarios/wide-table.yml +121 -0
- data/lib/rubino/llm/scenarios/with-approvals.yml +50 -0
- data/lib/rubino/llm/scenarios/with-artifacts.yml +98 -0
- data/lib/rubino/llm/scenarios/with-clarify.yml +32 -0
- data/lib/rubino/llm/scenarios/with-reasoning.yml +175 -0
- data/lib/rubino/llm/scenarios/with-uploads.yml +104 -0
- data/lib/rubino/llm/thinking_support.rb +84 -0
- data/lib/rubino/llm/tool_bridge.rb +89 -0
- data/lib/rubino/logger.rb +99 -0
- data/lib/rubino/mcp/manager.rb +180 -0
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +69 -0
- data/lib/rubino/mcp.rb +57 -0
- data/lib/rubino/memory/backend.rb +104 -0
- data/lib/rubino/memory/backends/default.rb +101 -0
- data/lib/rubino/memory/backends/sqlite.rb +653 -0
- data/lib/rubino/memory/backends.rb +53 -0
- data/lib/rubino/memory/deduplicator.rb +74 -0
- data/lib/rubino/memory/extractor.rb +85 -0
- data/lib/rubino/memory/flusher.rb +31 -0
- data/lib/rubino/memory/retriever.rb +50 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +70 -0
- data/lib/rubino/memory/sqlite_graph.rb +154 -0
- data/lib/rubino/memory/store.rb +228 -0
- data/lib/rubino/memory/threat_scanner.rb +68 -0
- data/lib/rubino/metrics.rb +175 -0
- data/lib/rubino/modes.rb +93 -0
- data/lib/rubino/oauth/connection_repository.rb +95 -0
- data/lib/rubino/oauth/provider/github.rb +75 -0
- data/lib/rubino/oauth/provider/google.rb +59 -0
- data/lib/rubino/oauth/provider.rb +149 -0
- data/lib/rubino/oauth/registry.rb +86 -0
- data/lib/rubino/oauth/token_encryptor.rb +87 -0
- data/lib/rubino/plugins/registry.rb +75 -0
- data/lib/rubino/plugins.rb +86 -0
- data/lib/rubino/run/approval_gate.rb +243 -0
- data/lib/rubino/run/attachment_downloader.rb +166 -0
- data/lib/rubino/run/event_store.rb +74 -0
- data/lib/rubino/run/executor.rb +383 -0
- data/lib/rubino/run/gate_registry.rb +39 -0
- data/lib/rubino/run/recorder.rb +69 -0
- data/lib/rubino/run/repository.rb +118 -0
- data/lib/rubino/run/session_approval_cache.rb +118 -0
- data/lib/rubino/security/allowlist_persister.rb +55 -0
- data/lib/rubino/security/approval_policy.rb +227 -0
- data/lib/rubino/security/command_allowlist.rb +24 -0
- data/lib/rubino/security/dangerous_patterns.rb +118 -0
- data/lib/rubino/security/deny_persister.rb +73 -0
- data/lib/rubino/security/doom_loop_detector.rb +43 -0
- data/lib/rubino/security/hardline_guard.rb +105 -0
- data/lib/rubino/security/pattern_matcher.rb +62 -0
- data/lib/rubino/security/prefix_deriver.rb +124 -0
- data/lib/rubino/security/readonly_commands.rb +211 -0
- data/lib/rubino/session/exporter.rb +101 -0
- data/lib/rubino/session/message.rb +77 -0
- data/lib/rubino/session/repository.rb +295 -0
- data/lib/rubino/session/store.rb +198 -0
- data/lib/rubino/session/summary_store.rb +65 -0
- data/lib/rubino/skills/prompt_index.rb +85 -0
- data/lib/rubino/skills/registry.rb +208 -0
- data/lib/rubino/skills/skill.rb +176 -0
- data/lib/rubino/skills/skill_tool.rb +215 -0
- data/lib/rubino/skills/state_repository.rb +37 -0
- data/lib/rubino/skills/toggle.rb +26 -0
- data/lib/rubino/tools/answer_child_tool.rb +83 -0
- data/lib/rubino/tools/ask_parent_tool.rb +232 -0
- data/lib/rubino/tools/attach_file_tool.rb +120 -0
- data/lib/rubino/tools/background_tasks.rb +520 -0
- data/lib/rubino/tools/base.rb +222 -0
- data/lib/rubino/tools/custom_tool_loader.rb +119 -0
- data/lib/rubino/tools/edit_tool.rb +122 -0
- data/lib/rubino/tools/git_tool.rb +71 -0
- data/lib/rubino/tools/github_tool.rb +233 -0
- data/lib/rubino/tools/glob_tool.rb +69 -0
- data/lib/rubino/tools/grep_tool.rb +206 -0
- data/lib/rubino/tools/memory_tool.rb +184 -0
- data/lib/rubino/tools/multi_edit_tool.rb +110 -0
- data/lib/rubino/tools/patch_tool.rb +260 -0
- data/lib/rubino/tools/probe_tool.rb +175 -0
- data/lib/rubino/tools/question_tool.rb +128 -0
- data/lib/rubino/tools/read_attachment_tool.rb +180 -0
- data/lib/rubino/tools/read_tool.rb +212 -0
- data/lib/rubino/tools/read_tracker.rb +98 -0
- data/lib/rubino/tools/registry.rb +166 -0
- data/lib/rubino/tools/result.rb +113 -0
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/session_search_tool.rb +103 -0
- data/lib/rubino/tools/shell_input_tool.rb +96 -0
- data/lib/rubino/tools/shell_kill_tool.rb +76 -0
- data/lib/rubino/tools/shell_output_tool.rb +72 -0
- data/lib/rubino/tools/shell_registry.rb +158 -0
- data/lib/rubino/tools/shell_tail_tool.rb +118 -0
- data/lib/rubino/tools/shell_tool.rb +330 -0
- data/lib/rubino/tools/steer_tool.rb +118 -0
- data/lib/rubino/tools/subagent_probe.rb +89 -0
- data/lib/rubino/tools/summarize_file_tool.rb +182 -0
- data/lib/rubino/tools/task_result_tool.rb +90 -0
- data/lib/rubino/tools/task_stop_tool.rb +80 -0
- data/lib/rubino/tools/task_tool.rb +622 -0
- data/lib/rubino/tools/test_tool.rb +454 -0
- data/lib/rubino/tools/todo_tool.rb +93 -0
- data/lib/rubino/tools/tool_call_repository.rb +33 -0
- data/lib/rubino/tools/vision_tool.rb +85 -0
- data/lib/rubino/tools/webfetch_tool.rb +153 -0
- data/lib/rubino/tools/websearch_tool.rb +179 -0
- data/lib/rubino/tools/write_tool.rb +61 -0
- data/lib/rubino/trust.rb +88 -0
- data/lib/rubino/ui/api.rb +296 -0
- data/lib/rubino/ui/base.rb +252 -0
- data/lib/rubino/ui/bottom_composer.rb +1599 -0
- data/lib/rubino/ui/cli.rb +1987 -0
- data/lib/rubino/ui/completion_menu.rb +321 -0
- data/lib/rubino/ui/completion_source.rb +284 -0
- data/lib/rubino/ui/escape_reader.rb +169 -0
- data/lib/rubino/ui/indented_io.rb +88 -0
- data/lib/rubino/ui/input_history.rb +108 -0
- data/lib/rubino/ui/live_region.rb +183 -0
- data/lib/rubino/ui/markdown_renderer.rb +506 -0
- data/lib/rubino/ui/notifier.rb +163 -0
- data/lib/rubino/ui/null.rb +195 -0
- data/lib/rubino/ui/paste_store.rb +176 -0
- data/lib/rubino/ui/printer_base.rb +79 -0
- data/lib/rubino/ui/probe_wait_indicator.rb +75 -0
- data/lib/rubino/ui/queued_indicators.rb +66 -0
- data/lib/rubino/ui/status_bar.rb +100 -0
- data/lib/rubino/ui/stdout_proxy.rb +161 -0
- data/lib/rubino/ui/streaming_markdown.rb +186 -0
- data/lib/rubino/ui/subagent_cards.rb +134 -0
- data/lib/rubino/ui/subagent_view.rb +255 -0
- data/lib/rubino/ui.rb +21 -0
- data/lib/rubino/update_check.rb +187 -0
- data/lib/rubino/util/duration.rb +23 -0
- data/lib/rubino/util/hyperlink.rb +105 -0
- data/lib/rubino/util/output.rb +145 -0
- data/lib/rubino/util/secrets_mask.rb +83 -0
- data/lib/rubino/version.rb +5 -0
- data/lib/rubino/workspace.rb +85 -0
- data/lib/rubino-agent.rb +5 -0
- data/lib/rubino.rb +318 -0
- data/mise.toml +2 -0
- data/rubino-agent.gemspec +103 -0
- data/skills/ruby-expert/SKILL.md +67 -0
- data/skills/ruby-expert/references/concurrency.md +357 -0
- data/skills/ruby-expert/references/datetime-and-encoding.md +363 -0
- data/skills/ruby-expert/references/errors-and-types.md +460 -0
- data/skills/ruby-expert/references/gem-authoring.md +459 -0
- data/skills/ruby-expert/references/language-idioms.md +465 -0
- data/skills/ruby-expert/references/metaprogramming.md +339 -0
- data/skills/ruby-expert/references/oo-design.md +553 -0
- data/skills/ruby-expert/references/performance.md +383 -0
- data/skills/ruby-expert/references/rails.md +424 -0
- data/skills/ruby-expert/references/security.md +404 -0
- data/skills/ruby-expert/references/testing.md +473 -0
- data/skills/ruby-expert/references/tooling.md +466 -0
- metadata +856 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Performance & memory
|
|
2
|
+
|
|
3
|
+
Making Ruby (3.2–3.4) and Rails (7.1–8.x) fast and lean. **Golden loop: profile → fix the single biggest cost → re-measure.** Never optimize on a hunch; Ruby's hotspots are routinely counter-intuitive (allocation and GC, not arithmetic).
|
|
4
|
+
|
|
5
|
+
## Measure first
|
|
6
|
+
|
|
7
|
+
Do not micro-optimize without data. A "faster" expression that runs 1% of the time is worthless. Find where the wall-clock and allocations actually go, then act.
|
|
8
|
+
|
|
9
|
+
### Benchmark / benchmark-ips
|
|
10
|
+
|
|
11
|
+
`Benchmark` (stdlib) gives raw timings; **`benchmark-ips`** (gem) is the right tool for *comparing implementations* — it warms up, runs to a stable iteration rate, and reports a relative comparison with error bars.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "benchmark/ips"
|
|
15
|
+
|
|
16
|
+
ARR = (1..10_000).to_a
|
|
17
|
+
|
|
18
|
+
Benchmark.ips do |x|
|
|
19
|
+
x.report("map+compact") { ARR.map { |n| n if n.even? }.compact }
|
|
20
|
+
x.report("filter_map") { ARR.filter_map { |n| n if n.even? } }
|
|
21
|
+
x.report("select+map") { ARR.select(&:even?).map { |n| n } }
|
|
22
|
+
x.compare! # prints "filter_map: 1.42x faster" etc.
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Rules:
|
|
27
|
+
- Always `compare!` — absolute ips numbers are meaningless across machines.
|
|
28
|
+
- Put real-sized data in the benchmark; tiny inputs hide allocation/GC cost.
|
|
29
|
+
- For allocation comparisons, pair ips with `memory_profiler` (below) — CPU and memory rankings often disagree.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# WRONG: timing once, no warmup, GC noise dominates
|
|
33
|
+
t = Time.now; do_work; puts Time.now - t
|
|
34
|
+
|
|
35
|
+
# RIGHT: benchmark-ips handles warmup, GC, and statistical stability
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Profilers
|
|
39
|
+
|
|
40
|
+
### stackprof — the default CPU/wall/object profiler
|
|
41
|
+
|
|
42
|
+
`stackprof` is a sampling profiler — low overhead, safe in production-like loads. Three modes:
|
|
43
|
+
- `:cpu` — on-CPU time (find compute hotspots).
|
|
44
|
+
- `:wall` — wall-clock (find where you *wait*: IO, locks, sleeps).
|
|
45
|
+
- `:object` — samples allocations (find what allocates the most).
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
require "stackprof"
|
|
49
|
+
|
|
50
|
+
StackProf.run(mode: :cpu, out: "tmp/stackprof-cpu.dump", interval: 1000) do
|
|
51
|
+
expensive_call
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
stackprof tmp/stackprof-cpu.dump --text --limit 20 # top frames
|
|
57
|
+
stackprof tmp/stackprof-cpu.dump --method 'MyClass#slow' # callers/callees
|
|
58
|
+
stackprof tmp/stackprof-cpu.dump --flamegraph > fg.html
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `mode: :object` when GC time is high — it tells you *which line* allocates so you can cut it.
|
|
62
|
+
|
|
63
|
+
### vernier — modern sampling profiler (prefer for Ruby 3.2+)
|
|
64
|
+
|
|
65
|
+
`vernier` is the current best-in-class sampler: thread-aware, captures GC and idle/IO time, low overhead, and emits a profile for the Firefox Profiler UI. Prefer it over stackprof on Ruby 3.2+, especially for multi-threaded apps (Puma, Sidekiq).
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "vernier"
|
|
69
|
+
|
|
70
|
+
Vernier.profile(out: "tmp/profile.json.gz") do
|
|
71
|
+
do_work
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
vernier run -- ruby script.rb # wrap a whole process
|
|
77
|
+
# Open tmp/profile.json.gz at https://profiler.firefox.com
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Vernier shows time *between* threads and GC pauses that single-thread profilers miss. See `references/concurrency.md` for the threading model it visualizes.
|
|
81
|
+
|
|
82
|
+
### ruby-prof — deterministic, call-graph detail
|
|
83
|
+
|
|
84
|
+
`ruby-prof` instruments every call (high overhead, NOT for production) but gives exact call counts and a full call graph. Reach for it when you need precise *call counts* or a callgrind graph, not for sampling under load.
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "ruby-prof"
|
|
88
|
+
result = RubyProf.profile { do_work }
|
|
89
|
+
RubyProf::FlatPrinter.new(result).print(STDOUT, min_percent: 2)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### rack-mini-profiler — web request profiling
|
|
93
|
+
|
|
94
|
+
For Rails/Rack, `rack-mini-profiler` adds an in-page speed badge with SQL, view, and allocation breakdowns per request. Pair with `flamegraph` and `stackprof` gems for `?pp=flamegraph`.
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
# Gemfile (development)
|
|
98
|
+
gem "rack-mini-profiler"
|
|
99
|
+
gem "stackprof" # enables ?pp=flamegraph
|
|
100
|
+
gem "memory_profiler" # enables ?pp=profile-memory
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
GET /page?pp=flamegraph # request flamegraph
|
|
105
|
+
GET /page?pp=profile-memory # allocation report for the request
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Memory & allocation
|
|
109
|
+
|
|
110
|
+
In Ruby, **allocations are the dominant cost** — every object created is future GC work. Cutting allocations usually beats algorithmic tweaks.
|
|
111
|
+
|
|
112
|
+
### memory_profiler — what allocates and what retains
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
require "memory_profiler"
|
|
116
|
+
report = MemoryProfiler.report { build_response }
|
|
117
|
+
report.pretty_print(to_file: "tmp/mem.txt")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Read two numbers: **allocated** (churn → GC pressure) and **retained** (lives past the block → leak/bloat). High allocated with low retained = GC thrash; high retained = a leak or oversized cache.
|
|
121
|
+
|
|
122
|
+
### derailed_benchmarks — Rails memory at boot and per-request
|
|
123
|
+
|
|
124
|
+
`derailed_benchmarks` finds gem memory bloat and per-request allocations.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
bundle exec derailed bundle:mem # memory used by requiring each gem
|
|
128
|
+
bundle exec derailed exec perf:mem # per-request memory
|
|
129
|
+
bundle exec derailed exec perf:objects
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Frozen string literals cut allocations
|
|
133
|
+
|
|
134
|
+
Each unfrozen string literal allocates a *new* object every time it's evaluated; `# frozen_string_literal: true` dedups them into one shared frozen object, and you allocate a mutable buffer explicitly with `+""`. Mandatory at the top of every file you control — the allocation win is why. Mechanics (the magic comment, `+""`/`-"..."`, building with `<<`/`join`): see `references/language-idioms.md`.
|
|
135
|
+
|
|
136
|
+
### Avoid intermediate arrays
|
|
137
|
+
|
|
138
|
+
Chained `map`/`select`/`reject` each allocate a full intermediate array. Collapse them.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
# WRONG: 3 intermediate arrays of size ~N
|
|
142
|
+
users.select(&:active?).map(&:email).uniq
|
|
143
|
+
|
|
144
|
+
# RIGHT (single pass, one accumulator):
|
|
145
|
+
users.each_with_object(Set.new) { |u, s| s << u.email if u.active? }
|
|
146
|
+
|
|
147
|
+
# RIGHT (fuse two passes into one):
|
|
148
|
+
users.filter_map { |u| u.email if u.active? }.uniq
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For large or lazy streams, use `lazy` so nothing is materialized until `.first`/`.take`/`.force`:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Reads/transforms only until 10 matches — no full intermediate arrays
|
|
155
|
+
File.foreach("huge.log").lazy
|
|
156
|
+
.map { |line| parse(line) }
|
|
157
|
+
.select { |e| e.error? }
|
|
158
|
+
.first(10)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Tool choice within these pipelines (`filter_map`, `each_with_object` vs `reduce`, `lazy`): see `references/language-idioms.md` for the full Enumerable toolbox.
|
|
162
|
+
|
|
163
|
+
### Symbol vs string allocation
|
|
164
|
+
|
|
165
|
+
Symbols are interned (one object per name) and never churn the heap, so they're the right key/identifier type for hot hash lookups. Never `to_sym` untrusted/unbounded input — those symbols accumulate as memory growth. Symbol-vs-string semantics: see `references/language-idioms.md`.
|
|
166
|
+
|
|
167
|
+
### Build strings without churn
|
|
168
|
+
|
|
169
|
+
`out += ...` in a loop is O(n²) copying; build with `<<` into a `+""` buffer, or `map`/`join` once — see `references/language-idioms.md`. For very large output, stream instead of accumulating (see Streaming below).
|
|
170
|
+
|
|
171
|
+
## The garbage collector
|
|
172
|
+
|
|
173
|
+
MRI's GC is **generational** (young objects collected cheaply and often; long-lived ones promoted to old gen and scanned rarely) and **incremental** (the costly old-gen mark is sliced to bound pause time). The practical lever you control is *allocating fewer objects*; tuning is a second-order adjustment.
|
|
174
|
+
|
|
175
|
+
### GC.stat — read before you tune
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
GC.stat(:major_gc_count) # major (full) collections — expensive; want few
|
|
179
|
+
GC.stat(:minor_gc_count) # minor — cheap & frequent is fine
|
|
180
|
+
GC.stat(:heap_live_slots)
|
|
181
|
+
GC.stat(:total_allocated_objects) # churn proxy across run
|
|
182
|
+
GC.total_time # ns spent in GC (Ruby 3.x)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
If `major_gc_count` climbs fast or `GC.total_time` is a large fraction of wall time, you have an allocation problem — go back to `memory_profiler`/stackprof `:object`, do not jump to env tuning.
|
|
186
|
+
|
|
187
|
+
### GC tuning env vars
|
|
188
|
+
|
|
189
|
+
Set via environment at process start (cannot change most after boot). Sensible starting points for a server that allocates heavily — raise *initial* slots so the heap doesn't repeatedly grow at boot:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
RUBY_GC_HEAP_INIT_SLOTS=600000 # 3.2 (3.3+ split per size pool)
|
|
193
|
+
RUBY_GC_HEAP_GROWTH_FACTOR=1.1 # grow heap gently, fewer big jumps
|
|
194
|
+
RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO=0.20
|
|
195
|
+
RUBY_GC_MALLOC_LIMIT=64000000 # delay GC triggered by malloc growth
|
|
196
|
+
RUBY_GC_OLDMALLOC_LIMIT=128000000
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Don't cargo-cult these — measure `GC.stat` before/after under realistic load. Wrong values waste RAM or trigger more majors. On Ruby 3.3+ the per-size-pool slot vars (`RUBY_GC_HEAP_%d_INIT_SLOTS`) exist; defaults are usually fine.
|
|
200
|
+
|
|
201
|
+
### GC.compact and auto-compaction
|
|
202
|
+
|
|
203
|
+
`GC.compact` defragments the heap, improving locality and CoW sharing across forked workers. Use `GC.auto_compact = true` or compact once after boot (after eager-load), before forking Puma/Sidekiq workers.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# In an initializer / after eager load, before fork:
|
|
207
|
+
GC.compact
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Out-of-band GC
|
|
211
|
+
|
|
212
|
+
In forking servers, run a major GC *between* requests (off the hot path) so requests don't pay for it. Puma's `out_of_band` hook or `gctools`:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
# config/puma.rb
|
|
216
|
+
out_of_band { GC.start } if defined?(out_of_band)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Modern Ruby's incremental GC reduces the need; measure pause percentiles before adding this. Concurrency/fork details: see `references/concurrency.md`.
|
|
220
|
+
|
|
221
|
+
## Common hotspots
|
|
222
|
+
|
|
223
|
+
### N+1 queries
|
|
224
|
+
|
|
225
|
+
The single most common Rails performance bug. Eager-load associations; never trigger a query per row.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
# WRONG: 1 + N queries
|
|
229
|
+
Post.all.each { |p| puts p.author.name }
|
|
230
|
+
|
|
231
|
+
# RIGHT: 2 queries
|
|
232
|
+
Post.includes(:author).each { |p| puts p.author.name }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Detect with the `bullet` gem in dev. Full query-interface guidance (`includes`/`preload`/`eager_load`, `select`/`pluck`): see `references/rails.md`.
|
|
236
|
+
|
|
237
|
+
### Unbounded loads
|
|
238
|
+
|
|
239
|
+
Never load an unbounded result set into memory.
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# WRONG: loads the whole table at once
|
|
243
|
+
User.all.each { |u| process(u) }
|
|
244
|
+
|
|
245
|
+
# RIGHT: batches of 1000, constant memory
|
|
246
|
+
User.find_each { |u| process(u) } # iterate rows
|
|
247
|
+
User.in_batches(of: 5000) { |rel| rel.update_all(...) } # batch operate
|
|
248
|
+
User.where(active: true).pluck(:id) # only the column you need
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`find_each`/`in_batches`/`pluck`/`select` semantics: see `references/rails.md`.
|
|
252
|
+
|
|
253
|
+
### Building huge strings / payloads
|
|
254
|
+
|
|
255
|
+
Stream, don't accumulate (see Streaming). For JSON, prefer `oj` or stream rather than building one giant string in memory.
|
|
256
|
+
|
|
257
|
+
### Regexp catastrophic backtracking
|
|
258
|
+
|
|
259
|
+
Nested quantifiers over the same input (`(a+)+`, `(\w+)*`) cause exponential time on a near-match — a CPU hotspot as well as a DoS vector. Fix by anchoring and using possessive/atomic groups (`/\A(?>\w+)\z/`), and set `Regexp.timeout` as a safety net. The defensive patterns and the `Regexp.timeout` API live in `references/security.md` (ReDoS).
|
|
260
|
+
|
|
261
|
+
### Date/Time parsing costs
|
|
262
|
+
|
|
263
|
+
`Date.parse`/`Time.parse` are slow (they sniff arbitrary formats) and ambiguous. When you know the format, use `strptime` — often 5–10x faster — or compare against precomputed values.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
# WRONG: format-sniffing on every row
|
|
267
|
+
Time.parse("2026-06-09T12:00:00Z")
|
|
268
|
+
|
|
269
|
+
# RIGHT: explicit format
|
|
270
|
+
Time.strptime("2026-06-09T12:00:00Z", "%Y-%m-%dT%H:%M:%S%Z")
|
|
271
|
+
Date.strptime("2026-06-09", "%Y-%m-%d")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Caching strategies
|
|
275
|
+
|
|
276
|
+
### Memoization with `||=`
|
|
277
|
+
|
|
278
|
+
`@config ||= load_config` computes once per instance — a real win for repeated expensive calls. Two caveats live in the owning files: the nil/false `defined?` sentinel (`references/language-idioms.md`) and the thread-safety hazard — `||=` is not atomic, so for shared mutable caches use a `Mutex`/`Concurrent::Map` (`references/concurrency.md`). For a *parameterized* method, memoize into a hash keyed by args, not one ivar:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
def stats_for(id) = (@stats ||= {})[id] ||= compute(id)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Store-based caching
|
|
285
|
+
|
|
286
|
+
For cross-request/process caching, use `Rails.cache` (low-level) with a sane expiry; pick the most specific store (Solid Cache / Redis / Memcached). Use `fetch` so misses populate atomically-enough.
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
Rails.cache.fetch("user/#{user.id}/summary", expires_in: 1.hour) { build_summary(user) }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Russian-doll / fragment caching for views: see `references/rails.md`.
|
|
293
|
+
|
|
294
|
+
## Streaming & batching large data
|
|
295
|
+
|
|
296
|
+
Hold a window, not the whole dataset. Combine `find_each`/`in_batches` (DB) with `lazy` (transforms) and streaming output.
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
# CSV streaming export — constant memory, no giant String
|
|
300
|
+
require "csv"
|
|
301
|
+
File.open("export.csv", "w") do |f|
|
|
302
|
+
f.write CSV.generate_line(%w[id email])
|
|
303
|
+
User.find_each { |u| f.write CSV.generate_line([u.id, u.email]) }
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Rails: stream a response body instead of buffering it
|
|
309
|
+
self.response_body = Enumerator.new do |yielder|
|
|
310
|
+
yielder << CSV.generate_line(%w[id email])
|
|
311
|
+
User.find_each { |u| yielder << CSV.generate_line([u.id, u.email]) }
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
# Lazy pipeline over an infinite/huge source
|
|
317
|
+
(1..Float::INFINITY).lazy.select(&:even?).map { |n| n**2 }.first(5)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Choosing data structures
|
|
321
|
+
|
|
322
|
+
Lookup cost dominates in hot loops. `Array#include?` is **O(n)**; `Set#include?` and `Hash#key?` are **O(1)**.
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
# WRONG: O(n) per check inside a loop → O(n*m) total
|
|
326
|
+
BANNED = ["a", "b", "c", "..."] # array
|
|
327
|
+
ids.select { |id| BANNED.include?(id) }
|
|
328
|
+
|
|
329
|
+
# RIGHT: O(1) membership
|
|
330
|
+
require "set"
|
|
331
|
+
BANNED = Set["a", "b", "c"].freeze
|
|
332
|
+
ids.select { |id| BANNED.include?(id) }
|
|
333
|
+
|
|
334
|
+
# RIGHT: Hash when you also need an associated value
|
|
335
|
+
INDEX = records.index_by(&:id) # Rails; one pass, then O(1) lookups
|
|
336
|
+
INDEX[some_id]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Use `Hash#group_by`/`tally`/`index_by` to pre-build O(1) indexes instead of repeated scans. Prefer `Comparable`/`<=>`-based sorting once over repeated `min`/`max` scans.
|
|
340
|
+
|
|
341
|
+
## YJIT
|
|
342
|
+
|
|
343
|
+
YJIT is Ruby's production JIT (mature since 3.2, faster/leaner each release). It speeds up CPU-bound Ruby method dispatch and arithmetic — typically 15–40% on real Rails apps — at a small memory cost. **Enable it; it rarely hurts.**
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
ruby --yjit script.rb
|
|
347
|
+
RUBYOPT="--yjit" rails server
|
|
348
|
+
# Or in code, early at boot:
|
|
349
|
+
```
|
|
350
|
+
```ruby
|
|
351
|
+
RubyVM::YJIT.enable # Ruby 3.3+: turn on after boot (e.g. after fork-safe point)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
RubyVM::YJIT.runtime_stats # inspect compiled ratio, etc.
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
When it helps most: CPU-bound, method-dispatch-heavy code (rendering, serialization). When it helps least: IO-bound waits (no Ruby running to compile) — there YJIT is neutral. Tune memory with `--yjit-exec-mem-size` if RSS matters. There is no good reason to leave YJIT off in production on 3.2+.
|
|
359
|
+
|
|
360
|
+
## When to drop to a lower level
|
|
361
|
+
|
|
362
|
+
After you've cut allocations, fixed algorithms, indexed lookups, and enabled YJIT, and a *measured* hotspot is still dominated by pure Ruby compute:
|
|
363
|
+
- Replace with a maintained C-extension gem (e.g. `oj` for JSON, `nokogiri` for XML, `blake3`/`bcrypt`) before writing your own.
|
|
364
|
+
- Write a C extension or use **Rust via `rb-sys`/Magnus** only for a tight, well-bounded, heavily-profiled kernel — it's a real maintenance/portability cost.
|
|
365
|
+
- Consider pushing work into the database (aggregate in SQL) or another process.
|
|
366
|
+
- Evaluate an alternate runtime (TruffleRuby/JRuby) for CPU-bound or true-parallel workloads — see `references/concurrency.md`.
|
|
367
|
+
|
|
368
|
+
Never reach here without a profile proving the Ruby code (not IO, not GC, not the DB) is the bottleneck.
|
|
369
|
+
|
|
370
|
+
## Quick checklist
|
|
371
|
+
|
|
372
|
+
- Profile first (vernier or stackprof; rack-mini-profiler for web). Fix the biggest item, then re-measure.
|
|
373
|
+
- Compare implementations with `benchmark-ips` + `compare!`, on realistically-sized data.
|
|
374
|
+
- Treat allocations as the cost: use `memory_profiler` (allocated vs retained); cut intermediate arrays with `filter_map`/`each_with_object`/`lazy`.
|
|
375
|
+
- Put `# frozen_string_literal: true` in every file; build with `<<` into a `+""` buffer or `join`, never `+=` in a loop.
|
|
376
|
+
- Use symbols for fixed keys; never `to_sym` untrusted/unbounded input.
|
|
377
|
+
- Eager-load to kill N+1; never load unbounded sets — `find_each`/`in_batches`/`pluck`/`select`.
|
|
378
|
+
- Use `Set`/`Hash` for membership in hot loops, not `Array#include?`; pre-build indexes with `index_by`/`tally`.
|
|
379
|
+
- Avoid `Time.parse`/`Date.parse` in hot paths — use `strptime`. Guard regexes against ReDoS; set `Regexp.timeout`.
|
|
380
|
+
- Memoize with `||=` (mind nil/false and thread-safety); use `Rails.cache.fetch` for cross-request caching.
|
|
381
|
+
- Read `GC.stat`/`GC.total_time` before touching `RUBY_GC_HEAP_*`; `GC.compact` after eager-load before fork.
|
|
382
|
+
- Enable YJIT in production on Ruby 3.2+.
|
|
383
|
+
- Drop to a C/Rust extension only for a profiled, isolated kernel — prefer an existing native gem.
|