advalcache 0.1.0__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.
- advalcache-0.1.0/.gitignore +35 -0
- advalcache-0.1.0/LICENSE +21 -0
- advalcache-0.1.0/PKG-INFO +199 -0
- advalcache-0.1.0/README.md +156 -0
- advalcache-0.1.0/advalcache/__init__.py +320 -0
- advalcache-0.1.0/advalcache/__main__.py +58 -0
- advalcache-0.1.0/advalcache/_engine.py +522 -0
- advalcache-0.1.0/advalcache/_eviction.py +93 -0
- advalcache-0.1.0/advalcache/_refresh.py +91 -0
- advalcache-0.1.0/advalcache/_scoring.py +157 -0
- advalcache-0.1.0/advalcache/adapters/__init__.py +2 -0
- advalcache-0.1.0/advalcache/adapters/django.py +139 -0
- advalcache-0.1.0/advalcache/adapters/fastapi.py +80 -0
- advalcache-0.1.0/advalcache/adapters/sqlalchemy.py +83 -0
- advalcache-0.1.0/advalcache/config.py +80 -0
- advalcache-0.1.0/advalcache/decorators.py +211 -0
- advalcache-0.1.0/advalcache/py.typed +0 -0
- advalcache-0.1.0/advalcache/server.py +118 -0
- advalcache-0.1.0/advalcache/stats.py +84 -0
- advalcache-0.1.0/docs/sdk/quickstart.md +228 -0
- advalcache-0.1.0/pyproject.toml +68 -0
- advalcache-0.1.0/results/_before_p6fix/README.md +12 -0
- advalcache-0.1.0/web_adval/README.md +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# generated traces are large; small committed samples live in traces/samples/
|
|
2
|
+
traces/*.jsonl
|
|
3
|
+
traces/*.catalog.json
|
|
4
|
+
traces/*.meta.json
|
|
5
|
+
!traces/samples/**
|
|
6
|
+
|
|
7
|
+
# generated results, except the committed report + charts
|
|
8
|
+
results/**/events_*.jsonl
|
|
9
|
+
results/**/windows_*.csv
|
|
10
|
+
results/**/all_windows.csv
|
|
11
|
+
results/*.log
|
|
12
|
+
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
.streamlit/
|
|
19
|
+
*.tmp
|
|
20
|
+
|
|
21
|
+
# React/Vite frontend: source is tracked, build output and deps are not
|
|
22
|
+
web_adval/node_modules/
|
|
23
|
+
web_adval/dist/
|
|
24
|
+
|
|
25
|
+
# Python package build artifacts
|
|
26
|
+
dist/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
|
|
29
|
+
# credentials: the DSN belongs here, never in a tracked file
|
|
30
|
+
.env
|
|
31
|
+
.env.*
|
|
32
|
+
!.env.example
|
|
33
|
+
|
|
34
|
+
# local convenience launcher, not a documented entry point
|
|
35
|
+
run_dashboards.py
|
advalcache-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manaal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: advalcache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AdVal adaptive in-memory cache SDK — value-scored, freshness-aware, zero dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mdtahseen7/VH26-PRESTORA
|
|
6
|
+
Project-URL: Repository, https://github.com/mdtahseen7/VH26-PRESTORA
|
|
7
|
+
Project-URL: Issues, https://github.com/mdtahseen7/VH26-PRESTORA/issues
|
|
8
|
+
Author-email: Md Tahseen <mdtahseen7378@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: adaptive,cache,caching,django,fastapi,in-memory,sdk
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: django>=3.2; extra == 'all'
|
|
25
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'all'
|
|
26
|
+
Requires-Dist: sqlalchemy>=1.4; extra == 'all'
|
|
27
|
+
Requires-Dist: uvicorn>=0.20.0; extra == 'all'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: httpx>=0.24; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=9.0; extra == 'dev'
|
|
33
|
+
Provides-Extra: django
|
|
34
|
+
Requires-Dist: django>=3.2; extra == 'django'
|
|
35
|
+
Provides-Extra: fastapi
|
|
36
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
37
|
+
Requires-Dist: uvicorn>=0.20.0; extra == 'fastapi'
|
|
38
|
+
Provides-Extra: redis
|
|
39
|
+
Requires-Dist: redis>=4.0; extra == 'redis'
|
|
40
|
+
Provides-Extra: sqlalchemy
|
|
41
|
+
Requires-Dist: sqlalchemy>=1.4; extra == 'sqlalchemy'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# AdVal-Cache
|
|
45
|
+
|
|
46
|
+
**A cache that knows the difference between a $0.31 recommendation vector and a stock
|
|
47
|
+
ticker that is worthless two seconds after you store it.**
|
|
48
|
+
|
|
49
|
+
LRU asks *when did you last touch this*. LFU asks *how often*. Neither asks the only
|
|
50
|
+
question a bill cares about: **what does losing this actually cost me?** In a real
|
|
51
|
+
service that answer spans four orders of magnitude — a free 4 MB product image against
|
|
52
|
+
a ten-second, fifty-cent ML inference. AdVal-Cache computes a per-object **value
|
|
53
|
+
score** from access pattern, resource footprint, regeneration cost *and* data
|
|
54
|
+
volatility, then decides retain / evict / **refresh** / **decline** — and separately
|
|
55
|
+
decides, in dollars, when buying more cache capacity is worth the rent.
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
python -m venv venv; venv\Scripts\activate; pip install -r requirements.txt
|
|
61
|
+
python -m benchmarks.run_all --seed 42 # 80 cells in ~4 min -> results/report.md
|
|
62
|
+
streamlit run dashboard/app.py # playback dashboard
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Everything is a deterministic simulation: no network, no Redis, no Docker, CPU only.
|
|
66
|
+
Same seed ⇒ byte-identical traces, results and charts.
|
|
67
|
+
|
|
68
|
+
## Results
|
|
69
|
+
|
|
70
|
+
60 simulated minutes per scenario, seed 42. The problem statement's baselines are LRU, LFU
|
|
71
|
+
and GDS; exact GDSF ships alongside because it is stronger than the brief requires (D29).
|
|
72
|
+
|
|
73
|
+
**AdVal is cheaper *and* fresher than the best baseline in 8 of 8 cells.**
|
|
74
|
+
|
|
75
|
+
| scenario / profile | best baseline | baseline $ | **AdVal $** | saving | baseline fresh | **AdVal fresh** | pre-D31 base $ | pre-D31 AdVal $ |
|
|
76
|
+
|---|---|---|---|---|---|---|---|---|
|
|
77
|
+
| chaos / compute_heavy_rec | gds | 2854.48 | **2816.52** | **1.3%** | 66.2% | **78.5%** | 98.91 | 124.13 |
|
|
78
|
+
| chaos / read_heavy_api | gds | 1086.05 | **986.37** | **9.2%** | 64.2% | **74.2%** | 98.91 | 117.41 |
|
|
79
|
+
| gradual_shift / compute_heavy_rec | gds | 3034.96 | **2989.81** | **1.5%** | 67.1% | **80.2%** | 98.91 | 124.51 |
|
|
80
|
+
| gradual_shift / read_heavy_api | gds | 1046.89 | **923.08** | **11.8%** | 64.7% | **77.7%** | 98.91 | 123.12 |
|
|
81
|
+
| steady / compute_heavy_rec | gds | 4275.14 | **4235.45** | **0.9%** | 59.7% | **73.1%** | 98.91 | 122.15 |
|
|
82
|
+
| steady / read_heavy_api | gdsf_exact | 1282.12 | **1142.86** | **10.9%** | 57.9% | **70.8%** | 100.01 | 120.88 |
|
|
83
|
+
| viral_spike / compute_heavy_rec | gds | 4850.61 | **4802.40** | **1.0%** | 60.2% | **72.6%** | 98.91 | 124.59 |
|
|
84
|
+
| viral_spike / read_heavy_api | gds | 1877.68 | **1755.22** | **6.5%** | 47.1% | **56.0%** | 98.91 | 120.20 |
|
|
85
|
+
|
|
86
|
+
### Read the last two columns before you believe the first ones
|
|
87
|
+
|
|
88
|
+
The cost model used to bill compute and memory and **nothing at all for serving a customer
|
|
89
|
+
stale data**. Nobody decided that; it is what you get by leaving wrong answers unpriced,
|
|
90
|
+
and it meant a cache-freshness system was scored on everything except the thing it does —
|
|
91
|
+
`gds` won on cost by serving 75,526 wrong answers an hour for free.
|
|
92
|
+
|
|
93
|
+
D31 fixes it on a principle rather than a knob: **a stale hit is not a hit, it is a miss
|
|
94
|
+
you failed to notice**, so it is charged the `regen_usd` of the very object it displaced.
|
|
95
|
+
The price is read per object out of the catalog, billed by the simulator so every policy
|
|
96
|
+
pays the same for the same event, and measured at $0.0077–$0.186 per stale serve. That is
|
|
97
|
+
7–170x the ~0.11 cent break-even, which is why the ranking flips.
|
|
98
|
+
|
|
99
|
+
**On the old model `gds` is still cheaper** — that is the `pre-D31` pair above, kept in
|
|
100
|
+
`summary.csv` as `total_usd_compute_only` in every run, and
|
|
101
|
+
`python -m benchmarks.run_all --stale-mult 0` reproduces it exactly (a test asserts it).
|
|
102
|
+
The honest caveat: at the shipped price the staleness bill is ~10x compute plus memory, so
|
|
103
|
+
the ranking now rests mostly on this one priced assumption. The full flat-price sweep,
|
|
104
|
+
including where AdVal loses, is in `results/dominance.md`.
|
|
105
|
+
|
|
106
|
+
Savings are 6.5–11.8% on `read_heavy_api` and only 0.9–1.5% on `compute_heavy_rec`, where
|
|
107
|
+
slow-changing `ml_result` objects dominate the staleness bill for every arm alike.
|
|
108
|
+
|
|
109
|
+
### The four answers that assume nothing
|
|
110
|
+
|
|
111
|
+
Separately from the cost model, `results/dominance.md`:
|
|
112
|
+
|
|
113
|
+
1. **Nothing in the study is both cheaper and fresher than AdVal** — 16/16 arm-cells, on
|
|
114
|
+
either cost model.
|
|
115
|
+
2. **Capacity cannot buy freshness.** `gds` serves 75,526 stale responses at 100 MB and
|
|
116
|
+
75,526 at 500 MB while its hit rate climbs 81.6% → 93.9%. AdVal at 100 MB is cheaper
|
|
117
|
+
*and* 12.2 pp fresher than `gds` at 500 MB.
|
|
118
|
+
3. **The flat-price sweep**: above ~0.11 cents per stale response AdVal is cheapest in all
|
|
119
|
+
8 cells, to 4.6–45 cents where LRU takes over. Both bounds published.
|
|
120
|
+
4. **At 50–60 MB, where memory is actually scarce**, AdVal is cheaper *and* fresher at
|
|
121
|
+
equal capacity even with staleness free.
|
|
122
|
+
|
|
123
|
+
Against LRU and LFU, AdVal is 92–99% cheaper on either model. It still loses on tail
|
|
124
|
+
latency — 158 ms p95 against 55 ms — because refreshing on read is not free. Full tables,
|
|
125
|
+
timelines, the ablation ladder and an auto-generated "where this result is weakest"
|
|
126
|
+
section: **`results/report.md`**.
|
|
127
|
+
|
|
128
|
+
## The score
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
score(i) = hotness(i) × pain(i) × freshness(i) / size_units(i)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **hotness** — LRFU decayed counter `c ← 1 + c·e^(−λΔt)`, λ = 0.001 (half-life ≈ 693 s);
|
|
135
|
+
recency at short lags, frequency at long ones, one number.
|
|
136
|
+
- **pain** — regeneration latency and regeneration dollars, each normalised against the
|
|
137
|
+
catalog's p99 so neither drowns the other. ~200× spread across object classes.
|
|
138
|
+
- **freshness** — `exp(−(now − last_sync) / change_interval)`, which for a Poisson
|
|
139
|
+
mutation process is exactly *P(the cached copy is still valid)*. Multiplying a value
|
|
140
|
+
by a probability gives an expected value, which is the right thing to rank by.
|
|
141
|
+
Exactly `1.0` for immutable objects and anything slower than 1800 s.
|
|
142
|
+
- **size_units** — `log1p(bytes / 1 KB)`, so 5 MB is penalised ~12× rather than ~5000×.
|
|
143
|
+
Large objects must justify themselves; they are not banned.
|
|
144
|
+
|
|
145
|
+
Two runtime loops keep it from being a set of hardcoded constants:
|
|
146
|
+
|
|
147
|
+
- **The cache A/B tests its own brain.** Every 5000 requests it shadow-replays the
|
|
148
|
+
window it just served through five candidate weight vectors, seeded from a snapshot
|
|
149
|
+
of its real state, and adopts whichever delivered the most value. `steady` picks
|
|
150
|
+
`freq_heavy`; `chaos` picks `fresh_heavy`. Nobody told it which signal matters.
|
|
151
|
+
- **Capacity is a dollar decision that checks itself.** A shadow zone measures the
|
|
152
|
+
marginal dollars the next slice of memory would have saved; the controller acts,
|
|
153
|
+
waits five windows, and reverts the change if realised spend per request did not fall
|
|
154
|
+
by more than the rent it took on. From a badly-sized 30 MB cache it cuts cost 72–84%.
|
|
155
|
+
|
|
156
|
+
`ARCHITECTURE.md` has the decision-engine diagram and the full justification.
|
|
157
|
+
|
|
158
|
+
## Repo layout
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
models.py THE CONTRACT: Request, CatalogEntry, object classes, ops, constants
|
|
162
|
+
proxy/ FastAPI reverse proxy sidecar · live cache engine · traffic generator
|
|
163
|
+
database/ Neon PostgreSQL client · schema · real-world dataset ingestion pipeline
|
|
164
|
+
cache/ base (interface, sampling, event sink) · lru · lfu · gdsf · adaptive · controller
|
|
165
|
+
generator/ catalog · popularity modes · scenario engine · trace writer · validator
|
|
166
|
+
sim/ simulator (replay + window aggregation) · metrics (the only cost model)
|
|
167
|
+
benchmarks/ run_all (the 80-cell matrix) · report (tables, charts, honest caveats)
|
|
168
|
+
dashboard/app.py Streamlit control center: batch replay scrubber & live dynamic proxy mode
|
|
169
|
+
web_adval/ React/Vite standalone web control UI mounted on proxy port 8000
|
|
170
|
+
traces/ trace samples & Postgres query trace generator
|
|
171
|
+
tests/ unit & integration tests: core, live cache, and proxy API
|
|
172
|
+
results/ event logs, window CSVs, summary.csv, report.md, charts/
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Verify it yourself
|
|
176
|
+
|
|
177
|
+
```powershell
|
|
178
|
+
python -m generator.validate --minutes 60 --seed 42 # 9 workload checks, ~6 s
|
|
179
|
+
python -m pytest -q # 63 tests, ~8 s
|
|
180
|
+
python -m benchmarks.run_all --seed 42 # 80 cells, ~4 min
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`generator/validate.py` is the workload referee: it asserts the arrival-rate curve
|
|
184
|
+
matches the scenario spec within 4σ on all four scenarios, that the top-10 popular keys
|
|
185
|
+
genuinely rotate during `gradual_shift` (0 shared keys), that the reuse working set is
|
|
186
|
+
**4.05× cache capacity** (a cache that never fills has no story), that `static_asset` is
|
|
187
|
+
99.4% of bytes but only 30.1% of requests, and that seed 42 reproduces byte-identical
|
|
188
|
+
files. A cache benchmark is only as honest as its workload.
|
|
189
|
+
|
|
190
|
+
## Reading the docs
|
|
191
|
+
|
|
192
|
+
| file | what is in it |
|
|
193
|
+
|---|---|
|
|
194
|
+
| `ARCHITECTURE.md` | decision-engine diagram, every term of the score justified, the two adaptive loops |
|
|
195
|
+
| `results/report.md` | generated results: headline tables, charts, ablation, capacity fairness, weaknesses |
|
|
196
|
+
| `DECISIONS.md` | **22 numbered assumptions and corrections**, including two places the problem statement's own numbers disagree, three bugs that only a benchmark could have caught, and one metric that was measuring the wrong thing |
|
|
197
|
+
| `DEMO.md` | the scripted 5-minute walkthrough, with fallbacks |
|
|
198
|
+
| `FORMATS.md` | every on-disk schema: catalog, trace, change stream, event log, window CSV |
|
|
199
|
+
| `PROGRESS.md` | phase-by-phase build log with the ACCEPT evidence for each |
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# AdVal-Cache
|
|
2
|
+
|
|
3
|
+
**A cache that knows the difference between a $0.31 recommendation vector and a stock
|
|
4
|
+
ticker that is worthless two seconds after you store it.**
|
|
5
|
+
|
|
6
|
+
LRU asks *when did you last touch this*. LFU asks *how often*. Neither asks the only
|
|
7
|
+
question a bill cares about: **what does losing this actually cost me?** In a real
|
|
8
|
+
service that answer spans four orders of magnitude — a free 4 MB product image against
|
|
9
|
+
a ten-second, fifty-cent ML inference. AdVal-Cache computes a per-object **value
|
|
10
|
+
score** from access pattern, resource footprint, regeneration cost *and* data
|
|
11
|
+
volatility, then decides retain / evict / **refresh** / **decline** — and separately
|
|
12
|
+
decides, in dollars, when buying more cache capacity is worth the rent.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
python -m venv venv; venv\Scripts\activate; pip install -r requirements.txt
|
|
18
|
+
python -m benchmarks.run_all --seed 42 # 80 cells in ~4 min -> results/report.md
|
|
19
|
+
streamlit run dashboard/app.py # playback dashboard
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Everything is a deterministic simulation: no network, no Redis, no Docker, CPU only.
|
|
23
|
+
Same seed ⇒ byte-identical traces, results and charts.
|
|
24
|
+
|
|
25
|
+
## Results
|
|
26
|
+
|
|
27
|
+
60 simulated minutes per scenario, seed 42. The problem statement's baselines are LRU, LFU
|
|
28
|
+
and GDS; exact GDSF ships alongside because it is stronger than the brief requires (D29).
|
|
29
|
+
|
|
30
|
+
**AdVal is cheaper *and* fresher than the best baseline in 8 of 8 cells.**
|
|
31
|
+
|
|
32
|
+
| scenario / profile | best baseline | baseline $ | **AdVal $** | saving | baseline fresh | **AdVal fresh** | pre-D31 base $ | pre-D31 AdVal $ |
|
|
33
|
+
|---|---|---|---|---|---|---|---|---|
|
|
34
|
+
| chaos / compute_heavy_rec | gds | 2854.48 | **2816.52** | **1.3%** | 66.2% | **78.5%** | 98.91 | 124.13 |
|
|
35
|
+
| chaos / read_heavy_api | gds | 1086.05 | **986.37** | **9.2%** | 64.2% | **74.2%** | 98.91 | 117.41 |
|
|
36
|
+
| gradual_shift / compute_heavy_rec | gds | 3034.96 | **2989.81** | **1.5%** | 67.1% | **80.2%** | 98.91 | 124.51 |
|
|
37
|
+
| gradual_shift / read_heavy_api | gds | 1046.89 | **923.08** | **11.8%** | 64.7% | **77.7%** | 98.91 | 123.12 |
|
|
38
|
+
| steady / compute_heavy_rec | gds | 4275.14 | **4235.45** | **0.9%** | 59.7% | **73.1%** | 98.91 | 122.15 |
|
|
39
|
+
| steady / read_heavy_api | gdsf_exact | 1282.12 | **1142.86** | **10.9%** | 57.9% | **70.8%** | 100.01 | 120.88 |
|
|
40
|
+
| viral_spike / compute_heavy_rec | gds | 4850.61 | **4802.40** | **1.0%** | 60.2% | **72.6%** | 98.91 | 124.59 |
|
|
41
|
+
| viral_spike / read_heavy_api | gds | 1877.68 | **1755.22** | **6.5%** | 47.1% | **56.0%** | 98.91 | 120.20 |
|
|
42
|
+
|
|
43
|
+
### Read the last two columns before you believe the first ones
|
|
44
|
+
|
|
45
|
+
The cost model used to bill compute and memory and **nothing at all for serving a customer
|
|
46
|
+
stale data**. Nobody decided that; it is what you get by leaving wrong answers unpriced,
|
|
47
|
+
and it meant a cache-freshness system was scored on everything except the thing it does —
|
|
48
|
+
`gds` won on cost by serving 75,526 wrong answers an hour for free.
|
|
49
|
+
|
|
50
|
+
D31 fixes it on a principle rather than a knob: **a stale hit is not a hit, it is a miss
|
|
51
|
+
you failed to notice**, so it is charged the `regen_usd` of the very object it displaced.
|
|
52
|
+
The price is read per object out of the catalog, billed by the simulator so every policy
|
|
53
|
+
pays the same for the same event, and measured at $0.0077–$0.186 per stale serve. That is
|
|
54
|
+
7–170x the ~0.11 cent break-even, which is why the ranking flips.
|
|
55
|
+
|
|
56
|
+
**On the old model `gds` is still cheaper** — that is the `pre-D31` pair above, kept in
|
|
57
|
+
`summary.csv` as `total_usd_compute_only` in every run, and
|
|
58
|
+
`python -m benchmarks.run_all --stale-mult 0` reproduces it exactly (a test asserts it).
|
|
59
|
+
The honest caveat: at the shipped price the staleness bill is ~10x compute plus memory, so
|
|
60
|
+
the ranking now rests mostly on this one priced assumption. The full flat-price sweep,
|
|
61
|
+
including where AdVal loses, is in `results/dominance.md`.
|
|
62
|
+
|
|
63
|
+
Savings are 6.5–11.8% on `read_heavy_api` and only 0.9–1.5% on `compute_heavy_rec`, where
|
|
64
|
+
slow-changing `ml_result` objects dominate the staleness bill for every arm alike.
|
|
65
|
+
|
|
66
|
+
### The four answers that assume nothing
|
|
67
|
+
|
|
68
|
+
Separately from the cost model, `results/dominance.md`:
|
|
69
|
+
|
|
70
|
+
1. **Nothing in the study is both cheaper and fresher than AdVal** — 16/16 arm-cells, on
|
|
71
|
+
either cost model.
|
|
72
|
+
2. **Capacity cannot buy freshness.** `gds` serves 75,526 stale responses at 100 MB and
|
|
73
|
+
75,526 at 500 MB while its hit rate climbs 81.6% → 93.9%. AdVal at 100 MB is cheaper
|
|
74
|
+
*and* 12.2 pp fresher than `gds` at 500 MB.
|
|
75
|
+
3. **The flat-price sweep**: above ~0.11 cents per stale response AdVal is cheapest in all
|
|
76
|
+
8 cells, to 4.6–45 cents where LRU takes over. Both bounds published.
|
|
77
|
+
4. **At 50–60 MB, where memory is actually scarce**, AdVal is cheaper *and* fresher at
|
|
78
|
+
equal capacity even with staleness free.
|
|
79
|
+
|
|
80
|
+
Against LRU and LFU, AdVal is 92–99% cheaper on either model. It still loses on tail
|
|
81
|
+
latency — 158 ms p95 against 55 ms — because refreshing on read is not free. Full tables,
|
|
82
|
+
timelines, the ablation ladder and an auto-generated "where this result is weakest"
|
|
83
|
+
section: **`results/report.md`**.
|
|
84
|
+
|
|
85
|
+
## The score
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
score(i) = hotness(i) × pain(i) × freshness(i) / size_units(i)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **hotness** — LRFU decayed counter `c ← 1 + c·e^(−λΔt)`, λ = 0.001 (half-life ≈ 693 s);
|
|
92
|
+
recency at short lags, frequency at long ones, one number.
|
|
93
|
+
- **pain** — regeneration latency and regeneration dollars, each normalised against the
|
|
94
|
+
catalog's p99 so neither drowns the other. ~200× spread across object classes.
|
|
95
|
+
- **freshness** — `exp(−(now − last_sync) / change_interval)`, which for a Poisson
|
|
96
|
+
mutation process is exactly *P(the cached copy is still valid)*. Multiplying a value
|
|
97
|
+
by a probability gives an expected value, which is the right thing to rank by.
|
|
98
|
+
Exactly `1.0` for immutable objects and anything slower than 1800 s.
|
|
99
|
+
- **size_units** — `log1p(bytes / 1 KB)`, so 5 MB is penalised ~12× rather than ~5000×.
|
|
100
|
+
Large objects must justify themselves; they are not banned.
|
|
101
|
+
|
|
102
|
+
Two runtime loops keep it from being a set of hardcoded constants:
|
|
103
|
+
|
|
104
|
+
- **The cache A/B tests its own brain.** Every 5000 requests it shadow-replays the
|
|
105
|
+
window it just served through five candidate weight vectors, seeded from a snapshot
|
|
106
|
+
of its real state, and adopts whichever delivered the most value. `steady` picks
|
|
107
|
+
`freq_heavy`; `chaos` picks `fresh_heavy`. Nobody told it which signal matters.
|
|
108
|
+
- **Capacity is a dollar decision that checks itself.** A shadow zone measures the
|
|
109
|
+
marginal dollars the next slice of memory would have saved; the controller acts,
|
|
110
|
+
waits five windows, and reverts the change if realised spend per request did not fall
|
|
111
|
+
by more than the rent it took on. From a badly-sized 30 MB cache it cuts cost 72–84%.
|
|
112
|
+
|
|
113
|
+
`ARCHITECTURE.md` has the decision-engine diagram and the full justification.
|
|
114
|
+
|
|
115
|
+
## Repo layout
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
models.py THE CONTRACT: Request, CatalogEntry, object classes, ops, constants
|
|
119
|
+
proxy/ FastAPI reverse proxy sidecar · live cache engine · traffic generator
|
|
120
|
+
database/ Neon PostgreSQL client · schema · real-world dataset ingestion pipeline
|
|
121
|
+
cache/ base (interface, sampling, event sink) · lru · lfu · gdsf · adaptive · controller
|
|
122
|
+
generator/ catalog · popularity modes · scenario engine · trace writer · validator
|
|
123
|
+
sim/ simulator (replay + window aggregation) · metrics (the only cost model)
|
|
124
|
+
benchmarks/ run_all (the 80-cell matrix) · report (tables, charts, honest caveats)
|
|
125
|
+
dashboard/app.py Streamlit control center: batch replay scrubber & live dynamic proxy mode
|
|
126
|
+
web_adval/ React/Vite standalone web control UI mounted on proxy port 8000
|
|
127
|
+
traces/ trace samples & Postgres query trace generator
|
|
128
|
+
tests/ unit & integration tests: core, live cache, and proxy API
|
|
129
|
+
results/ event logs, window CSVs, summary.csv, report.md, charts/
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Verify it yourself
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
python -m generator.validate --minutes 60 --seed 42 # 9 workload checks, ~6 s
|
|
136
|
+
python -m pytest -q # 63 tests, ~8 s
|
|
137
|
+
python -m benchmarks.run_all --seed 42 # 80 cells, ~4 min
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`generator/validate.py` is the workload referee: it asserts the arrival-rate curve
|
|
141
|
+
matches the scenario spec within 4σ on all four scenarios, that the top-10 popular keys
|
|
142
|
+
genuinely rotate during `gradual_shift` (0 shared keys), that the reuse working set is
|
|
143
|
+
**4.05× cache capacity** (a cache that never fills has no story), that `static_asset` is
|
|
144
|
+
99.4% of bytes but only 30.1% of requests, and that seed 42 reproduces byte-identical
|
|
145
|
+
files. A cache benchmark is only as honest as its workload.
|
|
146
|
+
|
|
147
|
+
## Reading the docs
|
|
148
|
+
|
|
149
|
+
| file | what is in it |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `ARCHITECTURE.md` | decision-engine diagram, every term of the score justified, the two adaptive loops |
|
|
152
|
+
| `results/report.md` | generated results: headline tables, charts, ablation, capacity fairness, weaknesses |
|
|
153
|
+
| `DECISIONS.md` | **22 numbered assumptions and corrections**, including two places the problem statement's own numbers disagree, three bugs that only a benchmark could have caught, and one metric that was measuring the wrong thing |
|
|
154
|
+
| `DEMO.md` | the scripted 5-minute walkthrough, with fallbacks |
|
|
155
|
+
| `FORMATS.md` | every on-disk schema: catalog, trace, change stream, event log, window CSV |
|
|
156
|
+
| `PROGRESS.md` | phase-by-phase build log with the ACCEPT evidence for each |
|