Performance Optimization¶
This guide covers techniques and best practices for optimizing PromptManager performance in production environments.
Caching Strategies¶
Prompt Content Caching¶
# Enable built-in caching
PromptManager.configure do |config|
config.cache_prompts = true
config.cache_ttl = 3600 # 1 hour
config.cache_store = ActiveSupport::Cache::RedisStore.new(
url: ENV['REDIS_URL'],
namespace: 'prompt_manager'
)
end
Custom Caching Layer¶
class CachedPromptManager
def self.render(prompt_id, parameters = {}, cache_options = {})
cache_key = generate_cache_key(prompt_id, parameters)
Rails.cache.fetch(cache_key, cache_options) do
prompt = PromptManager::Prompt.new(id: prompt_id)
prompt.render(parameters)
end
end
def self.invalidate_cache(prompt_id, parameters = nil)
if parameters
cache_key = generate_cache_key(prompt_id, parameters)
Rails.cache.delete(cache_key)
else
# Invalidate all cached versions of this prompt
Rails.cache.delete_matched("prompt:#{prompt_id}:*")
end
end
private
def self.generate_cache_key(prompt_id, parameters)
param_hash = Digest::MD5.hexdigest(parameters.to_json)
"prompt:#{prompt_id}:#{param_hash}"
end
end
# Usage
result = CachedPromptManager.render('welcome_email', { name: 'John' }, expires_in: 30.minutes)
Multi-level Caching¶
class HierarchicalPromptCache
def initialize
@l1_cache = ActiveSupport::Cache::MemoryStore.new(size: 100) # Fast, small
@l2_cache = Rails.cache # Redis, larger but slower
end
def fetch(key, options = {}, &block)
# Try L1 cache first
result = @l1_cache.read(key)
return result if result
# Try L2 cache
result = @l2_cache.fetch(key, options, &block)
# Store in L1 cache for next time
@l1_cache.write(key, result, expires_in: 5.minutes) if result
result
end
def invalidate(key_pattern)
@l1_cache.clear
@l2_cache.delete_matched(key_pattern)
end
end
Storage Optimization¶
Connection Pooling¶
class PooledDatabaseAdapter < PromptManager::Storage::ActiveRecordAdapter
def initialize(pool_size: 10, **options)
super(**options)
@connection_pool = ConnectionPool.new(size: pool_size) do
model_class.connection_pool.checkout
end
end
def read(prompt_id)
@connection_pool.with do |connection|
result = connection.exec_query(
"SELECT content FROM prompts WHERE prompt_id = ?",
'PromptManager::Read',
[prompt_id]
)
raise PromptNotFoundError unless result.any?
result.first['content']
end
end
def write(prompt_id, content)
@connection_pool.with do |connection|
connection.exec_insert(
"INSERT INTO prompts (prompt_id, content, updated_at) VALUES (?, ?, ?) " \
"ON CONFLICT (prompt_id) DO UPDATE SET content = ?, updated_at = ?",
'PromptManager::Write',
[prompt_id, content, Time.current, content, Time.current]
)
end
true
end
end
Bulk Operations¶
class BulkPromptOperations
def self.bulk_render(prompt_configs, batch_size: 100)
results = {}
prompt_configs.each_slice(batch_size) do |batch|
# Pre-load all prompts in the batch
prompt_contents = preload_prompts(batch.map { |config| config[:prompt_id] })
# Process batch in parallel
batch_results = Parallel.map(batch, in_threads: 4) do |config|
begin
content = prompt_contents[config[:prompt_id]]
next unless content
processor = PromptManager::DirectiveProcessor.new
result = processor.process(content, config[:parameters])
[config[:prompt_id], { success: true, result: result }]
rescue => e
[config[:prompt_id], { success: false, error: e.message }]
end
end.compact
batch_results.each do |prompt_id, result|
results[prompt_id] = result
end
end
results
end
private
def self.preload_prompts(prompt_ids)
# Batch load all prompts at once
if PromptManager.storage.respond_to?(:bulk_read)
PromptManager.storage.bulk_read(prompt_ids)
else
prompt_ids.each_with_object({}) do |id, hash|
begin
hash[id] = PromptManager.storage.read(id)
rescue PromptNotFoundError
# Skip missing prompts
end
end
end
end
end
# Usage
configs = [
{ prompt_id: 'welcome', parameters: { name: 'Alice' } },
{ prompt_id: 'welcome', parameters: { name: 'Bob' } },
{ prompt_id: 'reminder', parameters: { task: 'Meeting' } }
]
results = BulkPromptOperations.bulk_render(configs)
Directive Processing Optimization¶
Lazy Evaluation¶
class LazyDirectiveProcessor < PromptManager::DirectiveProcessor
def process(content, context = {})
# Only process directives that are actually needed
lazy_content = LazyContent.new(content, context)
lazy_content.to_s
end
end
class LazyContent
def initialize(content, context)
@content = content
@context = context
@processed = false
@result = nil
end
def to_s
return @result if @processed
# Process only when needed
@result = process_directives
@processed = true
@result
end
private
def process_directives
# Only process directives that appear in the content
directive_pattern = %r{^//(\w+)\s+(.*)$}
@content.gsub(directive_pattern) do |match|
directive_name = Regexp.last_match(1)
directive_args = Regexp.last_match(2)
# Skip processing if directive handler doesn't exist
next match unless directive_handlers.key?(directive_name)
# Process directive
handler = directive_handlers[directive_name]
handler.call(directive_args, @context)
end
end
end
Directive Compilation¶
class CompiledDirectiveProcessor
def initialize
@compiled_templates = {}
end
def compile(content)
template_id = Digest::MD5.hexdigest(content)
@compiled_templates[template_id] ||= compile_template(content)
end
def render(template_id, context)
compiled_template = @compiled_templates[template_id]
return nil unless compiled_template
compiled_template.call(context)
end
private
def compile_template(content)
# Pre-compile template into executable code
ruby_code = convert_to_ruby(content)
# Create a proc that can be called with context
eval("lambda { |context| #{ruby_code} }")
end
def convert_to_ruby(content)
# Convert directive syntax to Ruby code
content.gsub(%r{//include\s+(.+)}) do |match|
file_path = Regexp.last_match(1).strip
%{PromptManager.storage.read("#{file_path}")}
end.gsub(/\[(\w+)\]/) do |match|
param_name = Regexp.last_match(1).downcase
%{context[:parameters][:#{param_name}]}
end
end
end
Memory Management¶
Memory-Efficient Prompt Loading¶
class StreamingPromptProcessor
def process_large_prompt(prompt_id, parameters = {})
prompt_file = PromptManager.storage.file_path(prompt_id)
Enumerator.new do |yielder|
File.foreach(prompt_file) do |line|
processed_line = process_line(line, parameters)
yielder << processed_line unless processed_line.empty?
end
end
end
private
def process_line(line, parameters)
# Process parameters in this line
line.gsub(/\[(\w+)\]/) do |match|
param_name = Regexp.last_match(1).downcase.to_sym
parameters[param_name] || match
end
end
end
# Usage for large prompts
processor = StreamingPromptProcessor.new
prompt_stream = processor.process_large_prompt('huge_prompt', user_id: 123)
prompt_stream.each do |line|
# Process line by line without loading entire prompt into memory
output_stream.puts line
end
Object Pool Pattern¶
class PromptProcessorPool
def initialize(size: 10)
@pool = Queue.new
@size = size
size.times do
@pool << PromptManager::DirectiveProcessor.new
end
end
def with_processor
processor = @pool.pop
begin
yield processor
ensure
# Reset processor state
processor.reset_state if processor.respond_to?(:reset_state)
@pool << processor
end
end
end
# Global pool
PROCESSOR_POOL = PromptProcessorPool.new(size: 20)
# Usage
PROCESSOR_POOL.with_processor do |processor|
result = processor.process(content, context)
end
Database Query Optimization¶
Query Optimization for ActiveRecord Adapter¶
class OptimizedActiveRecordAdapter < PromptManager::Storage::ActiveRecordAdapter
def bulk_read(prompt_ids)
# Single query instead of N+1
prompts = model_class.where(id_column => prompt_ids)
.pluck(id_column, content_column)
.to_h
# Ensure all requested IDs are present
missing_ids = prompt_ids - prompts.keys
missing_ids.each { |id| prompts[id] = nil }
prompts
end
def read_with_metadata(prompt_id)
# Fetch prompt and metadata in single query
prompt = model_class.select(:id, :content, :metadata, :updated_at)
.find_by(id_column => prompt_id)
raise PromptNotFoundError unless prompt
{
content: prompt.send(content_column),
metadata: prompt.metadata,
last_modified: prompt.updated_at
}
end
def frequently_used_prompts(limit: 100)
# Cache frequently accessed prompts
model_class.joins(:usage_logs)
.group(id_column)
.order('COUNT(usage_logs.id) DESC')
.limit(limit)
.pluck(id_column, content_column)
.to_h
end
end
Index Optimization¶
-- Optimize database indexes for prompt queries
-- Primary lookup index
CREATE INDEX CONCURRENTLY idx_prompts_id_active
ON prompts(prompt_id) WHERE active = true;
-- Content search index (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_prompts_content_gin
ON prompts USING gin(to_tsvector('english', content));
-- Metadata search index (PostgreSQL with JSONB)
CREATE INDEX CONCURRENTLY idx_prompts_metadata_gin
ON prompts USING gin(metadata);
-- Usage-based queries
CREATE INDEX CONCURRENTLY idx_prompts_usage_updated
ON prompts(usage_count DESC, updated_at DESC);
-- Composite index for filtered queries
CREATE INDEX CONCURRENTLY idx_prompts_category_status_updated
ON prompts(category, status, updated_at DESC);
Monitoring and Profiling¶
Performance Monitoring¶
class PromptPerformanceMonitor
def self.monitor_render(prompt_id, parameters = {})
start_time = Time.current
memory_before = get_memory_usage
begin
result = yield
duration = Time.current - start_time
memory_after = get_memory_usage
memory_used = memory_after - memory_before
log_performance_metrics(prompt_id, {
duration: duration,
memory_used: memory_used,
parameters_count: parameters.size,
result_size: result.bytesize,
success: true
})
result
rescue => e
duration = Time.current - start_time
log_performance_metrics(prompt_id, {
duration: duration,
error: e.class.name,
success: false
})
raise e
end
end
private
def self.get_memory_usage
GC.stat[:heap_allocated_pages] * GC::INTERNAL_CONSTANTS[:HEAP_PAGE_SIZE]
end
def self.log_performance_metrics(prompt_id, metrics)
Rails.logger.info "PromptManager Performance: #{prompt_id}", metrics
# Send to monitoring service
if defined?(StatsD)
StatsD.histogram('prompt_manager.render_duration', metrics[:duration])
StatsD.histogram('prompt_manager.memory_usage', metrics[:memory_used]) if metrics[:memory_used]
StatsD.increment('prompt_manager.renders', tags: ["success:#{metrics[:success]}"])
end
end
end
# Usage
result = PromptPerformanceMonitor.monitor_render('welcome_email', name: 'John') do
prompt = PromptManager::Prompt.new(id: 'welcome_email')
prompt.render(name: 'John')
end
Custom Profiling¶
class PromptProfiler
def self.profile_render(prompt_id, parameters = {})
profiler = RubyProf.profile do
prompt = PromptManager::Prompt.new(id: prompt_id)
prompt.render(parameters)
end
# Generate reports
printer = RubyProf::GraphHtmlPrinter.new(profiler)
File.open("tmp/profile_#{prompt_id}_#{Time.current.to_i}.html", 'w') do |file|
printer.print(file)
end
end
def self.benchmark_operations(iterations: 100)
Benchmark.bmbm do |x|
x.report("File read:") do
iterations.times { PromptManager.storage.read('test_prompt') }
end
x.report("Template render:") do
prompt = PromptManager::Prompt.new(id: 'test_prompt')
iterations.times { prompt.render(name: 'test') }
end
x.report("Cached render:") do
iterations.times { CachedPromptManager.render('test_prompt', name: 'test') }
end
end
end
end
Production Deployment Optimization¶
Preloading and Warmup¶
class PromptPreloader
def self.preload_critical_prompts
critical_prompts = %w[
welcome_email
password_reset
order_confirmation
error_notification
]
critical_prompts.each do |prompt_id|
begin
prompt = PromptManager::Prompt.new(id: prompt_id)
# Preload into cache
CachedPromptManager.render(prompt_id, {}, expires_in: 1.hour)
Rails.logger.info "Preloaded prompt: #{prompt_id}"
rescue => e
Rails.logger.error "Failed to preload #{prompt_id}: #{e.message}"
end
end
end
def self.warmup_processor_pool
# Initialize processor pool
PROCESSOR_POOL.with_processor do |processor|
processor.process("//include test\nWarmup content [TEST]",
parameters: { test: 'value' })
end
Rails.logger.info "Processor pool warmed up"
end
end
# In Rails initializer or deployment script
Rails.application.config.after_initialize do
PromptPreloader.preload_critical_prompts
PromptPreloader.warmup_processor_pool
end
Configuration for Production¶
# config/environments/production.rb
PromptManager.configure do |config|
# Use optimized storage adapter
config.storage = OptimizedActiveRecordAdapter.new
# Enable aggressive caching
config.cache_prompts = true
config.cache_ttl = 3600 # 1 hour
config.cache_store = ActiveSupport::Cache::RedisStore.new(
url: ENV['REDIS_URL'],
pool_size: 10,
pool_timeout: 5
)
# Optimize processing
config.max_include_depth = 5 # Reduce for performance
config.directive_timeout = 10 # Shorter timeout
# Error handling
config.raise_on_missing_prompts = false
config.error_handler = ->(error, context) {
Rails.logger.error "Prompt error: #{error.message}"
ErrorTracker.notify(error, context)
'Content temporarily unavailable'
}
end
Best Practices Summary¶
- Cache Aggressively: Cache rendered prompts and frequently accessed content
- Batch Operations: Process multiple prompts together when possible
- Monitor Performance: Track render times, memory usage, and error rates
- Optimize Queries: Use proper indexes and minimize database roundtrips
- Pool Resources: Reuse expensive objects like processors and connections
- Profile Regularly: Identify bottlenecks in production workloads
- Preload Critical Content: Warm up caches with important prompts
- Handle Errors Gracefully: Provide fallbacks when performance degrades