Skip to content

Commit 2425042

Browse files
jensensclaude
andcommitted
docs: update performance docs for prefetch and final numbers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f70bd2b commit 2425042

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

docs/.vale/styles/config/vocabularies/Project/accept.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ZEO
2525
FileStorage
2626
ZCatalog
2727
BTree[s]?
28+
OOBTree[s]?
2829
PGJsonb
2930
GenericSetup
3031
VectorChord
@@ -243,4 +244,5 @@ pending
243244
[Aa]uto(detect(ed|s|ion)?|discover(ed|s|y)|creat(ed|es)|inject|appli(ed|es)|populat(ed|es))
244245
datetime
245246
[Pp]refetch(ed|er|es|ing)?
247+
[Pp]luggable
246248
roundtrips

docs/sources/explanation/performance.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,53 @@ N individual queries to 1 batch query. Window-based: only prefetches
314314
the next batch, not the entire result set (safe for large result sets
315315
where only a page is rendered).
316316

317+
#### Pluggable refs prefetch (zodb-pgjsonb v1.9.2)
318+
319+
In addition to the explicit `load_multiple()` batch on `getObject()`, the
320+
storage layer itself can prefetch an object's direct references (annotations,
321+
sub-mappings, OOBTrees) automatically on every `load()` call. This is
322+
controlled by a SQL expression registered via
323+
`storage.register_prefetch_refs_expr()`.
324+
325+
plone-pgcatalog registers the expression at startup:
326+
327+
```python
328+
storage.register_prefetch_refs_expr(
329+
"CASE WHEN idx IS NOT NULL THEN refs END"
330+
)
331+
```
332+
333+
This `CASE WHEN idx IS NOT NULL THEN refs END` expression is the result of
334+
a three-version saga:
335+
336+
- **v1.9.0** prefetched unconditionally for all objects. This caused severe
337+
over-fetching: internal ZODB structures (BTrees, PersistentMappings) have
338+
refs that cascade into thousands of objects that are never accessed by user
339+
code. Cold-start performance was 40--84% *slower* than without prefetch.
340+
- **v1.9.1** added a class-module blacklist to skip known non-content classes.
341+
This was fragile -- any addon adding new persistent classes could reintroduce
342+
over-fetching.
343+
- **v1.9.2** replaced the blacklist with the pluggable SQL expression. The
344+
`idx IS NOT NULL` filter is a reliable proxy for "is a content object" because
345+
only cataloged content objects have a non-NULL `idx` column in `object_state`.
346+
Non-content objects (BTrees, PersistentMappings, Length objects, etc.) never
347+
have catalog index data and produce NULL, suppressing the prefetch entirely.
348+
349+
#### Cold vs warm performance summary
350+
351+
With the final `idx IS NOT NULL` filter, the combined batch loading and refs
352+
prefetch achieves the intended benefit without the over-fetching penalty:
353+
354+
- **Cold start** (empty ZODB cache): page loads with 10--50 `getObject()` calls
355+
issue 1--2 batch queries instead of 10--50 individual queries. The refs
356+
prefetch warms annotations and workflow state objects ahead of access.
357+
- **Warm cache** (ZODB cache populated): prefetch calls hit the storage LRU
358+
cache and return immediately with no database roundtrips. The overhead of
359+
checking the cache is negligible (microseconds).
360+
- **Large result sets**: window-based batching ensures only the currently
361+
rendered page is prefetched. A search returning 10,000 results but rendering
362+
25 per page prefetches at most 100 objects (one batch window).
363+
317364
### ZODB cache sizing
318365

319366
The ZODB Connection cache is the primary performance lever for warm-cache

docs/sources/reference/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ need a separate `%import` directive.
6161
| `PGCATALOG_SLOW_QUERY_MS` | `10` | Threshold in milliseconds for slow query detection. Queries exceeding this are logged as warnings and recorded in the `pgcatalog_slow_queries` table for analysis via the ZMI Slow Queries tab. Set to `0` to disable. |
6262
| `PGCATALOG_QUERY_CACHE_SIZE` | `200` | Max cached query results per process. Set to `0` to disable. Invalidated when `MAX(tid)` changes (any ZODB commit). Cost-based eviction keeps expensive queries in cache. |
6363
| `PGCATALOG_QUERY_CACHE_TTR` | `60` | Time-to-round in seconds for datetime values in cache keys. Controls cache key granularity for effectiveRange queries. Higher values = more cache hits but slightly stale effectiveRange. |
64-
| `PGCATALOG_PREFETCH_BATCH` | `100` | Number of objects to prefetch when `brain.getObject()` is called. Set to `0` to disable. Requires zodb-pgjsonb >= 1.8.0. |
64+
| `PGCATALOG_PREFETCH_BATCH` | `100` | Number of objects to prefetch when `brain.getObject()` is called. Set to `0` to disable. Requires zodb-pgjsonb >= 1.8.0. In addition, automatic refs prefetch on `load()` is registered at startup when zodb-pgjsonb >= 1.9.2 is available (uses `idx IS NOT NULL` filter to limit prefetch to cataloged content). |
6565
| `ZODB_TEST_DSN` | `dbname=zodb_test host=localhost port=5433 user=zodb password=zodb` | DSN for test database (tests only). |
6666
| `BM25_TEST_DSN` | `dbname=zodb_test host=localhost port=5434 user=zodb password=zodb` | DSN for BM25 integration tests (tests only). |
6767

@@ -109,7 +109,7 @@ The following registrations are made:
109109
| `psycopg[binary,pool]>=3.1` | PostgreSQL adapter with connection pooling. |
110110
| `orjson>=3.9` | Fast JSONB deserialization. |
111111
| `Products.CMFPlone` | Plone framework. |
112-
| `zodb-pgjsonb>=1.1` | ZODB storage backend (provides the `object_state` table). |
112+
| `zodb-pgjsonb>=1.8` | ZODB storage backend (provides the `object_state` table). v1.8.0 adds `load_multiple()` for batch prefetch; v1.9.2 adds `register_prefetch_refs_expr()` for automatic refs prefetch on `load()`. |
113113

114114
## Optional dependencies
115115

0 commit comments

Comments
 (0)