simple_flow 0.1.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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.rubocop.yml +57 -0
  5. data/CHANGELOG.md +4 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE +21 -0
  8. data/README.md +481 -0
  9. data/Rakefile +15 -0
  10. data/benchmarks/parallel_vs_sequential.rb +98 -0
  11. data/benchmarks/pipeline_overhead.rb +130 -0
  12. data/docs/api/middleware.md +468 -0
  13. data/docs/api/parallel-step.md +363 -0
  14. data/docs/api/pipeline.md +382 -0
  15. data/docs/api/result.md +375 -0
  16. data/docs/concurrent/best-practices.md +687 -0
  17. data/docs/concurrent/introduction.md +246 -0
  18. data/docs/concurrent/parallel-steps.md +418 -0
  19. data/docs/concurrent/performance.md +481 -0
  20. data/docs/core-concepts/flow-control.md +452 -0
  21. data/docs/core-concepts/middleware.md +389 -0
  22. data/docs/core-concepts/overview.md +219 -0
  23. data/docs/core-concepts/pipeline.md +315 -0
  24. data/docs/core-concepts/result.md +168 -0
  25. data/docs/core-concepts/steps.md +391 -0
  26. data/docs/development/benchmarking.md +443 -0
  27. data/docs/development/contributing.md +380 -0
  28. data/docs/development/dagwood-concepts.md +435 -0
  29. data/docs/development/testing.md +514 -0
  30. data/docs/getting-started/examples.md +197 -0
  31. data/docs/getting-started/installation.md +62 -0
  32. data/docs/getting-started/quick-start.md +218 -0
  33. data/docs/guides/choosing-concurrency-model.md +441 -0
  34. data/docs/guides/complex-workflows.md +440 -0
  35. data/docs/guides/data-fetching.md +478 -0
  36. data/docs/guides/error-handling.md +635 -0
  37. data/docs/guides/file-processing.md +505 -0
  38. data/docs/guides/validation-patterns.md +496 -0
  39. data/docs/index.md +169 -0
  40. data/examples/.gitignore +3 -0
  41. data/examples/01_basic_pipeline.rb +112 -0
  42. data/examples/02_error_handling.rb +178 -0
  43. data/examples/03_middleware.rb +186 -0
  44. data/examples/04_parallel_automatic.rb +221 -0
  45. data/examples/05_parallel_explicit.rb +279 -0
  46. data/examples/06_real_world_ecommerce.rb +288 -0
  47. data/examples/07_real_world_etl.rb +277 -0
  48. data/examples/08_graph_visualization.rb +246 -0
  49. data/examples/09_pipeline_visualization.rb +266 -0
  50. data/examples/10_concurrency_control.rb +235 -0
  51. data/examples/11_sequential_dependencies.rb +243 -0
  52. data/examples/12_none_constant.rb +161 -0
  53. data/examples/README.md +374 -0
  54. data/examples/regression_test/01_basic_pipeline.txt +38 -0
  55. data/examples/regression_test/02_error_handling.txt +92 -0
  56. data/examples/regression_test/03_middleware.txt +61 -0
  57. data/examples/regression_test/04_parallel_automatic.txt +86 -0
  58. data/examples/regression_test/05_parallel_explicit.txt +80 -0
  59. data/examples/regression_test/06_real_world_ecommerce.txt +53 -0
  60. data/examples/regression_test/07_real_world_etl.txt +58 -0
  61. data/examples/regression_test/08_graph_visualization.txt +429 -0
  62. data/examples/regression_test/09_pipeline_visualization.txt +305 -0
  63. data/examples/regression_test/10_concurrency_control.txt +96 -0
  64. data/examples/regression_test/11_sequential_dependencies.txt +86 -0
  65. data/examples/regression_test/12_none_constant.txt +64 -0
  66. data/examples/regression_test.rb +105 -0
  67. data/lib/simple_flow/dependency_graph.rb +120 -0
  68. data/lib/simple_flow/dependency_graph_visualizer.rb +326 -0
  69. data/lib/simple_flow/middleware.rb +36 -0
  70. data/lib/simple_flow/parallel_executor.rb +80 -0
  71. data/lib/simple_flow/pipeline.rb +405 -0
  72. data/lib/simple_flow/result.rb +88 -0
  73. data/lib/simple_flow/step_tracker.rb +58 -0
  74. data/lib/simple_flow/version.rb +5 -0
  75. data/lib/simple_flow.rb +41 -0
  76. data/mkdocs.yml +146 -0
  77. data/pipeline_graph.dot +51 -0
  78. data/pipeline_graph.html +60 -0
  79. data/pipeline_graph.mmd +19 -0
  80. metadata +127 -0
