OpenL Tablets Performance Tuning Guide
OpenL Tablets Performance Tuning Guide
Last Updated: 2025-11-05 Version: 6.0.0-SNAPSHOT
Table of Contents
- Overview
- JVM Tuning
- Caching Strategy
- Database Optimization
- Repository Performance
- Web Application Tuning
- Rule Service Optimization
- Network and I/O
- Monitoring and Profiling
- Performance Testing
Overview
This guide provides comprehensive performance tuning recommendations for OpenL Tablets across all deployment scenarios.
Performance Goals
| Metric | Target | Critical Threshold |
|---|---|---|
| API Response Time | < 200ms (p95) | < 500ms |
| Rule Compilation | < 5s for typical project | < 15s |
| Project Load Time | < 2s | < 5s |
| Memory Usage | < 2GB (typical) | < 4GB |
| Database Query Time | < 50ms (p95) | < 200ms |
| Concurrent Users | 100+ | 50+ |
Performance Principles
- Measure First: Profile before optimizing
- Cache Aggressively: Cache compiled rules and metadata
- Lazy Loading: Load resources on demand
- Connection Pooling: Reuse expensive connections
- Async Processing: Use async for long-running tasks
- Minimize I/O: Reduce disk and network operations
JVM Tuning
Memory Configuration
Development Environment
JAVA_OPTS="-Xms512m -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
Production Environment
# Docker/Kubernetes (recommended)
JAVA_OPTS="-Xms32m -XX:MaxRAMPercentage=90.0"
# Traditional deployment
JAVA_OPTS="-Xms2g -Xmx4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1g"
Explanation:
-Xms: Initial heap size-Xmx: Maximum heap size-XX:MaxRAMPercentage: Use percentage of container memory (Docker/K8s)-XX:MetaspaceSize: Initial metaspace (class metadata)-XX:MaxMetaspaceSize: Maximum metaspace
Garbage Collection Tuning
G1 Garbage Collector (Recommended)
JAVA_OPTS="$JAVA_OPTS \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=16m \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:+ParallelRefProcEnabled"
Benefits:
- Low pause times (< 200ms)
- Good throughput
- Predictable GC behavior
ZGC (Low Latency)
JAVA_OPTS="$JAVA_OPTS \
-XX:+UseZGC \
-XX:ZCollectionInterval=5 \
-XX:ZAllocationSpikeTolerance=5"
Benefits:
- Ultra-low pause times (< 10ms)
- Scales to large heaps (TB+)
- Java 21+ recommended
GC Logging
JAVA_OPTS="$JAVA_OPTS \
-Xlog:gc*:file=/var/log/openl/gc.log:time,uptime,level,tags \
-Xlog:gc*:file=/var/log/openl/gc.log:time,uptime:filecount=5,filesize=100M"
JVM Monitoring Options
# Enable JMX
JAVA_OPTS="$JAVA_OPTS \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9010 \
-Dcom.sun.management.jmxremote.local.only=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false"
# Enable Flight Recorder
JAVA_OPTS="$JAVA_OPTS \
-XX:StartFlightRecording=disk=true,dumponexit=true,filename=/tmp/openl.jfr"
Thread Pool Tuning
# application.yml
server:
tomcat:
threads:
max: 200 # Maximum threads
min-spare: 10 # Minimum idle threads
max-connections: 10000
accept-count: 100 # Queue size
spring:
task:
execution:
pool:
core-size: 8
max-size: 16
queue-capacity: 100
Caching Strategy
Cache2K Configuration
OpenL Tablets uses Cache2K for caching.
Configuration File: STUDIO/org.openl.rules.webstudio/resources/cache2k.xml
The caches it declares, without the schema header and global settings the file opens with:
<caches>
<cache>
<name>aclCache</name>
<expireAfterWrite>2m</expireAfterWrite>
</cache>
<cache>
<name>missingAclCache</name>
<entryCapacity>60_000</entryCapacity>
<expireAfterWrite>15s</expireAfterWrite>
</cache>
<cache>
<name>userInfoOAuth2Cache</name>
<entryCapacity>100</entryCapacity>
<expireAfterWrite>30m</expireAfterWrite>
</cache>
<cache>
<name>projectTags</name>
<expireAfterWrite>30m</expireAfterWrite>
</cache>
<cache>
<name>projectDescriptors</name>
<expireAfterWrite>30m</expireAfterWrite>
</cache>
</caches>
A cache that names no entryCapacity takes the file’s default of 6,000 entries. A name the file does not
declare is refused at start-up rather than answered with nothing.
What the ACL Caches Hold
aclCache— the permission entries an identity carries.missingAclCache— the identities that carry no entries of their own, so that the walk to the parent identity skips a read that would only fail. Permissions granted on a repository rather than on single projects make this the common case: every project path reaches the grant through its parents.
A permission change made by this node evicts the affected identity from both caches at once, so it takes effect
immediately. expireAfterWrite bounds only how long a change made by another node against the same database
stays unseen.
The two caches are deliberately not given the same window. A stale aclCache entry answers from permissions that
have since changed, while a stale missingAclCache entry answers from the parent identity and so withholds a
permission that was just granted, which reaches the user as a refused operation. missingAclCache is therefore
kept short-lived.
Tuning Cache Settings
Increase Cache Size
<cache>
<name>aclCache</name>
<entryCapacity>10_000</entryCapacity> <!-- Increased from the 6,000 default -->
<expireAfterWrite>5m</expireAfterWrite>
</cache>
Add Project Cache
<cache>
<name>projectCache</name>
<entryCapacity>100</entryCapacity>
<expireAfterWrite>1h</expireAfterWrite>
</cache>
H2-Based Project Version Cache
OpenL Tablets uses H2 database for project version caching:
Classes:
ProjectVersionCacheManager: Cache managementProjectVersionH2CacheDB: H2 storage backendProjectVersionCacheMonitor: Cache monitoring
Configuration:
# Enable project version cache
project.version.cache.enabled=true
# Cache database location
project.version.cache.db.path=/path/to/cache/db
# Cache size
project.version.cache.size=1000
Spring Cache Configuration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
return new CaffeineCacheManager("projects", "users", "permissions");
}
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats();
}
}
Caching Best Practices
- Cache Compiled Rules: Most expensive operation
- Cache User Permissions: Frequent access, infrequent changes
- Cache Repository Metadata: Reduce repository calls
- Don’t Cache Large Objects: Keep cache entries small
- Monitor Hit Rates: Aim for > 80% hit rate
Database Optimization
Connection Pooling (HikariCP)
spring:
datasource:
hikari:
# Pool size
minimum-idle: 10
maximum-pool-size: 20
# Connection timeout
connection-timeout: 30000 # 30 seconds
idle-timeout: 600000 # 10 minutes
max-lifetime: 1800000 # 30 minutes
# Leak detection
leak-detection-threshold: 60000 # 60 seconds
# Performance
auto-commit: false
connection-test-query: SELECT 1
# Monitoring
register-mbeans: true
Hibernate Optimization
spring:
jpa:
properties:
hibernate:
# Batch processing
jdbc.batch_size: 50
order_inserts: true
order_updates: true
# Query optimization
default_batch_fetch_size: 16
max_fetch_depth: 3
# Statistics (disable in production)
generate_statistics: false
# Connection provider
connection:
provider_class: org.hibernate.hikaricp.internal.HikariCPConnectionProvider
Query Optimization
Use Pagination
// Bad: Load all projects
List<Project> projects = projectRepository.findAll();
// Good: Use pagination
Pageable pageable = PageRequest.of(0, 20);
Page<Project> projects = projectRepository.findAll(pageable);
Avoid N+1 Queries
// Bad: N+1 query
@Entity
public class Project {
@ManyToMany(fetch = FetchType.LAZY)
private List<User> users; // Lazy loaded, causes N+1
}
// Good: Use JOIN FETCH
@Query("SELECT p FROM Project p LEFT JOIN FETCH p.users WHERE p.id = :id")
Project findByIdWithUsers(@Param("id") Long id);
Use Indexes
-- Add indexes on frequently queried columns
CREATE INDEX idx_project_name ON projects(name);
CREATE INDEX idx_user_username ON users(username);
CREATE INDEX idx_acl_object ON acl_entry(acl_object_identity);
-- Composite indexes for multi-column queries
CREATE INDEX idx_project_name_version ON projects(name, version);
Database-Specific Tuning
PostgreSQL
# postgresql.conf
# Memory
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 16MB
maintenance_work_mem = 512MB
# Checkpoint
checkpoint_completion_target = 0.9
wal_buffers = 16MB
# Planner
random_page_cost = 1.1 # For SSD
effective_io_concurrency = 200
# Monitoring
log_min_duration_statement = 1000 # Log slow queries
MySQL
# my.cnf
[mysqld]
# InnoDB
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2
# Query cache
query_cache_type = 1
query_cache_size = 256M
# Connection
max_connections = 200
Query Monitoring
// Enable DataSource Proxy in tests
@Bean
public DataSource dataSource() {
return ProxyDataSourceBuilder
.create(realDataSource())
.countQuery()
.logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS)
.build();
}
Repository Performance
Git Repository Optimization
repository:
design:
type: git
uri: file:///path/to/repository
# Shallow clone for faster operations
depth: 1
# Connection pooling
connection-pool-size: 10
# Caching
cache-enabled: true
cache-size: 100
Git Best Practices
- Use Shallow Clones:
git clone --depth=1for CI/CD - Enable Git LFS: For large Excel files
- Prune Regularly:
git gc --aggressive --prune=now - Use .gitattributes: Optimize diff for binary files
# .gitattributes
*.xlsx binary
*.xls binary
*.jar binary
AWS S3 Repository Optimization
repository:
design:
type: aws
bucket: openl-repository
region: us-east-1
# Performance
max-connections: 50
connection-timeout: 10000
# Caching
metadata-cache-ttl: 300 # 5 minutes
# Multipart upload
multipart-threshold: 5242880 # 5 MB
S3 Best Practices
- Use S3 Transfer Acceleration: For cross-region access
- Enable CloudFront: CDN for frequently accessed files
- Use Multipart Upload: For files > 5MB
- Set Lifecycle Policies: Archive old versions
Azure Blob Storage Optimization
repository:
design:
type: azure
container: openl-repository
# Performance
max-connections: 50
connection-timeout: 10000
# Caching
cache-enabled: true
Web Application Tuning
Jetty Configuration
# Jetty settings
org.eclipse.jetty.server.Request.maxFormContentSize=-1
org.eclipse.jetty.server.Request.maxFormKeys=10000
jetty.httpConfig.requestHeaderSize=32768
jetty.httpConfig.responseHeaderSize=32768
jetty.httpConfig.outputBufferSize=32768
jetty.httpConfig.sendServerVersion=false
jetty.httpConfig.sendDateHeader=false
The form-key limit applies to each request independently. With OpenL Studio’s 8 KiB multipart threshold, 10,000 small parts can retain about 80 MiB of content plus metadata for every concurrent multipart request. Choose a lower limit when the deployment expects fewer parts or has limited heap, while allowing enough parts for the largest supported operation.
maxFormContentSize limits URL-encoded forms; it does not limit multipart uploads. OpenL Studio leaves the servlet
multipart file and request sizes unlimited to support large workbooks. Production deployments must also enforce a
finite total request-body limit at the application container or reverse proxy, sized above the largest supported
upload. A custom distribution can instead set finite servlet multipart maxFileSize and maxRequestSize values. This
byte limit bounds network and temporary-disk usage, while maxFormKeys bounds the number of in-memory parts.
GZIP Compression
server:
compression:
enabled: true
mime-types:
- text/html
- text/xml
- text/plain
- text/css
- text/javascript
- application/javascript
- application/json
- application/xml
min-response-size: 1024 # 1KB
Static Resource Caching
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)
.cachePublic());
}
}
HTTP/2 Configuration
server:
http2:
enabled: true
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: changeit
key-store-type: PKCS12
Rule Service Optimization
Rule Compilation Caching
# Cache compiled rules
ruleservice.compilation.cache.enabled=true
ruleservice.compilation.cache.size=100
ruleservice.compilation.cache.ttl=3600 # 1 hour
Lazy Loading
// Lazy load rules on first request
@Service
public class RuleService {
private final Map<String, CompiledRules> cache = new ConcurrentHashMap<>();
public CompiledRules getRules(String projectName) {
return cache.computeIfAbsent(projectName, this::compileRules);
}
}
Parallel Rule Execution
@Configuration
public class RuleServiceConfig {
@Bean
public ExecutorService ruleExecutor() {
return Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2
);
}
}
Kafka Integration Performance
spring:
kafka:
producer:
# Batching
batch-size: 16384
buffer-memory: 33554432
linger-ms: 10
# Compression
compression-type: lz4
# Acks
acks: 1 # Leader acknowledgment only
consumer:
# Fetch size
max-poll-records: 500
fetch-min-size: 1024
fetch-max-wait-ms: 500
# Session timeout
session-timeout-ms: 30000
Network and I/O
Request Timeouts
spring:
mvc:
async:
request-timeout: 30000 # 30 seconds
server:
tomcat:
connection-timeout: 20000 # 20 seconds
File Upload Optimization
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
file-size-threshold: 2MB
location: /tmp/uploads
Network Buffer Sizes
# TCP settings (Linux)
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
Monitoring and Profiling
OpenTelemetry Integration
OpenL Tablets includes OpenTelemetry support:
# application.yml
management:
tracing:
enabled: true
sampling:
probability: 0.1 # Sample 10% of requests
otel:
exporter:
otlp:
endpoint: http://localhost:4318
service:
name: openl-tablets
traces:
sampler: parentbased_traceidratio
sampler.arg: 0.1
JMX Metrics
@Component
public class PerformanceMetrics {
private final MeterRegistry registry;
@Autowired
public PerformanceMetrics(MeterRegistry registry) {
this.registry = registry;
}
public void recordRuleExecution(String ruleName, long durationMs) {
Timer.builder("rule.execution")
.tag("rule", ruleName)
.register(registry)
.record(durationMs, TimeUnit.MILLISECONDS);
}
}
Spring Boot Actuator
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,threaddump,heapdump
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
Profiling with Async Profiler
# Download Async Profiler
wget https://github.com/jvm-profiling-tools/async-profiler/releases/latest/download/async-profiler-linux-x64.tar.gz
tar -xzf async-profiler-linux-x64.tar.gz
# Profile application
./profiler.sh -d 60 -f /tmp/profile.html <pid>
# CPU profiling
./profiler.sh -e cpu -d 60 -f /tmp/cpu-profile.html <pid>
# Allocation profiling
./profiler.sh -e alloc -d 60 -f /tmp/alloc-profile.html <pid>
Flight Recorder
# Start Flight Recorder on JVM startup
java -XX:StartFlightRecording=disk=true,dumponexit=true,filename=/tmp/recording.jfr
# Or start on running JVM
jcmd <pid> JFR.start name=MyRecording settings=profile duration=60s filename=/tmp/recording.jfr
# Analyze with JMC
jmc /tmp/recording.jfr
Performance Testing
Load Testing with JMeter
<!-- JMeter Test Plan -->
<ThreadGroup>
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
</ThreadGroup>
<HTTPSamplerProxy>
<stringProp name="HTTPSampler.domain">localhost</stringProp>
<stringProp name="HTTPSampler.port">8080</stringProp>
<stringProp name="HTTPSampler.path">/api/projects</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
Load Testing with Gatling
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ProjectLoadTest extends Simulation {
val httpProtocol = http
.baseUrl("http://localhost:8080")
.acceptHeader("application/json")
val scn = scenario("Project Load Test")
.exec(http("List Projects")
.get("/api/projects")
.check(status.is(200)))
.pause(1)
setUp(
scn.inject(
rampUsers(100) during (60 seconds)
)
).protocols(httpProtocol)
}
Performance Benchmarks
@State(Scope.Benchmark)
public class RuleExecutionBenchmark {
private RuleService ruleService;
@Setup
public void setup() {
ruleService = new RuleService();
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testRuleExecution() {
ruleService.executeRule("my-rule", inputData);
}
}
Performance Checklist
Application Deployment
- Set appropriate JVM memory settings
- Enable G1GC or ZGC for low latency
- Configure connection pools (DB, HTTP)
- Enable caching for compiled rules
- Enable GZIP compression
- Configure appropriate timeouts
- Enable HTTP/2
- Set up monitoring (JMX, Prometheus)
Database
- Create indexes on frequently queried columns
- Configure connection pooling
- Enable query logging for slow queries
- Optimize Hibernate settings
- Use pagination for large result sets
- Avoid N+1 queries
Repository
- Use appropriate repository type for use case
- Enable repository caching
- Configure connection pooling for remote repositories
- Use shallow clones for Git
- Enable S3 Transfer Acceleration (if using S3)
Monitoring
- Enable OpenTelemetry tracing
- Configure Spring Boot Actuator
- Set up Prometheus/Grafana
- Monitor GC logs
- Set up alerting for performance degradation
Related Documentation
- Docker Guide - Docker performance tuning
- CI/CD Pipeline - Build performance
- Testing Guide - Performance testing
- Troubleshooting - Performance issues
Last Updated: 2025-11-05 Maintainer: OpenL Tablets Team