Files
windmill/benchmarks/results_batch_pull_3workers.md
HugoCasa 8c3ac22d8d feat: add as_worker_tag() helper, benchmark results and model
- Extract bunnative→nativets tag logic into ScriptLang::as_worker_tag()
- Add benchmark results for batch pull vs direct SQL (1W and 3W)
- Add throughput model script comparing batch vs SQL at scale
- Add nativets_sleep benchmark script support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:05:15 +01:00

124 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Batch Pull Benchmark Results — 3 Workers
Date: 2026-03-06
Setup: 1 server + 3 native workers (8 subworkers each = 24 subworkers)
Hardware: fedora, 1.5TB disk, ~1GB memory usage
DB: PostgreSQL local, windmill 270 MiB
## nativets — 1000 jobs
| | 3W Batch | 3W SQL | 1W Batch | 1W SQL |
|--|---------|--------|---------|--------|
| Duration | 3.7s | 3.5s | 9.2s | 11.4s |
| **Throughput** | **272 jobs/s** | **288 jobs/s** | **108 jobs/s** | **88 jobs/s** |
| vs 1W SQL | +209% | +227% | +23% | baseline |
Note: First 3W SQL run was 33 jobs/s (outlier due to cold start or background activity). Rerun gave 288 jobs/s.
## nativets — 10,000 jobs
| | 3W Batch | 3W SQL |
|--|---------|--------|
| Duration | 34.3s | 39.5s |
| **Throughput** | **291 jobs/s** | **253 jobs/s** |
| Improvement | **+15%** | baseline |
### pg_stat_statements (1000 jobs, first run)
| Query | 3W Batch calls | 3W Batch ms | 3W SQL calls | 3W SQL ms |
|-------|---------------|-------------|-------------|----------|
| Native pull (FOR UPDATE SKIP LOCKED) | 4,801 (0.01ms) | 59 | 20,367 (0.01ms) | 231 |
| Default worker pull | 353 (0.03ms) | 10 | 880 (0.02ms) | 16 |
| DELETE from queue | 1,001 (0.44ms) | 439 | 1,001 (0.43ms) | 427 |
| INSERT into completed | 1,001 (0.06ms) | 61 | 1,001 (0.05ms) | 55 |
| INSERT job_logs | 2,003 (0.03ms) | 67 | 2,003 (0.03ms) | 64 |
| Agent token blacklist | 7,867 (0.00ms) | 33 | — | — |
| Worker ping (job) | 350 (0.04ms) | 15 | — | — |
| Outstanding wait time | 664 (0.03ms) | 21 | 742 (0.03ms) | 23 |
### pg_stat_database (1000 jobs, first run)
| Metric | 3W Batch | 3W SQL |
|--------|---------|--------|
| Transactions committed | 22,070 | 29,301 |
| Blocks read (disk) | 308 | 395 |
| Blocks hit (cache) | 280,071 | 1,276,521 |
| Tuples returned | 3,368,255 | 28,695,830 |
| Tuples fetched | 140,864 | 1,089,774 |
| Tuples inserted | 6,682 | 6,760 |
| Tuples updated | 3,521 | 3,453 |
| Tuples deleted | 3,007 | 3,007 |
---
## nativets_sleep — 1000 jobs
Each job sleeps 300-700ms (random). Theoretical max with 24 workers: ~48 jobs/s.
| | 3W Batch | 3W SQL | 1W Batch | 1W SQL |
|--|---------|--------|---------|--------|
| Duration | 22.8s | 22.8s | 66.7s | 68.2s |
| **Throughput** | **43.8 jobs/s** | **43.8 jobs/s** | **15.0 jobs/s** | **14.7 jobs/s** |
| vs 3W SQL | ~same | baseline | — | — |
### pg_stat_statements
| Query | 3W Batch calls | 3W Batch ms | 3W SQL calls | 3W SQL ms |
|-------|---------------|-------------|-------------|----------|
| Native pull (FOR UPDATE SKIP LOCKED) | 4,898 (0.01ms) | 55 | 6,440 (0.02ms) | 113 |
| Default worker pull | 696 (0.06ms) | 43 | 654 (0.06ms) | 37 |
| DELETE from queue | 1,001 (0.38ms) | 379 | 1,001 (0.29ms) | 290 |
| INSERT into completed | 1,001 (0.05ms) | 46 | 1,001 (0.04ms) | 43 |
| INSERT job_logs | 2,003 (0.02ms) | 44 | 2,003 (0.02ms) | 43 |
| Agent token blacklist | 7,295 (0.00ms) | 31 | — | — |
| Job runtime ping | 1,499 (0.02ms) | 35 | 1,444 (0.02ms) | 35 |
| Worker ping (job) | 1,001 (0.03ms) | 32 | 1,001 (0.03ms) | 31 |
| Job stats | 549 (0.05ms) | 27 | 493 (0.05ms) | 26 |
| Outstanding wait time | 928 (0.02ms) | 20 | 944 (0.02ms) | 21 |
### pg_stat_database
| Metric | 3W Batch | 3W SQL |
|--------|---------|--------|
| Transactions committed | 25,896 | 18,646 |
| Blocks read (disk) | 115 | 204 |
| Blocks hit (cache) | 1,173,696 | 1,256,397 |
| Tuples returned | 21,074,537 | 22,063,593 |
| Tuples fetched | 1,200,878 | 1,262,900 |
| Tuples inserted | 7,495 | 7,461 |
| Tuples updated | 6,204 | 6,141 |
| Tuples deleted | 3,005 | 3,011 |
---
## Analysis
### nativets (CPU-bound): +15% with 10K jobs
With 10,000 jobs, batch pull achieves **291 jobs/s vs 253 jobs/s** (+15%). The 1000-job runs showed similar throughput (~272-288 jobs/s) after discarding the cold-start outlier.
**DB load difference** (from the 1000-job first run, which captured the worst-case SQL contention):
- **20,367 pull queries** (SQL) vs 4,801 (batch) — 4x more queries
- **28.7M tuples returned** (SQL) vs 3.4M (batch) — 8.5x more index scanning
- **1.3M cache hits** (SQL) vs 280K (batch) — 4.6x more buffer activity
The batch approach consolidates all 24 subworkers into a single `LIMIT 24` query, reducing contention on the queue index.
### nativets_sleep (I/O-bound): No throughput difference
Both achieve **43.8 jobs/s** (91% of theoretical 48 jobs/s max). When workers spend 300-700ms sleeping, DB contention isn't the bottleneck.
Batch pull still shows slightly lower DB load:
- **4,898 pull queries** vs 6,440 — 24% fewer
- **115 disk reads** vs 204 — 44% fewer
### Scaling summary
| Setup | Batch jobs/s | SQL jobs/s | Batch advantage |
|-------|-------------|-----------|----------------|
| 1W × 1000 jobs | 108 | 88 | +23% |
| 3W × 1000 jobs | 272 | 288 | ~same |
| 3W × 10,000 jobs | 291 | 253 | **+15%** |
At 24 subworkers, batch pull provides a consistent ~15% throughput improvement for sustained CPU-bound workloads, with significantly lower DB load (4x fewer pull queries, 8x fewer tuples scanned). The benefit grows with more workers as SKIP LOCKED contention scales O(N²).