Skip to content

Commit d99c439

Browse files
authored
Merge pull request #1 from agkloop/perf
Perf
2 parents 6588ce6 + 21af6a2 commit d99c439

17 files changed

+2889
-1140
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,20 @@ jobs:
3636
- name: Check code formatting with ruff
3737
run: uv run ruff format --check src/ tests/
3838

39-
- name: Run tests
39+
- name: Run unit tests
4040
run: uv run pytest tests/test_correctness.py -v --tb=short
4141

42+
- name: Run integration tests (Redis with testcontainers)
43+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
44+
run: uv run pytest tests/test_integration_redis.py -v --tb=short
45+
4246
- name: Run benchmarks
4347
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
4448
run: uv run python tests/benchmark.py
4549

4650
- name: Generate coverage report
4751
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
48-
run: uv run pytest tests/test_correctness.py --cov=src/advanced_caching --cov-report=xml --cov-report=term
52+
run: uv run pytest tests/test_correctness.py tests/test_integration_redis.py --cov=src/advanced_caching --cov-report=xml --cov-report=term
4953

5054
- name: Upload coverage to Codecov
5155
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,6 @@ dmypy.json
134134
.uv-venv/
135135
.venv/
136136
venv/
137+
benchmarks.log
138+
scalene_profile.json
137139

CHANGELOG.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Redis cluster support
1616
- DynamoDB backend example
1717

18+
## [0.1.4] - 2025-12-12
19+
20+
### Changed
21+
- Performance improvements in hot paths:
22+
- Reduced repeated cache initialization/lookups inside decorators.
23+
- Reduced repeated `time.time()` calls by reusing a single timestamp per operation.
24+
- `CacheEntry` is now a slotted dataclass to reduce per-entry memory/attribute overhead.
25+
- SWR background refresh now uses a shared thread pool (avoids spawning a new thread per refresh).
26+
27+
### Added
28+
- Benchmarking & profiling tooling updates:
29+
- Benchmarks can be configured via environment variables (e.g. `BENCH_WORK_MS`, `BENCH_RUNS`).
30+
- Helper to compare JSON benchmark runs in `benchmarks.log`.
31+
- Tight-loop profiler workload for decorator overhead.
32+
33+
### Documentation
34+
- README updated to reflect current APIs, uv usage, and storage/Redis examples.
35+
- Added step-by-step benchmarking/profiling guide in `docs/benchmarking-and-profiling.md`.
36+
1837
## [0.1.3] - 2025-12-10
1938

2039
### Changed
@@ -74,9 +93,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7493
- `storage.py` coverage improved to ~74%.
7594
- Ensured all tests pass under the documented `pyproject.toml` configuration.
7695

77-
[Unreleased]: https://github.com/namshiv2/advanced_caching/compare/v0.1.3...HEAD
78-
[0.1.3]: https://github.com/namshiv2/advanced_caching/compare/v0.1.2...v0.1.3
79-
[0.1.2]: https://github.com/namshiv2/advanced_caching/compare/v0.1.1...v0.1.2
96+
[Unreleased]: https://github.com/agkloop/advanced_caching/compare/v0.1.4...HEAD
97+
[0.1.4]: https://github.com/agkloop/advanced_caching/compare/v0.1.3...v0.1.4
98+
[0.1.3]: https://github.com/agkloop/advanced_caching/compare/v0.1.2...v0.1.3
99+
[0.1.2]: https://github.com/agkloop/advanced_caching/compare/v0.1.1...v0.1.2
80100
[0.1.1]: https://github.com/namshiv2/advanced_caching/releases/tag/v0.1.1
81101

82102
## [0.1.1] - 2025-12-10

README.md

Lines changed: 136 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Installation](#installation) – Get started in 30 seconds
1212
- [Quick Examples](#quick-start) – Copy-paste ready code
1313
- [API Reference](#api-reference) – Full decorator & backend docs
14+
- [Storage & Redis](#storage--redis) – Redis/Hybrid/custom storage examples
1415
- [Custom Storage](#custom-storage) – Implement your own backend
1516
- [Benchmarks](#benchmarks) – See the performance gains
1617
- [Use Cases](#use-cases) – Real-world examples
@@ -37,6 +38,8 @@ pip install advanced-caching
3738
uv pip install advanced-caching
3839
# with Redis support
3940
pip install "advanced-caching[redis]"
41+
# with Redis support (uv)
42+
uv pip install "advanced-caching[redis]"
4043
```
4144

4245
## Quick Start
@@ -90,6 +93,10 @@ user = await get_user_async(42)
9093
## Benchmarks
9194
Full benchmarks available in `tests/benchmark.py`.
9295

96+
Step-by-step benchmarking + profiling guide: `docs/benchmarking-and-profiling.md`.
97+
98+
Storage & Redis usage is documented below.
99+
93100
## API Reference
94101

95102
### Key templates & custom keys
@@ -159,7 +166,7 @@ Simple time-based cache with configurable TTL.
159166
TTLCache.cached(
160167
key: str | Callable[..., str],
161168
ttl: int,
162-
cache: CacheStorage | None = None,
169+
cache: CacheStorage | Callable[[], CacheStorage] | None = None,
163170
) -> Callable
164171
```
165172

@@ -172,7 +179,7 @@ TTLCache.cached(
172179

173180
Positional key:
174181
```python
175-
@TTLCache.cached("user:{},", ttl=300)
182+
@TTLCache.cached("user:{}", ttl=300)
176183
def get_user(user_id: int):
177184
return db.fetch(user_id)
178185

@@ -206,7 +213,7 @@ SWRCache.cached(
206213
key: str | Callable[..., str],
207214
ttl: int,
208215
stale_ttl: int = 0,
209-
cache: CacheStorage | None = None,
216+
cache: CacheStorage | Callable[[], CacheStorage] | None = None,
210217
enable_lock: bool = True,
211218
) -> Callable
212219
```
@@ -262,7 +269,7 @@ BGCache.register_loader(
262269
ttl: int | None = None,
263270
run_immediately: bool = True,
264271
on_error: Callable[[Exception], None] | None = None,
265-
cache: CacheStorage | None = None,
272+
cache: CacheStorage | Callable[[], CacheStorage] | None = None,
266273
) -> Callable
267274
```
268275

@@ -325,6 +332,97 @@ BGCache.shutdown(wait=True)
325332

326333
### Storage Backends
327334

335+
## Storage & Redis
336+
337+
### Install (uv)
338+
339+
```bash
340+
uv pip install advanced-caching
341+
uv pip install "advanced-caching[redis]" # for RedisCache / HybridCache
342+
```
343+
344+
### How storage is chosen
345+
346+
- If you don’t pass `cache=...`, each decorated function lazily creates its own `InMemCache` instance.
347+
- You can pass either a cache instance (`cache=my_cache`) or a cache factory (`cache=lambda: my_cache`).
348+
349+
### Share one storage instance
350+
351+
```python
352+
from advanced_caching import InMemCache, TTLCache
353+
354+
shared = InMemCache()
355+
356+
@TTLCache.cached("user:{}", ttl=60, cache=shared)
357+
def get_user(user_id: int) -> dict:
358+
return {"id": user_id}
359+
360+
@TTLCache.cached("org:{}", ttl=60, cache=shared)
361+
def get_org(org_id: int) -> dict:
362+
return {"id": org_id}
363+
```
364+
365+
### Use RedisCache (distributed)
366+
367+
`RedisCache` stores values in Redis using `pickle`.
368+
369+
```python
370+
import redis
371+
from advanced_caching import RedisCache, TTLCache
372+
373+
client = redis.Redis(host="localhost", port=6379)
374+
cache = RedisCache(client, prefix="app:")
375+
376+
@TTLCache.cached("user:{}", ttl=300, cache=cache)
377+
def get_user(user_id: int) -> dict:
378+
return {"id": user_id}
379+
```
380+
381+
### Use SWRCache with RedisCache (recommended)
382+
383+
`SWRCache` uses `get_entry`/`set_entry` so it can store freshness metadata.
384+
385+
```python
386+
import redis
387+
from advanced_caching import RedisCache, SWRCache
388+
389+
client = redis.Redis(host="localhost", port=6379)
390+
cache = RedisCache(client, prefix="products:")
391+
392+
@SWRCache.cached("product:{}", ttl=60, stale_ttl=30, cache=cache)
393+
def get_product(product_id: int) -> dict:
394+
return {"id": product_id}
395+
```
396+
397+
### Use HybridCache (L1 memory + L2 Redis)
398+
399+
`HybridCache` is a two-level cache:
400+
- **L1**: fast in-memory (`InMemCache`)
401+
- **L2**: Redis-backed (`RedisCache`)
402+
403+
Reads go to L1 first; on L1 miss it tries L2; on L2 hit it warms L1.
404+
405+
```python
406+
import redis
407+
from advanced_caching import HybridCache, InMemCache, RedisCache, TTLCache
408+
409+
client = redis.Redis(host="localhost", port=6379)
410+
411+
hybrid = HybridCache(
412+
l1_cache=InMemCache(),
413+
l2_cache=RedisCache(client, prefix="app:"),
414+
l1_ttl=60,
415+
)
416+
417+
@TTLCache.cached("user:{}", ttl=300, cache=hybrid)
418+
def get_user(user_id: int) -> dict:
419+
return {"id": user_id}
420+
```
421+
422+
Notes:
423+
- `ttl` on the decorator controls how long values are considered valid.
424+
- `l1_ttl` controls how long HybridCache keeps values in memory after an L2 hit.
425+
328426
#### InMemCache()
329427
Thread-safe in-memory cache with TTL.
330428

@@ -386,18 +484,16 @@ if entry and entry.is_fresh():
386484

387485
Implement the `CacheStorage` protocol for custom backends (DynamoDB, file-based, encrypted storage, etc.).
388486

389-
### File-Based Cache Example
487+
### File-based example
390488

391489
```python
392490
import json
393491
import time
394492
from pathlib import Path
395-
from advanced_caching import CacheStorage, TTLCache, validate_cache_storage
493+
from advanced_caching import CacheEntry, CacheStorage, TTLCache, validate_cache_storage
396494

397495

398496
class FileCache(CacheStorage):
399-
"""File-based cache storage."""
400-
401497
def __init__(self, directory: str = "/tmp/cache"):
402498
self.directory = Path(directory)
403499
self.directory.mkdir(parents=True, exist_ok=True)
@@ -406,26 +502,45 @@ class FileCache(CacheStorage):
406502
safe_key = key.replace("/", "_").replace(":", "_")
407503
return self.directory / f"{safe_key}.json"
408504

409-
def get(self, key: str):
505+
def get_entry(self, key: str) -> CacheEntry | None:
410506
path = self._get_path(key)
411507
if not path.exists():
412508
return None
413509
try:
414510
with open(path) as f:
415511
data = json.load(f)
416-
if data["fresh_until"] < time.time():
417-
path.unlink()
418-
return None
419-
return data["value"]
420-
except (json.JSONDecodeError, KeyError, OSError):
512+
return CacheEntry(
513+
value=data["value"],
514+
fresh_until=float(data["fresh_until"]),
515+
created_at=float(data["created_at"]),
516+
)
517+
except Exception:
518+
return None
519+
520+
def set_entry(self, key: str, entry: CacheEntry, ttl: int | None = None) -> None:
521+
now = time.time()
522+
if ttl is not None:
523+
fresh_until = now + ttl if ttl > 0 else float("inf")
524+
entry = CacheEntry(value=entry.value, fresh_until=fresh_until, created_at=now)
525+
with open(self._get_path(key), "w") as f:
526+
json.dump(
527+
{"value": entry.value, "fresh_until": entry.fresh_until, "created_at": entry.created_at},
528+
f,
529+
)
530+
531+
def get(self, key: str):
532+
entry = self.get_entry(key)
533+
if entry is None:
421534
return None
535+
if not entry.is_fresh():
536+
self.delete(key)
537+
return None
538+
return entry.value
422539

423540
def set(self, key: str, value, ttl: int = 0) -> None:
424541
now = time.time()
425542
fresh_until = now + ttl if ttl > 0 else float("inf")
426-
data = {"value": value, "fresh_until": fresh_until, "created_at": now}
427-
with open(self._get_path(key), "w") as f:
428-
json.dump(data, f)
543+
self.set_entry(key, CacheEntry(value=value, fresh_until=fresh_until, created_at=now))
429544

430545
def delete(self, key: str) -> None:
431546
self._get_path(key).unlink(missing_ok=True)
@@ -440,15 +555,12 @@ class FileCache(CacheStorage):
440555
return True
441556

442557

443-
# Use it
444558
cache = FileCache("/tmp/app_cache")
445559
assert validate_cache_storage(cache)
446560

447561
@TTLCache.cached("user:{}", ttl=300, cache=cache)
448562
def get_user(user_id: int):
449-
return {"id": user_id, "name": f"User {user_id}"}
450-
451-
user = get_user(42) # Stores in /tmp/app_cache/user_42.json
563+
return {"id": user_id}
452564
```
453565

454566
### Best Practices
@@ -463,12 +575,12 @@ user = get_user(42) # Stores in /tmp/app_cache/user_42.json
463575

464576
### Run Tests
465577
```bash
466-
pytest tests/test_correctness.py -v
578+
uv run pytest tests/test_correctness.py -v
467579
```
468580

469581
### Run Benchmarks
470582
```bash
471-
python tests/benchmark.py
583+
uv run python tests/benchmark.py
472584
```
473585

474586

@@ -564,39 +676,11 @@ Contributions welcome! Please:
564676
1. Fork the repository
565677
2. Create a feature branch (`git checkout -b feature/my-feature`)
566678
3. Add tests for new functionality
567-
4. Ensure all tests pass (`pytest`)
679+
4. Ensure all tests pass (`uv run pytest`)
568680
5. Submit a pull request
569681

570682
---
571683

572684
## License
573685

574686
MIT License – See [LICENSE](LICENSE) for details.
575-
576-
---
577-
578-
## Changelog
579-
580-
### 0.1.0 (Initial Release)
581-
- ✅ TTL Cache decorator
582-
- ✅ SWR Cache decorator
583-
- ✅ Background Cache with APScheduler
584-
- ✅ InMemCache, RedisCache, HybridCache storage backends
585-
- ✅ Full async/sync support
586-
- ✅ Custom storage protocol
587-
- ✅ Comprehensive test suite
588-
- ✅ Benchmark suite
589-
590-
---
591-
592-
## Roadmap
593-
594-
- [ ] Distributed tracing/observability
595-
- [ ] Metrics export (Prometheus)
596-
- [ ] Cache warming strategies
597-
- [ ] Serialization plugins (msgpack, protobuf)
598-
- [ ] Redis cluster support
599-
- [ ] DynamoDB backend example
600-
601-
---
602-

0 commit comments

Comments
 (0)