Skip to content

[AGENT] Improve performance, fix bugs, and restructure documentation#39

Merged
petergebala merged 8 commits into
masterfrom
feature/opera-improvements
Apr 15, 2026
Merged

[AGENT] Improve performance, fix bugs, and restructure documentation#39
petergebala merged 8 commits into
masterfrom
feature/opera-improvements

Conversation

@kikorb

@kikorb kikorb commented Apr 14, 2026

Copy link
Copy Markdown
Member

Summary

A focused set of improvements to Opera's internals, correctness, and developer experience.

  • Eliminate instruction hash mutation and Marshal.dump — ~40-55% throughput improvement
  • Make all executors explicit — no more super indirection or instruction[:kind] rewrites
  • Fix Config.development_mode? bug — referenced non-existent constant
  • Remove implicit Rails coupling from Config initialization
  • Restructure README from 1,175 lines to 221 lines with examples moved to docs/
  • Add benchmark script for ongoing performance testing

Changes

1. Stop mutating instruction hashes + remove Marshal.dump (c034edb)

Five executors (Success, Validate, FinishIf, Operation, Operations) previously rewrote instruction[:kind] = :step and called super to delegate to the Step executor. This mutation forced a Marshal.load(Marshal.dump(instructions)) deep copy of the entire instruction tree on every operation call.

Fix: Extracted execute_step as a shared primitive on the base Executor class. All executors now call it directly to invoke a step method, instrument it, and record execution — without touching the instruction hash. The Marshal.dump and while/shift loop are replaced with a simple each/break.

2. Make Within executor explicit (12a44d2)

Within was the last executor still using super to delegate to Executor#call. While it didn't mutate instruction hashes, it was inconsistent with the rest of the codebase after the refactor. Now calls evaluate_instructions(nested_instructions) directly — same behaviour, no indirection.

After this change, no executor uses super — each one calls exactly the primitive it needs (execute_step or evaluate_instructions).

3. Fix Config.development_mode? (d62a7af)

The class method referenced DEFAULT_MODE which does not exist — calling it would raise NameError. Fixed to reference DEVELOPMENT_MODE.

4. Remove custom_reporter Rails coupling (dd48ecb)

Config#custom_reporter implicitly read Rails.application.config.x.reporter.presence at initialization, silently overriding any explicitly configured reporter. Removed — the reporter is now set exclusively via the config block.