data/README.md ADDED
@@ -0,0 +1,481 @@
1
+ # SimpleFlow
2
+
3
+ [![Ruby Version](https://img.shields.io/badge/ruby-3.2%2B-ruby.svg)](https://www.ruby-lang.org)
4
+ [![Test Coverage](https://img.shields.io/badge/coverage-95.57%25-brightgreen.svg)](https://github.com/MadBomber/simple_flow)
5
+ [![Tests](https://img.shields.io/badge/tests-134%20passing-brightgreen.svg)](https://github.com/MadBomber/simple_flow)
6
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-blue.svg)](https://madbomber.github.io/simple_flow)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
9
+ A lightweight, modular Ruby framework for building composable data processing pipelines with middleware support, flow control, and parallel execution.
10
+
11
+ 📚 **[Full Documentation](https://madbomber.github.io/simple_flow)** | 🚀 **[Getting Started](https://madbomber.github.io/simple_flow/getting-started/quick-start/)** | 📖 **[API Reference](https://madbomber.github.io/simple_flow/api/result/)**
12
+
13
+ ## Overview
14
+
15
+ SimpleFlow provides a clean architecture for orchestrating multi-step workflows with:
16
+
17
+ - **Immutable Results** - Thread-safe value objects
18
+ - **Composable Steps** - Mix and match processing units
19
+ - **Flow Control** - Built-in halt/continue mechanisms
20
+ - **Middleware Support** - Cross-cutting concerns via decorator pattern
21
+ - **Parallel Execution** - Automatic and explicit concurrency
22
+ - **Visualization** - Export pipelines to Graphviz, Mermaid, HTML
23
+
24
+ ## Installation
25
+
26
+ Add to your `Gemfile`:
27
+
28
+ ```ruby
29
+ gem 'simple_flow'
30
+
31
+ # Optional: for fiber-based concurrency (recommended for I/O-bound tasks)
32
+ gem 'async', '~> 2.0'
33
+ ```
34
+
35
+ Then run:
36
+
37
+ ```bash
38
+ bundle install
39
+ ```
40
+
41
+ **Note on Parallel Execution:**
42
+ - **Without** `async` gem: Uses Ruby threads for parallel execution
43
+ - **With** `async` gem: Uses fiber-based concurrency (more efficient for I/O-bound operations)
44
+
45
+ ## Quick Start
46
+
47
+ ### Basic Pipeline
48
+
49
+ ```ruby
50
+ require 'simple_flow'
51
+
52
+ pipeline = SimpleFlow::Pipeline.new do
53
+ step ->(result) { result.continue(result.value.strip) }
54
+ step ->(result) { result.continue(result.value.upcase) }
55
+ step ->(result) { result.continue("Hello, #{result.value}!") }
56
+ end
57
+
58
+ result = pipeline.call(SimpleFlow::Result.new(" world "))
59
+ puts result.value # => "Hello, WORLD!"
60
+ ```
61
+
62
+ ### Error Handling
63
+
64
+ **Sequential steps automatically depend on the previous step's success.** When a step halts, the pipeline stops immediately and subsequent steps are not executed.
65
+
66
+ ```ruby
67
+ pipeline = SimpleFlow::Pipeline.new do
68
+ step ->(result) {
69
+ puts "Step 1: Validating..."
70
+ if result.value < 18
71
+ return result
72
+ .with_error(:validation, 'Must be 18+')
73
+ .halt # Pipeline stops here
74
+ end
75
+ result.continue(result.value)
76
+ }
77
+
78
+ step ->(result) {
79
+ puts "Step 2: Processing..." # This never executes if validation fails
80
+ result.continue("Age #{result.value} is valid")
81
+ }
82
+ end
83
+
84
+ result = pipeline.call(SimpleFlow::Result.new(15))
85
+ # Output: "Step 1: Validating..."
86
+ # (Step 2 is skipped because Step 1 halted)
87
+
88
+ puts result.continue? # => false
89
+ puts result.errors # => {:validation=>["Must be 18+"]}
90
+ ```
91
+
92
+ ## Architecture
93
+
94
+ ```mermaid
95
+ graph TB
96
+ subgraph Pipeline
97
+ MW[Middleware Stack]
98
+ S1[Step 1]
99
+ S2[Step 2]
100
+ S3[Step 3]
101
+ end
102
+
103
+ Input[Input Result] --> MW
104
+ MW --> S1
105
+ S1 -->|continue?| S2
106
+ S2 -->|continue?| S3
107
+ S3 --> Output[Output Result]
108
+
109
+ S1 -.->|halt| Output
110
+ S2 -.->|halt| Output
111
+
112
+ style MW fill:#e1f5ff
113
+ style Output fill:#d4edda
114
+ ```
115
+
116
+ ## Execution Modes
117
+
118
+ SimpleFlow supports two execution modes:
119
+
120
+ ### Sequential Steps (Default)
121
+
122
+ **Unnamed steps execute in order, with each step automatically depending on the previous step's success.**
123
+
124
+ ```ruby
125
+ pipeline = SimpleFlow::Pipeline.new do
126
+ step ->(result) { result.continue(result.value.strip) }
127
+ step ->(result) { result.continue(result.value.upcase) }
128
+ step ->(result) { result.continue("Hello, #{result.value}!") }
129
+ end
130
+
131
+ result = pipeline.call(SimpleFlow::Result.new(" world "))
132
+ ```
133
+
134
+ **Key behavior:**
135
+ - Steps run one at a time in definition order
136
+ - Each step receives the result from the previous step
137
+ - If any step halts, the pipeline stops immediately
138
+ - No explicit dependencies needed
139
+
140
+ ### Parallel Steps
141
+
142
+ **Named steps with dependencies run concurrently based on a dependency graph.**
143
+
144
+ ```ruby
145
+ pipeline = SimpleFlow::Pipeline.new do
146
+ step :validate, validator, depends_on: :none # Or use []
147
+ step :fetch_a, fetcher_a, depends_on: [:validate] # Parallel
148
+ step :fetch_b, fetcher_b, depends_on: [:validate] # Parallel
149
+ step :merge, merger, depends_on: [:fetch_a, :fetch_b]
150
+ end
151
+
152
+ result = pipeline.call_parallel(SimpleFlow::Result.new(data))
153
+ ```
154
+
155
+ **Key behavior:**
156
+ - Steps run based on dependency graph, not definition order
157
+ - Steps with satisfied dependencies run in parallel
158
+ - Must explicitly specify all dependencies
159
+ - Use `call_parallel` to execute
160
+
161
+ ## Core Concepts
162
+
163
+ ### Result Object
164
+
165
+ Immutable value object that carries data, context, and errors through the pipeline:
166
+
167
+ ```ruby
168
+ result = SimpleFlow::Result.new({ user: 'alice' })
169
+ .with_context(:timestamp, Time.now)
170
+ .with_error(:validation, 'Email required')
171
+ .continue({ user: 'alice', processed: true })
172
+ ```
173
+
174
+ **[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/result/)**
175
+
176
+ ### Pipeline
177
+
178
+ Orchestrates step execution with short-circuit evaluation:
179
+
180
+ ```ruby
181
+ pipeline = SimpleFlow::Pipeline.new do
182
+ use_middleware SimpleFlow::MiddleWare::Logging
183
+ use_middleware SimpleFlow::MiddleWare::Instrumentation, api_key: 'app'
184
+
185
+ step ->(result) { validate(result) }
186
+ step ->(result) { process(result) }
187
+ step ->(result) { save(result) }
188
+ end
189
+ ```
190
+
191
+ **[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/pipeline/)**
192
+
193
+ ### Middleware
194
+
195
+ Add cross-cutting concerns without modifying steps:
196
+
197
+ ```ruby
198
+ class CachingMiddleware
199
+ def initialize(callable, cache:)
200
+ @callable = callable
201
+ @cache = cache
202
+ end
203
+
204
+ def call(result)
205
+ cached = @cache.get(cache_key(result))
206
+ return result.continue(cached) if cached
207
+
208
+ result = @callable.call(result)
209
+ @cache.set(cache_key(result), result.value)
210
+ result
211
+ end
212
+ end
213
+
214
+ pipeline = SimpleFlow::Pipeline.new do
215
+ use CachingMiddleware, cache: Redis.new
216
+ step ->(result) { expensive_operation(result) }
217
+ end
218
+ ```
219
+
220
+ **[Learn more →](https://madbomber.github.io/simple_flow/core-concepts/middleware/)**
221
+
222
+ ## Parallel Execution
223
+
224
+ ### Automatic Parallelization
225
+
226
+ SimpleFlow automatically detects which steps can run in parallel based on dependencies:
227
+
228
+ ```ruby
229
+ pipeline = SimpleFlow::Pipeline.new do
230
+ step :validate, ->(r) { validate(r) }, depends_on: :none
231
+
232
+ # These run in parallel (both depend only on :validate)
233
+ step :fetch_orders, ->(r) { fetch_orders(r) }, depends_on: [:validate]
234
+ step :fetch_products, ->(r) { fetch_products(r) }, depends_on: [:validate]
235
+
236
+ # Waits for both parallel steps
237
+ step :calculate, ->(r) { calculate(r) }, depends_on: [:fetch_orders, :fetch_products]
238
+ end
239
+
240
+ result = pipeline.call_parallel(SimpleFlow::Result.new(data))
241
+ ```
242
+
243
+ **Note:** For steps with no dependencies, you can use either `depends_on: :none` (more readable) or `depends_on: []`.
244
+
245
+ **Execution flow:**
246
+
247
+ ```mermaid
248
+ graph TD
249
+ V[validate] --> O[fetch_orders]
250
+ V --> P[fetch_products]
251
+ O --> C[calculate]
252
+ P --> C
253
+
254
+ style V fill:#e1f5ff
255
+ style O fill:#fff3cd
256
+ style P fill:#fff3cd
257
+ style C fill:#d4edda
258
+
259
+ classDef parallel fill:#fff3cd,stroke:#ffc107
260
+ class O,P parallel
261
+ ```
262
+
263
+ ### Explicit Parallel Blocks
264
+
265
+ ```ruby
266
+ pipeline = SimpleFlow::Pipeline.new do
267
+ step ->(r) { validate(r) }
268
+
269
+ parallel do
270
+ step ->(r) { r.with_context(:api, fetch_api).continue(r.value) }
271
+ step ->(r) { r.with_context(:db, fetch_db).continue(r.value) }
272
+ step ->(r) { r.with_context(:cache, fetch_cache).continue(r.value) }
273
+ end
274
+
275
+ step ->(r) { merge_results(r) }
276
+ end
277
+ ```
278
+
279
+ ### Concurrency Control
280
+
281
+ Choose the concurrency model per pipeline:
282
+
283
+ ```ruby
284
+ # Auto-detect (default): uses async if available, otherwise threads
285
+ pipeline = SimpleFlow::Pipeline.new do
286
+ # steps...
287
+ end
288
+
289
+ # Force threads (even if async gem is installed)
290
+ user_pipeline = SimpleFlow::Pipeline.new(concurrency: :threads) do
291
+ step :fetch_profile, profile_fetcher, depends_on: []
292
+ step :fetch_settings, settings_fetcher, depends_on: []
293
+ end
294
+
295
+ # Require async (raises error if async gem not available)
296
+ batch_pipeline = SimpleFlow::Pipeline.new(concurrency: :async) do
297
+ step :load_batch, batch_loader, depends_on: []
298
+ step :process_batch, batch_processor, depends_on: [:load_batch]
299
+ end
300
+
301
+ # Mix concurrency models in the same application!
302
+ user_result = user_pipeline.call_parallel(user_data) # Uses threads
303
+ batch_result = batch_pipeline.call_parallel(batch_data) # Uses async
304
+ ```
305
+
306
+ **Concurrency options:**
307
+ - `:auto` (default) - Auto-detects best option (async if available, otherwise threads)
308
+ - `:threads` - Always uses Ruby threads (simpler, works with any gems)
309
+ - `:async` - Requires async gem (efficient for high-concurrency workloads)
310
+
311
+ **[Learn more →](https://madbomber.github.io/simple_flow/guides/choosing-concurrency-model/)**
312
+
313
+ **[Parallel execution →](https://madbomber.github.io/simple_flow/concurrent/parallel-steps/)**
314
+
315
+ ## Visualization
316
+
317
+ Visualize your pipelines to understand execution flow:
318
+
319
+ ```ruby
320
+ pipeline = SimpleFlow::Pipeline.new do
321
+ step :load, loader, depends_on: []
322
+ step :transform, transformer, depends_on: [:load]
323
+ step :validate, validator, depends_on: [:transform]
324
+ step :save, saver, depends_on: [:validate]
325
+ end
326
+
327
+ # ASCII visualization
328
+ puts pipeline.visualize_ascii
329
+
330
+ # Export to Graphviz
331
+ File.write('pipeline.dot', pipeline.visualize_dot)
332
+
333
+ # Export to Mermaid
334
+ File.write('pipeline.mmd', pipeline.visualize_mermaid)
335
+
336
+ # View execution plan
337
+ puts pipeline.execution_plan
338
+ ```
339
+
340
+ **Generated Mermaid diagram:**
341
+
342
+ ```mermaid
343
+ graph TB
344
+ load --> transform
345
+ transform --> validate
346
+ validate --> save
347
+
348
+ style load fill:#e1f5ff
349
+ style transform fill:#fff3cd
350
+ style validate fill:#fce4ec
351
+ style save fill:#d4edda
352
+ ```
353
+
354
+ **[Learn more →](https://madbomber.github.io/simple_flow/getting-started/examples/)**
355
+
356
+ ## Real-World Example
357
+
358
+ E-commerce order processing pipeline:
359
+
360
+ ```ruby
361
+ pipeline = SimpleFlow::Pipeline.new do
362
+ use SimpleFlow::MiddleWare::Logging
363
+ use SimpleFlow::MiddleWare::Instrumentation, api_key: 'orders'
364
+
365
+ step :validate_order, ->(r) {
366
+ # Validation logic
367
+ r.continue(r.value)
368
+ }, depends_on: []
369
+
370
+ # Run in parallel
371
+ step :check_inventory, ->(r) {
372
+ inventory = InventoryService.check(r.value[:items])
373
+ r.with_context(:inventory, inventory).continue(r.value)
374
+ }, depends_on: [:validate_order]
375
+
376
+ step :calculate_shipping, ->(r) {
377
+ shipping = ShippingService.calculate(r.value[:address])
378
+ r.with_context(:shipping, shipping).continue(r.value)
379
+ }, depends_on: [:validate_order]
380
+
381
+ # Wait for parallel steps
382
+ step :process_payment, ->(r) {
383
+ payment = PaymentService.charge(r.value, r.context)
384
+ r.with_context(:payment, payment).continue(r.value)
385
+ }, depends_on: [:check_inventory, :calculate_shipping]
386
+
387
+ step :send_confirmation, ->(r) {
388
+ EmailService.send_confirmation(r.value, r.context)
389
+ r.continue(r.value)
390
+ }, depends_on: [:process_payment]
391
+ end
392
+ ```
393
+
394
+ **Execution flow:**
395
+
396
+ ```mermaid
397
+ graph TB
398
+ V[validate_order] --> I[check_inventory]
399
+ V --> S[calculate_shipping]
400
+ I --> P[process_payment]
401
+ S --> P
402
+ P --> C[send_confirmation]
403
+
404
+ style V fill:#e1f5ff
405
+ style I fill:#fff3cd
406
+ style S fill:#fff3cd
407
+ style P fill:#fce4ec
408
+ style C fill:#d4edda
409
+
410
+ classDef parallel fill:#fff3cd,stroke:#ffc107,stroke-width:3px
411
+ class I,S parallel
412
+ ```
413
+
414
+ ## Testing
415
+
416
+ SimpleFlow has excellent test coverage:
417
+
418
+ ```bash
419
+ bundle exec rake test
420
+ ```
421
+
422
+ **Test Results:**
423
+ - ✅ 134 tests passing
424
+ - ✅ 480 assertions
425
+ - ✅ 95.57% line coverage
426
+
427
+ **[Testing Guide →](https://madbomber.github.io/simple_flow/development/testing/)**
428
+
429
+ ## Documentation
430
+
431
+ 📚 **Comprehensive documentation available at [madbomber.github.io/simple_flow](https://madbomber.github.io/simple_flow)**
432
+
433
+ ### Key Resources
434
+
435
+ - [Getting Started Guide](https://madbomber.github.io/simple_flow/getting-started/quick-start/) - Quick introduction
436
+ - [Core Concepts](https://madbomber.github.io/simple_flow/core-concepts/overview/) - Understanding the fundamentals
437
+ - [Parallel Execution](https://madbomber.github.io/simple_flow/concurrent/parallel-steps/) - Concurrent processing
438
+ - [Guides](https://madbomber.github.io/simple_flow/guides/error-handling/) - Error handling, validation, workflows
439
+ - [API Reference](https://madbomber.github.io/simple_flow/api/result/) - Complete API documentation
440
+ - [Contributing](https://madbomber.github.io/simple_flow/development/contributing/) - How to contribute
441
+
442
+ ## Examples
443
+
444
+ Check out the `examples/` directory for comprehensive examples:
445
+
446
+ 1. `01_basic_pipeline.rb` - Basic sequential processing
447
+ 2. `02_error_handling.rb` - Error handling patterns
448
+ 3. `03_middleware.rb` - Middleware usage
449
+ 4. `04_parallel_automatic.rb` - Automatic parallel discovery
450
+ 5. `05_parallel_explicit.rb` - Explicit parallel blocks
451
+ 6. `06_real_world_ecommerce.rb` - Complete e-commerce workflow
452
+ 7. `07_real_world_etl.rb` - ETL pipeline example
453
+ 8. `08_graph_visualization.rb` - Manual visualization
454
+ 9. `09_pipeline_visualization.rb` - Direct pipeline visualization
455
+ 10. `10_concurrency_control.rb` - Per-pipeline concurrency control
456
+ 11. `11_sequential_dependencies.rb` - Sequential step dependencies and halting
457
+ 12. `12_none_constant.rb` - Using reserved dependency symbols `:none` and `:nothing`
458
+
459
+ ## Requirements
460
+
461
+ - Ruby 3.2 or higher
462
+ - Optional: `async` gem (~> 2.0) for parallel execution
463
+
464
+ ## License
465
+
466
+ MIT License - See [LICENSE](LICENSE) file for details
467
+
468
+ ## Contributing
469
+
470
+ Contributions welcome! See [CONTRIBUTING.md](https://madbomber.github.io/simple_flow/development/contributing/) for guidelines.
471
+
472
+ ## Links
473
+
474
+ - 🏠 [Homepage](https://github.com/MadBomber/simple_flow)
475
+ - 📚 [Documentation](https://madbomber.github.io/simple_flow)
476
+ - 🐛 [Issue Tracker](https://github.com/MadBomber/simple_flow/issues)
477
+ - 📝 [Changelog](CHANGELOG.md)
478
+
479
+ ---
480
+
481
+ **Made with ❤️ by [Dewayne VanHoozer](https://github.com/MadBomber)**
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ t.verbose = true
11
+ # Load test_helper before any tests run to ensure SimpleCov starts first
12
+ t.ruby_opts << "-rtest_helper"
13
+ end
14
+
15
+ task default: :test
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark/ips'
6
+ require_relative '../lib/simple_flow'
7
+
8
+ puts '=' * 80
9
+ puts 'SimpleFlow Performance Benchmarks: Parallel vs Sequential Execution'
10
+ puts '=' * 80
11
+ puts
12
+
13
+ # Simulate I/O operations with different delays
14
+ def io_operation(delay = 0.01)
15
+ sleep delay
16
+ :completed
17
+ end
18
+
19
+ # Define steps that perform I/O operations
20
+ step1 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
21
+ step2 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
22
+ step3 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
23
+ step4 = ->(result) { io_operation(0.01); result.continue(result.value + 1) }
24
+
25
+ # Sequential pipeline
26
+ sequential_pipeline = SimpleFlow::Pipeline.new do
27
+ step step1
28
+ step step2
29
+ step step3
30
+ step step4
31
+ end
32
+
33
+ # Parallel pipeline
34
+ parallel_pipeline = SimpleFlow::Pipeline.new do
35
+ parallel do
36
+ step step1
37
+ step step2
38
+ step step3
39
+ step step4
40
+ end
41
+ end
42
+
43
+ puts "Benchmark: 4 I/O operations (0.01s each)"
44
+ puts "Expected sequential time: ~0.04s"
45
+ puts "Expected parallel time: ~0.01s"
46
+ puts
47
+
48
+ Benchmark.ips do |x|
49
+ x.config(time: 5, warmup: 2)
50
+
51
+ x.report('sequential') do
52
+ sequential_pipeline.call(SimpleFlow::Result.new(0))
53
+ end
54
+
55
+ x.report('parallel') do
56
+ parallel_pipeline.call(SimpleFlow::Result.new(0))
57
+ end
58
+
59
+ x.compare!
60
+ end
61
+
62
+ puts "\n" + '=' * 80
63
+ puts
64
+
65
+ # Benchmark with varying number of parallel steps
66
+ puts 'Benchmark: Scaling with different numbers of parallel steps'
67
+ puts '=' * 80
68
+ puts
69
+
70
+ [2, 4, 8, 16].each do |count|
71
+ steps = Array.new(count) { ->(r) { io_operation(0.005); r.continue(r.value) } }
72
+
73
+ seq_pipeline = SimpleFlow::Pipeline.new
74
+ steps.each { |s| seq_pipeline.step(s) }
75
+
76
+ par_pipeline = SimpleFlow::Pipeline.new do
77
+ parallel do
78
+ steps.each { |s| step(s) }
79
+ end
80
+ end
81
+
82
+ seq_time = Benchmark.realtime do
83
+ seq_pipeline.call(SimpleFlow::Result.new(nil))
84
+ end
85
+
86
+ par_time = Benchmark.realtime do
87
+ par_pipeline.call(SimpleFlow::Result.new(nil))
88
+ end
89
+
90
+ speedup = seq_time / par_time
91
+
92
+ puts "\n#{count} steps (0.005s each):"
93
+ puts " Sequential: #{(seq_time * 1000).round(2)}ms"
94
+ puts " Parallel: #{(par_time * 1000).round(2)}ms"
95
+ puts " Speedup: #{speedup.round(2)}x"
96
+ end
97
+
98
+ puts "\n" + '=' * 80