Skip to content

Commit 776b4cb

Browse files
committed
feat: add reproducible benchmark suite; align published perf claims with measured results
Every performance number DataFog publishes is now reproducible with one command: python benchmarks/run.py. Suites: core scan/redact throughput, LiteLLM guardrail (DataFogGuardrail), Claude Code hook (datafog-hook subprocess incl. startup), plus clearly-labeled comparisons against presidio-analyzer and spaCy NER on identical pinned payloads (manifest-verified detection counts before timing; en_core_web_sm pinned, favorable to comparison targets, so ratios are lower bounds). Claims aligned with measured results (Apple M5 Pro reference numbers in benchmarks/README.md): - LiteLLM guardrail: '~31µs per request' was really per-scan; reworded to ~40µs per message scanned (full 2-msg request: ~120µs clean / ~220µs with redaction+litellm logging). - Claude Code hook: ~70ms confirmed (69-89ms median incl. startup); reworded to ~70-90ms. - '190x performance advantage' tagline not reproducible (measured 103-170x vs Presidio, 114-140x vs spaCy); reworded to '100x+ faster than NER-based detection' in setup.py, setup_lean.py, __init__ docstrings, AGENTS.md, docs/roadmap.rst. Also refresh docs/roadmap.rst to current state (4.1-4.7.0 shipped, 4.7.x precision patches, v5.0.0 themes from the public roadmap) and add a ROADMAP.md stub pointing at it as the single source of truth (removing the stale case-insensitive Roadmap.md gitignore entry that swallowed it). Benchmark deps stay out of the core package (benchmarks/requirements.txt); benchmarks/ has no __init__.py so nothing ships in the wheel.
1 parent 60fa5fd commit 776b4cb

21 files changed

Lines changed: 2248 additions & 139 deletions

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,4 @@ docs/*
6969
*/**/__pycache__/
7070