BREAKING: Apps relying on config.x.reporter auto-detection (e.g., HAL's test.rb) must add config.reporter = ... to their Opera initializer when upgrading.

5. Restructure README (113f20b)

Before After
1,175 lines 221-line quick-start guide
Examples inline with raw #inspect output Examples in docs/examples/ (7 files, 962 lines)
Deprecated patterns shown without explanation Clean examples using current API
API reference in plain text DSL reference and Result API as tables

All existing examples preserved verbatim — nothing lost, just reorganized.

6. Add benchmark script (e6363b1, faa02cd)

benchmarks/operation_benchmark.rb exercises the full execution path:

  • ComplexOperation: validate → step → finish_if → transaction → operation (nested) → within → success → output
  • WithinOperation: step → within (steps + operation) → transaction (within (steps) + step) — nested within inside transaction
  • BatchOperation: operations (plural) with multiple inner ops
  • ValidationOperation: validate + transform
  • LeafOperation: minimal 2-step operation

Each runs 1,000 iterations. ComplexOperation spawns ~7,000 total operation instances per run.

Performance Report

Environment: Ruby 3.3.4 (arm64-darwin24), Apple Silicon
Method: 1,000 iterations × 3 runs per branch, median real time
Mode: production (no execution traces, matching real deployments)

Scenario master (before) feature (after) Improvement
ComplexOperation (7 ops/call, tx, within, validate) 86.2ms 52.0ms 39.7% faster
WithinOperation (within nested in tx, inner op) 41.2ms 18.8ms 54.4% faster
BatchOperation (6 ops/call, operations plural) 34.9ms 21.1ms 39.5% faster
ValidationOperation (validate + steps) 7.9ms 4.9ms 38.0% faster
LeafOperation (minimal 2-step) 5.5ms 3.2ms 41.8% faster

Times are wall-clock for 1,000 calls.

Why WithinOperation shows the largest improvement (54.4%)

within produces the deepest instruction tree nesting — a within block inside a transaction block creates three levels of nested instruction arrays. On master, Marshal.dump was called at every level of evaluate_instructions, so deeper nesting meant more serialization overhead. With the refactor, there is no copying at all — instructions are iterated directly. The deeper the nesting, the bigger the win.

This is particularly relevant because within is commonly used inside transaction blocks in production (e.g., wrapping steps with ActiveRecord::Base.connected_to for replica reads inside a write transaction).

How the benchmark was run

# On master
git checkout master
ruby benchmarks/operation_benchmark.rb   # 3 runs, recorded median

# On feature branch
git checkout feature/opera-improvements
ruby benchmarks/operation_benchmark.rb   # 3 runs, recorded median

Raw results

master — 3 runs
--- Run 1 ---
ComplexOperation (nested + tx):       0.085125   0.000554   0.085679 (  0.086234)
BatchOperation (operations):          0.034645   0.000114   0.034759 (  0.034872)
ValidationOperation (validate):       0.007859   0.000026   0.007885 (  0.007930)
WithinOperation (within + tx):        0.040594   0.000144   0.040738 (  0.040787)
LeafOperation (minimal):              0.005324   0.000018   0.005342 (  0.005361)

--- Run 2 ---
ComplexOperation (nested + tx):       0.085605   0.000723   0.086328 (  0.086591)
BatchOperation (operations):          0.034668   0.000335   0.035003 (  0.035093)
ValidationOperation (validate):       0.007835   0.000030   0.007865 (  0.007887)
WithinOperation (within + tx):        0.040899   0.000198   0.041097 (  0.041234)
LeafOperation (minimal):              0.005482   0.000038   0.005520 (  0.005562)

--- Run 3 ---
ComplexOperation (nested + tx):       0.082486   0.000568   0.083054 (  0.083142)
BatchOperation (operations):          0.034170   0.000189   0.034359 (  0.034390)
ValidationOperation (validate):       0.007788   0.000067   0.007855 (  0.007866)
WithinOperation (within + tx):        0.040847   0.000237   0.041084 (  0.041174)
LeafOperation (minimal):              0.005436   0.000056   0.005492 (  0.005530)
feature/opera-improvements — 3 runs
--- Run 1 ---
ComplexOperation (nested + tx):       0.051383   0.000343   0.051726 (  0.051912)
BatchOperation (operations):          0.021080   0.000141   0.021221 (  0.021256)
ValidationOperation (validate):       0.004878   0.000010   0.004888 (  0.004887)
WithinOperation (within + tx):        0.018886   0.000123   0.019009 (  0.019127)
LeafOperation (minimal):              0.003487   0.000012   0.003499 (  0.003506)

--- Run 2 ---
ComplexOperation (nested + tx):       0.052375   0.000463   0.052838 (  0.052990)
BatchOperation (operations):          0.020900   0.000158   0.021058 (  0.021151)
ValidationOperation (validate):       0.004858   0.000016   0.004874 (  0.004896)
WithinOperation (within + tx):        0.018317   0.000052   0.018369 (  0.018422)
LeafOperation (minimal):              0.003133   0.000009   0.003142 (  0.003156)

--- Run 3 ---
ComplexOperation (nested + tx):       0.053207   0.000449   0.053656 (  0.054016)
BatchOperation (operations):          0.020927   0.000121   0.021048 (  0.021127)
ValidationOperation (validate):       0.005132   0.000035   0.005167 (  0.005186)
WithinOperation (within + tx):        0.018642   0.000088   0.018730 (  0.018824)
LeafOperation (minimal):              0.003440   0.000045   0.003485 (  0.003528)

The benchmark script is included in this PR at benchmarks/operation_benchmark.rb so these numbers can be reproduced and tracked over time.

Test Results

All 100 existing specs pass on every commit — no test changes required.

kikorb added 5 commits April 14, 2026 15:44
Executors previously rewrote instruction[:kind] to :step to delegate
to the Step executor via super, which forced a Marshal.dump deep copy
of the entire instruction tree on every operation call.

Extract execute_step as a shared primitive on the base Executor class
that any executor can call directly to invoke a step method, instrument
it, and record the execution -- without mutating the instruction hash.

This eliminates the need for any copying and yields ~40% throughput
improvement on operation calls.
…nstant

The class method referenced DEFAULT_MODE which does not exist, causing
a NameError if ever called. Corrected to DEVELOPMENT_MODE to match
the constant defined at the top of the class.
Config previously reached into Rails.application.config.x.reporter at
initialization time, silently overriding any explicitly configured
reporter. The reporter should be set by the consumer via the config
block, not auto-detected from the host framework.

BREAKING: Apps relying on config.x.reporter must now set the reporter
explicitly in their Opera initializer.
Slim the README from 1,175 to 221 lines, focusing on what developers
need to get started: installation, one complete example, configuration,
DSL reference table, and Result API table.

All existing examples are preserved verbatim in docs/examples/ as
separate files organized by topic: basic operations, validations,
transactions, success blocks, finish_if, inner operations, within,
and context/params/dependencies.
Exercises the full execution path across four operation types:
ComplexOperation (nested ops + transaction + within + validate +
success + finish_if), BatchOperation (operations plural),
ValidationOperation, and LeafOperation (minimal). Runs 1000
iterations of each, spawning ~7000 total operation instances for
the complex scenario.
@@ -0,0 +1,240 @@
# frozen_string_literal: true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is here temporarily for the reviewers to understand the benchmark data, we can remove it later

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep it for future. It is branch agnostic. We can run it for sanity check before and after every major refactor

@kikorb kikorb changed the title Improve performance, fix bugs, and restructure documentation [AGENT] Improve performance, fix bugs, and restructure documentation Apr 14, 2026
@kikorb kikorb marked this pull request as ready for review April 14, 2026 14:54
@kikorb kikorb requested review from cintrzyk, msx2 and petergebala April 14, 2026 15:06
kikorb added 2 commits April 14, 2026 17:11
Exercises within nested inside a transaction with multiple steps and
an inner operation, giving a dedicated measurement for the within
executor path which benefits most from the Marshal.dump removal due
to its deeper instruction tree nesting.
Replace super delegation with an explicit evaluate_instructions call,
consistent with how all other executors now work after the execute_step
refactor. No behavioral change -- just removes the last indirect super
call from the executor hierarchy.

@petergebala petergebala left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets update changelog and bump version too :)

@kikorb kikorb force-pushed the feature/opera-improvements branch from 1df3e94 to 70af2bc Compare April 15, 2026 09:10
@petergebala petergebala merged commit 744f51c into master Apr 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants