query-cascade 0.2.4__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- query_cascade-0.3.2/PKG-INFO +381 -0
- query_cascade-0.3.2/README.md +363 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/pyproject.toml +7 -1
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/__init__.py +2 -0
- query_cascade-0.3.2/src/cascade/_canonical.py +185 -0
- query_cascade-0.3.2/src/cascade/_disk_cache.py +186 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_errors.py +4 -0
- query_cascade-0.3.2/src/cascade/_evaluator.py +616 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_store.py +12 -1
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/engine.py +36 -5
- query_cascade-0.3.2/src/query_cascade.egg-info/PKG-INFO +381 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/query_cascade.egg-info/SOURCES.txt +5 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/query_cascade.egg-info/requires.txt +6 -0
- query_cascade-0.3.2/tests/test_disk_cache.py +845 -0
- query_cascade-0.3.2/tests/test_engine_private.py +100 -0
- query_cascade-0.3.2/tests/test_engine_private2.py +36 -0
- query_cascade-0.2.4/PKG-INFO +0 -148
- query_cascade-0.2.4/README.md +0 -135
- query_cascade-0.2.4/src/cascade/_evaluator.py +0 -305
- query_cascade-0.2.4/src/query_cascade.egg-info/PKG-INFO +0 -148
- {query_cascade-0.2.4 → query_cascade-0.3.2}/setup.cfg +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_graph_export.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_persistence.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_runtime.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_scheduler.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_serde.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_state.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/cascade/_synthetic_graph.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/query_cascade.egg-info/dependency_links.txt +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/src/query_cascade.egg-info/top_level.txt +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_dependency_parallelism.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_engine.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_engine_compute_many.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_engine_concurrency.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_engine_persistence_and_tracing.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_examples.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_internal_invariants.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_performance.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_repro_touch_memo_keyerror.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_scale_behavior.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_serde.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_stateful_engine_invariants.py +0 -0
- {query_cascade-0.2.4 → query_cascade-0.3.2}/tests/test_subgraph_stats_export.py +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: query-cascade
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Minimal demand-driven query framework for incremental computation.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: disk
|
|
8
|
+
Requires-Dist: lmdb; extra == "disk"
|
|
9
|
+
Requires-Dist: msgpack; extra == "disk"
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
12
|
+
Requires-Dist: lmdb; extra == "dev"
|
|
13
|
+
Requires-Dist: msgpack; extra == "dev"
|
|
14
|
+
Requires-Dist: mutmut; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# Cascade Query
|
|
20
|
+
|
|
21
|
+
Cascade Query is a Python library for incremental dependency tracking. It caches function results and re-executes them only when their specific inputs or upstream dependencies change.
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/query-cascade/)
|
|
24
|
+
[](https://pypi.org/project/query-cascade/)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core Principles
|
|
29
|
+
|
|
30
|
+
1. **Automatic Caching:** Results are stored. If dependencies are unchanged, the function body does not execute.
|
|
31
|
+
2. **Dependency Tracking:** Cascade records every `@engine.input` or `@engine.query` accessed during execution.
|
|
32
|
+
3. **Targeted Updates:** When an input changes, Cascade identifies and invalidates only the affected downstream functions.
|
|
33
|
+
4. **Early Bail-out:** If a function's output remains identical after its dependencies change, re-computation stops for that branch.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import time
|
|
41
|
+
from cascade import Engine
|
|
42
|
+
|
|
43
|
+
engine = Engine()
|
|
44
|
+
|
|
45
|
+
@engine.input
|
|
46
|
+
def user_id():
|
|
47
|
+
return "user_1"
|
|
48
|
+
|
|
49
|
+
@engine.query
|
|
50
|
+
def fetch_data():
|
|
51
|
+
time.sleep(2)
|
|
52
|
+
return {"id": user_id(), "data": "value"}
|
|
53
|
+
|
|
54
|
+
@engine.query
|
|
55
|
+
def get_result():
|
|
56
|
+
data = fetch_data()
|
|
57
|
+
return f"Result for {data['id']}"
|
|
58
|
+
|
|
59
|
+
# First run: Executes for 2 seconds.
|
|
60
|
+
print(get_result())
|
|
61
|
+
|
|
62
|
+
# Second run: Returns immediately from cache.
|
|
63
|
+
print(get_result())
|
|
64
|
+
|
|
65
|
+
# Update input:
|
|
66
|
+
user_id.set("user_2")
|
|
67
|
+
|
|
68
|
+
# Third run: Executes for 2 seconds to refresh.
|
|
69
|
+
print(get_result())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Engine API
|
|
75
|
+
|
|
76
|
+
### Core Methods
|
|
77
|
+
* **`Engine(max_entries=10000, stats=False, cache_dir=None, cache_map_size=2**30)`**: Initializes the engine. `max_entries` sets the limit for the Least Recently Used (LRU) cache. Passing `cache_dir` enables persistent disk caching (see below).
|
|
78
|
+
* **`@engine.input`**: Decorator for mutable data roots.
|
|
79
|
+
* `input.set(value)`: Updates the value and increments the global revision.
|
|
80
|
+
* `input.set(*args, value=value)`: Updates a keyed input.
|
|
81
|
+
* **`@engine.query`**: Decorator for cached computations.
|
|
82
|
+
* **`engine.snapshot()`**: Returns a `Snapshot` object pinning the current global revision. Use `query(snapshot=s)` to read data as it existed at that revision.
|
|
83
|
+
* **`engine.save(path)` / `engine.load(path)`**: Persists all inputs and cached results to a SQLite database.
|
|
84
|
+
* **`engine.clear_disk_cache()`**: Deletes every entry in the persistent disk cache. Raises if the engine was created without `cache_dir`.
|
|
85
|
+
|
|
86
|
+
### Parallel & Background Execution
|
|
87
|
+
* **`engine.compute_many(calls, workers=None)`**: Executes a list of queries in parallel using a thread pool.
|
|
88
|
+
* **`engine.submit(query, *args, executor=None)`**: Schedules a query for background execution. Returns a `concurrent.futures.Future`.
|
|
89
|
+
* **`QueryCancelled`**: Exception raised if a background query's dependencies change before it completes.
|
|
90
|
+
|
|
91
|
+
### Graph Utilities
|
|
92
|
+
* **`engine.inspect_graph()`**: Returns a dictionary of all nodes and edges in the dependency graph.
|
|
93
|
+
* **`engine.subgraph(roots, direction="deps")`**: Filters the graph to the dependency chain of the specified root nodes.
|
|
94
|
+
* **`engine.prune(roots, vacuum_disk=False)`**: Removes cached query results from the in-memory LRU cache that are not reachable from the specified roots. Set `vacuum_disk=True` to also do a deep vacuum of the persistent LMDB disk cache, deleting all orphaned blobs and metadata.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Persistent Disk Caching
|
|
99
|
+
|
|
100
|
+
Passing `cache_dir` to the `Engine` turns on zero-config persistence. Cascade provisions an embedded LMDB store in that directory, serializes query results with a deterministic msgpack encoding, and fingerprints every input value by hashing its serialized bytes with blake2b. Nothing else changes: queries and inputs are written exactly as before.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from cascade import Engine
|
|
104
|
+
|
|
105
|
+
engine = Engine(max_entries=10_000, cache_dir=".cascade_cache")
|
|
106
|
+
|
|
107
|
+
@engine.input
|
|
108
|
+
def package_source_text(pkg: str) -> str:
|
|
109
|
+
with open(pkg, "r") as f:
|
|
110
|
+
return f.read()
|
|
111
|
+
|
|
112
|
+
@engine.query
|
|
113
|
+
def parsed_package_ast(pkg: str):
|
|
114
|
+
return parse(package_source_text(pkg))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The first run executes normally and writes each result to disk. A later run in a new process starts with an empty in-memory cache, finds the entry on disk, and verifies it top-down: leaf inputs are re-executed and re-hashed (for the input above, that means re-reading the file), and the current hashes are compared against the fingerprints saved with the entry. If everything matches, the stored value is deserialized and returned without running any query body. If a file changed, its hash mismatches, and exactly the queries downstream of that file recompute. Early bail-out works across sessions too, since dependency fingerprints are content hashes: a whitespace-only edit that leaves an intermediate result unchanged will not recompute anything past it.
|
|
118
|
+
|
|
119
|
+
Accumulator effects are stored with each entry and replayed on disk hits, so a warning emitted in run 1 still appears in run 2 even when the query is served from disk.
|
|
120
|
+
|
|
121
|
+
`lmdb` and `msgpack` are required once `cache_dir` is set; there is no fallback, and the engine raises `PersistentCacheError` with install instructions if either is missing:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pip install query-cascade[disk]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A few things to know:
|
|
128
|
+
|
|
129
|
+
* Values and arguments must be serializable: primitives, bytes, `list`/`tuple`/`set`/`frozenset`/`dict`, `@dataclass` instances, and `typing.NamedTuple` instances. A query that returns anything else raises at compute time when persistence is on. A query called with an unserializable argument still computes and memoizes in memory, it just skips the disk.
|
|
130
|
+
* Cache addresses are derived from the function id (`module:qualname`) and the hashed arguments, so renaming or moving a function starts it from a cold cache. Editing a function body does not invalidate its entries; bump the cache with `engine.clear_disk_cache()` or delete the directory when query logic changes.
|
|
131
|
+
* The store supports concurrent access from multiple processes through LMDB's own locking. Within one process, engines sharing a `cache_dir` share one LMDB environment; the first opener's `cache_map_size` wins.
|
|
132
|
+
* The default `cache_map_size` is 1 GiB. LMDB allocates this lazily, so the file only grows as entries are written. If the cache fills up, `PersistentCacheError` explains the options.
|
|
133
|
+
* The on-disk data is a cache: clearing it is always safe and only costs recomputation. Cascade wipes it automatically when its own storage format version changes.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Advanced Features
|
|
138
|
+
|
|
139
|
+
### Side-Effect Accumulators
|
|
140
|
+
Queries must be pure functions. Use `Accumulator` to record side-effects (like logs or warnings) that must be replayed when a result is served from the cache.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
warnings = engine.accumulator("warnings")
|
|
144
|
+
|
|
145
|
+
@engine.query
|
|
146
|
+
def validate_data():
|
|
147
|
+
data = fetch_data()
|
|
148
|
+
if not data:
|
|
149
|
+
warnings.push("No data found")
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
# On cache hit, 'warnings' are re-populated into the effects dictionary.
|
|
153
|
+
effects = {}
|
|
154
|
+
validate_data(effects=effects)
|
|
155
|
+
print(effects["warnings"])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Performance Metrics
|
|
159
|
+
Set `stats=True` in the `Engine` constructor to track execution timing.
|
|
160
|
+
* **`engine.stats_summary()`**: Returns wall-clock time spent in function bodies and cache eviction counts.
|
|
161
|
+
* **`engine.reset_stats()`**: Clears accumulated timing data.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Visualization
|
|
166
|
+
Cascade provides renderers for the dependency graph.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from cascade import export_dot, export_mermaid
|
|
170
|
+
|
|
171
|
+
graph = engine.inspect_graph()
|
|
172
|
+
# Generate Graphviz DOT format
|
|
173
|
+
dot_text = export_dot(graph)
|
|
174
|
+
# Generate Mermaid flowchart format
|
|
175
|
+
mermaid_text = export_mermaid(graph)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Limitations
|
|
181
|
+
|
|
182
|
+
1. **Cycle Detection:** Cascade detects and rejects recursive function calls (cycles) with a `CycleError`.
|
|
183
|
+
2. **Thread Safety:** While Cascade supports parallel query execution, the `Engine` object itself should be modified (`.set()`, `@engine.query`) from a single thread or with external synchronization.
|
|
184
|
+
3. **Persistence Security:** `engine.load()` and the persistent disk cache resolve `@dataclass` and `NamedTuple` types via `importlib`. Only load databases or open cache directories from trusted sources.
|
|
185
|
+
4. **Python Version:** Optimization for parallel CPU-bound work requires **CPython 3.14+ free-threaded** builds with `PYTHON_GIL=0`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Installation
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pip install query-cascade
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Examples (in `examples/`)
|
|
196
|
+
|
|
197
|
+
| Script | What it shows |
|
|
198
|
+
|--------|----------------|
|
|
199
|
+
| `compiler_pipeline.py` | `source → parse → symbols → typecheck`, warnings accumulator, cache-hit narration |
|
|
200
|
+
| `dynamic_macro_expansion.py` | Query that **changes downstream dependencies** at runtime |
|
|
201
|
+
| `snapshot_isolation.py` | Snapshot reads while live inputs change |
|
|
202
|
+
| `concurrent_background_work.py` | Dedup under concurrency + cancellation after input changes |
|
|
203
|
+
| `compute_many_with_accumulators.py` | `compute_many(..., effects=...)` accumulator collection |
|
|
204
|
+
| `persistence_and_inspection.py` | Save/load and graph summaries |
|
|
205
|
+
| `gil_parallel_speedup.py` | Threaded CPU benchmark: GIL vs free-threaded |
|
|
206
|
+
|
|
207
|
+
Run one:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
python3.14t examples/compiler_pipeline.py
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Run all (Unix-style shell):
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
for example in examples/*.py; do
|
|
217
|
+
echo "Running $example"
|
|
218
|
+
python3.14t "$example"
|
|
219
|
+
done
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Examples print narration as they run so you can follow each behavior.
|
|
223
|
+
|
|
224
|
+
### Compare GIL vs free-threaded (same machine)
|
|
225
|
+
|
|
226
|
+
Install both **3.14** and **3.14t** if you want apples-to-apples. On Ubuntu (deadsnakes):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
sudo add-apt-repository ppa:deadsnakes/ppa
|
|
230
|
+
sudo apt update
|
|
231
|
+
sudo apt install -y python3.14 python3.14-venv python3.14t python3.14t-venv
|
|
232
|
+
python3.14 -m pip install -e .
|
|
233
|
+
python3.14t -m pip install -e .
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Quick check on free-threaded build:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
python3.14t -c "import sys, sysconfig; print('Py_GIL_DISABLED=', sysconfig.get_config_var('Py_GIL_DISABLED')); print('GIL enabled?', sys._is_gil_enabled())"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Same interpreter, toggle GIL at runtime:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
PYTHON_GIL=1 python3.14t examples/gil_parallel_speedup.py --workers 8 --tasks 96 --rounds 300000 --repeats 5
|
|
246
|
+
PYTHON_GIL=0 python3.14t examples/gil_parallel_speedup.py --workers 8 --tasks 96 --rounds 300000 --repeats 5
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Or compare `python3.14` vs `PYTHON_GIL=0 python3.14t` on the same script.
|
|
250
|
+
|
|
251
|
+
Compare **`median parallel seconds`** (lower is better) and **`threaded speedup in this runtime`** (higher is better). Keep args identical, reduce background load, and use `--repeats` (e.g. `5`) to smooth noise. On multi-core machines, **free-threaded + GIL off** usually wins clearly for this CPU-bound demo.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Design stance
|
|
256
|
+
|
|
257
|
+
The core is intentionally minimal: **pull-based** evaluation, dependency capture, red/green style bailout, dedup, snapshots, cancellation, accumulator replay, tracing, and persistence. That set is enough for many real pipelines without baking in advanced internals (e.g. fixed-point cycle solving or custom AST red/green structures). CPU-bound parallelism is expected to matter when you use **free-threaded CPython with the GIL disabled**.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
### Tests (match main CI)
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
export PYTHON_GIL=0 # Windows: set PYTHON_GIL=0
|
|
267
|
+
python3.14t -m pip install -e ".[dev]"
|
|
268
|
+
python3.14t -c "import sys, sysconfig; print('Py_GIL_DISABLED=', sysconfig.get_config_var('Py_GIL_DISABLED')); print('GIL enabled?', sys._is_gil_enabled())"
|
|
269
|
+
python3.14t -m pytest -q \
|
|
270
|
+
--ignore=tests/test_performance.py \
|
|
271
|
+
--cov=src/cascade \
|
|
272
|
+
--cov-branch \
|
|
273
|
+
--cov-report=term-missing \
|
|
274
|
+
--cov-fail-under=95
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Branch coverage check (CI uses an equivalent step on `coverage.json`):
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
python3.14t - <<'PY'
|
|
281
|
+
import json
|
|
282
|
+
with open("coverage.json", encoding="utf-8") as fh:
|
|
283
|
+
b = json.load(fh)["totals"]["percent_branches_covered"]
|
|
284
|
+
print(f"branch coverage: {b:.2f}%")
|
|
285
|
+
assert b >= 90.0
|
|
286
|
+
PY
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Stateful fuzz:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
PYTHON_GIL=0 python3.14t -m pytest -q tests/test_stateful_engine_invariants.py
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Mutation testing:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
PATH="$HOME/.local/bin:$PATH" PYTHON_GIL=0 mutmut run
|
|
299
|
+
PATH="$HOME/.local/bin:$PATH" PYTHON_GIL=0 mutmut results
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Use the `mutmut` CLI (`mutmut run`), not `python -m mutmut run`. Bounded local loop:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
PYTHON_GIL=0 MUTMUT_MAX_CHILDREN=2 ./scripts/mutation_fast.sh
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Focused mutants:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
PYTHON_GIL=0 MUTMUT_MAX_CHILDREN=2 ./scripts/mutation_fast.sh "<mutant-name>" "<mutant-name>"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
See `docs/mutation_triage.md` for survivor triage.
|
|
315
|
+
|
|
316
|
+
### Formal model (TLA+)
|
|
317
|
+
|
|
318
|
+
Specs live under `docs/formal/`:
|
|
319
|
+
|
|
320
|
+
- `docs/formal/cascade_core.tla`
|
|
321
|
+
- `docs/formal/cascade_core.cfg`
|
|
322
|
+
|
|
323
|
+
Run TLC (example):
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
java -cp tla2tools.jar tlc2.TLC docs/formal/cascade_core.tla -config docs/formal/cascade_core.cfg
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Checked properties include snapshot consistency, active-dependency validity (red/green alignment), and cancellation epoch monotonicity.
|
|
330
|
+
|
|
331
|
+
### Performance suite
|
|
332
|
+
|
|
333
|
+
Heavy behavior clusters around cache hits vs full recompute, concurrent dedup, `compute_many` throughput on free-threaded workloads, large-graph mutation vs rebuild, mark-green cost vs depth, and prune scaling.
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
python -m benchmarks.performance_suite --report-dir artifacts/performance --assert-thresholds
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Outputs:
|
|
340
|
+
|
|
341
|
+
- `artifacts/performance/performance-report.json`
|
|
342
|
+
- `artifacts/performance/performance-report.md`
|
|
343
|
+
|
|
344
|
+
CI runs the same suite and uploads **`performance-report`**.
|
|
345
|
+
|
|
346
|
+
The **`compute-many-parallel-speedup`** scenario (and `tests/test_performance.py::test_compute_many_parallel_speedup_scenario`) is sensitive to CPU scheduling. On a busy laptop or small VM, thresholds may flap without a real regression. Mitigations:
|
|
347
|
+
|
|
348
|
+
- Re-run the test, or set **`CASCADE_QUERY_PARALLEL_PERF_RETRIES`** (e.g. `3`).
|
|
349
|
+
- To skip while iterating: **`CASCADE_QUERY_SKIP_PARALLEL_PERF=1`** (CI does not set this).
|
|
350
|
+
|
|
351
|
+
**Nightly:** `.github/workflows/nightly-performance.yml` runs a longer sweep (e.g. 8 runs) and publishes **`nightly-performance-report`**.
|
|
352
|
+
|
|
353
|
+
### Scale and stress tests
|
|
354
|
+
|
|
355
|
+
`tests/test_scale_behavior.py` covers large-graph invalidation, dynamic dependency churn, prune stress, persistence at scale, eviction under churn, and mixed concurrency (`submit` + `compute_many` + writes). The heaviest cases are marked **`@pytest.mark.slow`**; default `pytest` skips them via `pyproject.toml`.
|
|
356
|
+
|
|
357
|
+
Internal invariants are concentrated in `tests/test_internal_invariants.py` (via `engine._internals`) to limit coupling while keeping safety checks.
|
|
358
|
+
|
|
359
|
+
Default CI-like run (no perf file, no slow):
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
PYTHON_GIL=0 python3.14t -m pytest -q --ignore=tests/test_performance.py
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Slow only:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
pytest -q -m slow
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Everything including slow:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
pytest -q -m "slow or not slow"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### CI overview
|
|
378
|
+
|
|
379
|
+
- Workflow: `.github/workflows/ci.yml` (pushes and PRs).
|
|
380
|
+
- **Ruff** before tests.
|
|
381
|
+
- Separate **package build** (`python -m build`) to catch packaging issues early.
|