Initial benchmark suite: 8 graded models + cyberpunk dashboard generator

- prompts/: LFU cache exam + 5-pillar grading rubric
- outputs/: 8 model .py outputs (local + cloud baseline)
- data/benchmark_history.json: graded results (scores, metrics, bugs, patches)
- generate_dashboard.py: builds dashboard.html + pages/*.html from JSON
- Dockerfile + DEPLOY.md: Gitea→Coolify deploy (build-step, nginx static)
- .gitignore: generated HTML excluded (built on deploy)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-28 14:00:40 -07:00
co-authored by Claude
commit f0281f2878
17 changed files with 4363 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
# --- Generated / build artifacts ---
# Dashboard HTML is generated by generate_dashboard.py from data/benchmark_history.json.
# Keep them OUT of git so the repo only holds source — the site regenerates on deploy.
# (If you'd rather commit the built HTML instead, comment out these lines.)
/dashboard.html
/pages/
# /data/benchmark_history.json # <-- uncomment to keep history local-only too
# --- Scratch / previews / tooling ---
*.png
/playwright-mcp/
/.playwright-mcp/
__pycache__/
*.pyc
.DS_Store
# --- Local env ---
.env
.env.*
*.local
+72
View File
@@ -0,0 +1,72 @@
# CLAUDE.md — Local LLM Benchmark Testing Project
## What this project is
A personal test harness for evaluating **local LLM models** running in **LM Studio** on an **Apple M3 Max MacBook Pro (48 GB unified memory)**. A fixed coding prompt (LFU cache) is fed to each model, the model's Python output is graded against a strict rubric, and the results are accumulated into a persistent JSON store and rendered into a standalone HTML dashboard.
This is **not** a git repo and is **not** an application with a build/run cycle. It is a folder of prompt files, model outputs, and generated artifacts.
## Folder layout
```
prompts/
lfu_cache_prompt.txt # The fixed prompt given to every model (the "exam question")
grading.txt # The grader's instructions / rubric (read at session start)
outputs/ # Raw model outputs (the .py files each model produced)
<model-name>-<quant>.py
data/
benchmark_history.json # Persistent results store (created on first grading run)
dashboard.html # Generated standalone dashboard (dark-mode, Chart.js via CDN)
*.py # Loose model outputs in root (e.g. deepseekv4flash.py) — legacy/unsorted
```
## The workflow (what to do when the user submits a model's output)
Follow the 4 steps in `prompts/grading.txt`:
1. **Audit** the `.py` file against the 5 pillars (each 020 → 0100 total):
- Complexity violations (true O(1) — no heaps, `sorted()`, linear scans)
- Async race conditions / deadlocks (locks held across `sleep`/IO, unsynced shared state)
- Transactional isolation leaks (read-your-own-writes, commit/rollback correctness)
- Memory leaks & edge cases (DLL unlink, `min_freq` updates, `time.monotonic()`, `__slots__`)
- Test coverage integrity (real edge cases vs trivial always-pass asserts)
2. **Extract metrics**: model name, quant, tok/sec, verdict, archetype/best-for, critical bugs, patch code.
3. **Update `data/benchmark_history.json`** — append/update the model's entry (schema in `grading.txt`). Create the file if missing.
4. **Regenerate `dashboard.html`** — full overwrite, standalone single file, Chart.js (bar + radar), leaderboard table, collapsible audit cards, color-coded verdict badges (green 90+, blue/yellow 7589, red <75).
The exam prompt (`lfu_cache_prompt.txt`) requires a **pure-stdlib Python 3.11+ async LFU cache** with frequency-bucket O(1), dual-layer TTL eviction, ACID-like transactions, and an `async def main()` test suite. Use it as the spec when judging correctness.
## Model output — naming convention
Model output filenames encode the **model + quant/format** so a model is identifiable from the filename alone. The convention the user uses:
`<model-family><version>-<param-size>-<variant>-<quant-or-format>.py`
Lowercase, hyphen-separated, no spaces. Examples seen so far:
| Filename | Reads as |
|---|---|
| `qwen3.6-35b-a3b-6bit-mlx.py` | Qwen 3.6, 35B-A3B (MoE), 6-bit, MLX format |
| `qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive-gguf.py` | Qwen 3.6 35B-A3B, uncensored fine-tune, GGUF |
| `gemma-4-31b-qat-gguf.py` | Gemma 4 31B, QAT, GGUF |
| `gemma-4-12b-coder-fable5-composer2.5-v1-uncensored-heretic-mxfp8-mlx.py` | merged/model-card-style name, MLX |
| `kat-coder-v2.5-dev-xl-mlx.py` | KAT Coder v2.5 Dev XL, MLX |
Tokens: `mlx` = Apple MLX format; `gguf` = llama.cpp GGUF; quant suffixes like `q6k`, `6bit`, `4bit`, `qat`, `mxfp8` go in the name. When in doubt about what a filename denotes, **parse it loosely into model + quant** rather than guessing a wrong label — the grading step pulls model/quant from "file name or user input," so either source is valid.
### When the user pastes raw output instead of giving a file
If the user pastes a model's output text directly (no file), **save it as a `.py` file in `outputs/`** using the naming convention above before grading. Ask the user for the model name + quant only if it cannot be reasonably inferred from context. Match the existing lowercase-hyphen style.
## Hardware / runtime context
- **Machine:** Apple M3 Max, 48 GB unified memory. Local inference only.
- **Inference server:** LM Studio (OpenAI-compatible local endpoint).
- Quants/formats that fit 48 GB comfortably: ~35B dense at 4-bit/6-bit, larger MoE models (only active params loaded). MLX is preferred for Apple Silicon; GGUF via llama.cpp also works.
## Notes / gotchas
- `data/` starts **empty**`benchmark_history.json` does not exist until the first grading run creates it. Don't assume it's there; read defensively, create on write.
- `dashboard.html` is **regenerated** (overwritten), not appended to. The source of truth is the JSON file.
- No build step, no tests to run, no dependencies to install — pure stdlib Python outputs + a static HTML file.
- Treat the loose `.py` files in the project root (like `deepseekv4flash.py`) as unsorted outputs that belong in `outputs/` per the convention. Don't move them unless asked.
+76
View File
@@ -0,0 +1,76 @@
# Deploying to Coolify (via Gitea)
Repo: `ssh://git@git.itsaygea.com:2222/admin/modelTesting.git`
The generated `dashboard.html` and `pages/` are **gitignored** — they're built from
`data/benchmark_history.json` by `generate_dashboard.py`. So Coolify must run that script
during build, then serve the result.
---
## Option A — Build step, regenerate on every push *(recommended)*
The site always reflects the latest `benchmark_history.json`. Push a new grade → re-deploy → site updates.
### Coolify service setup
1. **New Resource** → choose your Gitea project `admin/modelTesting`.
2. Choose **"Dockerfile"** as the build pack (we ship a tiny one).
Add this `Dockerfile` to the repo (already included):
```dockerfile
# build stage: regenerate the site from source
FROM python:3.12-slim AS build
WORKDIR /app
COPY . .
RUN python3 generate_dashboard.py
# serve stage: static files via nginx
FROM nginx:alpine
COPY --from=build /app/dashboard.html /usr/share/nginx/html/dashboard.html
COPY --from=build /app/pages /usr/share/nginx/html/pages
# root path redirects to the dashboard
RUN printf 'location = / { return 302 /dashboard.html; }\n' > /etc/nginx/conf.d/default.conf
EXPOSE 80
```
3. **Port:** set the published port to **80** (Coolify maps it to your domain).
4. **Domain:** attach your domain. Coolify provisions the HTTPS cert automatically.
5. **Deploy.** Push to `main` → Coolify rebuilds → site updates.
### Why a Dockerfile
Coolify's "Nixpacks / static" presets can run a build command, but the Dockerfile path is
the most predictable: explicit Python build step + nginx static serving, no surprises.
The image is ~30 MB.
---
## Option B — Pure static, commit the built HTML *(simplest, no build)*
If you'd rather not build on the server:
1. Comment out `/dashboard.html` and `/pages/` in `.gitignore`.
2. Run `python3 generate_dashboard.py` locally, then `git add` + commit the HTML.
3. In Coolify: **New Resource** → the repo → build pack **"Static Site"** (or Nixpacks with
`OUTPUT_DIRECTORY=/`), publish dir `/`, port 80.
Downside: you must rebuild + commit locally every time you add a model. Use Option A unless
your Coolify can't run Docker.
---
## Verifying after deploy
Open the domain → you should land on the dashboard. If you see a blank page, check the
Coolify build logs for `generate_dashboard.py` errors (usually a malformed
`data/benchmark_history.json`).
## Local preview before pushing
```bash
python3 generate_dashboard.py
python3 -m http.server 8000
# open http://localhost:8000/dashboard.html
```
+21
View File
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1
# Build stage: regenerate the static site from source data.
FROM python:3.12-slim AS build
WORKDIR /app
COPY . .
RUN python3 generate_dashboard.py
# Serve stage: nginx serves the generated static files.
FROM nginx:alpine
# Replace the default nginx server block so root redirects to the dashboard
RUN printf '%s\n' \
'server {' \
' listen 80;' \
' server_name _;' \
' root /usr/share/nginx/html;' \
' index dashboard.html;' \
' location = / { return 302 /dashboard.html; }' \
'}' > /etc/nginx/conf.d/default.conf
COPY --from=build /app/dashboard.html /usr/share/nginx/html/dashboard.html
COPY --from=build /app/pages /usr/share/nginx/html/pages
EXPOSE 80
+53
View File
@@ -0,0 +1,53 @@
# 🧪 Local LLM Benchmark Suite
Grades local LLM models (run via **LM Studio** on an Apple M3 Max / 48 GB) on a strict
systems-coding prompt — a pure-stdlib Python **concurrent async LFU cache with TTL
eviction and ACID transactions** — and renders the results into a cyberpunk-terminal
dashboard.
## What's in here
```
prompts/
lfu_cache_prompt.txt # the exam prompt every model gets
grading.txt # the grader's rubric (5 pillars × 20 pts = 100)
outputs/ # raw model .py outputs (named <model>-<quant>.py)
data/
benchmark_history.json # persistent results store (source of truth)
generate_dashboard.py # reads the JSON → builds dashboard.html + pages/*.html
dashboard.html # generated — main leaderboard + charts (gitignored)
pages/ # generated — per-model detail pages (gitignored)
```
## Workflow
1. Feed `prompts/lfu_cache_prompt.txt` to a model in LM Studio.
2. Save its output to `outputs/<model>-<quant>.py`.
3. Grade it (audit the 5 pillars, capture tok/sec + tokens + TTFT), and append its
entry to `data/benchmark_history.json`. See `prompts/grading.txt` for the rubric.
4. Regenerate the site:
```bash
python3 generate_dashboard.py
```
This (re)writes `dashboard.html` and every `pages/<model>.html`.
## Viewing locally
```bash
python3 -m http.server 8000
# open http://localhost:8000/dashboard.html
```
## Deploying (Gitea + Coolify)
The repo holds **source only** (`outputs/`, `data/`, `generate_dashboard.py`, `prompts/`).
The generated `dashboard.html` and `pages/` are gitignored — Coolify runs
`python3 generate_dashboard.py` as a build step, then serves the static files.
See `DEPLOY.md` for the exact Coolify service config.
## Hardware
Apple M3 Max, 48 GB unified memory. Local inference via LM Studio.
A cloud model (DeepSeek V4 Flash) is included as a quality baseline.
+269
View File
@@ -0,0 +1,269 @@
{
"meta": {
"project": "Local LLM Benchmark Suite — LFU Cache & ACID Audit",
"machine": "Apple M3 Max, 48GB unified memory, LM Studio",
"exam_prompt": "prompts/lfu_cache_prompt.txt",
"grading_rubric": "prompts/grading.txt",
"pillars": ["complexity", "concurrency", "isolation", "memory_edge_cases", "test_integrity"],
"max_per_pillar": 20,
"schema_version": 1
},
"models": [
{
"id": "qwen3.6-35b-a3b-6bit-mlx",
"timestamp": "2026-07-28T13:00:00Z",
"model_name": "Qwen 3.6 35B-A3B",
"quant": "6-bit MLX",
"format": "mlx",
"tok_sec": 68.86,
"total_tokens": 14883,
"ttft_sec": 0.94,
"filename": "outputs/qwen3.6-35b-a3b-6bit-mlx.py",
"tests_pass": true,
"total_score": 82,
"breakdown": {
"complexity": 16,
"concurrency": 16,
"isolation": 17,
"memory_edge_cases": 16,
"test_integrity": 17
},
"verdict": "Minor Logic Flaws",
"best_for": "Solid daily-driver scaffolding for ACID/async patterns — produces runnable, well-structured code, but needs a human pass for __slots__, monotonic clocks, and lock granularity before production.",
"critical_bugs": [
"No __slots__ declared on Node/Transaction/LFUCache — rubric explicitly required it for memory efficiency.",
"Uses time.time() (system clock) throughout instead of time.monotonic() — NTP adjustments can cause premature/incorrect TTL eviction.",
"_cleanup_freq_lists() performs a hidden O(F) scan (iterates all freq tiers + min()), called after every eviction, background batch, AND inside commit() — violates the strict O(1) requirement.",
"Transaction commit holds the single cache lock across all write/delete/bump loops + cleanup — coarse-grained, blocks all readers for the whole commit window; no fine-grained locking.",
"Lost-update risk: commit applies tx-local writes without any MVCC/version check, so a key modified by the background evictor or another committer between tx.get() and commit() is overwritten blindly.",
"Tests dodge hard cases: 50-task stress uses unique keys with capacity 100, so no eviction-under-contention ever happens; no test for rollback-after-partial-application or mid-commit read isolation."
],
"patch_code": "# FIX 1: Add __slots__ for memory efficiency\n@dataclass\nclass Node:\n __slots__ = ('key', 'value', 'freq', 'expires_at', 'prev', 'next')\n key: Any\n value: Any\n freq: int\n expires_at: Optional[float]\n prev: Optional['Node'] = None\n next: Optional['Node'] = None\n\n# FIX 2: Use monotonic clock everywhere (get/put/_add_node/commit)\n# time.time() -> time.monotonic()\n# e.g.\nexpires_at = time.monotonic() + ttl_seconds if ttl_seconds else None\n\n# FIX 3: Make _cleanup_freq_lists O(1) — bump min_freq incrementally\n# instead of recomputing min() across all tiers:\n# In _update_freq, when emptying the min_freq bucket, only bump min_freq\n# if you're evicting from it; otherwise leave it. Delete the global\n# min(self.freq_map.keys()) scan. For background sweeps, prune empty\n# buckets lazily on next _evict() rather than scanning proactively.\n\n# FIX 4: Shrink commit critical section — apply writes into a staging\n# structure under the lock, then release; or use per-bucket locks so\n# readers on unrelated keys aren't blocked.\n\n# FIX 5: Add MVCC version to Node; in commit, raise/abort if the\n# stored version != the version seen at tx.get() time (lost-update detect)."
},
{
"id": "qwen3.6-35b-a3b-4bit-mlx",
"timestamp": "2026-07-28T13:05:00Z",
"model_name": "Qwen 3.6 35B-A3B",
"quant": "4-bit MLX",
"format": "mlx",
"tok_sec": 83.31,
"total_tokens": 13278,
"ttft_sec": 0.73,
"filename": "outputs/qwen3.6-35b-a3b-4bit-mlx.py",
"tests_pass": false,
"total_score": 57,
"breakdown": {
"complexity": 15,
"concurrency": 15,
"isolation": 13,
"memory_edge_cases": 10,
"test_integrity": 4
},
"verdict": "Critical Bugs",
"best_for": "Not recommended for systems code as-is. The 4-bit quant degrades logic sharply vs the 6-bit sibling (82->57) — usable only for boilerplate/scaffolding drafts that a human will heavily rewrite.",
"critical_bugs": [
"FATAL: _evict() double-removes nodes — pop() already calls remove() (nulling node.prev/next), then _remove_node() calls remove() AGAIN -> AttributeError: 'NoneType' on first eviction. The cache cannot survive reaching capacity.",
"Test suite never executes: test_lfu_eviction crashes at the first eviction, so the 'All tests passed' message is unreachable and assertions are effectively unverified.",
"Background _eviction_loop materializes list(self.key_to_node.keys())[:50] every sweep — an O(N) linear scan, forbidden by the strict O(1) requirement.",
"_FreqList.pop() has no empty-guard — calling pop() on an empty list dereferences self.head.next (the dummy tail) and corrupts the DLL.",
"Transaction _apply_put applies the buffered value into the EXACT original_node captured at tx.put() time; if the global cache evicted/relocated that node between put and commit, you mutate a stale/dangling node (no MVCC/version check).",
"Commit is not atomic across exceptions: a crash mid-_apply loop leaves half-applied global state with no rollback.",
"Uses time.time() (system clock) throughout instead of time.monotonic() — NTP jumps corrupt TTL eviction."
],
"patch_code": "# FIX 1 (the crash): _evict double-removes. pop() already unlinks,\n# so do NOT call _remove_node on a popped node. Either:\n# (a) pop and then only delete the key_map entry + min_freq bookkeeping:\ndef _evict(self):\n if not self.freq_to_list:\n return\n evict_list = self.freq_to_list[self.min_freq]\n if evict_list.size == 0: # guard against empty\n del self.freq_to_list[self.min_freq]\n return\n node = evict_list.pop() # pop() unlinks + nulls prev/next\n del self.key_to_node[node.key] # DON'T call _remove_node again\n if self.freq_to_list[self.min_freq].size == 0:\n del self.freq_to_list[self.min_freq]\n self.min_freq += 1\n\n# FIX 2: _FreqList.pop empty-guard\ndef pop(self) -> _Node:\n if self.size == 0:\n raise IndexError('pop from empty _FreqList')\n node = self.head.next\n self.remove(node)\n return node\n\n# FIX 3: kill the O(N) scan in background sweep — maintain a separate\n# set of keys that have a TTL, and iterate that set in batches:\nasync with self.lock:\n batch = list(self._ttl_keys)[:50]\n for k in batch:\n node = self.key_to_node.get(k)\n if node and 0 < node.expires_at <= time.monotonic():\n self._remove_node(node)\n\n# FIX 4: time.time() -> time.monotonic() everywhere.\n# FIX 5: add node.version; in tx._apply_put, abort/refresh if\n# cache.key_to_node[key] is a different node than original_node."
},
{
"id": "qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive-gguf",
"timestamp": "2026-07-28T13:10:00Z",
"model_name": "Qwen 3.6 35B-A3B (uncensored hauhaucs aggressive)",
"quant": "GGUF",
"format": "gguf",
"tok_sec": 62.54,
"total_tokens": 13897,
"ttft_sec": 1.09,
"filename": "outputs/qwen3.6-35b-a3b-uncensored-hauhaucs-aggressive-gguf.py",
"tests_pass": false,
"total_score": 49,
"breakdown": {
"complexity": 12,
"concurrency": 10,
"isolation": 12,
"memory_edge_cases": 11,
"test_integrity": 4
},
"verdict": "Critical Bugs",
"best_for": "Not recommended for production code. Reasonable API shape and correctly used time.monotonic(), but the module does not parse (syntax error), contains an infinite while:pass loop, and has data races. Avoid for systems/concurrency work.",
"critical_bugs": [
"SyntaxError: line 305 'assert val := await cache.get(...)' is invalid Python — walrus operator cannot appear in an assert statement. The ENTIRE module fails to compile, so nothing runs and no test can execute.",
"Infinite busy-loop: _evict_lfu lines 159-160 — 'while self.min_freq in self.freq_map and self.min_freq < max(...): pass' has an empty body that never updates min_freq, recomputes max() (O(F)) each iteration, and can never terminate.",
"Hidden O(F) scan: min(self.freq_map.keys()) / max(self.freq_map.keys()) appears at 6 call sites (every eviction and removal) — violates the strict O(1) requirement.",
"Race condition: get() and put() perform lazy-TTL _remove_key() BEFORE acquiring the lock (lines 71-73, 107-108), mutating shared state unlocked while other coroutines read/write.",
"Deadlock risk: background_loop holds self._lock, then calls await self._remove_key() which is itself a lock-acquiring method — asyncio.Lock is NOT reentrant -> deadlock when the evictor runs.",
"No __slots__ on _Node despite using a dataclass (rubric required it for memory efficiency).",
"_remove_key will KeyError on self.freq_map[freq] if a concurrent operation already deleted that bucket."
],
"patch_code": "# FIX 1 (the parse error): assign first, then assert.\nval_d = await cache.get(\"D\")\nassert val_d, \"D should exist\"\nval_a = await cache.get(\"A\")\nassert val_a, \"A should exist (highest freq)\"\n\n# FIX 2: delete the broken while:pass loop. Bump min_freq incrementally:\n# only when the min_freq bucket empties, and only ever UP by 1 (a key\n# whose freq increased must land at min_freq+1). Never call min()/max().\nif not old_bucket:\n del self.freq_map[old_freq]\n if self.min_freq == old_freq:\n self.min_freq += 1 # next tier up; never scan\n\n# FIX 3: do ALL lazy eviction INSIDE the lock, not before it:\nasync def get(self, key):\n async with self._lock:\n if self.ttl_map.get(key, inf) <= time.monotonic():\n await self._remove_key(key) # now locked\n return None\n ...\n\n# FIX 4: make _remove_key a non-locking private helper, called from\n# inside already-locked public methods, so background_loop doesn't try\n# to re-acquire the non-reentrant asyncio.Lock.\n\n# FIX 5: add __slots__ = ('key','value','ttl_expiry') to _Node."
},
{
"id": "kat-coder-v2.5-dev-xl-mlx",
"timestamp": "2026-07-28T13:15:00Z",
"model_name": "KAT-Coder v2.5 Dev XL",
"quant": "MLX",
"format": "mlx",
"tok_sec": 65.26,
"total_tokens": 6172,
"ttft_sec": 7.42,
"filename": "outputs/kat-coder-v2.5-dev-xl-mlx.py",
"tests_pass": false,
"total_score": 65,
"breakdown": {
"complexity": 15,
"concurrency": 16,
"isolation": 16,
"memory_edge_cases": 13,
"test_integrity": 5
},
"verdict": "Critical Bugs",
"best_for": "Promising code-design instincts (cleanest abstractions and best transaction isolation design in the set) but undone by a single fatal one-line bug that stops it running. With the bug fixed it would likely score 80+; as-is, only useful as a structural reference.",
"critical_bugs": [
"FATAL: _put_internal line 305 inserts a NEW key with 'self._freq_map[1].push_front(...)' but never ensures the freq-1 bucket exists — KeyError: 1 on the very first put. The cache cannot store a single key. The _ensure_freq_list(1) helper it should use exists and is used correctly everywhere else (lines 294, 354).",
"Transaction.commit() calls _put_internal for buffered writes, so it hits the same KeyError: 1 — committed transactions crash too.",
"Test suite cannot execute: crashes at the first cache.put() in main(); the well-built test harness (pass/fail counter, 4 real scenarios) validates nothing.",
"No __slots__ on _DLLNode/_CacheNode/_DoublyLinkedList despite the rubric requiring it for memory efficiency.",
"_evict_node uses min(self._freq_map) (line 325) when the min-tier empties — a hidden O(F) scan, violating strict O(1).",
"No MVCC/version check on transaction commit (lost-update possible if the global key is modified between tx.get and commit); commit is not exception-safe across the writes-vs-deletes loops."
],
"patch_code": "# FIX 1 (the fatal one-liner): use the helper that already exists.\n# line 305, in _put_internal, new-key branch:\n- self._freq_map[1].push_front(dll_node)\n+ self._ensure_freq_list(1).push_front(dll_node)\n# (This single change makes the cache and transactions functional.)\n\n# FIX 2: replace the O(F) min() scan with an incremental bump:\n# in _evict_node, when the min-tier bucket empties, min_freq is the\n# lowest remaining tier. Since freq only ever increments by 1, the\n# next min is almost always min_freq+1; track it incrementally rather\n# than scanning. Or, since this only happens on full eviction, accept\n# O(F) but only on the empty-cache edge — document it.\n\n# FIX 3: add __slots__ to all internal classes:\nclass _CacheNode:\n __slots__ = ('key','value','ttl_seconds','expiry_time','freq','dll_node')\n ...\n\n# FIX 4: wrap commit applies in try/except so a mid-commit exception\n# does not leave a half-applied global state; consider abort semantics.\n# FIX 5: add node.version; in tx commit, abort if the global node for\n# a key is not the one seen at tx.get() time."
},
{
"id": "gemma4-31b-gguf",
"timestamp": "2026-07-28T13:20:00Z",
"model_name": "Gemma 4 31B",
"quant": "GGUF",
"format": "gguf",
"tok_sec": 10.09,
"total_tokens": 4536,
"ttft_sec": 4.39,
"speed_caveat": "All Gemma 4 models ran abnormally slow (GPU offload appeared inactive despite being set), so tok/sec and TTFT are NOT representative of the model itself — likely an LM Studio/GGUF config issue. Treat speed numbers for the Gemma 4 batch as suspect.",
"filename": "outputs/gemma4-31b-gguf.py",
"tests_pass": true,
"total_score": 78,
"breakdown": {
"complexity": 17,
"concurrency": 16,
"isolation": 14,
"memory_edge_cases": 15,
"test_integrity": 16
},
"verdict": "Minor Logic Flaws",
"best_for": "Clean, correct, runnable code with solid O(1) structure and good concurrency granularity. A reliable pick for everyday caching/async work after a monotonic-clock + __slots__ pass.",
"critical_bugs": [
"Isolation leak: Transaction.get falls back to the PUBLIC cache.get, which calls _update_frequency — so reading a key inside a transaction mutates GLOBAL frequency state before commit, leaking uncommitted access patterns into global eviction order. Spec requires tx reads not to alter global freq.",
"Uses time.time() (system clock) throughout instead of time.monotonic() — NTP adjustments corrupt TTL eviction.",
"No __slots__ on Node/DoublyLinkedList/LFUCache/Transaction — rubric required it for memory efficiency.",
"_delete_internal deliberately leaves empty frequency buckets in freq_map (documented but a minor memory leak: empty DoublyLinkedList objects accumulate).",
"Background evictor does list(self.cache.keys()) = O(N) snapshot every interval — a linear scan, forbidden by strict O(1).",
"No MVCC/version check on commit (lost-update possible); commit is not exception-safe across the deletes-vs-puts loops.",
"Tests pass but don't probe mid-commit read isolation or eviction-under-real-contention (capacity sized so all keys fit), so the isolation leak above goes undetected."
],
"patch_code": "# FIX 1 (the isolation leak): tx reads must NOT mutate global freq.\n# Add a read-only global lookup (no _update_frequency) and use it in tx.get:\nasync def _read_raw(self, key): # no freq bump\n node = self.cache.get(key)\n if node is None: return None\n if time.monotonic() > node.expiry:\n await self._delete_internal(key)\n return None\n return node.value\n# then in Transaction.get fallback:\n return await self._cache._read_raw(key) # NOT cache.get\n\n# FIX 2: time.time() -> time.monotonic() everywhere (get/put/_put_internal/bg loop).\n# FIX 3: add __slots__ to Node, DoublyLinkedList, LFUCache, Transaction.\n# FIX 4: prune empty freq buckets on delete, or have _evict_lfu drop them.\n# FIX 5: iterate a dedicated _ttl_keys set (batched) in the bg evictor\n# instead of list(self.cache.keys()) to stay O(batch), not O(N)."
},
{
"id": "gemma-4-31b-qat-gguf",
"timestamp": "2026-07-28T13:25:00Z",
"model_name": "Gemma 4 31B QAT",
"quant": "QAT GGUF",
"format": "gguf",
"tok_sec": 15.0,
"total_tokens": 4552,
"ttft_sec": 4.01,
"speed_caveat": "Same as the Gemma 4 batch: GPU offload appeared inactive so tok/sec/TTFT are NOT representative of the model. Suspected LM Studio/GGUF config issue.",
"filename": "outputs/gemma-4-31b-qat-gguf.py",
"tests_pass": true,
"total_score": 70,
"breakdown": {
"complexity": 16,
"concurrency": 16,
"isolation": 11,
"memory_edge_cases": 13,
"test_integrity": 14
},
"verdict": "Critical Bugs",
"best_for": "Runnable and structurally sound, but the LFU eviction has a stale-min_freq capacity-breach path and the transaction API doesn't match the spec (no commit/rollback). Usable for prototypes if you fix eviction and re-skin transactions.",
"critical_bugs": [
"Capacity breach: eviction does self.freq_map[self.min_freq].pop_tail() with NO guard that the bucket exists or is non-empty, and never prunes emptied freq buckets. After manual deletes empty the min-tier, pop_tail returns None silently -> eviction fails -> cache grows PAST capacity. This is the exact stale-min_freq capacity-breach the rubric flags.",
"Non-conformant transaction API: Transaction has NO commit() or rollback() method (both required by spec). Commit happens via a separate cache.apply_transaction_changes(tx._state) — wrong surface; the test only passes because it uses this internal path.",
"Isolation leak: Transaction.get falls back to the public cache.get, which bumps global frequency before commit — uncommitted tx reads alter global eviction order.",
"Duplicated eviction logic in apply_transaction_changes re-introduces the stale-min_freq bug at line 211.",
"Uses time.time() (system clock) via _get_now() instead of time.monotonic() — NTP jumps corrupt TTL (though _get_now is a clean single fix point).",
"No __slots__ on Node/DoublyLinkedList/LFUCache/TransactionState/Transaction.",
"Background sweep does list(self.cache.keys()) = O(N) per interval.",
"Tests pass but use the non-spec commit path and don't probe capacity breach or isolation leak."
],
"patch_code": "# FIX 1 (capacity breach): guard + prune empty buckets on eviction:\nwhile self.min_freq in self.freq_map and self.freq_map[self.min_freq].size == 0:\n del self.freq_map[self.min_freq]\n self.min_freq += 1\n if not self.freq_map:\n break\nif self.min_freq not in self.freq_map:\n return # nothing to evict\nevicted = self.freq_map[self.min_freq].pop_tail()\nif evicted and self.freq_map[self.min_freq].size == 0:\n del self.freq_map[self.min_freq]\n\n# FIX 2 (conformant API): add commit/rollback to Transaction:\nasync def commit(self):\n await self._cache.apply_transaction_changes(self._state)\n self._committed = True\ndef rollback(self):\n self._state.writes.clear()\n self._committed = True\n\n# FIX 3 (isolation leak): tx.get should use a read-only global lookup\n# (no _update_freq), not the public cache.get.\n# FIX 4: _get_now returns time.monotonic().\n# FIX 5: add __slots__ to all node/list classes."
},
{
"id": "gemma-4-12b-coder-heretic-mxfp8-mlx",
"timestamp": "2026-07-28T13:30:00Z",
"model_name": "Gemma 4 12B Coder (fable5-composer2.5-v1-uncensored-heretic merge)",
"quant": "mxfp8 MLX",
"format": "mlx",
"tok_sec": 25.28,
"total_tokens": 2625,
"ttft_sec": 3.21,
"filename": "outputs/gemma-4-12b-coder-fable5-composer2.5-v1-uncensored-heretic-mxfp8-mlx.py",
"tests_pass": false,
"total_score": 43,
"breakdown": {
"complexity": 13,
"concurrency": 7,
"isolation": 10,
"memory_edge_cases": 9,
"test_integrity": 4
},
"verdict": "Critical Bugs",
"best_for": "Not usable as-is — the cache cannot store its first key and the background evictor would crash the event loop. The smallest model in the set (12B) and lowest-quality output. Avoid for systems work.",
"critical_bugs": [
"FATAL: put() line 112 does 'bucket = self.freq_buckets[self.min_freq]' after setting min_freq=1 but NEVER creates freq_buckets[1] -> KeyError: 1 on the very first put. Cache is unusable.",
"Background evictor is fundamentally broken: start_evictor defines a SYNC 'def evict_loop' and passes it to create_task; inside it calls blocking time.sleep(interval) (freezes the event loop) AND asyncio.run(...) from within a running loop -> RuntimeError. Would crash hard if ever reached.",
"Eviction-by-re-put: expired keys are 'evicted' by re-inserting them with TTL 0 (line 122) instead of deleting them — wrong semantics and triggers immediate re-eviction.",
"Transaction duplicates the entire LFU machinery (local_cache + local_freq + local_min_freq) for snapshot isolation, but _update_local_freq has the same missing-bucket KeyError (line 140).",
"Class name typo 'DoublyLinkedListList' (doubled word).",
"No __slots__; time.time() (not monotonic) throughout.",
"Tests cannot run — crash at first put."
],
"patch_code": "# FIX 1 (the fatal KeyError): create the bucket before use.\n# In put(), new-key branch:\n- bucket = self.freq_buckets[self.min_freq]\n+ bucket = self.freq_buckets.setdefault(self.min_freq, DoublyLinkedListList())\n# Same fix in _update_freq and Transaction._update_local_freq (use setdefault).\n\n# FIX 2 (the broken evictor): make it a real async task that deletes:\nasync def _evict_loop(self, interval):\n while True:\n await asyncio.sleep(interval) # async, non-blocking\n now = time.monotonic()\n async with self.global_lock:\n expired = [k for k, n in list(self.cache.items()) if now > n.ttl_expiry]\n for k in expired:\n node = self.cache.pop(k, None)\n if node:\n self.freq_buckets[node.freq].remove(node) # DELETE, not re-put\n\nasync def start_evictor(self, interval=1.0):\n self.evictor_task = asyncio.create_task(self._evict_loop(interval))\n\n# FIX 3: delete expired keys; do NOT re-insert with TTL 0.\n# FIX 4: time.time() -> time.monotonic().\n# FIX 5: add __slots__ to Node / DoublyLinkedListList / ConcurrentLFUCache / Transaction."
},
{
"id": "deepseekv4flash-cloud",
"timestamp": "2026-07-28T13:35:00Z",
"model_name": "DeepSeek V4 Flash (CLOUD baseline)",
"quant": "n/a (cloud)",
"format": "cloud",
"tok_sec": null,
"total_tokens": null,
"ttft_sec": null,
"speed_caveat": "Cloud model (run via opencode, not LM Studio) — tok_sec/tokens/TTFT are N/A (not measured for cloud). Included as a quality baseline against the local models. NOTE: it took 3 attempts to produce any output and ~12 minutes of thinking before succeeding — so it is a QUALITY benchmark, not a speed/usability one.",
"filename": "deepseekv4flash.py",
"tests_pass": true,
"total_score": 91,
"breakdown": {
"complexity": 18,
"concurrency": 18,
"isolation": 19,
"memory_edge_cases": 18,
"test_integrity": 18
},
"verdict": "Production Ready",
"best_for": "Reference-quality baseline (91/100) — the bar the local models are measured against. Only submission with __slots__ + time.monotonic() + delta-based transactional frequency accounting. Sets the ceiling for correctness, though its unreliability (3 attempts, 12-min think time) makes it a poor *local* daily-driver.",
"critical_bugs": [
"Two min(self._freq_to_list) linear scans in _evict_one's defensive recovery path (lines 415, 429) — only triggered when min_freq desyncs, not per-operation, but still a non-O(1) path. Could be replaced with incremental tracking.",
"Single coarse lock held across the whole commit-apply loop — not the fine-grained locking the prompt asked for.",
"__slots__ present on _Node and _DLL but not extended to Transaction / LFUCache.",
"No explicit lost-update/conflict abort on commit (delta-based freq is applied unconditionally).",
"Tests pass 20/20 but don't include a mid-commit read-isolation probe or adversarial eviction-under-contention stress."
],
"patch_code": "# These are minor refinements on an already production-ready file.\n\n# FIX 1: eliminate the recovery min() scans by keeping min_freq\n# strictly in sync on every insert/bump/remove (it already does on\n# the hot path), so the _evict_one recovery branch is unreachable and\n# can assert instead of scanning:\nassert self._min_freq in self._freq_to_list or not self._freq_to_list\n\n# FIX 2: extend __slots__ to Transaction and LFUCache.\nclass LFUCache(Generic[KT, VT]):\n __slots__ = ('_capacity','_key_to_node','_freq_to_list','_min_freq',\n '_lock','_ttl_index','_evictor_task','_closed')\n\n# FIX 3 (optional): on commit, if a key's global node changed since the\n# tx snapshot, raise LFUCacheError('lost update') instead of overwriting."
}
]
}
+600
View File
@@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction & Atomic Transactions.
Pure Python 3.11+, no external dependencies.
O(1) average-time get/put via frequency buckets with doubly-linked lists.
Dual-layer TTL: lazy on access + background async sweep.
Atomic transactions with read-your-own-writes, commit, and rollback.
"""
import asyncio
import time
from typing import (Dict, Generic, Hashable, Optional, Set, TypeVar)
KT = TypeVar('KT', bound=Hashable)
VT = TypeVar('VT')
_SENTINEL = object()
class LFUCacheError(Exception):
"""Base exception for LFU cache errors."""
class _Node(Generic[KT, VT]):
"""Doubly-linked list node holding key, value, frequency, and expiry."""
__slots__ = ('key', 'value', 'freq', 'expires_at', 'prev', 'next')
def __init__(self, key: KT, value: VT) -> None:
self.key = key
self.value = value
self.freq: int = 0
self.expires_at: Optional[float] = None
self.prev: Optional['_Node[KT, VT]'] = None
self.next: Optional['_Node[KT, VT]'] = None
class _DLL(Generic[KT, VT]):
"""
Doubly-linked list with sentinel head/tail.
O(1) append (tail/MRU), remove (by reference), and pop_left (head/LRU).
"""
__slots__ = ('_head', '_tail', 'size')
def __init__(self) -> None:
self._head = _Node[KT, VT](_SENTINEL, None) # type: ignore[arg-type]
self._tail = _Node[KT, VT](_SENTINEL, None) # type: ignore[arg-type]
self._head.next = self._tail
self._tail.prev = self._head
self.size: int = 0
def append(self, node: _Node[KT, VT]) -> None:
"""Add node to tail (most-recently-used position). O(1)."""
last = self._tail.prev
last.next = node
node.prev = last
node.next = self._tail
self._tail.prev = node
self.size += 1
def remove(self, node: _Node[KT, VT]) -> None:
"""Remove node that must be in this list. O(1)."""
node.prev.next = node.next
node.next.prev = node.prev
node.prev = None
node.next = None
self.size -= 1
def pop_left(self) -> Optional[_Node[KT, VT]]:
"""Remove and return the head node (LRU position). O(1)."""
if self.size == 0:
return None
node = self._head.next
self.remove(node)
return node
class Transaction(Generic[KT, VT]):
"""
Atomic transaction with read-your-own-writes isolation.
Uncommitted writes are invisible to global cache readers.
All local state is discarded on rollback without touching global structures.
"""
def __init__(self, cache: 'LFUCache[KT, VT]') -> None:
self._cache = cache
self._writes: Dict[KT, VT] = {}
self._deletes: Set[KT] = set()
self._snapshot: Dict[KT, VT] = {}
self._freq_deltas: Dict[KT, int] = {}
self._ttls: Dict[KT, Optional[float]] = {}
self._committed: bool = False
self._rolled_back: bool = False
def _check_active(self) -> None:
if self._committed:
raise LFUCacheError("Transaction already committed")
if self._rolled_back:
raise LFUCacheError("Transaction already rolled back")
async def get(self, key: KT) -> Optional[VT]:
"""Read key with read-your-own-writes and snapshot isolation."""
self._check_active()
if key in self._deletes:
return None
if key in self._writes:
self._freq_deltas[key] = self._freq_deltas.get(key, 0) + 1
return self._writes[key]
if key in self._snapshot:
self._freq_deltas[key] = self._freq_deltas.get(key, 0) + 1
return self._snapshot[key]
async with self._cache._lock:
node = self._cache._key_to_node.get(key)
if node is None:
return None
if node.expires_at is not None and node.expires_at <= time.monotonic():
self._cache._remove_key(key)
return None
self._snapshot[key] = node.value
self._freq_deltas[key] = self._freq_deltas.get(key, 0) + 1
return self._snapshot[key]
async def put(self, key: KT, value: VT,
ttl_seconds: Optional[float] = None) -> None:
"""Write key within transaction."""
self._check_active()
self._writes[key] = value
self._deletes.discard(key)
if ttl_seconds is not None:
self._ttls[key] = ttl_seconds if ttl_seconds > 0 else None
self._freq_deltas[key] = self._freq_deltas.get(key, 0) + 1
async def delete(self, key: KT) -> None:
"""Mark key for deletion on commit."""
self._check_active()
self._deletes.add(key)
self._writes.pop(key, None)
self._snapshot.pop(key, None)
self._freq_deltas.pop(key, None)
self._ttls.pop(key, None)
async def commit(self) -> None:
"""Atomically apply all pending changes to the global cache."""
if self._rolled_back:
raise LFUCacheError("Transaction already rolled back")
if self._committed:
return
async with self._cache._lock:
try:
for key in self._deletes:
self._cache._remove_key(key)
for key, value in self._writes.items():
node = self._cache._key_to_node.get(key)
ttl = self._ttls.get(key)
delta = self._freq_deltas.get(key, 1)
if node is not None:
node.value = value
if ttl is not None:
node.expires_at = (
time.monotonic() + ttl if ttl is not None else None
)
self._cache._change_freq(node, delta)
else:
while len(self._cache._key_to_node) >= self._cache._capacity:
self._cache._evict_one()
node = _Node(key, value)
node.freq = delta
if ttl is not None:
node.expires_at = time.monotonic() + ttl
self._cache._key_to_node[key] = node
self._cache._add_to_freq_list(node)
for key, delta in self._freq_deltas.items():
if key not in self._writes and key not in self._deletes:
node = self._cache._key_to_node.get(key)
if node is not None:
self._cache._change_freq(node, delta)
while len(self._cache._key_to_node) > self._cache._capacity:
self._cache._evict_one()
self._committed = True
except BaseException:
self._rolled_back = True
raise
async def rollback(self) -> None:
"""Discard all pending changes. Global cache is untouched."""
if self._committed:
raise LFUCacheError("Transaction already committed")
self._rolled_back = True
class LFUCache(Generic[KT, VT]):
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction.
O(1) average-time get/put using frequency buckets with doubly-linked lists.
Dual-layer TTL eviction: lazy on access + background async sweep.
"""
def __init__(self, capacity: int = 1000,
evictor_interval: float = 1.0,
evictor_batch_size: int = 10,
evictor_scan_budget: int = 100) -> None:
if capacity < 1:
raise ValueError("Capacity must be >= 1")
self._capacity = capacity
self._evictor_interval = evictor_interval
self._evictor_batch_size = evictor_batch_size
self._evictor_scan_budget = evictor_scan_budget
self._key_to_node: Dict[KT, _Node[KT, VT]] = {}
self._freq_to_list: Dict[int, _DLL[KT, VT]] = {}
self._min_freq: int = 0
self._lock = asyncio.Lock()
self._evictor_running: bool = False
self._evictor_task: Optional[asyncio.Task] = None
# ---- Public API ---------------------------------------------------------
async def get(self, key: KT) -> Optional[VT]:
"""Retrieve value by key. Returns None if missing or expired.
Accesses increment the key's frequency (LFU tracking).
Expired keys are lazily evicted on access.
"""
async with self._lock:
node = self._key_to_node.get(key)
if node is None:
return None
if node.expires_at is not None and node.expires_at <= time.monotonic():
self._remove_key(key)
return None
self._change_freq(node, 1)
return node.value
async def put(self, key: KT, value: VT,
ttl_seconds: Optional[float] = None) -> None:
"""Insert or update a key-value pair.
If *ttl_seconds* is None (default) the entry never expires.
If *ttl_seconds* is <= 0 it is treated as no expiration.
If the cache is at capacity the least-frequently-used item is evicted
(tie-broken by least-recently-used within the minimum frequency tier).
"""
async with self._lock:
node = self._key_to_node.get(key)
if node is not None:
if node.expires_at is not None and node.expires_at <= time.monotonic():
self._remove_key(key)
node = None
else:
node.value = value
if ttl_seconds is not None:
node.expires_at = (
time.monotonic() + ttl_seconds if ttl_seconds > 0 else None
)
self._change_freq(node, 1)
return
while len(self._key_to_node) >= self._capacity:
self._evict_one()
node = _Node(key, value)
node.freq = 1
if ttl_seconds is not None and ttl_seconds > 0:
node.expires_at = time.monotonic() + ttl_seconds
self._key_to_node[key] = node
self._add_to_freq_list(node)
async def delete(self, key: KT) -> bool:
"""Remove *key* from the cache. Returns True if the key existed."""
async with self._lock:
return self._remove_key(key)
def begin_transaction(self) -> Transaction[KT, VT]:
"""Open an atomic transaction for batched reads/writes."""
return Transaction(self)
@property
def capacity(self) -> int:
return self._capacity
@property
def size(self) -> int:
return len(self._key_to_node)
# ---- Background TTL Evictor --------------------------------------------
def start_evictor(self) -> None:
"""Launch the background TTL eviction loop as an asyncio task."""
if self._evictor_running:
return
self._evictor_running = True
self._evictor_task = asyncio.create_task(self._evictor_loop())
async def stop_evictor(self) -> None:
"""Cancel and wait for the background eviction task to finish."""
if not self._evictor_running:
return
self._evictor_running = False
if self._evictor_task is not None:
self._evictor_task.cancel()
try:
await self._evictor_task
except asyncio.CancelledError:
pass
self._evictor_task = None
async def _evictor_loop(self) -> None:
"""
Background loop: periodically scan a limited batch of keys and remove
expired entries. Releases the lock between batches so concurrent
reads/writes are not blocked for extended periods.
"""
cursor = 0
keys: list = []
try:
while self._evictor_running:
await asyncio.sleep(self._evictor_interval)
async with self._lock:
if cursor >= len(keys):
keys = list(self._key_to_node.keys())
cursor = 0
if not keys:
continue
now = time.monotonic()
removed = 0
scan = self._evictor_scan_budget
while scan > 0 and cursor < len(keys):
key = keys[cursor]
cursor += 1
scan -= 1
node = self._key_to_node.get(key)
if (node is not None and
node.expires_at is not None and
node.expires_at <= now):
self._remove_key(key)
removed += 1
if removed >= self._evictor_batch_size:
break
except asyncio.CancelledError:
pass
# ---- O(1) Internal Helpers ---------------------------------------------
def _add_to_freq_list(self, node: _Node[KT, VT]) -> None:
"""Insert *node* into its frequency bucket and update *min_freq*."""
freq = node.freq
if freq not in self._freq_to_list:
self._freq_to_list[freq] = _DLL()
self._freq_to_list[freq].append(node)
if self._min_freq not in self._freq_to_list or freq < self._min_freq:
self._min_freq = freq
def _remove_from_freq_list(self, node: _Node[KT, VT]) -> None:
"""Remove *node* from its frequency bucket. No-op if not linked."""
if node.prev is None or node.next is None:
return
freq = node.freq
lst = self._freq_to_list.get(freq)
if lst is None:
return
lst.remove(node)
if lst.size == 0:
del self._freq_to_list[freq]
def _change_freq(self, node: _Node[KT, VT], delta: int) -> None:
"""Atomically increase *node*'s frequency by *delta* and
move it to the corresponding bucket. O(1)."""
if delta <= 0:
return
self._remove_from_freq_list(node)
node.freq += delta
self._add_to_freq_list(node)
def _remove_key(self, key: KT) -> bool:
"""Remove *key* from all internal structures. Returns True if existed."""
node = self._key_to_node.pop(key, None)
if node is None:
return False
self._remove_from_freq_list(node)
return True
def _evict_one(self) -> Optional[KT]:
"""
Evict one item: LRU among the minimum-frequency bucket.
Returns the evicted key, or None if the cache is empty.
"""
if not self._key_to_node:
return None
if self._min_freq not in self._freq_to_list:
if not self._freq_to_list:
return None
self._min_freq = min(self._freq_to_list)
lst = self._freq_to_list.get(self._min_freq)
if lst is None or lst.size == 0:
return None
node = lst.pop_left()
if node is None:
return None
del self._key_to_node[node.key]
if lst.size == 0:
del self._freq_to_list[self._min_freq]
if self._freq_to_list:
self._min_freq = min(self._freq_to_list)
return node.key
# ---- Async Context Manager ---------------------------------------------
async def __aenter__(self) -> 'LFUCache[KT, VT]':
return self
async def __aexit__(self, *exc_info) -> None:
await self.stop_evictor()
# ============================================================================
# Unit Tests
# ============================================================================
async def _run_tests() -> None:
passed = 0
total = 0
def check(cond: bool, msg: str):
nonlocal passed, total
total += 1
if cond:
passed += 1
else:
print(f" FAIL: {msg}")
# ------------------------------------------------------------------
# 1. O(1) LFU eviction order
# ------------------------------------------------------------------
print("1. LFU eviction order ... ", end="", flush=True)
cache = LFUCache(capacity=3)
await cache.put("a", 1)
await cache.put("b", 2)
await cache.put("c", 3)
await cache.get("a")
await cache.get("a")
await cache.get("b")
# freqs: a=3, b=2, c=1
await cache.put("d", 4) # evicts c (freq=1)
check(await cache.get("c") is None, "c was evicted (freq=1)")
check(await cache.get("d") == 4, "d is present")
check(await cache.get("a") == 1, "a is present")
check(await cache.get("b") == 2, "b is present")
# a=4, b=3, d=1 after the gets above
await cache.put("e", 5) # evicts d (freq=1)
check(await cache.get("d") is None, "d was evicted")
check(await cache.get("e") == 5, "e is present")
check(cache.size == 3, "cache size == 3")
print("OK")
# ------------------------------------------------------------------
# 2. TTL eviction
# ------------------------------------------------------------------
print("2. TTL eviction ... ", end="", flush=True)
# 2a. Lazy eviction
cache2 = LFUCache(capacity=10)
await cache2.put("lazy", "alive", ttl_seconds=0.02)
await asyncio.sleep(0.03)
check(await cache2.get("lazy") is None, "lazy TTL eviction on get")
await cache2.put("lazy2", "alive", ttl_seconds=0.02)
await asyncio.sleep(0.03)
check(await cache2.get("lazy2") is None, "lazy TTL eviction on second get")
# 2b. Background eviction
cache3 = LFUCache(capacity=10, evictor_interval=0.02, evictor_batch_size=5)
cache3.start_evictor()
await cache3.put("bg", "data", ttl_seconds=0.01)
await asyncio.sleep(0.10)
check(await cache3.get("bg") is None, "background evictor removes expired key")
await cache3.stop_evictor()
print("OK")
# ------------------------------------------------------------------
# 3. Transactions
# ------------------------------------------------------------------
print("3. Transactions ... ", end="", flush=True)
# 3a. Commit
cache4 = LFUCache(capacity=10)
await cache4.put("x", 10)
tx = cache4.begin_transaction()
await tx.put("x", 20)
check(await tx.get("x") == 20, "tx reads its own write")
check(await cache4.get("x") == 10, "global does NOT see uncommitted write")
await tx.commit()
check(await cache4.get("x") == 20, "global sees committed value")
# 3b. Rollback
await cache4.put("y", 100)
tx2 = cache4.begin_transaction()
await tx2.put("y", 200)
await tx2.rollback()
check(await cache4.get("y") == 100, "rollback discards writes")
# 3c. Delete in transaction
await cache4.put("z", 300)
tx3 = cache4.begin_transaction()
await tx3.delete("z")
check(await tx3.get("z") is None, "tx sees its own delete")
check(await cache4.get("z") == 300, "global not affected before commit")
await tx3.commit()
check(await cache4.get("z") is None, "global sees committed delete")
# 3d. Rollback after delete
await cache4.put("w", 400)
tx4 = cache4.begin_transaction()
await tx4.delete("w")
await tx4.rollback()
check(await cache4.get("w") == 400, "rollback restores deleted key")
# 3e. Read own writes after rollback raises
tx5 = cache4.begin_transaction()
await tx5.put("n", 1)
await tx5.rollback()
try:
await tx5.get("n")
check(False, "get on rolled-back tx should raise")
except LFUCacheError:
check(True, "rolled-back tx raises on get")
print("OK")
# ------------------------------------------------------------------
# 4. Concurrent stress
# ------------------------------------------------------------------
print("4. Concurrent stress (50 workers) ... ", end="", flush=True)
cache5 = LFUCache(capacity=50, evictor_interval=0.05, evictor_batch_size=5)
cache5.start_evictor()
async def worker(uid: int):
for i in range(30):
key = f"k_{(uid + i) % 40}"
await cache5.put(key, uid * 1000 + i, ttl_seconds=0.5)
_ = await cache5.get(key)
await cache5.get(f"k_{(uid + i + 1) % 40}")
if i % 5 == 0:
await cache5.delete(f"k_{(uid + i + 2) % 40}")
await asyncio.sleep(0)
workers = [worker(i) for i in range(50)]
await asyncio.gather(*workers)
await cache5.stop_evictor()
check(cache5.size <= 50, f"cache size {cache5.size} <= capacity 50")
print("OK")
# ------------------------------------------------------------------
print(f"\nResults: {passed}/{total} checks passed")
if passed < total:
raise SystemExit(1)
async def main() -> None:
await _run_tests()
if __name__ == "__main__":
asyncio.run(main())
+403
View File
@@ -0,0 +1,403 @@
#!/usr/bin/env python3
"""
Generator: builds dashboard.html + pages/<slug>..html from data/benchmark_history.json.
Re-run after each grading batch to regenerate everything.
Cyberpunk-terminal aesthetic. Pure stdlib + Chart.js via CDN.
"""
import json, html, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data", "benchmark_history.json")
OUT_DASH = os.path.join(HERE, "dashboard.html")
PAGES_DIR = os.path.join(HERE, "pages")
PILLARS = ["complexity", "concurrency", "isolation", "memory_edge_cases", "test_integrity"]
PILLAR_LABELS = {
"complexity": "Complexity (O(1))",
"concurrency": "Concurrency / Races",
"isolation": "Tx Isolation",
"memory_edge_cases": "Memory & Edges",
"test_integrity": "Test Integrity",
}
# ---- cyberpunk palette ----
NEON_CYAN = "#00ffc8"
NEON_MAG = "#ff2bd6"
NEON_LIME = "#b6ff3a"
NEON_AMBER= "#ffb000"
NEON_RED = "#ff3b5c"
BG = "#0a0a0f"
PANEL = "#101018"
INK = "#d7e0e6" # body text
INK_DIM = "#7a8590" # muted
def verdict_meta(v):
return {
"Production Ready": (NEON_LIME, "PROD"),
"Minor Logic Flaws": (NEON_AMBER, "FLAWS"),
"Critical Bugs": (NEON_RED, "CRIT"),
"Broken / Unusable": (NEON_RED, "DEAD"),
}.get(v, (INK_DIM, "???"))
def esc(s):
return html.escape(str(s)) if s is not None else ""
def speed_str(m):
if m.get("tok_sec") is None:
return "N/A"
return f"{m['tok_sec']:.1f}"
# series colors for charts (cycles through neon accents)
SERIES = [NEON_CYAN, NEON_MAG, NEON_LIME, NEON_AMBER, "#5b8cff", "#c084fc", "#34d399", "#f472b6"]
# shared <head> CSS — embedded so each page is standalone
def shared_css():
return """
:root{
--bg:#0a0a0f; --panel:#101018; --panel2:#14141f; --ink:#d7e0e6; --dim:#7a8590;
--cyan:#00ffc8; --mag:#ff2bd6; --lime:#b6ff3a; --amber:#ffb000; --red:#ff3b5c; --blue:#5b8cff;
--grid: rgba(0,255,200,0.04);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
background:var(--bg); color:var(--ink);
font-family:'Fira Sans',system-ui,-apple-system,sans-serif;
line-height:1.6; min-height:100vh;
/* scanline + grid texture overlay */
background-image:
repeating-linear-gradient(0deg, rgba(0,255,200,0.025) 0px, rgba(0,255,200,0.025) 1px, transparent 1px, transparent 3px),
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px),
radial-gradient(1200px 600px at 70% -10%, rgba(91,139,255,0.10), transparent 60%),
radial-gradient(900px 500px at 10% 110%, rgba(255,43,214,0.08), transparent 60%);
background-size: 3px 3px, 44px 44px, 44px 44px, 100% 100%, 100% 100%;
background-attachment: fixed;
}
mono,.mono,h1,h2,h3,.hud,th,.code,.badge,.stat-num,.lead-model,.verdict-chip{font-family:'Fira Code',ui-monospace,'SF Mono',Menlo,monospace}
a{color:var(--cyan);text-decoration:none}
a:hover{text-shadow:0 0 8px var(--cyan)}
.wrap{max-width:1180px;margin:0 auto;padding:28px 20px 80px}
header.hud-bar{
border:1px solid rgba(0,255,200,0.25);
background:linear-gradient(180deg,rgba(0,255,200,0.06),rgba(0,0,0,0));
box-shadow:0 0 24px rgba(0,255,200,0.08), inset 0 0 30px rgba(0,0,0,0.5);
border-radius:6px; padding:18px 22px; margin-bottom:24px;
position:relative; overflow:hidden;
}
header.hud-bar::before{
content:""; position:absolute; inset:0;
background:repeating-linear-gradient(90deg, transparent 0 7px, rgba(0,255,200,0.03) 7px 8px);
pointer-events:none;
}
.kicker{color:var(--cyan); letter-spacing:.32em; font-size:.72rem; text-transform:uppercase; text-shadow:0 0 10px rgba(0,255,200,0.5)}
h1{font-size:1.5rem; margin:.3em 0 0; letter-spacing:.02em; text-shadow:0 0 16px rgba(0,255,200,0.35)}
.subtitle{color:var(--dim); font-size:.9rem; margin-top:.35em}
.stats{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:18px 0 26px}
.stat{background:var(--panel); border:1px solid rgba(255,255,255,0.06); border-radius:6px; padding:14px 16px; position:relative; overflow:hidden}
.stat::after{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--cyan);box-shadow:0 0 12px var(--cyan)}
.stat .lbl{color:var(--dim);font-size:.68rem;letter-spacing:.18em;text-transform:uppercase}
.stat .num{font-size:1.6rem;margin-top:4px;color:var(--ink)}
.grid2{display:grid;grid-template-columns:1.1fr .9fr;gap:18px;margin-bottom:26px}
.panel{background:var(--panel);border:1px solid rgba(255,255,255,0.07);border-radius:8px;padding:18px}
.panel h2{font-size:.95rem;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan);margin:0 0 14px;text-shadow:0 0 10px rgba(0,255,200,0.35)}
.chart-box{position:relative;height:340px}
@media(max-width:900px){.grid2{grid-template-columns:1fr}.stats{grid-template-columns:repeat(2,1fr)}}
/* leaderboard */
table{width:100%;border-collapse:collapse;font-size:.86rem}
thead th{text-align:left;color:var(--dim);font-size:.66rem;letter-spacing:.16em;text-transform:uppercase;border-bottom:1px solid rgba(0,255,200,0.2);padding:8px 10px}
tbody td{padding:11px 10px;border-bottom:1px solid rgba(255,255,255,0.05);vertical-align:middle}
tbody tr{transition:background .15s, box-shadow .15s}
tbody tr:hover{background:rgba(0,255,200,0.05);box-shadow:inset 0 0 0 1px rgba(0,255,200,0.25)}
.rank{color:var(--dim);width:34px}
.rank.top{color:var(--lime);text-shadow:0 0 8px var(--lime)}
.model-name{color:var(--ink)}
.quant{color:var(--dim);font-size:.78rem}
.badge{display:inline-block;padding:2px 9px;border-radius:3px;font-size:.68rem;letter-spacing:.1em;border:1px solid currentColor}
.btn{display:inline-block;padding:5px 12px;border:1px solid var(--cyan);color:var(--cyan);border-radius:4px;font-size:.74rem;letter-spacing:.08em;cursor:pointer;transition:all .18s}
.btn:hover{background:rgba(0,255,200,0.12);box-shadow:0 0 14px rgba(0,255,200,0.4)}
.bar-cell{display:flex;align-items:center;gap:10px}
.score-bar{flex:1;height:8px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden;min-width:70px}
.score-bar > i{display:block;height:100%;border-radius:2px;transition:width .4s, box-shadow .2s}
tr:hover .score-bar > i{box-shadow:0 0 12px currentColor}
.caveat{color:var(--amber);font-size:.72rem}
.cloud-tag{color:var(--blue);font-size:.7rem;border:1px solid rgba(91,139,255,.4);padding:1px 6px;border-radius:3px;margin-left:6px}
footer{color:var(--dim);font-size:.74rem;margin-top:40px;border-top:1px solid rgba(255,255,255,0.06);padding-top:14px;text-align:center}
@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}}
"""
def head_html(title):
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{esc(title)}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<style>{shared_css()}</style>
</head>
<body><div class="wrap">
"""
FOOT = """</div>
<script>if(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches&&window.Chart){Chart.defaults.animation=false;Chart.defaults.animations.colors=false;Chart.defaults.animations.numbers=false;}</script>
</body></html>"""
def render_dashboard(data):
models = sorted(data["models"], key=lambda m: -m["total_score"])
n = len(models)
avg = sum(m["total_score"] for m in models) / n if n else 0
top = models[0] if models else None
prod = sum(1 for m in models if m["verdict"] == "Production Ready")
# chart datasets (exclude cloud model from speed bars — has no tok/sec)
local = [m for m in models if m.get("tok_sec") is not None]
local_sorted = sorted(local, key=lambda m: -m["tok_sec"])
radar_models = [m for m in models if m.get("tok_sec") is not None or True][:3] # top 3 by score already
rows = []
for i, m in enumerate(models, 1):
col, chip = verdict_meta(m["verdict"])
caveat = ""
if m.get("speed_caveat"):
caveat = '<div class="caveat">⚠ speed suspect</div>'
cloud = '<span class="cloud-tag">CLOUD</span>' if m.get("format") == "cloud" else ""
bar_color = col
rows.append(f"""<tr>
<td class="rank {'top' if i<=3 else ''}">#{i}</td>
<td><div class="model-name">{esc(m['model_name'])}{cloud}</div><div class="quant">{esc(m['quant'])}</div>{caveat}</td>
<td class="mono">{speed_str(m)} <span style="color:var(--dim);font-size:.72rem">t/s</span></td>
<td><div class="bar-cell"><span class="mono" style="width:34px;color:{col}">{m['total_score']}</span>
<span class="score-bar"><i style="width:{m['total_score']}%;background:{bar_color};color:{bar_color}"></i></span></div></td>
<td><span class="badge verdict-chip" style="color:{col}">{chip}</span></td>
<td style="color:var(--dim);font-size:.8rem">{esc(m['best_for'])[:70]}…</td>
<td><a class="btn" href="pages/{esc(m['id'])}.html">DECODE ▸</a></td>
</tr>""")
# JSON for charts
bar_labels = json.dumps([m["model_name"].split("(")[0].strip()[:18] for m in local_sorted])
bar_speed = json.dumps([m["tok_sec"] for m in local_sorted])
bar_score = json.dumps([m["total_score"] for m in local_sorted])
radar_labels = json.dumps([PILLAR_LABELS[p] for p in PILLARS])
radar_sets = []
for idx, m in enumerate(radar_models):
col = SERIES[idx % len(SERIES)]
radar_sets.append({
"label": m["model_name"][:24],
"data": [m["breakdown"][p] for p in PILLARS],
"borderColor": col,
"backgroundColor": col + "22",
})
radar_json = json.dumps(radar_sets)
stats = f"""
<div class="stat"><div class="lbl">Models Tested</div><div class="num mono">{n}</div></div>
<div class="stat"><div class="lbl">Top Score</div><div class="num mono" style="color:var(--lime)">{top['total_score'] if top else ''}</div></div>
<div class="stat"><div class="lbl">Average</div><div class="num mono" style="color:var(--amber)">{avg:.1f}</div></div>
<div class="stat"><div class="lbl">Prod-Ready</div><div class="num mono" style="color:var(--cyan)">{prod}/{n}</div></div>
"""
top_name = esc(top["model_name"]) if top else ""
body = f"""
{head_html("LLM Benchmark Suite")}
<header class="hud-bar">
<div class="kicker">▚ LOCAL LLM BENCHMARK SUITE // LFU CACHE &amp; ACID AUDIT</div>
<h1>// BENCHMARK_RESULTS <span style="color:var(--mag)">.json</span></h1>
<div class="subtitle">{n} models graded on a strict 5-pillar / 100-pt rubric · O(1) LFU + ACID transactions · M3 Max · LM Studio
&nbsp;·&nbsp; <span style="color:var(--cyan)">TOP: {top_name}</span></div>
</header>
{stats}
<div class="grid2">
<div class="panel">
<h2>▮ Score vs Throughput (tok/sec)</h2>
<div class="chart-box"><canvas id="bar"></canvas></div>
<div style="color:var(--dim);font-size:.72rem;margin-top:8px">Local models only — cloud baseline (DeepSeek) excluded from speed axis. Gemma 4 bars flagged ⚠ (GPU-offload suspect).</div>
</div>
<div class="panel">
<h2>▮ 5-Pillar Radar — Top 3</h2>
<div class="chart-box"><canvas id="radar"></canvas></div>
<div style="color:var(--dim);font-size:.72rem;margin-top:8px">Each pillar scored 020. Outer = stronger.</div>
</div>
</div>
<div class="panel" style="margin-bottom:26px">
<h2>▮ LEADERBOARD</h2>
<div style="overflow-x:auto">
<table>
<thead><tr><th>#</th><th>Model</th><th>Speed</th><th>Score</th><th>Verdict</th><th>Best For</th><th></th></tr></thead>
<tbody>{''.join(rows)}</tbody>
</table></div>
</div>
<footer>Generated from <span class="mono">data/benchmark_history.json</span> · re-run <span class="mono">generate_dashboard.py</span> to refresh · cyberpunk-terminal UI</footer>
<script>
const NEON={{cyan:'#00ffc8',mag:'#ff2bd6',lime:'#b6ff3a',amber:'#ffb000',red:'#ff3b5c',blue:'#5b8cff'}};
new Chart(document.getElementById('bar'),{{
type:'bar',
data:{{labels:{bar_labels},
datasets:[
{{label:'Score /100',data:{bar_score},backgroundColor:'rgba(0,255,200,0.85)',borderColor:NEON.cyan,borderRadius:3,yAxisID:'y'}},
{{label:'tok/sec',data:{bar_speed},backgroundColor:'rgba(255,43,214,0.7)',borderColor:NEON.mag,borderRadius:3,yAxisID:'y1'}}
]}},
options:{{maintainAspectRatio:false,responsive:true,
plugins:{{legend:{{labels:{{color:'#d7e0e6',font:{{family:'Fira Code'}}}}}}}},
scales:{{
x:{{ticks:{{color:'#7a8590',font:{{family:'Fira Code',size:10}}}},grid:{{color:'rgba(255,255,255,0.05)'}}}},
y:{{position:'left',max:100,title:{{display:true,text:'Score',color:'#00ffc8'}},ticks:{{color:'#7a8590'}},grid:{{color:'rgba(255,255,255,0.05)'}}}},
y1:{{position:'right',title:{{display:true,text:'tok/sec',color:'#ff2bd6'}},grid:{{drawOnChartArea:false}},ticks:{{color:'#7a8590'}}}}
}}}}
}});
new Chart(document.getElementById('radar'),{{
type:'radar',
data:{{labels:{radar_labels},datasets:{radar_json}}},
options:{{maintainAspectRatio:false,responsive:true,
plugins:{{legend:{{labels:{{color:'#d7e0e6',font:{{family:'Fira Code',size:11}}}}}}}},
scales:{{r:{{min:0,max:20,
angleLines:{{color:'rgba(0,255,200,0.15)'}},
grid:{{color:'rgba(0,255,200,0.12)'}},
pointLabels:{{color:'#d7e0e6',font:{{family:'Fira Code',size:10}}}},
ticks:{{color:'#7a8590',backdropColor:'transparent',stepSize:5}}
}}}}
}}
}});
</script>
{FOOT}"""
return body
def render_detail(m, data):
col, chip = verdict_meta(m["verdict"])
# derive "what went right" from high pillars, "wrong" from low + critical_bugs
bd = m["breakdown"]
ranked = sorted(PILLARS, key=lambda p: -bd[p])
rights = [f"{PILLAR_LABELS[p]} ({bd[p]}/20)" for p in ranked if bd[p] >= 16]
wrongs_pillars = [f"{PILLAR_LABELS[p]} ({bd[p]}/20)" for p in ranked if bd[p] <= 13]
pillar_bars = ""
for p in PILLARS:
v = bd[p]
c = NEON_LIME if v >= 17 else (NEON_AMBER if v >= 13 else NEON_RED)
pillar_bars += f"""
<div class="prow">
<div class="plabel">{PILLAR_LABELS[p]}</div>
<div class="pbar"><i style="width:{v/20*100}%;background:{c};color:{c}"></i></div>
<div class="pval mono" style="color:{c}">{v}<span style="color:var(--dim)">/20</span></div>
</div>"""
bugs_html = "".join(f"<li>{esc(b)}</li>" for b in m.get("critical_bugs", []))
patch = m.get("patch_code", "")
metrics_block = ""
if m.get("tok_sec") is not None:
metrics_block = f"""
<div class="mini"><span class="mlbl">tok/sec</span><span class="mval mono" style="color:var(--mag)">{m['tok_sec']:.2f}</span></div>
<div class="mini"><span class="mlbl">tokens</span><span class="mval mono">{m.get('total_tokens') or ''}</span></div>
<div class="mini"><span class="mlbl">TTFT</span><span class="mval mono">{m.get('ttft_sec'):.2f}s</span> </div>"""
else:
metrics_block = '<div class="mini" style="grid-column:1/-1"><span class="mlbl">runtime</span><span class="mval mono" style="color:var(--blue)">CLOUD — not measured</span></div>'
caveat_html = f'<div class="caveat-box">⚠ SPEED CAVEAT: {esc(m["speed_caveat"])}</div>' if m.get("speed_caveat") else ""
tests_html = '<span class="ok">PASS</span>' if m.get("tests_pass") else '<span class="bad">CRASH</span>'
cloud_tag = '<span class="cloud-tag">CLOUD BASELINE</span>' if m.get("format") == "cloud" else ""
rights_html = "".join(f"<li>{esc(r)}</li>" for r in rights) or "<li class='dim'>No pillar reached 16+ — no standout strengths.</li>"
wrongs_html = "".join(f"<li>{esc(w)}</li>" for w in wrongs_pillars) or "<li class='dim'>No pillar fell below 14 — solid across the board.</li>"
extra_css = """
.mini-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:14px 0}
.mini{background:var(--panel2);border:1px solid rgba(255,255,255,0.06);border-radius:5px;padding:10px 12px}
.mlbl{display:block;color:var(--dim);font-size:.62rem;letter-spacing:.14em;text-transform:uppercase}
.mval{display:block;font-size:1.15rem;margin-top:3px}
.caveat-box{background:rgba(255,176,0,0.08);border:1px solid rgba(255,176,0,0.4);color:var(--amber);padding:10px 14px;border-radius:5px;font-size:.82rem;margin:12px 0}
.pillar-list{margin-top:8px}
.prow{display:grid;grid-template-columns:170px 1fr 56px;align-items:center;gap:12px;margin:9px 0}
.plabel{color:var(--ink);font-size:.84rem}
.pbar{height:9px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden}
.pbar > i{display:block;height:100%;border-radius:2px}
.pval{text-align:right;font-size:.92rem}
.ok{color:var(--lime);border:1px solid var(--lime);padding:1px 8px;border-radius:3px;font-size:.7rem}
.bad{color:var(--red);border:1px solid var(--red);padding:1px 8px;border-radius:3px;font-size:.7rem}
.section{background:var(--panel);border:1px solid rgba(255,255,255,0.07);border-radius:8px;padding:18px;margin-top:18px}
.section h2{font-size:.9rem;letter-spacing:.12em;text-transform:uppercase;color:var(--cyan);margin:0 0 12px;text-shadow:0 0 10px rgba(0,255,200,0.35)}
ul.clean{list-style:none;padding:0;margin:0}
ul.clean li{padding:7px 0 7px 18px;border-bottom:1px solid rgba(255,255,255,0.05);position:relative;font-size:.88rem}
ul.clean li::before{content:"";position:absolute;left:0;color:var(--cyan)}
ul.clean li.dim{color:var(--dim)}
ul.clean li.dim::before{color:var(--dim)}
.bugs li::before{content:"";color:var(--red)}
.rights li::before{content:"";color:var(--lime)}
pre.code{background:#06060a;border:1px solid rgba(0,255,200,0.2);border-radius:6px;padding:14px;overflow-x:auto;color:#cfe;font-size:.78rem;line-height:1.5;box-shadow:inset 0 0 30px rgba(0,0,0,0.6)}
pre.code::before{content:"// patch.py";display:block;color:var(--dim);font-size:.68rem;margin-bottom:8px}
.back{display:inline-block;margin-bottom:18px;font-size:.8rem}
.verdict-chip-lg{display:inline-block;padding:4px 14px;border-radius:3px;border:1px solid currentColor;font-size:.78rem;letter-spacing:.1em}
.best-box{background:linear-gradient(135deg,rgba(0,255,200,0.08),rgba(91,139,255,0.05));border:1px solid rgba(0,255,200,0.25);border-radius:8px;padding:16px 18px;font-size:.95rem;line-height:1.6}
@media(max-width:640px){.prow{grid-template-columns:1fr auto}.mini-grid{grid-template-columns:repeat(2,1fr)}}
"""
head = head_html(m["model_name"])
# inject extra css before </style>
head = head.replace("</style>", extra_css + "</style>")
return f"""
{head}
<a class="back" href="../dashboard.html">◂ BACK TO LEADERBOARD</a>
<header class="hud-bar">
<div class="kicker">▚ MODEL AUDIT // {esc(m['quant'])}</div>
<h1>// {esc(m['model_name'])} {cloud_tag}</h1>
<div style="margin-top:8px;display:flex;gap:14px;align-items:center;flex-wrap:wrap">
<span class="verdict-chip-lg" style="color:{col}">{chip}{esc(m['verdict'])}</span>
<span class="mono" style="font-size:1.5rem;color:{col}">{m['total_score']}<span style="color:var(--dim);font-size:.8rem">/100</span></span>
<span>tests: {tests_html}</span>
</div>
</header>
{caveat_html}
<div class="mini-grid">{metrics_block}</div>
<div class="section">
<h2>▮ PILLAR BREAKDOWN</h2>
<div class="pillar-list">{pillar_bars}</div>
</div>
<div class="grid2">
<div class="section" style="margin-top:0">
<h2 style="color:var(--lime);text-shadow:0 0 10px rgba(182,255,58,0.4)">✓ WENT RIGHT</h2>
<ul class="clean rights">{rights_html}</ul>
</div>
<div class="section" style="margin-top:0">
<h2 style="color:var(--red);text-shadow:0 0 10px rgba(255,59,92,0.4)">✗ WENT WRONG</h2>
<ul class="clean wrongs">{wrongs_html}</ul>
</div>
</div>
<div class="section">
<h2>▮ CRITICAL BUGS</h2>
<ul class="clean bugs">{bugs_html or '<li class="dim">None flagged.</li>'}</ul>
</div>
<div class="section">
<h2>▮ RECOMMENDED USE</h2>
<div class="best-box">{esc(m['best_for'])}</div>
</div>
<div class="section">
<h2>▮ REFACTORED PATCH</h2>
<pre class="code">{esc(patch)}</pre>
</div>
<footer>Source: <span class="mono">{esc(m['filename'])}</span> · graded {esc(m.get('timestamp','')[:10])}</footer>
</div>
{FOOT}"""
def main():
with open(DATA) as f:
data = json.load(f)
os.makedirs(PAGES_DIR, exist_ok=True)
with open(OUT_DASH, "w") as f:
f.write(render_dashboard(data))
for m in data["models"]:
with open(os.path.join(PAGES_DIR, m["id"] + ".html"), "w") as f:
f.write(render_detail(m, data))
print(f"OK: dashboard.html + {len(data['models'])} pages in pages/")
if __name__ == "__main__":
main()
@@ -0,0 +1,262 @@
import asyncio
import time
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
@dataclass
class Node:
key: str
value: Any
ttl_expiry: float
freq: int = 1
def __repr__(self):
return f"Node({self.key}, freq={self.freq})"
class DoublyLinkedListList:
def __init__(self):
self.head = Node("", "", 0) # Dummy head
self.tail = Node("", "", 0) # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
def add_to_front(self, node: Node):
node.next = self.head.next
node.prev = self.head
self.head.next = node
node.next.prev = node
def remove(self, node: Node):
node.prev.next = node.next
node.next.prev = node.prev
def move_to_front(self, node: Node):
self.remove(node)
self.add_to_front(node)
def pop_tail(self) -> Optional[Node]:
if self.tail == self.tail.prev: # Only dummy tail left
return None
node = self.tail.prev
self.remove(node)
return node
class ConcurrentLFUCache:
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("Capacity must be positive")
self.capacity = capacity
self.cache: Dict[str, Node] = {} # key -> node
self.freq_buckets: Dict[int, DoublyLinkedListList] = {} # freq -> list
self.min_freq = 0
self.global_lock = asyncio.Lock() # Protects global cache structure
self.evictor_task: Optional[asyncio.Task] = None
def _update_freq(self, node: Node):
old_freq = node.freq
bucket = self.freq_buckets[old_freq]
bucket.remove(node)
if old_freq == self.min_freq and bucket.tail == bucket.head:
self.min_freq += 1
node.freq += 1
if node.freq not in self.freq_buckets:
self.freq_buckets[node.freq] = DoublyLinkedListList()
bucket = self.freq_buckets[node.freq]
bucket.add_to_front(node)
async def get(self, key: str) -> Optional[Any]:
now = time.time()
async with self.global_lock:
node = self.cache.get(key)
if not node:
return None
if now > node.ttl_expiry: # Lazy eviction
bucket = self.freq_buckets[node.freq]
bucket.remove(node)
del self.cache[key]
return None
self._update_freq(node)
return node.value
async def put(self, key: str, value: Any, ttl_seconds: float):
now = time.time()
expiry = now + ttl_seconds
async with self.global_lock:
if key in self.cache:
node = self.cache[key]
bucket = self.freq_buckets[node.freq]
bucket.remove(node)
node.value = value
node.ttl_expiry = expiry
self._update_freq(node)
else:
if len(self.cache) >= self.capacity:
bucket = self.freq_buckets[self.min_freq]
evicted = bucket.pop_tail()
if evicted:
del self.cache[evicted.key]
new_node = Node(key, value, expiry)
self.cache[key] = new_node
if self.min_freq == 0:
self.min_freq = 1
bucket = self.freq_buckets[self.min_freq]
bucket.add_to_front(new_node)
async def start_evictor(self, interval: float = 1.0):
def evict_loop():
while True:
time.sleep(interval)
now = time.time()
expired_keys = [k for k, n in self.cache.items() if now > n.ttl_expiry]
for k in expired_keys:
asyncio.run(self.put(k, self.cache[k].value, 0)) # Re-insert with 0 TTL
self.evictor_task = asyncio.create_task(evict_loop())
async def stop_evictor(self):
if self.evictor_task:
self.evictor_task.cancel()
class Transaction:
def __init__(self, cache):
self.cache = cache
self.local_cache: Dict[str, Node] = {} # key -> node
self.local_freq: Dict[int, DoublyLinkedListList] = {} # freq -> list
self.local_min_freq = 0
def _update_local_freq(self, node: Node):
old_freq = node.freq
bucket = self.local_freq[old_freq]
bucket.remove(node)
if old_freq == self.local_min_freq and bucket.tail == bucket.head:
self.local_min_freq += 1
node.freq += 1
if node.freq not in self.local_freq:
self.local_freq[node.freq] = DoublyLinkedListList()
bucket = self.local_freq[node.freq]
bucket.add_to_front(node)
async def get(self, key: str) -> Optional[Any]:
if key in self.local_cache:
node = self.local_cache[key]
self._update_local_freq(node)
return node.value
async with self.cache.global_lock:
node = self.cache.cache.get(key)
if not node:
return None
# Copy the node so local changes don't affect global cache until commit
new_node = Node(key, node.value, node.ttl_expiry, node.freq)
self.local_cache[key] = new_node
if self.local_min_freq == 0:
self.local_min_freq = new_node.freq
bucket = self.local_freq[new_node.freq]
bucket.add_to_front(new_node)
return new_node.value
async def put(self, key: str, value: Any, ttl_seconds: float):
now = time.time()
expiry = now + ttl_seconds
if key in self.local_cache:
node = self.local_cache[key]
bucket = self.local_freq[node.freq]
bucket.remove(node)
node.value = value
node.ttl_expiry = expiry
self._update_local_freq(node)
else:
if len(self.local_cache) >= self.cache.capacity:
bucket = self.local_freq[self.local_min_freq]
evicted = bucket.pop_tail()
if evicted:
del self.local_cache[evicted.key]
new_node = Node(key, value, expiry)
self.local_cache[key] = new_node
if self.local_min_freq == 0:
self.local_min_freq = new_node.freq
bucket = self.local_freq[new_node.freq]
bucket.add_to_front(new_node)
async def commit(self):
async with self.cache.global_lock:
for key, node in self.local_cache.items():
if key in self.cache.cache:
old_node = self.cache.cache[key]
bucket = self.cache.freq_buckets[old_node.freq]
bucket.remove(old_node)
self.cache.cache[key] = node
if old_node.freq == self.cache.min_freq and bucket.tail == bucket.head:
self.cache.min_freq += 1
node.freq = node.freq # freq is already updated in local_cache
bucket = self.cache.freq_buckets[node.freq]
if node.freq not in self.cache.freq_buckets:
self.cache.freq_buckets[node.freq] = DoublyLinkedListList()
bucket.add_to_front(node)
async def rollback(self):
self.local_cache = {}
self.local_freq = {}
self.local_min_freq = 0
async def main():
cache = ConcurrentLFUCache(capacity=3)
# a. O(1) LFU eviction order
await cache.put("a", "val_a", 60) # freq=1
await cache.put("b", "val_b", 60) # freq=1
await cache.put("c", "val_c", 60) # freq=1
await cache.get("a") # a:freq=2, b/c:freq=1
await cache.get("b") # a:freq=2, b:freq=2, c:freq=1
await cache.put("d", "val_d", 60) # c is evicted (lowest freq=1)
assert await cache.get("c") is None
# b. Lazy vs Background eviction
await cache.put("e", "val_e", 0) # expire immediately
assert await cache.get("e") is None # lazy eviction
# c. Transaction commit vs rollback
tx = await cache.begin_transaction() # helper below
await tx.put("f", "val_f", 60)
assert await tx.get("f") == "val_f" # Read Your Own Writes
assert await cache.get("f") is None # Global not yet committed
await tx.rollback()
assert await cache.get("f") is None # Rollback restored
await tx.commit()
assert await cache.get("f") == "val_f" # Committed
# d. Stress test
async def worker(i):
await cache.put(f"key_{i}", i, 10)
await cache.get(f"key_{i}")
tasks = [worker(i) for i in range(50)]
await asyncio.gather(*tasks)
def begin_transaction(): # Helper for the test suite
return Transaction(ConcurrentLFUCache(capacity=3))
if __name__ == "__main__":
asyncio.run(main())
+343
View File
@@ -0,0 +1,343 @@
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, Generic, TypeVar, Set
T = TypeVar("T")
# --- Internal Data Structures for O(1) LFU ---
@dataclass
class Node:
"""A node in the LFU frequency doubly linked list."""
key: Any
value: Any
freq: int = 1
expiry: Optional[float] = None
prev: Optional['Node'] = None
next: Optional['Node'] = None
class DoublyLinkedList:
"""A standard DLL to maintain nodes of the same frequency."""
def __init__(self):
self.head: Optional[Node] = None
self.tail: Optional[Node] = None
self.size = 0
def append(self, node: Node):
"""Add node to the front (most recently used in this freq bucket)."""
node.next = self.head
node.prev = None
if self.head:
self.head.prev = node
self.head = node
if not self.tail:
self.tail = node
self.size += 1
def pop_tail(self) -> Optional[Node]:
"""Remove and return the least recently used node in this freq bucket."""
if not self.tail:
return None
node = self.tail
self.remove(node)
return node
def remove(self, node: Node):
"""Remove a specific node from the list in O(1)."""
if node.prev:
node.prev.next = node.next
else:
self.head = node.next
if node.next:
node.next.prev = node.prev
else:
self.tail = node.prev
node.next = None
node.prev = None
self.size -= 1
# --- Cache Implementation ---
class LFUCache:
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("Capacity must be greater than 0")
self.capacity = capacity
self.cache: Dict[Any, Node] = {} # key -> Node
self.freq_map: Dict[int, DoublyLinkedList] = {} # freq -> DLL
self.min_freq = 0
self.lock = asyncio.Lock()
self._evictor_task: Optional[asyncio.Task] = None
def _get_now(self) -> float:
return time.time()
def _is_expired(self, node: Node) -> bool:
return node.expiry is not None and self._get_now() > node.expiry
async def _update_freq(self, node: Node):
"""Moves a node to the next frequency bucket in O(1)."""
old_freq = node.freq
dll = self.freq_map[old_freq]
dll.remove(node)
if old_freq == self.min_freq and dll.size == 0:
self.min_freq += 1
node.freq += 1
if node.freq not in self.freq_map:
self.freq_map[node.freq] = DoublyLinkedList()
self.freq_map[node.freq].append(node)
async def get(self, key: Any) -> Optional[Any]:
"""O(1) Access with Lazy TTL check."""
async with self.lock:
if key not in self.cache:
return None
node = self.cache[key]
if self._is_expired(node):
await self._delete_node(node)
return None
await self._update_freq(node)
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
"""O(1) Insertion with LFU eviction and Lazy TTL check."""
async with self.lock:
expiry = self._get_now() + ttl_seconds if ttl_seconds else None
if key in self.cache:
node = self.cache[key]
node.value = value
node.expiry = expiry
await self._update_freq(node)
return
# Evict if capacity reached
if len(self.cache) >= self.capacity:
# Priority 1: Try to find an expired key first (optimistic)
# Otherwise: Evict the LFU (min_freq tail)
evicted = self.freq_map[self.min_freq].pop_tail()
if evicted:
del self.cache[evicted.key]
# Create new node
new_node = Node(key=key, value=value, expiry=expiry)
self.cache[key] = new_node
self.min_freq = 1
if 1 not in self.freq_map:
self.freq_map[1] = DoublyLinkedList()
self.freq_map[1].append(new_node)
async def delete(self, key: Any):
"""O(1) deletion."""
async with self.lock:
if key in self.cache:
await self._delete_node(self.cache[key])
async def _delete_node(self, node: Node):
"""Internal helper to remove node from all tracking structures."""
dll = self.freq_map.get(node.freq)
if dll:
dll.remove(node)
if node.key in self.cache:
del self.cache[node.key]
# --- Background Evictor ---
async def start_evictor(self, interval: float = 1.0, batch_size: int = 100):
"""Starts a non-blocking background task to purge expired keys."""
if self._evictor_task:
return
async def evict_loop():
while True:
await asyncio.sleep(interval)
# Collect keys to check outside the lock to prevent blocking reads
keys_to_check = list(self.cache.keys())
# Process in small batches to yield to the event loop
for i in range(0, len(keys_to_check), batch_size):
batch = keys_to_check[i : i + batch_size]
async with self.lock:
for k in batch:
node = self.cache.get(k)
if node and self._is_expired(node):
await self._delete_node(node)
# Yield control back to asyncio
await asyncio.sleep(0)
self._evictor_task = asyncio.create_task(evict_loop())
async def stop_evictor(self):
"""Stops the background eviction task."""
if self._evictor_task:
self._evictor_task.cancel()
try:
await self._evictor_task
except asyncio.CancelledError:
pass
self._evictor_task = None
# --- Transaction Support ---
def begin_transaction(self) -> 'Transaction':
return Transaction(self)
async def apply_transaction_changes(self, changes: 'TransactionState'):
"""Atomically apply buffered transaction writes to the global state."""
async with self.lock:
for key, action in changes.writes.items():
if action == "DELETE":
if key in self.cache:
await self._delete_node(self.cache[key])
else:
val, ttl = action
# Using put logic inside the lock
expiry = self._get_now() + ttl if ttl else None
if key in self.cache:
node = self.cache[key]
node.value = val
node.expiry = expiry
await self._update_freq(node)
else:
if len(self.cache) >= self.capacity:
evicted = self.freq_map[self.min_freq].pop_tail()
if evicted:
del self.cache[evicted.key]
new_node = Node(key=key, value=val, expiry=expiry)
self.cache[key] = new_node
self.min_freq = 1
if 1 not in self.freq_map:
self.freq_map[1] = DoublyLinkedList()
self.freq_map[1].append(new_node)
@dataclass
class TransactionState:
writes: Dict[Any, Any] = None # key -> (value, ttl) or "DELETE"
class Transaction:
def __init__(self, cache: LFUCache):
self._cache = cache
self._state = TransactionState(writes={})
self._committed = False
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
if self._committed: raise RuntimeError("Transaction already committed")
self._state.writes[key] = (value, ttl_seconds)
async def get(self, key: Any) -> Optional[Any]:
if self._committed: raise RuntimeError("Transaction already committed")
# Read-Your-Own-Writes (RYOW)
if key in self._state.writes:
action = self._state.writes[key]
if action == "DELETE":
return None
return action[0]
# Otherwise read from global cache
return await self._cache.get(key)
async def delete(self, key: Any):
if self._committed: raise RuntimeError("Transaction already committed")
self._state.writes[key] = "DELETE"
async def commit(self):
if self._committed: raise RuntimeError("Transaction already committed")
await self._cache.apply_transaction_changes(self._state)
self._committed = True
async def rollback(self):
if self._committed: raise RuntimeError("Transaction already committed")
self._state.writes.clear()
self._committed = True
# --- Executable Unit Tests ---
async def main():
print("🚀 Starting LFU Cache Suite...")
# 1. Test O(1) LFU Eviction
print("\nTesting LFU Eviction Order...", end=" ")
cache = LFUCache(capacity=2)
await cache.put("a", 1)
await cache.put("b", 2)
await cache.get("a") # freq(a)=2, freq(b)=1
await cache.put("c", 3) # b should be evicted
assert await cache.get("b") is None
assert await cache.get("a") == 1
assert await cache.get("c") == 3
print("✅ PASSED")
# 2. Test Dual-Layer TTL
print("\nTesting TTL Eviction...", end=" ")
ttl_cache = LFUCache(capacity=10)
await ttl_cache.put("lazy", "val", ttl_seconds=0.1)
await ttl_cache.put("async", "val", ttl_seconds=0.1)
# Lazy check
await asyncio.sleep(0.2)
assert await ttl_cache.get("lazy") is None, "Lazy eviction failed"
# Background check
await ttl_cache.start_evictor(interval=0.1, batch_size=10)
await asyncio.sleep(0.2)
async with ttl_cache.lock:
assert "async" not in ttl_cache.cache, "Background evictor failed"
await ttl_cache.stop_evictor()
print("✅ PASSED")
# 3. Test Atomic Transactions
print("\nTesting Atomic Transactions...", end=" ")
tx_cache = LFUCache(capacity=10)
await tx_cache.put("base", 100)
# Test Commit
tx1 = tx_cache.begin_transaction()
await tx1.put("base", 200)
await tx1.put("new", 300)
assert await tx1.get("base") == 200 # RYOW
assert await tx_cache.get("base") == 100 # Isolation
await tx1.commit()
assert await tx_cache.get("base") == 200
assert await tx_cache.get("new") == 300
# Test Rollback
tx2 = tx_cache.begin_transaction()
await tx2.put("base", 999)
await tx2.get("base")
await tx2.rollback()
assert await tx_cache.get("base") == 200
print("✅ PASSED")
# 4. Stress Test: Concurrent Async Tasks
print("\nStress Testing 50 concurrent tasks...", end=" ")
stress_cache = LFUCache(capacity=20)
async def worker(worker_id):
for i in range(100):
key = f"key_{i % 30}" # Contention on keys
if i % 3 == 0:
await stress_cache.put(key, f"val_{worker_id}_{i}")
elif i % 3 == 1:
await stress_cache.get(key)
else:
tx = stress_cache.begin_transaction()
await tx.put(key, f"tx_{worker_id}_{i}")
await tx.commit()
await asyncio.gather(*(worker(i) for i in range(50)))
print("✅ PASSED")
print("\n✨ All tests passed successfully!")
if __name__ == "__main__":
asyncio.run(main())
+339
View File
@@ -0,0 +1,339 @@
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, Generic, TypeVar, Set
T = TypeVar("T")
@dataclass
class Node:
"""A node in the LFU doubly linked list."""
key: Any
value: Any
freq: int = 1
expiry: float = float('inf')
prev: Optional['Node'] = None
next: Optional['Node'] = None
class DoublyLinkedList:
"""Standard O(1) doubly linked list for maintaining frequency buckets."""
def __init__(self):
self.head = Node(None, None) # Sentinel head
self.tail = Node(None, None) # Sentinel tail
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def append(self, node: Node):
"""Adds a node to the end (most recently used in this frequency)."""
node.next = self.tail
node.prev = self.tail.prev
self.tail.prev.next = node
self.tail.prev = node
self.size += 1
def remove(self, node: Node):
"""Removes a specific node from the list."""
if node.prev:
node.prev.next = node.next
if node.next:
node.next.prev = node.prev
node.prev = None
node.next = None
self.size -= 1
def pop_front(self) -> Optional[Node]:
"""Removes and returns the oldest node (LFU candidate)."""
if self.size == 0:
return None
node = self.head.next
self.remove(node)
return node
def is_empty(self) -> bool:
return self.size == 0
class LFUCache:
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions.
Time Complexity: O(1) for get and put.
Space Complexity: O(N).
"""
def __init__(self, capacity: int):
if capacity <= 0:
raise ValueError("Capacity must be greater than 0")
self.capacity = capacity
self.cache: Dict[Any, Node] = {} # Key -> Node
self.freq_map: Dict[int, DoublyLinkedList] = {} # Freq -> DLL
self.min_freq = 0
self._lock = asyncio.Lock()
self._evictor_task: Optional[asyncio.Task] = None
async def get(self, key: Any) -> Optional[Any]:
async with self._lock:
return await self._get_internal(key)
async def _get_internal(self, key: Any) -> Optional[Any]:
"""Internal get without lock for transaction use."""
if key not in self.cache:
return None
node = self.cache[key]
# Lazy Eviction check
if time.time() > node.expiry:
await self._delete_internal(key)
return None
self._update_frequency(node)
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
async with self._lock:
await self._put_internal(key, value, ttl_seconds)
async def _put_internal(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
"""Internal put without lock for transaction use."""
expiry = time.time() + ttl_seconds if ttl_seconds is not None else float('inf')
if key in self.cache:
node = self.cache[key]
node.value = value
node.expiry = expiry
self._update_frequency(node)
else:
# Capacity Management
if len(self.cache) >= self.capacity:
await self._evict_lfu()
new_node = Node(key, value, freq=1, expiry=expiry)
self.cache[key] = new_node
self._add_to_freq_bucket(1, new_node)
self.min_freq = 1
async def delete(self, key: Any):
async with self._lock:
await self._delete_internal(key)
async def _delete_internal(self, key: Any):
"""Internal delete without lock for transaction use."""
if key in self.cache:
node = self.cache.pop(key)
self.freq_map[node.freq].remove(node)
# Note: We don't strictly need to update min_freq here because
# _evict_lfu handles empty buckets by incrementing.
def _update_frequency(self, node: Node):
"""Moves a node to the next frequency bucket in O(1)."""
old_freq = node.freq
self.freq_map[old_freq].remove(node)
if old_freq == self.min_freq and self.freq_map[old_freq].is_empty():
self.min_freq += 1
node.freq += 1
self._add_to_freq_bucket(node.freq, node)
def _add_to_freq_bucket(self, freq: int, node: Node):
if freq not in self.freq_map:
self.freq_map[freq] = DoublyLinkedList()
self.freq_map[freq].append(node)
async def _evict_lfu(self):
"""Evicts the least frequently used (and oldest within that freq) item."""
while self.min_freq not in self.freq_map or self.freq_map[self.min_freq].is_empty():
# This handles cases where items were deleted manually
if not self.cache: return
self.min_freq += 1 # Search for the next available freq bucket
evicted_node = self.freq_map[self.min_freq].pop_front()
if evicted_node:
del self.cache[evicted_node.key]
# --- Background Evictor ---
async def start_evictor(self, interval: float = 1.0):
"""Starts the background task that purges expired keys."""
if self._evictor_task is not None:
return
self._evictor_task = asyncio.create_task(self._eviction_loop(interval))
async def stop_evictor(self):
"""Stops the background eviction task."""
if self._evictor_task:
self._evictor_task.cancel()
try:
await self._evictor_task
except asyncio.CancelledError:
pass
self._evictor_task = None
async def _eviction_loop(self, interval: float):
while True:
await asyncio.sleep(interval)
# Process in small batches to avoid blocking the main lock for too long
now = time.time()
keys_to_check = list(self.cache.keys())
batch_size = 100
for i in range(0, len(keys_to_check), batch_size):
async with self._lock:
batch = keys_to_check[i : i + batch_size]
for k in batch:
if k in self.cache and now > self.cache[k].expiry:
await self._delete_internal(k)
# --- Transaction Logic ---
def begin_transaction(self):
return Transaction(self)
class Transaction:
"""
Provides ACID-like atomic updates to the LFUCache.
Implements 'Read Your Own Writes' (RYOW).
"""
def __init__(self, cache: LFUCache):
self._cache = cache
self._pending_puts: Dict[Any, tuple[Any, Optional[float]]] = {}
self._pending_deletes: Set[Any] = set()
self._committed = False
async def get(self, key: Any) -> Optional[Any]:
if self._committed: raise RuntimeError("Transaction already committed")
# 1. Check local deletes first
if key in self._pending_deletes:
return None
# 2. Check local writes (RYOW)
if key in self._pending_puts:
return self._pending_puts[key][0]
# 3. Fallback to global cache
return await self._cache.get(key)
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
if self._committed: raise RuntimeError("Transaction already committed")
self._pending_deletes.discard(key)
self._pending_puts[key] = (value, ttl_seconds)
async def delete(self, key: Any):
if self._committed: raise RuntimeError("Transaction already committed")
self._pending_puts.pop(key, None)
self._pending_deletes.add(key)
async def commit(self):
"""Applies all pending changes to the global cache atomically."""
if self._committed: return
async with self._cache._lock:
# Apply deletes first
for key in self._pending_deletes:
await self._cache._delete_internal(key)
# Apply puts
for key, (val, ttl) in self._pending_puts.items():
await self._cache._put_internal(key, val, ttl)
self._committed = True
async def rollback(self):
"""Discards all pending changes."""
self._pending_puts.clear()
self._pending_deletes.clear()
self._committed = True
# ==========================================
# Unit Tests
# ==========================================
async def main():
print("Starting LFU Cache Comprehensive Test Suite...")
# 1. Test O(1) LFU Eviction Order
print("\nTesting LFU Eviction Order...", end=" ")
cache = LFUCache(capacity=2)
await cache.put("a", 1) # freq 1
await cache.put("b", 2) # freq 1
await cache.get("a") # freq 2
await cache.put("c", 3) # "b" should be evicted as it's the LFU (freq 1)
assert await cache.get("b") is None, "Key 'b' should have been evicted"
assert await cache.get("a") == 1, "Key 'a' should still exist"
assert await cache.get("c") == 3, "Key 'c' should still exist"
print("PASSED")
# 2. Test TTL Eviction (Lazy & Background)
print("Testing Dual-Layer TTL...", end=" ")
cache = LFUCache(capacity=10)
await cache.start_evictor(interval=0.1)
await cache.put("lazy", "val", ttl_seconds=0.1)
await asyncio.sleep(0.2)
# Lazy check: get() should trigger deletion
assert await cache.get("lazy") is None, "Lazy eviction failed"
await cache.put("bg", "val", ttl_seconds=0.1)
await asyncio.sleep(0.3)
# Background check: key should be gone from internal map without calling get()
async with cache._lock:
assert "bg" not in cache.cache, "Background eviction failed"
await cache.stop_evictor()
print("PASSED")
# 3. Test Atomic Transactions (Commit vs Rollback)
print("Testing Transaction Isolation...", end=" ")
cache = LFUCache(capacity=10)
await cache.put("shared", "original")
tx = cache.begin_transaction()
await tx.put("shared", "modified")
await tx.put("new", "added")
# Verify Isolation: Global reader shouldn't see changes
assert await cache.get("shared") == "original"
assert await cache.get("new") is None
# Verify RYOW: Transaction should see its own writes
assert await tx.get("shared") == "modified"
assert await tx.get("new") == "added"
await tx.commit()
assert await cache.get("shared") == "modified", "Commit failed to update global state"
assert await cache.get("new") == "added", "Commit failed to add new key"
tx2 = cache.begin_transaction()
await tx2.put("shared", "rollback_this")
assert await tx2.get("shared") == "rollback_this"
await tx2.rollback()
assert await cache.get("shared") == "modified", "Rollback mutated global state"
print("PASSED")
# 4. Stress Test: Concurrent Access
print("Testing High Concurrency (50 tasks)...", end=" ")
cache = LFUCache(capacity=100)
async def worker(worker_id):
for i in range(100):
key = f"key_{i % 20}" # Create contention on keys
if i % 3 == 0:
await cache.put(key, f"val_{worker_id}_{i}", ttl_seconds=0.5)
elif i % 3 == 1:
await cache.get(key)
else:
tx = cache.begin_transaction()
await tx.put(f"tx_{worker_id}", i)
await tx.commit()
tasks = [worker(i) for i in range(50)]
await asyncio.gather(*tasks)
print("PASSED")
print("\nAll tests passed successfully!")
if __name__ == "__main__":
asyncio.run(main())
+550
View File
@@ -0,0 +1,550 @@
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions.
Pure Python 3.11+ implementation — no third-party dependencies.
"""
from __future__ import annotations
import asyncio
import time
import weakref
from dataclasses import dataclass, field
from typing import Any, Optional
# ---------------------------------------------------------------------------
# Doubly-Linked List (O(1) move/remove/add)
# ---------------------------------------------------------------------------
@dataclass
class _DLLNode:
"""Element inside a frequency bucket's doubly-linked list."""
key: Any
value: Any
ttl_seconds: float
expiry_time: float
freq: int = 0
prev: Optional[_DLLNode] = field(default=None, repr=False)
next: Optional[_DLLNode] = field(default=None, repr=False)
class _DoublyLinkedList:
"""
Doubly-linked list with sentinel head/tail for O(1) prepend, pop, and remove.
The *tail* side holds the least-recently-used node within a frequency tier,
which is also the LFU candidate for eviction.
"""
def __init__(self) -> None:
self._head = _DLLNode(None, None, 0.0, 0.0) # sentinel
self._tail = _DLLNode(None, None, 0.0, 0.0) # sentinel
self._head.next = self._tail
self._tail.prev = self._head
self.size = 0
# -- internal helpers ---------------------------------------------------
def _insert_after(self, node: _DLLNode, prev: _DLLNode) -> None:
nxt = prev.next
prev.next = node
node.prev = prev
node.next = nxt
nxt.prev = node
def _unlink(self, node: _DLLNode) -> None:
prev, nxt = node.prev, node.next
prev.next = nxt
nxt.prev = prev
node.prev = node.next = None
# -- public API ---------------------------------------------------------
def push_front(self, node: _DLLNode) -> None:
"""Insert *node* right after the head sentinel (most-recent)."""
self._insert_after(node, self._head)
self.size += 1
def pop_tail(self) -> Optional[_DLLNode]:
"""Remove and return the node just before the tail sentinel (LRU)."""
if self.size == 0:
return None
node = self._tail.prev
self._unlink(node)
return node
def remove(self, node: _DLLNode) -> None:
"""Remove an arbitrary node from the list."""
self._unlink(node)
def is_empty(self) -> bool:
return self.size == 0
# ---------------------------------------------------------------------------
# Cache Node (wraps the DLL node + TTL metadata)
# ---------------------------------------------------------------------------
@dataclass
class _CacheNode:
key: Any
value: Any
ttl_seconds: float
expiry_time: float
freq: int = 1
dll_node: Optional[_DLLNode] = field(default=None, repr=False)
# ---------------------------------------------------------------------------
# Transaction Handle
# ---------------------------------------------------------------------------
class Transaction:
"""
Represents an isolated sub-session on the cache.
- ``tx.put(key, value, ttl)`` buffers a write locally.
- ``tx.get(key)`` reads from local buffer first, then falls back to the
global cache (without mutating global frequency state).
- ``tx.delete(key)`` buffers a deletion locally.
- ``await tx.commit()`` atomically applies all buffered writes to the global cache.
- ``tx.rollback()`` discards everything.
"""
def __init__(self, cache: "LFUCache") -> None:
self._cache = cache
self._pending_writes: dict[Any, tuple[Any, float]] = {} # key -> (value, expiry)
self._pending_deletes: set[Any] = set()
self._committed = False
# -- read ----------------------------------------------------------------
def get(self, key: Any) -> Optional[Any]:
"""Read with read-your-own-writes semantics."""
if self._committed:
raise RuntimeError("Transaction already committed")
# 1. Check local pending writes first
if key in self._pending_writes:
return self._pending_writes[key][0]
if key in self._pending_deletes:
return None
# 2. Fall back to global cache (read-only, no frequency bump)
return self._cache._get_raw(key)
# -- write ---------------------------------------------------------------
def put(self, key: Any, value: Any, ttl_seconds: float = 60.0) -> None:
"""Buffer a write locally; not visible to others until commit."""
if self._committed:
raise RuntimeError("Transaction already committed")
expiry = time.monotonic() + ttl_seconds
self._pending_writes[key] = (value, expiry)
# If previously deleted in this txn, re-add overrides the delete.
self._pending_deletes.discard(key)
def delete(self, key: Any) -> None:
"""Buffer a deletion locally."""
if self._committed:
raise RuntimeError("Transaction already committed")
self._pending_deletes.add(key)
self._pending_writes.pop(key, None)
# -- commit / rollback ---------------------------------------------------
async def commit(self) -> None:
"""Atomically apply all buffered changes to the global cache."""
if self._committed:
raise RuntimeError("Transaction already committed")
async with self._cache._lock:
for key, (value, expiry) in self._pending_writes.items():
await self._cache._put_internal(key, value, expiry)
for key in self._pending_deletes:
await self._cache._delete_internal(key)
self._committed = True
def rollback(self) -> None:
"""Discard all pending changes."""
self._pending_writes.clear()
self._pending_deletes.clear()
self._committed = True # mark so further ops raise
# ---------------------------------------------------------------------------
# LFU Cache
# ---------------------------------------------------------------------------
class LFUCache:
"""
In-memory concurrent LFU cache with O(1) get/put, async TTL eviction,
and atomic transaction support.
"""
def __init__(self, capacity: int = 1024) -> None:
if capacity < 1:
raise ValueError("capacity must be >= 1")
self._capacity = capacity
self._lock = asyncio.Lock()
self._evictor_task: Optional[asyncio.Task[None]] = None
# Core O(1) structures
self._cache_map: dict[Any, _CacheNode] = {} # key -> CacheNode
self._freq_map: dict[int, _DoublyLinkedList] = {} # freq -> DLL
self._min_freq: int = 1
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
async def get(self, key: Any) -> Optional[Any]:
"""O(1) lookup with lazy TTL eviction."""
async with self._lock:
node = self._cache_map.get(key)
if node is None:
return None
# Lazy TTL check
if time.monotonic() > node.expiry_time:
await self._evict_node(key, node)
return None
# Bump frequency — O(1)
await self._bump_freq(node)
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: float = 60.0) -> None:
"""O(1) insert/update with lazy TTL eviction of LRU-LFU victim if needed."""
expiry = time.monotonic() + ttl_seconds
async with self._lock:
await self._put_internal(key, value, expiry)
async def delete(self, key: Any) -> bool:
"""O(1) deletion."""
async with self._lock:
return await self._delete_internal(key)
def begin_transaction(self) -> Transaction:
"""Start a new isolated transaction."""
return Transaction(self)
# ------------------------------------------------------------------
# TTL Eviction Loop
# ------------------------------------------------------------------
def start_evictor(self, interval_seconds: float = 1.0) -> None:
"""Start the background async TTL sweep task."""
if self._evictor_task is not None and not self._evictor_task.done():
return
self._evictor_task = asyncio.create_task(self._eviction_loop(interval_seconds))
def stop_evictor(self) -> None:
"""Stop the background eviction task."""
if self._evictor_task is not None:
self._evictor_task.cancel()
self._evictor_task = None
async def _eviction_loop(self, interval: float) -> None:
"""Periodically purge expired entries in small batches."""
try:
while True:
await asyncio.sleep(interval)
async with self._lock:
now = time.monotonic()
# Collect expired keys in a snapshot to avoid dict-changed-size
expired = [
k for k, n in self._cache_map.items()
if now > n.expiry_time
]
for k in expired:
node = self._cache_map.get(k)
if node is not None and now > node.expiry_time:
await self._evict_node(k, node)
except asyncio.CancelledError:
return
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
async def _get_raw(self, key: Any) -> Optional[Any]:
"""Read-only global lookup — does NOT bump frequency. For transactions."""
node = self._cache_map.get(key)
if node is None:
return None
if time.monotonic() > node.expiry_time:
await self._evict_node(key, node)
return None
return node.value
async def _put_internal(self, key: Any, value: Any, expiry: float) -> None:
"""Core put logic (must be called under lock)."""
# If key already exists, update in place
if key in self._cache_map:
node = self._cache_map[key]
# Remove from old freq list, update value/ttl/freq
self._freq_map[node.freq].remove(node.dll_node)
if self._freq_map[node.freq].is_empty():
del self._freq_map[node.freq]
if node.freq == self._min_freq:
self._min_freq += 1
node.value = value
node.ttl_seconds = expiry - time.monotonic()
node.expiry_time = expiry
node.freq = 1
self._ensure_freq_list(1).push_front(node.dll_node)
return
# Evict if at capacity
if len(self._cache_map) >= self._capacity:
await self._evict_lfu()
# Insert new node
dll_node = _DLLNode(key, value, expiry - time.monotonic(), expiry)
cache_node = _CacheNode(key, value, expiry - time.monotonic(), expiry, freq=1, dll_node=dll_node)
self._cache_map[key] = cache_node
self._freq_map[1].push_front(dll_node)
self._min_freq = 1
async def _delete_internal(self, key: Any) -> bool:
"""Core delete logic (must be called under lock)."""
node = self._cache_map.get(key)
if node is None:
return False
await self._evict_node(key, node)
return True
async def _evict_node(self, key: Any, node: _CacheNode) -> None:
"""Remove a single node from all structures."""
dll = self._freq_map.get(node.freq)
if dll is not None:
dll.remove(node.dll_node)
if dll.is_empty():
del self._freq_map[node.freq]
if node.freq == self._min_freq:
# Find new min freq
new_min = min(self._freq_map) if self._freq_map else 1
self._min_freq = new_min
del self._cache_map[key]
async def _evict_lfu(self) -> None:
"""Evict the least-frequently-used (and least-recently-used within that freq) node."""
if not self._freq_map:
return
dll = self._freq_map.get(self._min_freq)
if dll is None or dll.is_empty():
# Shouldn't happen, but safeguard
self._min_freq += 1
await self._evict_lfu()
return
victim = dll.pop_tail()
if victim is not None:
await self._evict_node(victim.key, self._cache_map[victim.key])
async def _bump_freq(self, node: _CacheNode) -> None:
"""Move a node from freq f to freq f+1. O(1)."""
old_freq = node.freq
old_dll = self._freq_map[old_freq]
old_dll.remove(node.dll_node)
if old_dll.is_empty():
del self._freq_map[old_freq]
if old_freq == self._min_freq:
self._min_freq += 1
node.freq += 1
new_dll = self._ensure_freq_list(node.freq)
new_dll.push_front(node.dll_node)
def _ensure_freq_list(self, freq: int) -> _DoublyLinkedList:
"""Return (or create) the DLL for *freq*."""
if freq not in self._freq_map:
self._freq_map[freq] = _DoublyLinkedList()
return self._freq_map[freq]
# ------------------------------------------------------------------
# Diagnostics
# ------------------------------------------------------------------
@property
def size(self) -> int:
return len(self._cache_map)
@property
def capacity(self) -> int:
return self._capacity
# ---------------------------------------------------------------------------
# Executable Test Suite
# ---------------------------------------------------------------------------
async def main() -> None:
passed = 0
failed = 0
def _check(name: str, condition: bool) -> None:
nonlocal passed, failed
if condition:
passed += 1
print(f"{name}")
else:
failed += 1
print(f"{name}")
# ======================================================================
# (a) O(1) LFU eviction order
# ======================================================================
print("\n=== (a) LFU Eviction Order ===")
cache = LFUCache(capacity=3)
# Insert 3 items
await cache.put("a", 1, ttl_seconds=60.0)
await cache.put("b", 2, ttl_seconds=60.0)
await cache.put("c", 3, ttl_seconds=60.0)
# Access "a" and "b" once each → freq=2; "c" stays at freq=1
await cache.get("a")
await cache.get("b")
# Insert "d" — should evict "c" (lowest freq)
await cache.put("d", 4, ttl_seconds=60.0)
_check("a still present after eviction", await cache.get("a") == 1)
_check("b still present after eviction", await cache.get("b") == 2)
_check("c evicted (lowest freq)", await cache.get("c") is None)
_check("d present", await cache.get("d") == 4)
# Now access "a" again → freq=3; "b" and "d" at freq=2
await cache.get("a")
# Insert "e" — should evict either "b" or "d" (both freq=2, LRU wins)
await cache.put("e", 5, ttl_seconds=60.0)
_check("a still present", await cache.get("a") == 1)
_check("e present", await cache.get("e") == 5)
# ======================================================================
# (b) Lazy TTL vs Background Async Sweep
# ======================================================================
print("\n=== (b) TTL Eviction (Lazy + Background) ===")
cache2 = LFUCache(capacity=10)
await cache2.put("lazy_key", "lazy_val", ttl_seconds=0.1)
await cache2.put("bg_key", "bg_val", ttl_seconds=0.1)
# Lazy eviction: access lazy_key after expiry
await asyncio.sleep(0.15)
_check("Lazy eviction: get returns None after TTL", await cache2.get("lazy_key") is None)
_check("Lazy eviction: bg_key still there (not accessed)", await cache2.get("bg_key") == "bg_val")
# Start background evictor
cache2.start_evictor(interval_seconds=0.2)
await asyncio.sleep(0.3) # let background sweep run
_check("Background eviction: bg_key purged by sweep", await cache2.get("bg_key") is None)
cache2.stop_evictor()
# ======================================================================
# (c) Transaction commit visibility vs rollback
# ======================================================================
print("\n=== (c) Atomic Transactions ===")
cache3 = LFUCache(capacity=10)
await cache3.put("x", 10, ttl_seconds=60.0)
await cache3.put("y", 20, ttl_seconds=60.0)
# --- Commit test ---
tx1 = cache3.begin_transaction()
tx1.put("x", 99, ttl_seconds=60.0) # local write
tx1.put("z", 30, ttl_seconds=60.0) # new key
_check("TX: read-your-own-write (x)", tx1.get("x") == 99)
_check("TX: read-your-own-write (z)", tx1.get("z") == 30)
_check("TX: global still sees old x", await cache3.get("x") == 10)
await tx1.commit()
_check("TX: after commit, global sees x=99", await cache3.get("x") == 99)
_check("TX: after commit, global sees z=30", await cache3.get("z") == 30)
# --- Rollback test ---
tx2 = cache3.begin_transaction()
tx2.put("x", -1, ttl_seconds=60.0)
tx2.delete("y")
_check("TX rollback: local sees x=-1", tx2.get("x") == -1)
_check("TX rollback: local sees y deleted", tx2.get("y") is None)
_check("TX rollback: global still sees x=99", await cache3.get("x") == 99)
_check("TX rollback: global still sees y=20", await cache3.get("y") == 20)
tx2.rollback()
_check("TX rollback: global unchanged after rollback", await cache3.get("x") == 99)
_check("TX rollback: y still present after rollback", await cache3.get("y") == 20)
# --- Double commit / rollback raises ---
tx3 = cache3.begin_transaction()
await tx3.commit()
try:
tx3.put("x", 1)
_check("TX: double commit raises", False)
except RuntimeError:
_check("TX: double commit raises", True)
tx4 = cache3.begin_transaction()
tx4.rollback()
try:
tx4.put("x", 1)
_check("TX: op after rollback raises", False)
except RuntimeError:
_check("TX: op after rollback raises", True)
# ======================================================================
# (d) Stress test: 50 concurrent async tasks
# ======================================================================
print("\n=== (d) Stress Test — 50 Concurrent Tasks ===")
cache4 = LFUCache(capacity=200)
errors: list[str] = []
async def worker(task_id: int, base_key: int) -> None:
try:
for i in range(50):
key = f"t{task_id}_k{i}"
val = task_id * 1000 + i
await cache4.put(key, val, ttl_seconds=5.0)
result = await cache4.get(key)
if result != val:
errors.append(f"task={task_id} key={key} expected={val} got={result}")
# Occasional transaction
if i % 10 == 0:
tx = cache4.begin_transaction()
tx.put(f"tx_{task_id}_{i}", val * 2, ttl_seconds=5.0)
r = tx.get(f"tx_{task_id}_{i}")
if r != val * 2:
errors.append(f"task={task_id} tx key mismatch")
await tx.commit()
except Exception as e:
errors.append(f"task={task_id} exception: {e}")
tasks = [asyncio.create_task(worker(tid, tid)) for tid in range(50)]
await asyncio.gather(*tasks)
_check("Stress: no errors", len(errors) == 0)
_check("Stress: cache size within capacity", cache4.size <= 200)
if errors:
for e in errors[:5]:
print(f" error: {e}")
# ======================================================================
# Summary
# ======================================================================
total = passed + failed
print(f"\n{'='*50}")
print(f"Results: {passed}/{total} passed, {failed} failed")
if failed == 0:
print("All tests passed! ✓")
else:
print(f"{failed} test(s) FAILED ✗")
if __name__ == "__main__":
asyncio.run(main())
+365
View File
@@ -0,0 +1,365 @@
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions.
Pure Python 3.11+ implementation using only standard library modules.
"""
import asyncio
import time
from typing import Any, Dict, List, Optional, Tuple
from asyncio import Lock
# ---------------------------------------------------------------------------
# Internal Data Structures (O(1) LFU Core)
# ---------------------------------------------------------------------------
class _Node:
"""Doubly linked list node tracking key, value, frequency, and TTL."""
__slots__ = ('key', 'value', 'freq', 'expires_at', 'prev', 'next')
def __init__(self, key: Any, value: Any, freq: int = 1, expires_at: float = 0.0):
self.key = key
self.value = value
self.freq = freq
self.expires_at = expires_at
self.prev = None
self.next = None
class _FreqList:
"""Doubly linked list maintaining nodes of a specific frequency tier."""
__slots__ = ('head', 'tail', 'size')
def __init__(self):
self.head = _Node(None, None) # Dummy head
self.tail = _Node(None, None) # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def add(self, node: _Node):
"""Add node to tail (most recently used in this frequency)."""
last = self.tail.prev
last.next = node
node.prev = last
node.next = self.tail
self.tail.prev = node
self.size += 1
def remove(self, node: _Node):
"""Remove node from the list in O(1)."""
node.prev.next = node.next
node.next.prev = node.prev
node.prev = None
node.next = None
self.size -= 1
def pop(self) -> _Node:
"""Remove and return node from head (least recently used)."""
node = self.head.next
self.remove(node)
return node
# ---------------------------------------------------------------------------
# Main Cache Implementation
# ---------------------------------------------------------------------------
class LFUCache:
"""
O(1) In-Memory Concurrent LFU Cache with Async TTL Eviction.
Uses a hash map for key lookup and a hash map of frequency-buckets
with doubly linked lists for O(1) frequency updates and eviction.
"""
def __init__(self, capacity: int, evict_interval: float = 1.0):
self.capacity = capacity
self.evict_interval = evict_interval
self.key_to_node: Dict[Any, _Node] = {}
self.freq_to_list: Dict[int, _FreqList] = {}
self.min_freq: int = 1
self.lock: Lock = asyncio.Lock()
self._evict_task: Optional[asyncio.Task] = None
self._running = False
async def start_evictor(self):
"""Start the non-blocking background TTL eviction loop."""
self._running = True
self._evict_task = asyncio.create_task(self._eviction_loop())
async def stop_evictor(self):
"""Gracefully stop the background eviction task."""
self._running = False
if self._evict_task:
self._evict_task.cancel()
try: await self._evict_task
except asyncio.CancelledError: pass
async def get(self, key: Any) -> Optional[Any]:
"""Retrieve value in O(1). Performs lazy TTL eviction and frequency update."""
async with self.lock:
node = self.key_to_node.get(key)
if not node:
return None
# Lazy TTL Eviction
if node.expires_at > 0 and node.expires_at <= time.time():
self._remove_node(node)
return None
# O(1) Frequency Update
self._update_freq(node)
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0):
"""Insert/update value in O(1). Handles capacity eviction and TTL."""
async with self.lock:
node = self.key_to_node.get(key)
if node:
# Update existing node
node.value = value
if ttl_seconds > 0:
node.expires_at = time.time() + ttl_seconds
self._update_freq(node)
else:
# Evict if capacity reached
if len(self.key_to_node) >= self.capacity:
self._evict()
# Insert new node with freq=1
node = _Node(
key, value, freq=1,
expires_at=float('inf') if ttl_seconds <= 0 else time.time() + ttl_seconds
)
self.key_to_node[key] = node
self._add_to_freq(node)
self.min_freq = 1 # New keys always start at freq 1
async def begin_transaction(self) -> 'Transaction':
"""Create an isolated transaction session."""
return Transaction(self)
# --- Internal Cache Helpers (O(1)) ---
def _remove_node(self, node: _Node):
"""Remove node from global structures."""
del self.key_to_node[node.key]
self.freq_to_list[node.freq].remove(node)
if self.freq_to_list[node.freq].size == 0:
del self.freq_to_list[node.freq]
if self.min_freq == node.freq:
self.min_freq += 1
def _update_freq(self, node: _Node):
"""Move node to next frequency bucket in O(1)."""
old_freq = node.freq
self.freq_to_list[old_freq].remove(node)
if self.freq_to_list[old_freq].size == 0:
del self.freq_to_list[old_freq]
if self.min_freq == old_freq:
self.min_freq += 1
node.freq += 1
self._add_to_freq(node)
def _add_to_freq(self, node: _Node):
"""Add node to its frequency bucket."""
if node.freq not in self.freq_to_list:
self.freq_to_list[node.freq] = _FreqList()
self.freq_to_list[node.freq].add(node)
def _evict(self):
"""Evict least frequently used (then LRU) node in O(1)."""
if not self.freq_to_list:
return
evict_list = self.freq_to_list[self.min_freq]
node = evict_list.pop()
self._remove_node(node)
async def _eviction_loop(self):
"""Background async task that purges expired keys in small batches."""
while self._running:
await asyncio.sleep(self.evict_interval)
async with self.lock:
# Batch scan limited to avoid blocking reads
keys_to_check = list(self.key_to_node.keys())[:50]
for k in keys_to_check:
node = self.key_to_node.get(k)
if node and node.expires_at > 0 and node.expires_at <= time.time():
self._remove_node(node)
# ---------------------------------------------------------------------------
# Transaction Isolation Layer
# ---------------------------------------------------------------------------
class Transaction:
"""
Atomic transaction session providing Read-Your-Own-Writes isolation.
Global readers remain blind to uncommitted changes until commit().
"""
def __init__(self, cache: LFUCache):
self.cache = cache
self.buffer: List[Dict] = []
self.committed = False
async def get(self, key: Any) -> Optional[Any]:
"""Read with local isolation. Does not update global frequencies."""
# Read-Your-Own-Writes: check buffer first
for op in reversed(self.buffer):
if op['key'] == key:
return None if op['op'] == 'delete' else op['value']
# Global read (no freq update to preserve isolation)
async with self.cache.lock:
node = self.cache.key_to_node.get(key)
if not node:
return None
# Lazy TTL check without mutation
if node.expires_at > 0 and node.expires_at <= time.time():
return None
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: float = 0.0):
"""Buffer a put operation without mutating global state."""
original = self.cache.key_to_node.get(key)
self.buffer.append({'op': 'put', 'key': key, 'value': value, 'ttl': ttl_seconds, 'original': original})
async def delete(self, key: Any):
"""Buffer a delete operation without mutating global state."""
original = self.cache.key_to_node.get(key)
self.buffer.append({'op': 'delete', 'key': key, 'original': original})
async def commit(self):
"""Apply buffered changes to global cache atomically."""
async with self.cache.lock:
# Merge buffer: keep only the latest operation per key
merged = {}
for op in self.buffer:
merged[op['key']] = op
for op in merged.values():
if op['op'] == 'put':
await self._apply_put(op['key'], op['value'], op['ttl'], self.cache.key_to_node.get(op['key']))
elif op['op'] == 'delete':
await self._apply_delete(op['key'], self.cache.key_to_node.get(op['key']))
self.buffer.clear()
self.committed = True
async def rollback(self):
"""Discard all pending changes without affecting global frequencies or TTL."""
self.buffer.clear()
self.committed = False
async def _apply_put(self, key: Any, value: Any, ttl: float, original_node: Optional[_Node]):
"""Apply buffered put to global cache."""
if original_node:
original_node.value = value
if ttl > 0:
original_node.expires_at = time.time() + ttl
self.cache._update_freq(original_node)
else:
if len(self.cache.key_to_node) >= self.cache.capacity:
self.cache._evict()
node = _Node(
key, value, freq=1,
expires_at=float('inf') if ttl <= 0 else time.time() + ttl
)
self.cache.key_to_node[key] = node
self.cache._add_to_freq(node)
self.cache.min_freq = 1
async def _apply_delete(self, key: Any, original_node: Optional[_Node]):
"""Apply buffered delete to global cache."""
if original_node:
self.cache._remove_node(original_node)
# ---------------------------------------------------------------------------
# Executable Unit Tests
# ---------------------------------------------------------------------------
async def main():
print("🧪 Running LFU Cache Test Suite...\n")
await test_lfu_eviction()
await test_ttl_eviction()
await test_transactions()
await test_stress()
print("✅ All tests passed successfully!")
async def test_lfu_eviction():
"""Verifies strict O(1) LFU eviction order when capacity is reached."""
cache = LFUCache(3)
await cache.put(1, 1)
await cache.put(2, 2)
await cache.put(3, 3)
# Access 1 -> freq becomes 2
assert await cache.get(1) == 1
# Insert 4 -> evicts key 2 (freq=1, LRU among freq=1)
await cache.put(4, 4)
assert await cache.get(2) is None, "Key 2 should be evicted"
assert await cache.get(1) == 1
assert await cache.get(3) == 3
assert await cache.get(4) == 4
print(" ✓ O(1) LFU eviction order verified")
async def test_ttl_eviction():
"""Verifies Lazy vs Background Async Sweep eviction."""
cache = LFUCache(10, evict_interval=0.1)
await cache.start_evictor()
# Lazy eviction on access
await cache.put('a', 1, ttl_seconds=0.2)
assert await cache.get('a') == 1
await asyncio.sleep(0.3)
assert await cache.get('a') is None, "Lazy eviction failed"
# Background async sweep
await cache.put('b', 2, ttl_seconds=0.1)
await asyncio.sleep(0.2) # Wait for background loop
assert await cache.get('b') is None, "Background sweep failed"
await cache.stop_evictor()
print(" ✓ Dual-layer TTL eviction (Lazy + Background) verified")
async def test_transactions():
"""Verifies Transaction commit visibility vs rollback state restoration."""
cache = LFUCache(5)
await cache.put('x', 10)
# Test 1: Isolation & Read-Your-Own-Writes
tx = await cache.begin_transaction()
await tx.put('x', 20)
assert await tx.get('x') == 20, "Transaction failed Read-Your-Own-Writes"
assert await cache.get('x') == 10, "Global cache leaked uncommitted write"
await tx.commit()
assert await cache.get('x') == 20, "Commit failed to apply changes"
# Test 2: Rollback restores state
tx2 = await cache.begin_transaction()
await tx2.put('y', 30)
await tx2.rollback()
assert await cache.get('y') is None, "Rollback failed to discard changes"
print(" ✓ Transaction commit/rollback isolation verified")
async def test_stress():
"""Stress test with 50 concurrent async tasks reading/writing simultaneously."""
cache = LFUCache(100)
await cache.start_evictor()
tasks = []
for i in range(50):
async def worker(idx):
tx = await cache.begin_transaction()
await tx.put(f'key_{idx}', idx)
await asyncio.sleep(0.01)
val = await tx.get(f'key_{idx}')
assert val == idx, f"Transaction read mismatch for key_{idx}"
await tx.commit()
# Verify global visibility post-commit
assert await cache.get(f'key_{idx}') == idx, f"Global read mismatch for key_{idx}"
tasks.append(asyncio.create_task(worker(i)))
await asyncio.gather(*tasks)
await cache.stop_evictor()
print(" ✓ Stress test (50 concurrent tasks) passed without race conditions")
if __name__ == '__main__':
asyncio.run(main())
+426
View File
@@ -0,0 +1,426 @@
#!/usr/bin/env python3
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction and Atomic Transactions.
Pure Python 3.11+ implementation. No external dependencies.
"""
import asyncio
import time
from dataclasses import dataclass, field
from typing import Any, Optional, Dict, Set, List
# ---------------------------------------------------------------------------
# Core Data Structures for O(1) Operations
# ---------------------------------------------------------------------------
@dataclass
class Node:
key: Any
value: Any
freq: int
expires_at: Optional[float]
prev: Optional['Node'] = None
next: Optional['Node'] = None
class DoublyLinkedList:
"""Sentinel-based doubly linked list supporting O(1) add/remove/pop."""
def __init__(self):
self.head = Node(None, None, 0, None)
self.tail = Node(None, None, 0, None)
self.head.next = self.tail
self.tail.prev = self.head
self.size = 0
def add_to_tail(self, node: Node):
node.prev = self.tail.prev
node.next = self.tail
self.tail.prev.next = node
self.tail.prev = node
self.size += 1
def remove_node(self, node: Node):
if node.prev is None or node.next is None:
return
node.prev.next = node.next
node.next.prev = node.prev
node.prev = node.next = None
self.size -= 1
def pop_head(self) -> Optional[Node]:
if self.size == 0:
return None
node = self.head.next
self.remove_node(node)
return node
def is_empty(self) -> bool:
return self.size == 0
# ---------------------------------------------------------------------------
# Transaction Isolation Layer
# ---------------------------------------------------------------------------
class Transaction:
"""
Provides ACID-like sub-sessions with Read-Your-Own-Writes isolation.
Changes remain local until commit(), where they are applied atomically.
"""
def __init__(self, cache: 'LFUCache'):
self._cache = cache
self._local_writes: Dict[Any, tuple] = {} # key -> (value, ttl)
self._local_deletes: Set[Any] = set()
self._local_freq_bumps: Set[Any] = set() # Track accesses for LFU bump
self._committed = False
self._rolled_back = False
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
if self._committed or self._rolled_back:
raise RuntimeError("Transaction already finished")
self._local_writes[key] = (value, ttl_seconds)
self._local_deletes.discard(key)
self._local_freq_bumps.add(key)
async def get(self, key: Any) -> Optional[Any]:
if self._committed or self._rolled_back:
raise RuntimeError("Transaction already finished")
# Read Your Own Writes
if key in self._local_writes:
val, _ = self._local_writes[key]
self._local_freq_bumps.add(key)
return val
if key in self._local_deletes:
return None
# Read Global State
val = await self._cache.get(key)
if val is not None:
self._local_freq_bumps.add(key)
return val
async def delete(self, key: Any) -> bool:
if self._committed or self._rolled_back:
raise RuntimeError("Transaction already finished")
self._local_deletes.add(key)
self._local_writes.pop(key, None)
return True
async def commit(self) -> bool:
if self._committed or self._rolled_back:
raise RuntimeError("Transaction already finished")
self._committed = True
async with self._cache._lock:
# 1. Apply Writes
for key, (value, ttl) in self._local_writes.items():
if key in self._cache.store:
node = self._cache.store[key]
if self._cache._is_expired(node):
self._cache._remove_node(key)
else:
node.value = value
node.expires_at = time.time() + ttl if ttl else None
self._cache._update_freq(node)
else:
self._cache._add_node(key, value, ttl)
# 2. Apply Deletes
for key in self._local_deletes:
self._cache._remove_node(key)
# 3. Apply Frequency Bumps (for reads during transaction)
for key in self._local_freq_bumps:
if key in self._cache.store and key not in self._local_deletes:
node = self._cache.store[key]
self._cache._update_freq(node)
self._cache._cleanup_freq_lists()
return True
async def rollback(self):
if self._committed or self._rolled_back:
raise RuntimeError("Transaction already finished")
self._rolled_back = True
# Completely discard pending changes without mutating global state
self._local_writes.clear()
self._local_deletes.clear()
self._local_freq_bumps.clear()
# ---------------------------------------------------------------------------
# Main Cache Implementation
# ---------------------------------------------------------------------------
class LFUCache:
"""
Concurrent LFU Cache with O(1) get/put, dual-layer TTL eviction,
and atomic transaction support.
"""
def __init__(self, capacity: int, eviction_interval: float = 0.5):
if capacity <= 0:
raise ValueError("Cache capacity must be positive")
self.capacity = capacity
self.store: Dict[Any, Node] = {}
self.freq_map: Dict[int, DoublyLinkedList] = {}
self.min_freq: int = 0
self.eviction_interval = eviction_interval
self._lock = asyncio.Lock()
self._evictor_task: Optional[asyncio.Task] = None
self._evictor_running = False
self._ttl_keys: Set[Any] = set()
def begin_transaction(self) -> Transaction:
return Transaction(self)
async def start_evictor(self):
"""Starts the background async TTL eviction loop."""
if self._evictor_running:
return
self._evictor_running = True
self._evictor_task = asyncio.create_task(self._eviction_loop())
async def stop_evictor(self):
"""Stops the background async TTL eviction loop."""
self._evictor_running = False
if self._evictor_task:
self._evictor_task.cancel()
try:
await self._evictor_task
except asyncio.CancelledError:
pass
self._evictor_task = None
async def _eviction_loop(self):
while self._evictor_running:
await asyncio.sleep(self.eviction_interval)
if not self._evictor_running:
break
await self._evict_batch()
async def _evict_batch(self):
"""Background sweep: acquires lock briefly, removes expired keys in batches."""
async with self._lock:
expired_keys = [
k for k in list(self._ttl_keys)
if k in self.store and self.store[k].expires_at and time.time() > self.store[k].expires_at
]
batch = expired_keys[:100] # Small batch to avoid lock contention
for k in batch:
self._remove_node(k)
self._ttl_keys.discard(k)
self._cleanup_freq_lists()
def _cleanup_freq_lists(self):
"""Remove empty frequency buckets and update min_freq."""
empty_freqs = [f for f, dll in self.freq_map.items() if dll.is_empty()]
for f in empty_freqs:
del self.freq_map[f]
if self.freq_map:
self.min_freq = min(self.freq_map.keys())
else:
self.min_freq = 0
def _is_expired(self, node: Node) -> bool:
return node.expires_at is not None and time.time() > node.expires_at
def _remove_node(self, key: Any):
"""Removes a key from store and its frequency linked list."""
if key not in self.store:
return
node = self.store.pop(key)
self._ttl_keys.discard(key)
freq = node.freq
if freq in self.freq_map:
self.freq_map[freq].remove_node(node)
def _update_freq(self, node: Node):
"""Moves node to next frequency bucket in O(1)."""
freq = node.freq
self.freq_map[freq].remove_node(node)
if self.freq_map[freq].is_empty():
del self.freq_map[freq]
if self.min_freq == freq:
self.min_freq += 1
node.freq += 1
new_freq = node.freq
if new_freq not in self.freq_map:
self.freq_map[new_freq] = DoublyLinkedList()
self.freq_map[new_freq].add_to_tail(node)
async def get(self, key: Any) -> Optional[Any]:
"""O(1) get with lazy TTL eviction and frequency bump."""
async with self._lock:
if key not in self.store:
return None
node = self.store[key]
if self._is_expired(node):
self._remove_node(key)
return None
self._update_freq(node)
return node.value
async def put(self, key: Any, value: Any, ttl_seconds: Optional[float] = None):
"""O(1) put with capacity eviction and TTL handling."""
async with self._lock:
if key in self.store:
node = self.store[key]
if self._is_expired(node):
self._remove_node(key)
else:
node.value = value
node.expires_at = time.time() + ttl_seconds if ttl_seconds else None
self._update_freq(node)
return
if len(self.store) >= self.capacity:
self._evict()
self._add_node(key, value, ttl_seconds)
def _evict(self):
"""Evicts the least frequently used item (O(1) via min_freq bucket)."""
if self.min_freq in self.freq_map:
dll = self.freq_map[self.min_freq]
node = dll.pop_head()
if node:
self.store.pop(node.key, None)
self._ttl_keys.discard(node.key)
self._cleanup_freq_lists()
def _add_node(self, key: Any, value: Any, ttl_seconds: Optional[float]):
"""Inserts new node into freq 1 bucket."""
node = Node(key, value, 1, time.time() + ttl_seconds if ttl_seconds else None)
self.store[key] = node
if ttl_seconds:
self._ttl_keys.add(key)
if 1 not in self.freq_map:
self.freq_map[1] = DoublyLinkedList()
self.min_freq = 1
self.freq_map[1].add_to_tail(node)
async def delete(self, key: Any) -> bool:
async with self._lock:
if key in self.store:
self._remove_node(key)
return True
return False
async def size(self) -> int:
async with self._lock:
return len(self.store)
# ---------------------------------------------------------------------------
# Executable Unit Tests
# ---------------------------------------------------------------------------
async def test_lfu_eviction_order():
"""a) O(1) LFU eviction order when capacity is reached."""
cache = LFUCache(3)
await cache.put(1, 'a')
await cache.put(2, 'b')
await cache.put(3, 'c')
# Access 1 twice to make it most frequent
await cache.get(1)
await cache.get(1)
# Insert 4 -> should evict 2 (freq 1, least used)
await cache.put(4, 'd')
assert await cache.get(2) is None, "Failed: Least frequent key should be evicted"
assert await cache.get(1) == 'a', "Failed: Key 1 should still exist"
assert await cache.get(3) == 'c', "Failed: Key 3 should still exist"
assert await cache.get(4) == 'd', "Failed: Key 4 should exist"
print("✅ Test a) LFU eviction order: PASSED")
async def test_ttl_eviction():
"""b) Lazy TTL vs. Background Async Sweep eviction."""
cache = LFUCache(10)
await cache.start_evictor()
# Lazy eviction
await cache.put('lazy', 'val', ttl_seconds=0.1)
assert await cache.get('lazy') == 'val', "Failed: Should return value before TTL"
await asyncio.sleep(0.15)
assert await cache.get('lazy') is None, "Failed: Lazy eviction should trigger on next get"
# Background sweep eviction
await cache.put('bg', 'val2', ttl_seconds=0.1)
await asyncio.sleep(0.25) # Wait for background task cycle
assert await cache.get('bg') is None, "Failed: Background sweep should evict expired key"
await cache.stop_evictor()
print("✅ Test b) TTL eviction (Lazy & Background): PASSED")
async def test_transaction_isolation():
"""c) Transaction commit visibility vs. rollback state restoration."""
cache = LFUCache(10)
await cache.put('a', 1)
# Commit test
tx = cache.begin_transaction()
await tx.put('a', 2)
await tx.put('b', 3)
# Read your own writes
assert await tx.get('a') == 2, "Failed: Should see local write"
assert await tx.get('b') == 3, "Failed: Should see local write"
# Global shouldn't see uncommitted changes
assert await cache.get('a') == 1, "Failed: Global should not see uncommitted write"
await tx.commit()
assert await cache.get('a') == 2, "Failed: Global should see committed write"
assert await cache.get('b') == 3, "Failed: Global should see committed write"
# Rollback test
tx2 = cache.begin_transaction()
await tx2.put('c', 4)
assert await tx2.get('c') == 4, "Failed: Should see local write"
await tx2.rollback()
assert await cache.get('c') is None, "Failed: Rollback should discard changes"
print("✅ Test c) Transaction isolation (Commit & Rollback): PASSED")
async def test_stress_concurrency():
"""d) Stress test with 50 concurrent async tasks reading/writing simultaneously."""
cache = LFUCache(100)
await cache.start_evictor()
tasks = []
for i in range(50):
async def worker(idx: int):
for j in range(10):
key = f"k{idx}"
await cache.put(key, f"v{idx}_{j}")
val = await cache.get(key)
assert val == f"v{idx}_{j}", f"Failed: Read mismatch for {key}"
tasks.append(asyncio.create_task(worker(i)))
await asyncio.gather(*tasks)
# All 50 keys should exist
assert await cache.size() == 50, f"Failed: Expected 50 keys, got {await cache.size()}"
# Verify concurrent reads/writes didn't corrupt data
for i in range(50):
assert await cache.get(f"k{i}") == f"v{i}_9", f"Failed: Data corruption for k{i}"
await cache.stop_evictor()
print("✅ Test d) Stress concurrency (50 tasks): PASSED")
async def main():
print("🚀 Running LFU Cache Unit Tests...\n")
await test_lfu_eviction_order()
await test_ttl_eviction()
await test_transaction_isolation()
await test_stress_concurrency()
print("\n🎉 All tests passed successfully.")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,398 @@
"""
In-Memory Concurrent LFU Cache with Async TTL Eviction & Atomic Transactions.
Pure Python 3.11+ implementation using asyncio, dataclasses, and collections.OrderedDict.
"""
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, Set, Tuple
from collections import OrderedDict
@dataclass
class _Node:
"""Internal node storing cache value and TTL expiry timestamp."""
key: str
value: Any
ttl_expiry: float
class LFUCache:
"""
O(1) Least Frequently Used Cache with dual-layer TTL eviction and async concurrency.
Data Structures:
- self.cache: Dict[str, _Node] -> Direct O(1) key lookup
- self.freq_map: Dict[int, OrderedDict[str, None]] -> Frequency buckets maintaining insertion order (LRU within same freq)
- self.key_freq: Dict[str, int] -> Tracks current frequency of each key for O(1) updates
- self.ttl_map: Dict[str, float] -> Stores absolute TTL expiry timestamps
"""
def __init__(self, capacity: int):
self.capacity = max(0, capacity)
self.cache: Dict[str, _Node] = {}
self.freq_map: Dict[int, OrderedDict[str, None]] = {}
self.key_freq: Dict[str, int] = {}
self.ttl_map: Dict[str, float] = {}
self.min_freq: int = 0
self._lock = asyncio.Lock()
self._evictor_task: Optional[asyncio.Task] = None
# ------------------------------------------------------------------ #
# PUBLIC API #
# ------------------------------------------------------------------ #
async def start_evictor(self) -> None:
"""Start the non-blocking background TTL eviction loop."""
if self._evictor_task is not None:
return
self._evictor_task = asyncio.create_task(self._background_loop())
async def stop_evictor(self) -> None:
"""Gracefully stop the background eviction task."""
if self._evictor_task:
self._evictor_task.cancel()
try:
await self._evictor_task
except asyncio.CancelledError:
pass
self._evictor_task = None
async def get(self, key: str) -> Optional[Any]:
"""
Retrieve value by key. O(1) average time complexity.
Performs lazy TTL eviction upon access.
"""
current_time = time.monotonic()
expiry = self.ttl_map.get(key)
# Lazy TTL Eviction
if expiry is not None and current_time >= expiry:
await self._remove_key(key)
return None
async with self._lock:
if key not in self.cache:
return None
node = self.cache[key]
# Frequency Increment & Bucket Migration (O(1))
old_freq = self.key_freq[key]
new_freq = old_freq + 1
self.key_freq[key] = new_freq
old_bucket = self.freq_map[old_freq]
del old_bucket[key]
if not old_bucket:
del self.freq_map[old_freq]
if self.min_freq == old_freq:
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
new_bucket[key] = None # Store key reference in bucket
return node.value
async def put(self, key: str, value: Any, ttl_seconds: float) -> None:
"""
Insert or update key-value pair with TTL. O(1) average time complexity.
Performs lazy TTL eviction before insertion if needed.
"""
current_time = time.monotonic()
expiry = self.ttl_map.get(key)
# Lazy TTL Eviction for stale keys
if expiry is not None and current_time >= expiry:
await self._remove_key(key)
async with self._lock:
if key in self.cache:
# Update existing: increment frequency & migrate bucket
old_freq = self.key_freq[key]
new_freq = old_freq + 1
self.key_freq[key] = new_freq
old_bucket = self.freq_map[old_freq]
del old_bucket[key]
if not old_bucket:
del self.freq_map[old_freq]
if self.min_freq == old_freq:
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
new_bucket[key] = None
else:
# Insert new: evict LFU if at capacity
if len(self.cache) >= self.capacity and self.capacity > 0:
await self._evict_lfu()
freq = 1
self.key_freq[key] = freq
bucket = self.freq_map.setdefault(freq, OrderedDict())
bucket[key] = None
self.min_freq = 1
# Update node & TTL map
self.cache[key] = _Node(key=key, value=value, ttl_expiry=current_time + ttl_seconds)
self.ttl_map[key] = current_time + ttl_seconds
async def delete(self, key: str) -> bool:
"""Delete a key from the cache. O(1)."""
async with self._lock:
if key not in self.cache:
return False
await self._remove_key(key)
return True
# ------------------------------------------------------------------ #
# INTERNAL HELPERS #
# ------------------------------------------------------------------ #
async def _evict_lfu(self) -> None:
"""Evict the least frequently used key (oldest among ties). O(1)."""
if not self.freq_map or self.min_freq == 0:
return
# Ensure min_freq points to a valid tier
while self.min_freq in self.freq_map and self.min_freq < max(self.freq_map.keys()):
pass
if self.min_freq not in self.freq_map:
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
bucket = self.freq_map[self.min_freq]
evict_key, _ = bucket.popitem(last=False)
await self._remove_key(evict_key)
async def _remove_key(self, key: str) -> None:
"""Remove key from all internal structures. O(1)."""
if key not in self.cache:
return
node = self.cache.pop(key)
self.ttl_map.pop(key, None)
freq = self.key_freq.pop(key)
bucket = self.freq_map[freq]
del bucket[key]
if not bucket:
del self.freq_map[freq]
if self.min_freq == freq:
# Find next valid minimum frequency
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
async def _background_loop(self) -> None:
"""Non-blocking background task that purges expired keys in batches."""
while True:
await asyncio.sleep(0.1) # Check interval
current_time = time.monotonic()
async with self._lock:
# Collect expired keys safely
expired_keys = [k for k, exp in list(self.ttl_map.items()) if current_time >= exp]
# Purge in batch (yields control between removals implicitly via await)
for key in expired_keys:
await self._remove_key(key)
async def apply_transaction(self, tx: "Transaction") -> None:
"""Atomically apply transaction buffers to global state."""
async with self._lock:
# 1. Apply local deletes first (frees capacity for puts)
for key in list(tx._local_deletes):
if key in self.cache:
await self._remove_key(key)
tx._local_puts.pop(key, None)
# 2. Apply local puts
for key, (value, expiry) in tx._local_puts.items():
if key in self.cache:
old_freq = self.key_freq[key]
new_freq = old_freq + 1
self.key_freq[key] = new_freq
old_bucket = self.freq_map[old_freq]
del old_bucket[key]
if not old_bucket:
del self.freq_map[old_freq]
if self.min_freq == old_freq:
self.min_freq = min(self.freq_map.keys()) if self.freq_map else 0
new_bucket = self.freq_map.setdefault(new_freq, OrderedDict())
new_bucket[key] = None
else:
if len(self.cache) >= self.capacity and self.capacity > 0:
await self._evict_lfu()
freq = 1
self.key_freq[key] = freq
bucket = self.freq_map.setdefault(freq, OrderedDict())
bucket[key] = None
self.min_freq = 1
self.cache[key] = _Node(key=key, value=value, ttl_expiry=expiry)
self.ttl_map[key] = expiry
class Transaction:
"""
ACID-like sub-session handle supporting Read-Your-Own-Writes and isolation.
Global readers do not see uncommitted writes until commit().
"""
def __init__(self, cache: LFUCache):
self.cache = cache
self._local_puts: Dict[str, Tuple[Any, float]] = {} # key -> (value, absolute_expiry)
self._local_deletes: Set[str] = set()
async def get(self, key: str) -> Optional[Any]:
"""Read with local buffer priority (Read-Your-Own-Writes)."""
if key in self._local_deletes:
return None
if key in self._local_puts:
val, _ = self._local_puts[key]
return val
# Fall back to global cache (handles lazy eviction & lock)
return await self.cache.get(key)
async def put(self, key: str, value: Any, ttl_seconds: float) -> None:
"""Buffer write locally. Does not affect global state until commit."""
current_time = time.monotonic()
self._local_puts[key] = (value, current_time + ttl_seconds)
if key in self._local_deletes:
self._local_deletes.remove(key)
async def delete(self, key: str) -> None:
"""Buffer deletion locally."""
self._local_deletes.add(key)
self._local_puts.pop(key, None)
async def commit(self) -> None:
"""Atomically apply all buffered changes to the global cache."""
await self.cache.apply_transaction(self)
def rollback(self) -> None:
"""Discard all pending local changes without mutating global state."""
self._local_puts.clear()
self._local_deletes.clear()
# ------------------------------------------------------------------ #
# TEST SUITE #
# ------------------------------------------------------------------ #
async def main():
print("🧪 Starting LFU Cache Test Suite...\n")
# a) O(1) LFU Eviction Order
print("[a] Testing O(1) LFU Eviction Order...")
cache = LFUCache(capacity=3)
await cache.put("A", 1, ttl_seconds=60)
await cache.put("B", 2, ttl_seconds=60)
await cache.put("C", 3, ttl_seconds=60)
# Access A twice to increase its frequency
await cache.get("A")
await cache.get("A")
# Insert D. Should evict B or C (both freq=1). LFU policy guarantees one of them is gone.
await cache.put("D", 4, ttl_seconds=60)
val_b = await cache.get("B")
val_c = await cache.get("C")
assert val_d := await cache.get("D"), "D should exist"
assert val_a := await cache.get("A"), "A should exist (highest freq)"
assert val_b is None or val_c is None, f"LFU eviction failed: B={val_b}, C={val_c}"
print(f" ✅ LFU Eviction verified. Evicted key had lower frequency than A & D.")
# b) Lazy TTL vs Background Async Sweep
print("\n[b] Testing Dual-Layer TTL Eviction...")
cache2 = LFUCache(capacity=10)
# Lazy Eviction Test
await cache2.put("lazy_key", "val", ttl_seconds=0.2)
assert await cache2.get("lazy_key") == "val"
await asyncio.sleep(0.3)
assert await cache2.get("lazy_key") is None, "Lazy eviction failed"
# Background Eviction Test
await cache2.start_evictor()
await cache2.put("bg_key", "val", ttl_seconds=0.15)
await asyncio.sleep(0.3) # Wait past TTL without accessing key
assert await cache2.get("bg_key") is None, "Background async sweep failed"
await cache2.stop_evictor()
print(" ✅ Lazy & Background TTL eviction verified.")
# c) Transaction Commit Visibility vs Rollback
print("\n[c] Testing Transaction Isolation & Rollback...")
cache3 = LFUCache(capacity=10)
tx1 = cache3.begin_transaction() if hasattr(cache3, 'begin_transaction') else None
class TxWrapper:
def __init__(self, c): self.c = c
def begin(self): return Transaction(self.c)
tw = TxWrapper(cache3)
# Commit visibility
tx_put = tw.begin()
await tx_put.put("committed", 100, ttl_seconds=60)
assert await cache3.get("committed") is None, "Uncommitted write should be invisible"
await tx_put.commit()
assert await cache3.get("committed") == 100, "Committed write should be visible globally"
# Rollback state restoration
tx_roll = tw.begin()
await tx_roll.put("rolled_back", 200, ttl_seconds=60)
await tx_roll.rollback()
assert await cache3.get("rolled_back") is None, "Rolled back write should not persist"
# Read-Your-Own-Writes inside transaction
tx_ryo = tw.begin()
await tx_ryo.put("local", 999, ttl_seconds=60)
assert await tx_ryo.get("local") == 999, "Transaction should see its own writes"
print(" ✅ Transaction commit visibility & rollback verified.")
# d) Stress Test: 50 Concurrent Tasks
print("\n[d] Running Stress Test (50 concurrent async tasks)...")
cache4 = LFUCache(capacity=100)
await cache4.start_evictor()
errors = []
results = {"gets": 0, "puts": 0}
async def worker(task_id: int):
try:
for i in range(20):
key = f"stress_{task_id}_{i}"
op_type = i % 3
if op_type == 0:
await cache4.put(key, f"v_{task_id}_{i}", ttl_seconds=1.0)
results["puts"] += 1
elif op_type == 1:
val = await cache4.get(key)
if val is not None:
results["gets"] += 1
else:
tx = Transaction(cache4)
await tx.put(f"tx_{task_id}_{i}", "tx_val", ttl_seconds=0.5)
assert await tx.get(f"tx_{task_id}_{i}") == "tx_val"
await tx.commit()
except Exception as e:
errors.append((task_id, str(e)))
tasks = [asyncio.create_task(worker(i)) for i in range(50)]
await asyncio.gather(*tasks)
assert len(errors) == 0, f"Stress test failed with errors: {errors}"
print(f" ✅ Stress test passed. Processed {results['puts']} puts & {results['gets']} gets across 50 tasks without race conditions.")
await cache4.stop_evictor()
print("\n🎉 All tests passed successfully!")
if __name__ == "__main__":
asyncio.run(main())
+135
View File
@@ -0,0 +1,135 @@
```markdown
You are a Principal Systems Engineer and Automated LLM Benchmark Auditor.
Your objective is to evaluate Python code generated by local LLMs, store the evaluation data in a persistent JSON file (`~/dev/modelTesting/data/benchmark_history.json`), and build/update an interactive HTML reporting dashboard (`~/dev/modelTesting/dashboard.html`).
---
### STEP 1: Rigorous Code Audit
Analyze the provided Python file against these 5 technical pillars (score each from 0 to 20):
1. Complexity Violations (O(1) Audit):
- Check for hidden O(N) or O(log N) operations (e.g., `min()`, `sorted()`, heaps, linear scans during eviction or frequency node relocation, or key iterations).
2. Async Race Conditions & Deadlocks:
- Check for un-synchronized shared state access between async background tasks (e.g., TTL sweep) and foreground operations (`get`, `put`, `commit`).
- Check for potential deadlocks or holding `asyncio.Lock` across `asyncio.sleep` or external IO.
3. Transactional Isolation Leaks:
- Verify Read-Your-Own-Writes and Snapshot isolation. Does uncommitted write/delete state leak into the global cache?
- Does `rollback()` cleanly clear local buffers without leaving dangling nodes or modified frequency counters?
- Does `commit()` handle mid-execution exceptions gracefully?
4. Memory Leaks & Edge Cases:
- Check doubly-linked list node unlinking. Does `unlink` update list `size` and prune empty frequency buckets from `freq_map`?
- Does key deletion or TTL eviction correctly adjust `min_freq`?
- Is `time.monotonic()` used instead of system clock `time.time()`? Are `__slots__` declared for memory efficiency?
5. Test Coverage Integrity:
- Does the included test suite validate real edge cases (concurrency, race conditions, rollback isolation), or are assertions trivial/always-pass?
---
### STEP 2: Metrics & Categorization
From your audit, extract the following structured data:
- Overall Score (0-100 sum of the 5 pillar scores)
- Quant/Model Details (Extracted from file name or user input, e.g., "Qwen 3.6 35B Q6_K")
- Token Speed (tok/sec, if provided)
- Verdict: ["Production Ready", "Minor Logic Flaws", "Critical Bugs", "Broken / Unusable"]
- Archetype / Best For: 1-sentence recommendation (e.g., "Great for rapid local prototyping, but needs manual checks on pointer deletions.")
- Critical Flaws Summary: Short bullet list of identified bugs.
- Refactored Code Patches: Code snippets fixing the specific bugs.
---
### STEP 3: Maintain Persistent JSON Storage
Read `~/dev/modelTesting/data/benchmark_history.json` (create it if it doesn't exist).
Append or update the entry for the evaluated model in the following format:
```json
{
"timestamp": "2026-07-28T12:00:00Z",
"model_name": "Qwen 3.6 35B",
"quant": "Q6_K",
"tok_sec": 68.86,
"filename": "qwen3.6-35b-q6k-2026-07-28.py",
"total_score": 88,
"breakdown": {
"complexity": 18,
"concurrency": 16,
"isolation": 18,
"memory_edge_cases": 18,
"test_integrity": 18
},
"verdict": "Minor Logic Flaws",
"best_for": "Fast offline daily driving & agentic refactoring.",
"critical_bugs": [
"Stale min_freq on key deletion causes capacity breach."
],
"patch_code": "..."
}
```
---
### STEP 4: Regenerate/Update `~/dev/modelTesting/dashboard.html`
Generate (or overwrite) `~/dev/modelTesting/dashboard.html` with a standalone, dark-themed HTML dashboard.
The HTML dashboard MUST include:
1. **Header & Summary Stats:** Total models tested, top performer, average benchmark score.
2. **Chart.js CDN Integration:**
* **Bar Chart:** Model Scores vs. Generation Speed (tok/sec).
* **Radar Chart:** Multi-axis comparison of the top 3 models across the 5 audit pillars.
3. **Leaderboard Table:**
* Columns: Model Name | Quant | Speed | Score | Verdict | Archetype | Details Button
4. **Collapsible / Accordion Audit Cards:**
* Detailed breakdowns for every tested model showing Pillar Scores, Critical Bugs, and exact Python Patch Snippets.
5. **Styling:** Modern dark-mode UI (Tailwind CSS via CDN or raw clean CSS grid/flexbox) with color-coded badges:
* Green for Production Ready (90-100)
* Yellow/Blue for Minor Flaws (75-89)
* Red for Critical Flaws/Broken (<75)
Execute these steps automatically whenever a new model code output is submitted for evaluation.
```
---
## 📊 What the Resulting Dashboard Will Look Like
When your Grading Agent processes outputs, it will write out a clean, standalone single-file `dashboard.html` that you can open in any browser (`open ~/dev/modelTesting/dashboard.html`).
```text
+-----------------------------------------------------------------------------+
| 🧪 LOCAL LLM BENCHMARK SUITE - LFU CACHE & ACID AUDIT |
| Total Models Tested: 5 | Top Performer: DeepSeek V4 Flash Max | Avg: 78.4 |
+-----------------------------------------------------------------------------+
| |
| [ Chart: Score vs tok/sec ] [ Radar: 5-Pillar Comparison ] |
| |
+-----------------------------------------------------------------------------+
| LEADERBOARD |
| Model Quant Speed Score Verdict Best For |
| ------------------------------------------------------------------------- |
| DeepSeek V4 Flash MoE Cloud 98 [PROD] ACID Logic |
| Qwen 3.6 35B Q6_K 68.8 t/s 88 [FLAWS] Daily Driver |
| KAT-Coder 2.5 MLX 65.2 t/s 85 [FLAWS] OOP Scaffolding|
+-----------------------------------------------------------------------------+
| ▼ DETAILED AUDIT: Qwen 3.6 35B (Q6_K) |
| - Complexity: 18/20 | Concurrency: 16/20 | Isolation: 18/20 |
| - Critical Bugs: Stale min_freq on key deletion causes capacity breach. |
| - Code Patch: [ View Refactored Diff ] |
+-----------------------------------------------------------------------------+
```
+31
View File
@@ -0,0 +1,31 @@
Write a complete, single-file Python 3.11+ module that implements an In-Memory Concurrent LFU (Least Frequently Used) Cache with Async TTL Eviction and Atomic Transactions.
DO NOT use any external third-party libraries—use only pure Python built-ins (asyncio, dataclasses, typing, weakref, time, etc.).
### Strict Functional Requirements:
1. Strict O(1) Time Complexity:
- Both `get(key)` and `put(key, value, ttl_seconds)` MUST run in true O(1) average time complexity.
- You MUST use a frequency-bucket system with doubly linked lists (or an equivalent O(1) data structure) to maintain access counts and frequency tiers. A heap/priority queue or linear scan is strictly forbidden as it is O(log N) or O(N).
2. Dual-Layer TTL Eviction:
- Lazy Eviction: `get()` and `put()` must check and evict stale keys immediately upon access.
- Background Async Eviction Loop: Implement a non-blocking background `asyncio` task (`start_evictor()`, `stop_evictor()`) that periodically purges expired keys in small batches without holding a lock across the entire dataset or blocking reads.
3. Atomic Transaction Isolation (ACID-like sub-sessions):
- Implement `cache.begin_transaction() -> Transaction` which returns a transaction handle.
- Inside a transaction, calls to `tx.put()`, `tx.get()`, and `tx.delete()` must support "Read Your Own Writes" (uncommitted local changes are visible to this transaction).
- Global cache readers MUST NOT see uncommitted transaction writes until `await tx.commit()` is called.
- Calling `tx.rollback()` must completely discard all pending changes without mutating global frequencies or TTL states.
4. Async Concurrency & Thread-Safety:
- Handle race conditions between concurrent readers, writers, background evictors, and transaction commits using fine-grained `asyncio.Lock` primitives.
5. Included Executable Unit Tests:
- At the bottom of the file, include an `async def main()` test suite that verifies:
a) O(1) LFU eviction order when capacity is reached.
b) Lazy TTL vs. Background Async Sweep eviction.
c) Transaction commit visibility vs. Transaction rollback state restoration.
d) Stress test with 50 concurrent async tasks reading/writing simultaneously.
Provide clean, well-commented code that executes directly via `python script.py`.