kbs 0.0.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +68 -2
- data/README.md +235 -334
- data/docs/DOCUMENTATION_STATUS.md +158 -0
- data/docs/advanced/custom-persistence.md +775 -0
- data/docs/advanced/debugging.md +726 -0
- data/docs/advanced/index.md +8 -0
- data/docs/advanced/performance.md +832 -0
- data/docs/advanced/testing.md +691 -0
- data/docs/api/blackboard.md +1157 -0
- data/docs/api/engine.md +978 -0
- data/docs/api/facts.md +1212 -0
- data/docs/api/index.md +12 -0
- data/docs/api/rules.md +1034 -0
- data/docs/architecture/blackboard.md +553 -0
- data/docs/architecture/index.md +277 -0
- data/docs/architecture/network-structure.md +343 -0
- data/docs/architecture/rete-algorithm.md +737 -0
- data/docs/assets/css/custom.css +83 -0
- data/docs/assets/images/blackboard-architecture.svg +136 -0
- data/docs/assets/images/compiled-network.svg +101 -0
- data/docs/assets/images/fact-assertion-flow.svg +117 -0
- data/docs/assets/images/kbs.jpg +0 -0
- data/docs/assets/images/pattern-matching-trace.svg +136 -0
- data/docs/assets/images/rete-network-layers.svg +96 -0
- data/docs/assets/images/system-layers.svg +69 -0
- data/docs/assets/images/trading-signal-network.svg +139 -0
- data/docs/assets/js/mathjax.js +17 -0
- data/docs/examples/expert-systems.md +1031 -0
- data/docs/examples/index.md +9 -0
- data/docs/examples/multi-agent.md +1335 -0
- data/docs/examples/stock-trading.md +488 -0
- data/docs/guides/blackboard-memory.md +558 -0
- data/docs/guides/dsl.md +1321 -0
- data/docs/guides/facts.md +652 -0
- data/docs/guides/getting-started.md +383 -0
- data/docs/guides/index.md +23 -0
- data/docs/guides/negation.md +529 -0
- data/docs/guides/pattern-matching.md +561 -0
- data/docs/guides/persistence.md +451 -0
- data/docs/guides/variable-binding.md +491 -0
- data/docs/guides/writing-rules.md +755 -0
- data/docs/index.md +157 -0
- data/docs/installation.md +156 -0
- data/docs/quick-start.md +228 -0
- data/examples/README.md +2 -2
- data/examples/advanced_example.rb +2 -2
- data/examples/advanced_example_dsl.rb +224 -0
- data/examples/ai_enhanced_kbs.rb +1 -1
- data/examples/ai_enhanced_kbs_dsl.rb +538 -0
- data/examples/blackboard_demo_dsl.rb +50 -0
- data/examples/car_diagnostic.rb +1 -1
- data/examples/car_diagnostic_dsl.rb +54 -0
- data/examples/concurrent_inference_demo.rb +5 -5
- data/examples/concurrent_inference_demo_dsl.rb +363 -0
- data/examples/csv_trading_system.rb +1 -1
- data/examples/csv_trading_system_dsl.rb +525 -0
- data/examples/knowledge_base.db +0 -0
- data/examples/portfolio_rebalancing_system.rb +2 -2
- data/examples/portfolio_rebalancing_system_dsl.rb +613 -0
- data/examples/redis_trading_demo_dsl.rb +177 -0
- data/examples/run_all.rb +50 -0
- data/examples/run_all_dsl.rb +49 -0
- data/examples/stock_trading_advanced.rb +1 -1
- data/examples/stock_trading_advanced_dsl.rb +404 -0
- data/examples/temp.txt +7693 -0
- data/examples/temp_dsl.txt +8447 -0
- data/examples/timestamped_trading.rb +1 -1
- data/examples/timestamped_trading_dsl.rb +258 -0
- data/examples/trading_demo.rb +1 -1
- data/examples/trading_demo_dsl.rb +322 -0
- data/examples/working_demo.rb +1 -1
- data/examples/working_demo_dsl.rb +160 -0
- data/lib/kbs/blackboard/engine.rb +3 -3
- data/lib/kbs/blackboard/fact.rb +1 -1
- data/lib/kbs/condition.rb +1 -1
- data/lib/kbs/dsl/knowledge_base.rb +1 -1
- data/lib/kbs/dsl/variable.rb +1 -1
- data/lib/kbs/{rete_engine.rb → engine.rb} +1 -1
- data/lib/kbs/fact.rb +1 -1
- data/lib/kbs/version.rb +1 -1
- data/lib/kbs.rb +2 -2
- data/mkdocs.yml +181 -0
- metadata +66 -6
- data/examples/stock_trading_system.rb.bak +0 -563
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# Performance Tuning
|
|
2
|
+
|
|
3
|
+
Optimize KBS applications for speed, scalability, and efficiency. This guide covers profiling, benchmarking, rule optimization, and storage backend selection.
|
|
4
|
+
|
|
5
|
+
## Performance Overview
|
|
6
|
+
|
|
7
|
+
KBS performance depends on:
|
|
8
|
+
|
|
9
|
+
1. **Rule Complexity** - Number of conditions, predicates, and joins
|
|
10
|
+
2. **Fact Volume** - Size of working memory
|
|
11
|
+
3. **Network Structure** - Shared nodes and network branching
|
|
12
|
+
4. **Storage Backend** - SQLite, Redis, or in-memory
|
|
13
|
+
5. **Action Efficiency** - Time spent in rule actions
|
|
14
|
+
|
|
15
|
+
## Benchmarking
|
|
16
|
+
|
|
17
|
+
### Basic Benchmark
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require 'benchmark'
|
|
21
|
+
require 'kbs'
|
|
22
|
+
|
|
23
|
+
engine = KBS::Engine.new
|
|
24
|
+
|
|
25
|
+
# Add rules
|
|
26
|
+
rule = KBS::Rule.new("simple_rule") do |r|
|
|
27
|
+
r.conditions = [
|
|
28
|
+
KBS::Condition.new(:fact, { value: :v? })
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
r.action = lambda do |facts, bindings|
|
|
32
|
+
# Simple action
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
engine.add_rule(rule)
|
|
37
|
+
|
|
38
|
+
# Benchmark fact addition
|
|
39
|
+
time = Benchmark.measure do
|
|
40
|
+
10_000.times do |i|
|
|
41
|
+
engine.add_fact(:fact, { value: i })
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "Added 10,000 facts in #{time.real} seconds"
|
|
46
|
+
puts "#{(10_000 / time.real).round(2)} facts/second"
|
|
47
|
+
|
|
48
|
+
# Benchmark engine run
|
|
49
|
+
time = Benchmark.measure do
|
|
50
|
+
engine.run
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts "Ran engine in #{time.real} seconds"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Comprehensive Benchmark
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require 'benchmark'
|
|
60
|
+
|
|
61
|
+
class KBSBenchmark
|
|
62
|
+
def initialize(engine_type: :memory)
|
|
63
|
+
@engine_type = engine_type
|
|
64
|
+
@results = {}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def setup_engine
|
|
68
|
+
case @engine_type
|
|
69
|
+
when :memory
|
|
70
|
+
KBS::Engine.new
|
|
71
|
+
when :blackboard_sqlite
|
|
72
|
+
KBS::Blackboard::Engine.new(db_path: ':memory:')
|
|
73
|
+
when :blackboard_redis
|
|
74
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
75
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
76
|
+
url: 'redis://localhost:6379/15' # Test database
|
|
77
|
+
)
|
|
78
|
+
KBS::Blackboard::Engine.new(store: store)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def benchmark_fact_addition(count: 10_000)
|
|
83
|
+
engine = setup_engine
|
|
84
|
+
|
|
85
|
+
time = Benchmark.measure do
|
|
86
|
+
count.times do |i|
|
|
87
|
+
engine.add_fact(:fact, { id: i, value: rand(1000) })
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@results[:fact_addition] = {
|
|
92
|
+
count: count,
|
|
93
|
+
time: time.real,
|
|
94
|
+
rate: (count / time.real).round(2)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def benchmark_simple_rules(fact_count: 1000, rule_count: 10)
|
|
99
|
+
engine = setup_engine
|
|
100
|
+
|
|
101
|
+
# Add rules
|
|
102
|
+
rule_count.times do |i|
|
|
103
|
+
rule = KBS::Rule.new("rule_#{i}") do |r|
|
|
104
|
+
r.conditions = [
|
|
105
|
+
KBS::Condition.new(:fact, { value: :v? })
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
r.action = lambda do |facts, bindings|
|
|
109
|
+
# Minimal action
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
engine.add_rule(rule)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Add facts
|
|
116
|
+
fact_count.times do |i|
|
|
117
|
+
engine.add_fact(:fact, { value: i })
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Benchmark engine run
|
|
121
|
+
time = Benchmark.measure do
|
|
122
|
+
engine.run
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
@results[:simple_rules] = {
|
|
126
|
+
fact_count: fact_count,
|
|
127
|
+
rule_count: rule_count,
|
|
128
|
+
time: time.real
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def benchmark_complex_joins(fact_count: 500)
|
|
133
|
+
engine = setup_engine
|
|
134
|
+
|
|
135
|
+
# Rule with 3-way join
|
|
136
|
+
rule = KBS::Rule.new("complex_join") do |r|
|
|
137
|
+
r.conditions = [
|
|
138
|
+
KBS::Condition.new(:a, { id: :id?, value: :v? }),
|
|
139
|
+
KBS::Condition.new(:b, { a_id: :id?, score: :s? }),
|
|
140
|
+
KBS::Condition.new(:c, { b_score: :s? })
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
r.action = lambda do |facts, bindings|
|
|
144
|
+
# Action
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
engine.add_rule(rule)
|
|
149
|
+
|
|
150
|
+
# Add facts
|
|
151
|
+
fact_count.times do |i|
|
|
152
|
+
engine.add_fact(:a, { id: i, value: rand(100) })
|
|
153
|
+
engine.add_fact(:b, { a_id: i, score: rand(100) })
|
|
154
|
+
engine.add_fact(:c, { b_score: i })
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Benchmark
|
|
158
|
+
time = Benchmark.measure do
|
|
159
|
+
engine.run
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@results[:complex_joins] = {
|
|
163
|
+
fact_count: fact_count * 3,
|
|
164
|
+
time: time.real
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def benchmark_negation(fact_count: 1000)
|
|
169
|
+
engine = setup_engine
|
|
170
|
+
|
|
171
|
+
# Rule with negation
|
|
172
|
+
rule = KBS::Rule.new("negation_rule") do |r|
|
|
173
|
+
r.conditions = [
|
|
174
|
+
KBS::Condition.new(:positive, { id: :id? }),
|
|
175
|
+
KBS::Condition.new(:negative, { id: :id? }, negated: true)
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
r.action = lambda do |facts, bindings|
|
|
179
|
+
# Action
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
engine.add_rule(rule)
|
|
184
|
+
|
|
185
|
+
# Add facts (50% will match)
|
|
186
|
+
fact_count.times do |i|
|
|
187
|
+
engine.add_fact(:positive, { id: i })
|
|
188
|
+
engine.add_fact(:negative, { id: i }) if i.even?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Benchmark
|
|
192
|
+
time = Benchmark.measure do
|
|
193
|
+
engine.run
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
@results[:negation] = {
|
|
197
|
+
fact_count: fact_count + (fact_count / 2),
|
|
198
|
+
time: time.real
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def run_all
|
|
203
|
+
puts "=== KBS Performance Benchmark (#{@engine_type}) ==="
|
|
204
|
+
|
|
205
|
+
benchmark_fact_addition
|
|
206
|
+
puts "\nFact Addition:"
|
|
207
|
+
puts " #{@results[:fact_addition][:count]} facts in #{@results[:fact_addition][:time].round(4)}s"
|
|
208
|
+
puts " Rate: #{@results[:fact_addition][:rate]} facts/sec"
|
|
209
|
+
|
|
210
|
+
benchmark_simple_rules
|
|
211
|
+
puts "\nSimple Rules:"
|
|
212
|
+
puts " #{@results[:simple_rules][:rule_count]} rules, #{@results[:simple_rules][:fact_count]} facts"
|
|
213
|
+
puts " Time: #{@results[:simple_rules][:time].round(4)}s"
|
|
214
|
+
|
|
215
|
+
benchmark_complex_joins
|
|
216
|
+
puts "\nComplex Joins (3-way):"
|
|
217
|
+
puts " #{@results[:complex_joins][:fact_count]} facts"
|
|
218
|
+
puts " Time: #{@results[:complex_joins][:time].round(4)}s"
|
|
219
|
+
|
|
220
|
+
benchmark_negation
|
|
221
|
+
puts "\nNegation:"
|
|
222
|
+
puts " #{@results[:negation][:fact_count]} facts"
|
|
223
|
+
puts " Time: #{@results[:negation][:time].round(4)}s"
|
|
224
|
+
|
|
225
|
+
@results
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Run benchmarks
|
|
230
|
+
memory_bench = KBSBenchmark.new(engine_type: :memory)
|
|
231
|
+
memory_results = memory_bench.run_all
|
|
232
|
+
|
|
233
|
+
# Compare with blackboard
|
|
234
|
+
blackboard_bench = KBSBenchmark.new(engine_type: :blackboard_sqlite)
|
|
235
|
+
blackboard_results = blackboard_bench.run_all
|
|
236
|
+
|
|
237
|
+
# Compare
|
|
238
|
+
puts "\n=== Performance Comparison ==="
|
|
239
|
+
puts "Fact addition: Memory is #{(blackboard_results[:fact_addition][:time] / memory_results[:fact_addition][:time]).round(2)}x faster"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Rule Optimization
|
|
243
|
+
|
|
244
|
+
### Condition Ordering
|
|
245
|
+
|
|
246
|
+
Order conditions from most to least selective:
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# Bad: Generic condition first
|
|
250
|
+
KBS::Rule.new("inefficient") do |r|
|
|
251
|
+
r.conditions = [
|
|
252
|
+
KBS::Condition.new(:any_event, {}), # Matches ALL events (large alpha memory)
|
|
253
|
+
KBS::Condition.new(:critical_error, { severity: "critical" }) # Selective
|
|
254
|
+
]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Good: Selective condition first
|
|
258
|
+
KBS::Rule.new("efficient") do |r|
|
|
259
|
+
r.conditions = [
|
|
260
|
+
KBS::Condition.new(:critical_error, { severity: "critical" }), # Selective
|
|
261
|
+
KBS::Condition.new(:any_event, { error_id: :id? }) # Filtered by join
|
|
262
|
+
]
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Why it matters:**
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Bad ordering:
|
|
270
|
+
any_event alpha: 10,000 facts
|
|
271
|
+
Join produces 10,000 tokens
|
|
272
|
+
critical_error alpha: 5 facts
|
|
273
|
+
Join filters down to 5 final matches
|
|
274
|
+
→ 10,000 token propagations
|
|
275
|
+
|
|
276
|
+
Good ordering:
|
|
277
|
+
critical_error alpha: 5 facts
|
|
278
|
+
Join produces 5 tokens
|
|
279
|
+
any_event alpha: 10,000 facts
|
|
280
|
+
Join filters to 5 final matches
|
|
281
|
+
→ 5 token propagations (2000x fewer!)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Predicate Efficiency
|
|
285
|
+
|
|
286
|
+
Use simple predicates:
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# Bad: Complex predicate
|
|
290
|
+
KBS::Condition.new(:data, { value: :v? }, predicate: lambda { |f|
|
|
291
|
+
# Expensive operations
|
|
292
|
+
json = JSON.parse(f[:raw_data])
|
|
293
|
+
result = ComplexCalculation.new(json).process
|
|
294
|
+
result > threshold
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
# Good: Pre-process data
|
|
298
|
+
engine.add_fact(:data, {
|
|
299
|
+
value: calculate_value(raw_data), # Pre-calculated
|
|
300
|
+
processed: true
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
KBS::Condition.new(:data, { value: :v? }, predicate: lambda { |f|
|
|
304
|
+
f[:value] > threshold # Simple comparison
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Network Sharing
|
|
309
|
+
|
|
310
|
+
Leverage shared alpha and beta memories:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
# Inefficient: Duplicate alpha nodes
|
|
314
|
+
rule1 = KBS::Rule.new("rule1") do |r|
|
|
315
|
+
r.conditions = [
|
|
316
|
+
KBS::Condition.new(:sensor, { type: "temperature", value: :v1? })
|
|
317
|
+
]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
rule2 = KBS::Rule.new("rule2") do |r|
|
|
321
|
+
r.conditions = [
|
|
322
|
+
KBS::Condition.new(:sensor, { type: "temperature", value: :v2? }) # SAME pattern
|
|
323
|
+
]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Engine automatically shares alpha memory for :sensor + type="temperature"
|
|
327
|
+
# Adding 1 temperature sensor fact activates BOTH rules efficiently
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Sharing visualization:**
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
Facts → AlphaMemory(:sensor, type=temperature) ──┬─→ Rule1
|
|
334
|
+
└─→ Rule2
|
|
335
|
+
|
|
336
|
+
Instead of:
|
|
337
|
+
Facts → AlphaMemory1(:sensor) → Rule1
|
|
338
|
+
└→ AlphaMemory2(:sensor) → Rule2 (duplicate work)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Minimize Negations
|
|
342
|
+
|
|
343
|
+
Negations are expensive:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
# Expensive: Multiple negations
|
|
347
|
+
KBS::Rule.new("many_negations") do |r|
|
|
348
|
+
r.conditions = [
|
|
349
|
+
KBS::Condition.new(:a, {}),
|
|
350
|
+
KBS::Condition.new(:b, {}, negated: true),
|
|
351
|
+
KBS::Condition.new(:c, {}, negated: true),
|
|
352
|
+
KBS::Condition.new(:d, {}, negated: true)
|
|
353
|
+
]
|
|
354
|
+
end
|
|
355
|
+
# Each negation checks alpha memory on every token
|
|
356
|
+
|
|
357
|
+
# Better: Use positive logic
|
|
358
|
+
engine.add_fact(:conditions_clear, {}) unless b_exists? || c_exists? || d_exists?
|
|
359
|
+
|
|
360
|
+
KBS::Rule.new("positive_logic") do |r|
|
|
361
|
+
r.conditions = [
|
|
362
|
+
KBS::Condition.new(:a, {}),
|
|
363
|
+
KBS::Condition.new(:conditions_clear, {})
|
|
364
|
+
]
|
|
365
|
+
end
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Batch Operations
|
|
369
|
+
|
|
370
|
+
Group related operations:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
# Inefficient: Add facts one by one with run after each
|
|
374
|
+
1000.times do |i|
|
|
375
|
+
engine.add_fact(:item, { id: i })
|
|
376
|
+
engine.run # Run engine 1000 times!
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Efficient: Batch add, then run once
|
|
380
|
+
1000.times do |i|
|
|
381
|
+
engine.add_fact(:item, { id: i })
|
|
382
|
+
end
|
|
383
|
+
engine.run # Run engine once
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Storage Backend Selection
|
|
387
|
+
|
|
388
|
+
### Performance Characteristics
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
require 'benchmark'
|
|
392
|
+
|
|
393
|
+
# In-memory (fastest)
|
|
394
|
+
memory_engine = KBS::Engine.new
|
|
395
|
+
|
|
396
|
+
# SQLite (persistent, slower)
|
|
397
|
+
sqlite_engine = KBS::Blackboard::Engine.new(db_path: 'test.db')
|
|
398
|
+
|
|
399
|
+
# Redis (persistent, fast)
|
|
400
|
+
require 'kbs/blackboard/persistence/redis_store'
|
|
401
|
+
redis_store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
402
|
+
url: 'redis://localhost:6379/0'
|
|
403
|
+
)
|
|
404
|
+
redis_engine = KBS::Blackboard::Engine.new(store: redis_store)
|
|
405
|
+
|
|
406
|
+
# Benchmark
|
|
407
|
+
engines = {
|
|
408
|
+
memory: memory_engine,
|
|
409
|
+
sqlite: sqlite_engine,
|
|
410
|
+
redis: redis_engine
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
engines.each do |name, engine|
|
|
414
|
+
time = Benchmark.measure do
|
|
415
|
+
10_000.times { |i| engine.add_fact(:test, { value: i }) }
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
puts "#{name}: #{(10_000 / time.real).round(2)} facts/sec"
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Typical results:
|
|
422
|
+
# memory: 50,000 facts/sec
|
|
423
|
+
# sqlite: 5,000 facts/sec
|
|
424
|
+
# redis: 25,000 facts/sec
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Backend Decision Matrix
|
|
428
|
+
|
|
429
|
+
**In-Memory (`KBS::Engine`)**:
|
|
430
|
+
- ✅ Fastest (no I/O)
|
|
431
|
+
- ✅ Simple (no setup)
|
|
432
|
+
- ❌ No persistence
|
|
433
|
+
- **Use when:** Prototyping, short-lived processes, pure computation
|
|
434
|
+
|
|
435
|
+
**SQLite (`KBS::Blackboard::Engine`)**:
|
|
436
|
+
- ✅ Persistent
|
|
437
|
+
- ✅ ACID transactions
|
|
438
|
+
- ✅ No dependencies
|
|
439
|
+
- ❌ Slower writes (~5,000/sec)
|
|
440
|
+
- **Use when:** Single process, moderate load, need durability
|
|
441
|
+
|
|
442
|
+
**Redis (`RedisStore`)**:
|
|
443
|
+
- ✅ Fast (~25,000/sec)
|
|
444
|
+
- ✅ Distributed
|
|
445
|
+
- ✅ Scalable
|
|
446
|
+
- ❌ Requires Redis server
|
|
447
|
+
- **Use when:** High throughput, multiple processes, real-time systems
|
|
448
|
+
|
|
449
|
+
**Hybrid (`HybridStore`)**:
|
|
450
|
+
- ✅ Fast (Redis) + durable (SQLite)
|
|
451
|
+
- ❌ Most complex
|
|
452
|
+
- **Use when:** Production, need both speed and audit trail
|
|
453
|
+
|
|
454
|
+
### SQLite Optimization
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
engine = KBS::Blackboard::Engine.new(
|
|
458
|
+
db_path: 'optimized.db',
|
|
459
|
+
journal_mode: 'WAL', # Write-Ahead Logging (better concurrency)
|
|
460
|
+
synchronous: 'NORMAL', # Balance safety/speed
|
|
461
|
+
cache_size: -64000, # 64MB cache
|
|
462
|
+
busy_timeout: 5000 # Wait 5s for locks
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Results: 2-3x faster than default settings
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Redis Optimization
|
|
469
|
+
|
|
470
|
+
```ruby
|
|
471
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
472
|
+
url: 'redis://localhost:6379/0',
|
|
473
|
+
pool_size: 10, # Connection pooling
|
|
474
|
+
pool_timeout: 5, # Pool timeout
|
|
475
|
+
reconnect_attempts: 3 # Retry on failure
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
479
|
+
|
|
480
|
+
# Enable Redis persistence (optional)
|
|
481
|
+
# In redis.conf:
|
|
482
|
+
# save 900 1
|
|
483
|
+
# appendonly yes
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Profiling
|
|
487
|
+
|
|
488
|
+
### Ruby Profiler
|
|
489
|
+
|
|
490
|
+
```ruby
|
|
491
|
+
require 'ruby-prof'
|
|
492
|
+
|
|
493
|
+
engine = KBS::Engine.new
|
|
494
|
+
|
|
495
|
+
# Add rules and facts
|
|
496
|
+
# ...
|
|
497
|
+
|
|
498
|
+
# Profile engine run
|
|
499
|
+
result = RubyProf.profile do
|
|
500
|
+
engine.run
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Print results
|
|
504
|
+
printer = RubyProf::FlatPrinter.new(result)
|
|
505
|
+
printer.print(STDOUT, min_percent: 2)
|
|
506
|
+
|
|
507
|
+
# Or use call graph
|
|
508
|
+
printer = RubyProf::CallTreePrinter.new(result)
|
|
509
|
+
File.open('profile.out', 'w') { |f| printer.print(f) }
|
|
510
|
+
# View with kcachegrind or qcachegrind
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Stackprof (Sampling Profiler)
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
require 'stackprof'
|
|
517
|
+
|
|
518
|
+
engine = KBS::Engine.new
|
|
519
|
+
|
|
520
|
+
# Add rules and facts
|
|
521
|
+
# ...
|
|
522
|
+
|
|
523
|
+
# Profile
|
|
524
|
+
StackProf.run(mode: :cpu, out: 'stackprof.dump') do
|
|
525
|
+
1000.times { engine.run }
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Analyze
|
|
529
|
+
# $ stackprof stackprof.dump --text
|
|
530
|
+
# $ stackprof stackprof.dump --method 'KBS::JoinNode#left_activate'
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Custom Instrumentation
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
class InstrumentedEngine < KBS::Engine
|
|
537
|
+
def initialize
|
|
538
|
+
super
|
|
539
|
+
@metrics = {
|
|
540
|
+
fact_additions: 0,
|
|
541
|
+
rule_firings: 0,
|
|
542
|
+
alpha_activations: 0,
|
|
543
|
+
beta_activations: 0
|
|
544
|
+
}
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def add_fact(type, attributes = {})
|
|
548
|
+
@metrics[:fact_additions] += 1
|
|
549
|
+
super
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def run
|
|
553
|
+
start = Time.now
|
|
554
|
+
result = super
|
|
555
|
+
elapsed = Time.now - start
|
|
556
|
+
|
|
557
|
+
puts "Engine run: #{elapsed}s"
|
|
558
|
+
puts " Facts: #{facts.size}"
|
|
559
|
+
puts " Rules fired: #{@metrics[:rule_firings]}"
|
|
560
|
+
|
|
561
|
+
result
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def report_metrics
|
|
565
|
+
@metrics
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## Common Bottlenecks
|
|
571
|
+
|
|
572
|
+
### 1. Large Alpha Memories
|
|
573
|
+
|
|
574
|
+
**Problem**: Conditions matching many facts slow down joins
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
# Slow: Matches ALL events
|
|
578
|
+
KBS::Condition.new(:event, {}) # Alpha memory: 100,000 facts
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Solution**: Add constraints
|
|
582
|
+
|
|
583
|
+
```ruby
|
|
584
|
+
# Fast: Matches specific events
|
|
585
|
+
KBS::Condition.new(:event, { type: "error", severity: "critical" })
|
|
586
|
+
# Alpha memory: 50 facts
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### 2. Expensive Predicates
|
|
590
|
+
|
|
591
|
+
**Problem**: Complex predicates evaluated repeatedly
|
|
592
|
+
|
|
593
|
+
```ruby
|
|
594
|
+
# Slow: Expensive predicate called for every fact
|
|
595
|
+
KBS::Condition.new(:data, {}, predicate: lambda { |f|
|
|
596
|
+
expensive_calculation(f[:raw_data])
|
|
597
|
+
})
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**Solution**: Pre-calculate
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
# Fast: Calculate once when adding fact
|
|
604
|
+
processed_value = expensive_calculation(raw_data)
|
|
605
|
+
engine.add_fact(:data, { processed: processed_value })
|
|
606
|
+
|
|
607
|
+
KBS::Condition.new(:data, { processed: :v? })
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 3. Action Overhead
|
|
611
|
+
|
|
612
|
+
**Problem**: Slow actions block engine
|
|
613
|
+
|
|
614
|
+
```ruby
|
|
615
|
+
# Slow: Action makes API call
|
|
616
|
+
r.action = lambda do |facts, bindings|
|
|
617
|
+
result = HTTParty.get("https://api.example.com/process") # Blocks!
|
|
618
|
+
engine.add_fact(:result, result)
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Solution**: Async processing
|
|
623
|
+
|
|
624
|
+
```ruby
|
|
625
|
+
# Fast: Queue action, process asynchronously
|
|
626
|
+
r.action = lambda do |facts, bindings|
|
|
627
|
+
engine.send_message(:api_queue, {
|
|
628
|
+
url: "https://api.example.com/process",
|
|
629
|
+
fact_id: facts[0].id
|
|
630
|
+
}, priority: 50)
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Separate worker processes messages
|
|
634
|
+
worker = Thread.new do
|
|
635
|
+
loop do
|
|
636
|
+
msg = engine.pop_message(:api_queue)
|
|
637
|
+
break unless msg
|
|
638
|
+
|
|
639
|
+
result = HTTParty.get(msg[:content][:url])
|
|
640
|
+
engine.add_fact(:result, result)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### 4. Memory Leaks
|
|
646
|
+
|
|
647
|
+
**Problem**: Facts accumulate indefinitely
|
|
648
|
+
|
|
649
|
+
```ruby
|
|
650
|
+
# Memory grows unbounded
|
|
651
|
+
loop do
|
|
652
|
+
engine.add_fact(:sensor_reading, {
|
|
653
|
+
value: read_sensor(),
|
|
654
|
+
timestamp: Time.now
|
|
655
|
+
})
|
|
656
|
+
engine.run
|
|
657
|
+
end
|
|
658
|
+
# After 1 hour: 360,000 facts in memory!
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Solution**: Clean up old facts
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
# Cleanup rule
|
|
665
|
+
cleanup_rule = KBS::Rule.new("cleanup_old_readings", priority: 1) do |r|
|
|
666
|
+
r.conditions = [
|
|
667
|
+
KBS::Condition.new(:sensor_reading, {
|
|
668
|
+
timestamp: :time?
|
|
669
|
+
}, predicate: lambda { |f|
|
|
670
|
+
(Time.now - f[:timestamp]) > 300 # 5 minutes old
|
|
671
|
+
})
|
|
672
|
+
]
|
|
673
|
+
|
|
674
|
+
r.action = lambda do |facts, bindings|
|
|
675
|
+
engine.remove_fact(facts[0])
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
## Optimization Checklist
|
|
681
|
+
|
|
682
|
+
### Rule Design
|
|
683
|
+
|
|
684
|
+
- [ ] Order conditions from most to least selective
|
|
685
|
+
- [ ] Minimize negations (use positive logic where possible)
|
|
686
|
+
- [ ] Keep predicates simple
|
|
687
|
+
- [ ] Pre-calculate expensive values
|
|
688
|
+
- [ ] Share patterns across rules
|
|
689
|
+
|
|
690
|
+
### Fact Management
|
|
691
|
+
|
|
692
|
+
- [ ] Remove facts when no longer needed
|
|
693
|
+
- [ ] Batch fact additions
|
|
694
|
+
- [ ] Use specific fact types (not generic `:data`)
|
|
695
|
+
- [ ] Avoid duplicate facts
|
|
696
|
+
|
|
697
|
+
### Actions
|
|
698
|
+
|
|
699
|
+
- [ ] Keep actions fast
|
|
700
|
+
- [ ] Avoid blocking I/O in actions
|
|
701
|
+
- [ ] Use message passing for async work
|
|
702
|
+
- [ ] Don't add/remove many facts in single action
|
|
703
|
+
|
|
704
|
+
### Storage
|
|
705
|
+
|
|
706
|
+
- [ ] Choose backend based on requirements:
|
|
707
|
+
- In-memory for speed
|
|
708
|
+
- SQLite for persistence + moderate load
|
|
709
|
+
- Redis for persistence + high load
|
|
710
|
+
- Hybrid for production
|
|
711
|
+
- [ ] Optimize SQLite with WAL mode
|
|
712
|
+
- [ ] Use connection pooling for Redis
|
|
713
|
+
- [ ] Monitor database size
|
|
714
|
+
|
|
715
|
+
### Monitoring
|
|
716
|
+
|
|
717
|
+
- [ ] Profile before optimizing
|
|
718
|
+
- [ ] Measure fact addition rate
|
|
719
|
+
- [ ] Track engine run time
|
|
720
|
+
- [ ] Monitor memory usage
|
|
721
|
+
- [ ] Log rule firing frequency
|
|
722
|
+
|
|
723
|
+
## Performance Targets
|
|
724
|
+
|
|
725
|
+
### Expected Performance (In-Memory)
|
|
726
|
+
|
|
727
|
+
| Operation | Target | Notes |
|
|
728
|
+
|-----------|--------|-------|
|
|
729
|
+
| Add fact | 50,000/sec | Simple facts, no rules |
|
|
730
|
+
| Simple rule (1 condition) | 10,000/sec | Per fact |
|
|
731
|
+
| Complex rule (3+ conditions) | 1,000/sec | Per fact |
|
|
732
|
+
| Engine run (1000 facts, 10 rules) | < 100ms | Total time |
|
|
733
|
+
| Negation check | 10,000/sec | Per token |
|
|
734
|
+
|
|
735
|
+
### Expected Performance (SQLite)
|
|
736
|
+
|
|
737
|
+
| Operation | Target | Notes |
|
|
738
|
+
|-----------|--------|-------|
|
|
739
|
+
| Add fact | 5,000/sec | With WAL mode |
|
|
740
|
+
| Query facts | 100,000/sec | Indexed queries |
|
|
741
|
+
| Transaction | 1,000/sec | Commit rate |
|
|
742
|
+
|
|
743
|
+
### Expected Performance (Redis)
|
|
744
|
+
|
|
745
|
+
| Operation | Target | Notes |
|
|
746
|
+
|-----------|--------|-------|
|
|
747
|
+
| Add fact | 25,000/sec | Network overhead |
|
|
748
|
+
| Query facts | 50,000/sec | Hash operations |
|
|
749
|
+
| Message queue | 50,000/sec | Sorted set operations |
|
|
750
|
+
|
|
751
|
+
## Scaling Strategies
|
|
752
|
+
|
|
753
|
+
### Vertical Scaling
|
|
754
|
+
|
|
755
|
+
**Increase single-process performance:**
|
|
756
|
+
|
|
757
|
+
```ruby
|
|
758
|
+
# 1. Use faster backend
|
|
759
|
+
store = KBS::Blackboard::Persistence::RedisStore.new(...)
|
|
760
|
+
engine = KBS::Blackboard::Engine.new(store: store)
|
|
761
|
+
|
|
762
|
+
# 2. Optimize rules
|
|
763
|
+
# - Order conditions
|
|
764
|
+
# - Minimize negations
|
|
765
|
+
# - Batch operations
|
|
766
|
+
|
|
767
|
+
# 3. Pre-process data
|
|
768
|
+
# - Calculate values before adding facts
|
|
769
|
+
# - Index frequently queried attributes
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Horizontal Scaling
|
|
773
|
+
|
|
774
|
+
**Multiple processes sharing Redis:**
|
|
775
|
+
|
|
776
|
+
```ruby
|
|
777
|
+
# Process 1: Data collector
|
|
778
|
+
collector_store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
779
|
+
url: 'redis://localhost:6379/0'
|
|
780
|
+
)
|
|
781
|
+
collector = KBS::Blackboard::Engine.new(store: collector_store)
|
|
782
|
+
|
|
783
|
+
# Collect data
|
|
784
|
+
loop do
|
|
785
|
+
data = fetch_data()
|
|
786
|
+
collector.add_fact(:raw_data, data)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Process 2: Rule processor
|
|
790
|
+
processor_store = KBS::Blackboard::Persistence::RedisStore.new(
|
|
791
|
+
url: 'redis://localhost:6379/0' # Same Redis!
|
|
792
|
+
)
|
|
793
|
+
processor = KBS::Blackboard::Engine.new(store: processor_store)
|
|
794
|
+
|
|
795
|
+
# Add rules
|
|
796
|
+
processor.add_rule(...)
|
|
797
|
+
|
|
798
|
+
# Process data
|
|
799
|
+
loop do
|
|
800
|
+
processor.run
|
|
801
|
+
sleep 1
|
|
802
|
+
end
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### Partitioning
|
|
806
|
+
|
|
807
|
+
**Split facts by domain:**
|
|
808
|
+
|
|
809
|
+
```ruby
|
|
810
|
+
# Engine 1: Temperature monitoring
|
|
811
|
+
temp_engine = KBS::Blackboard::Engine.new(db_path: 'temp.db')
|
|
812
|
+
# Handles :temperature_reading, :hvac_control
|
|
813
|
+
|
|
814
|
+
# Engine 2: Security monitoring
|
|
815
|
+
security_engine = KBS::Blackboard::Engine.new(db_path: 'security.db')
|
|
816
|
+
# Handles :motion_sensor, :door_sensor, :alarm
|
|
817
|
+
|
|
818
|
+
# Coordinator: Coordinates between engines
|
|
819
|
+
coordinator_engine = KBS::Blackboard::Engine.new(db_path: 'coordinator.db')
|
|
820
|
+
# Handles cross-domain rules
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
## Next Steps
|
|
824
|
+
|
|
825
|
+
- **[Debugging Guide](debugging.md)** - Debug performance issues
|
|
826
|
+
- **[Testing Guide](testing.md)** - Performance testing strategies
|
|
827
|
+
- **[Custom Persistence](custom-persistence.md)** - Optimize custom backends
|
|
828
|
+
- **[Architecture](../architecture/index.md)** - Understand network structure
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
*Premature optimization is the root of all evil. Profile first, then optimize the bottlenecks.*
|