7171
notes/benchmarking_notes.md
72-
Roadmap.md
7372
notes/*

AGENTS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Core Value Proposition
88

9-
- **Ultra-Fast Performance**: 190x faster than spaCy for structured PII, 32x faster with GLiNER
9+
- **Ultra-Fast Performance**: 100x+ faster than spaCy/Presidio for structured PII (reproduce: `python benchmarks/run.py`), 32x faster with GLiNER
1010
- **Lightweight Core**: <2MB package with optional ML extras
1111
- **Modern Engine Options**: Regex, GLiNER, spaCy, and smart cascading
1212
- **Production Ready**: Comprehensive testing, CI/CD, and performance validation
@@ -24,7 +24,7 @@
2424
- **GLiNER Integration**: Modern NER engine with PII-specialized models
2525
- **Smart Cascading**: Intelligent regex → GLiNER → spaCy progression
2626
- **Enhanced CLI**: Model management with `--engine` flags
27-
- **Performance Validation**: 190x regex, 32x GLiNER benchmarks confirmed
27+
- **Performance Validation**: 100x+ regex speedup reproducible via `benchmarks/`; 32x GLiNER
2828
- **CI/CD Consolidation**: 7 workflows → 3 (ci, release, benchmark)
2929

3030
## Quick Development Setup
@@ -56,7 +56,7 @@ python -c "from datafog.services.text_service import TextService; print('✅ All
5656
from datafog.services.text_service import TextService
5757

5858
# Core engines (always available)
59-
regex_service = TextService(engine="regex") # 190x faster, structured PII
59+
regex_service = TextService(engine="regex") # 100x+ faster, structured PII
6060

6161
# ML engines (require extras)
6262
gliner_service = TextService(engine="gliner") # 32x faster, modern NER
@@ -71,7 +71,7 @@ auto_service = TextService(engine="auto") # Legacy: regex→spaCy
7171

7272
| Engine | Speed vs spaCy | Accuracy | Use Case | Install |
7373
| -------- | --------------- | ----------------- | --------------------------- | ---------------- |
74-
| `regex` | **190x faster** | High (structured) | Emails, phones, SSNs | Core only |
74+
| `regex` | **100x+ faster** | High (structured) | Emails, phones, SSNs | Core only |
7575
| `gliner` | **32x faster** | Very High | Modern NER, custom entities | `[nlp-advanced]` |
7676
| `spacy` | 1x (baseline) | Good | Traditional NLP | `[nlp]` |
7777
| `smart` | **60x faster** | Highest | Best balance | `[nlp-advanced]` |
@@ -281,7 +281,7 @@ export PYTHONPATH=$(pwd) # Local development imports
281281
## Performance Requirements
282282

283283
- **Core Package**: <2MB (from ~8MB in v4.0.x)
284-
- **Regex Engine**: 150x+ faster than spaCy (currently 190x)
284+
- **Regex Engine**: 100x+ faster than spaCy (103–170x measured across payloads; see `benchmarks/`)
285285
- **GLiNER Engine**: 25x+ faster than spaCy (currently 32x)
286286
- **Memory Usage**: Graceful handling of large texts (1MB+ chunks)
287287
- **Model Loading**: Cache GLiNER models to avoid repeated downloads

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ values never echoed into logs or transcripts:
1919

2020
- **Claude Code hook** (`datafog-hook`): gates agent tool calls (shell
2121
commands, web requests, file writes, MCP tools) and warns the model when
22-
prompts or tool results carry PII. ~70ms per invocation including process
23-
startup. Easiest install is the
22+
prompts or tool results carry PII. ~70–90ms per invocation including
23+
process startup. Easiest install is the
2424
[Claude Code plugin](https://github.com/DataFog/datafog-claude-plugin):
2525

2626
```
@@ -32,7 +32,8 @@ values never echoed into logs or transcripts:
3232

3333
- **LiteLLM guardrail** (`DataFogGuardrail`): redacts or blocks PII in
3434
requests and responses at the gateway, for any LiteLLM-proxied provider.
35-
In-process (~31µs per request), no sidecar service. Setup:
35+
In-process (~40µs per message scanned; a request clears the guardrail in
36+
well under a millisecond), no sidecar service. Setup:
3637
[examples/litellm_guardrail/](examples/litellm_guardrail/).
3738

3839
Both default to the high-precision entity set (`EMAIL`, `PHONE`,
@@ -43,6 +44,10 @@ unix timestamps matching as phone numbers) — available in both adapters and
4344
the API. Presidio-style entity names (`EMAIL_ADDRESS`, `PHONE_NUMBER`,
4445
`US_SSN`) are accepted as aliases for easy migration.
4546

47+
Every performance number above is reproducible with one command —
48+
methodology, pinned payloads, and comparisons against Presidio and spaCy
49+
NER live in [benchmarks/](benchmarks/).
50+
4651
## Installation
4752

4853
```bash

ROADMAP.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# DataFog Roadmap
2+
3+
The roadmap — current release status (4.7.x) and v5.0.0 plans — lives in
4+
[docs/roadmap.rst](docs/roadmap.rst).
5+
6+
Roadmap priorities are shaped by user feedback — open a
7+
[GitHub issue](https://github.com/DataFog/datafog-python/issues) or join
8+
the [Discord](https://discord.gg/bzDth394R4).

benchmarks/README.md

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# DataFog benchmarks
2+
3+
Every performance number DataFog publishes should be reproducible by a
4+
skeptic with one command. This directory is that command:
5+
6+
```bash
7+
pip install -e . -r benchmarks/requirements.txt
8+
python -m spacy download en_core_web_sm # comparison suites only
9+
python benchmarks/run.py
10+
```
11+
12+
Suites that are missing optional dependencies are skipped with an install
13+
hint — `python benchmarks/run.py --suite core,hook` runs with nothing but
14+
datafog itself installed. `--json results.json` writes machine-readable
15+
output; `--quick` is a fast smoke run (fewer repeats, large payloads
16+
skipped).
17+
18+
## Methodology
19+
20+
- **In-process timings** use stdlib `timeit`: `Timer.autorange()` picks an
21+
inner loop count so each timed repeat runs ≥0.2s, then 5 repeats (3 in
22+
comparison suites) are reported as median ± stdev. No mocking — every
23+
call goes through the same public code path a user hits.
24+
- **The hook suite measures wall-clock subprocess time**: a fresh
25+
`datafog-hook` process per iteration, JSON payload on stdin, exactly as
26+
Claude Code invokes it. 3 warmup runs, 30 timed runs, median and p90
27+
reported. Python interpreter startup is *included* — that is the honest
28+
cost of a per-tool-call hook.
29+
- **Pinned payloads** live in [payloads/](payloads/) and are checked in,
30+
not generated at runtime. [payloads/manifest.json](payloads/manifest.json)
31+
records the entity counts the regex engine must find in each payload; the
32+
runner verifies those counts *before* timing anything, so the suite fails
33+
loudly rather than silently benchmarking an engine that stopped
34+
detecting. All PII values are industry-standard synthetic test data
35+
(reserved `.invalid`/`example.com` domains, fictional 555-01XX phone
36+
numbers, public Luhn-valid test cards, SSA example SSNs).
37+
38+
| Payload | Size | Content |
39+
| --- | --- | --- |
40+
| `core_1kb_dense.txt` | 1.2 KB | support ticket, 8 entities (PII-dense) |
41+
| `core_10kb_mixed.txt` | 10 KB | business document, 12 entities in prose |
42+
| `core_100kb_sparse.txt` | 100 KB | machine logs, 18 entities among UUIDs/timestamps (false-positive pressure) |
43+
| `chat_request_clean.json` || 2-message LiteLLM chat body, no PII |
44+
| `chat_request_pii.json` || 2-message LiteLLM chat body, email + card + phone |
45+
| `hook_pretooluse.json` || Claude Code `PreToolUse` payload, `curl` command carrying PII |
46+
47+
All suites use the production default entity set
48+
(`EMAIL, PHONE, CREDIT_CARD, SSN`).
49+
50+
## Reference results
51+
52+
Apple M5 Pro, macOS, CPython 3.13, datafog 4.7.0, litellm 1.91.0,
53+
presidio-analyzer 2.2.363, spaCy 3.8 (`en_core_web_sm`). Medians of the
54+
full (non-quick) run; expect different absolute numbers on different
55+
hardware, but the ratios and orders of magnitude should hold.
56+
57+
### core — `datafog.scan` / `datafog.redact`, regex engine
58+
59+
| Operation | Median | Throughput |
60+
| --- | --- | --- |
61+
| scan 1.2 KB dense | 231 µs | 5.3 MB/s |
62+
| redact 1.2 KB dense | 240 µs | 5.1 MB/s |
63+
| scan 10 KB mixed | 1.20 ms | 8.6 MB/s |
64+
| redact 10 KB mixed | 1.19 ms | 8.6 MB/s |
65+
| scan 100 KB sparse | 11.9 ms | 8.4 MB/s |
66+
| redact 100 KB sparse | 12.1 ms | 8.3 MB/s |
67+
68+
### guardrail — `DataFogGuardrail` (LiteLLM), in-process
69+
70+
| Operation | Median |
71+
| --- | --- |
72+
| single message redact | 43 µs |
73+
| `pre_call`, clean 2-message request | 118 µs |
74+
| `pre_call`, PII 2-message request (redact) | 222 µs |
75+
| event-loop dispatch (harness overhead, included above) | 27 µs |
76+
77+
The PII-request figure includes litellm's own guardrail-intervention
78+
logging (~85 µs), not just detection. In a live proxy the event loop is
79+
already running, so the ~27 µs dispatch overhead in the two per-request
80+
rows is an artifact of benchmarking with `run_until_complete`.
81+
82+
### hook — `datafog-hook` (Claude Code)
83+
84+
| Operation | Median |
85+
| --- | --- |
86+
| end-to-end subprocess, incl. Python startup | 69–89 ms across runs (p90 ~77–96 ms) |
87+
| the scan itself (in-process) | 75 µs |
88+
89+
The end-to-end cost is ~99.9% Python interpreter + import startup; the
90+
scan is microseconds. Cold (first-ever) invocations can spike to several
91+
hundred ms while the OS warms caches — the warmup runs exclude that, the
92+
p90 shows steady-state spread.
93+
94+
### Comparisons — same payloads, same four entity types
95+
96+
Both comparison targets are pinned to `en_core_web_sm`, the *smallest*
97+
English spaCy model — deliberately favorable to them (smaller model =
98+
faster inference), so these ratios are lower bounds. Model/engine setup
99+
time (~0.1–1 s) is excluded from per-scan figures. Presidio runs in-process
100+
via `AnalyzerEngine` with a documented `NlpEngineProvider` config — no
101+
sidecar, no network hop, which again flatters Presidio relative to its
102+
usual proxy deployment.
103+
104+
| Payload | datafog regex | Presidio | spaCy NER | datafog vs Presidio | datafog vs spaCy |
105+
| --- | --- | --- | --- | --- | --- |
106+
| 1.2 KB dense | 222–251 µs | 22.8 ms | 28.6 ms | 103x | 114x |
107+
| 10 KB mixed | 1.2 ms | 174 ms | 171 ms | 148x | 140x |
108+
| 100 KB sparse | 11.9–12.2 ms | 2.02 s | 1.60 s | 170x | 131x |
109+
110+
Speed is not the only axis: the runner records what each engine actually
111+
detected on each payload (see the `detected` metadata in the output).
112+
Recall differs in both directions — e.g. Presidio's email recognizer does
113+
not match addresses on the reserved `.invalid` domain, and context-based
114+
scoring drops some entities the regex engine finds; conversely NER models
115+
detect names and locations the regex engine does not target at all. The
116+
comparison is a fair *speed* comparison on identical inputs and entity
117+
types, not an accuracy study.
118+
119+
## How these map to published claims
120+
121+
- **"~31µs per request" (LiteLLM guardrail, project README /
122+
datafog.ai)** — not reproduced *as worded*. ~31–43 µs is the cost of
123+
scanning/redacting **one short message**; a full 2-message request
124+
through `async_pre_call_hook` is ~120 µs clean / ~220 µs with redaction
125+
and litellm logging. Still 100–1000x below a sidecar's network hop, but
126+
the claim should be reworded to per-message, e.g. *"~40µs per message —
127+
a request clears the guardrail in well under a millisecond"*. Reproduce:
128+
`python benchmarks/run.py --suite guardrail`.
129+
- **"~70ms per invocation including process startup" (Claude Code
130+
hook)** — reproduced: 69–89 ms median across runs on this machine
131+
(~70 ms idle, higher under load), dominated by interpreter startup.
132+
*"~70–90ms"* (or "under 100ms") is the defensible phrasing. Reproduce: `python benchmarks/run.py --suite hook`.
133+
- **"190x performance advantage" (PyPI tagline)** — not reproduced at
134+
190x by this suite. Measured: 103–170x vs Presidio and 114–140x vs spaCy
135+
NER, varying by payload. The historical 190x came from a different
136+
(13.3 KB, entity-dense) document and engine configuration. Honest
137+
phrasings this suite supports: *"100x+ faster than NER-based PII
138+
detection"* or *"up to 170x faster than Presidio on identical
139+
payloads"*. Reproduce: `python benchmarks/run.py --suite spacy,presidio`.
140+
141+
## Fairness notes / known limitations
142+
143+
- Single-threaded, single-machine, synthetic payloads. Real chat traffic
144+
and real documents will differ; the payloads are designed to bracket the
145+
realistic range (dense, mixed, sparse).
146+
- `timeit` medians hide GC pauses and tail latency by design; the hook
147+
suite's p90 is the only tail-latency figure here.
148+
- The comparison suites measure the engines *as configured here*
149+
(documented model, in-process, setup excluded). Different Presidio
150+
recognizer registries or larger spaCy models change both speed and
151+
recall — in Presidio's favor on recall, against it on speed.

0 commit comments

Comments
 (0)