Système de cache

Système de cache pluggable, écrit en Rust, avec un protocole simple pour brancher d'autres backends.

Architecture

Mermaid diagram dev__CACHE--m001 Mermaid diagram dev__CACHE--m001

Composants Rust

catnip_rs/src/cache/mod.rs - Types de base :

  • CacheType : enum des types de contenu (SOURCE, AST, BYTECODE, RESULT)
  • CacheKey : clé avec hash xxhash (content + options + version)
  • CacheEntry : entrée avec métadonnées
  • MemoryCache : cache mémoire IndexMap FIFO + stats

catnip_rs/src/cache/backend.rs - Adapter compilation :

  • CatnipCache : adapter haut niveau pour compilation (AST, bytecode)
  • Duck typing via PyO3 : appelle .get(), .set() dynamiquement sur le backend

catnip_rs/src/cache/disk.rs - Cache persistant :

  • DiskCache : stockage XDG avec TTL, max_size, LRU eviction

catnip_rs/src/cache/memoization.rs - Mémoïsation :

  • Memoization : cache résultats de fonctions avec index HashMap
  • get(func_name, args, kwargs) : retourne la valeur cached ou None (ne distingue pas miss de valeur None)
  • get_entry(func_name, args, kwargs) : retourne le CacheEntry complet ou None (distingue miss de valeur cached None)
  • set, invalidate, invalidate_key : écriture et invalidation
  • make_key : canonicalise les kwargs (sorted(kwargs.items())) pour que l'ordre n'affecte pas la clé

Composants Python

catnip/cachesys/base.py - Protocole Python :

  • class CacheBackend(Protocol) : interface pour backends custom Python
  • Méthodes requises : get, set, delete, clear, exists, stats

catnip/cachesys/memoization.py - Wrapper Python :

  • CachedWrapper : utilisé par context.py pour le décorateur cached()
  • standalone_cached : le builtin cached() du host standalone -- VMHost le cherche par nom au seed des globals (vm/host.rs) ; adossé à une instance Memoization Rust unique par process
  • Utilise get_entry() pour distinguer un cache miss d'une valeur cached None
  • Passe args et kwargs séparément au backend pour bénéficier de la canonicalisation des kwargs

catnip/cachesys/__init__.py - Réexports :

  • Wrapper itérable CacheType (metaclass pour for cache_type in CacheType)
  • Réexports depuis Rust

Backends intégrés

MemoryCache

Cache en mémoire rapide avec éviction FIFO.

from catnip._rs import MemoryCache

cache = MemoryCache(max_size=1000)
cache.set(key, value)
entry = cache.get(key)
stats = cache.stats()

Features :

  • IndexMap FIFO (ordre préservé)
  • Statistiques hit/miss
  • Max size configurable
  • O(1) get/set

DiskCache

Cache persistant sur disque avec TTL.

from catnip._rs import DiskCache

cache = DiskCache(
    cache_dir="/path/to/cache",
    ttl_seconds=3600,
    max_size_mb=100
)

Features :

  • XDG Base Directory (par défaut)
  • TTL (time-to-live)
  • LRU eviction
  • Sérialisation pickle
  • Atomic writes (temp + rename) pour sécurité multiprocessus

CatnipCache

Adapter haut niveau pour compilation.

from catnip._rs import CatnipCache, MemoryCache

cache = CatnipCache(
    backend=MemoryCache(),
    cache_ast=True,
    cache_bytecode=True
)

# Automatique dans Catnip class
ast = cache.get_parsed(source, optimize=True, tco_enabled=True)
cache.set_bytecode(source, bytecode, optimize=True)

Protocole pour backends personnalisés

Les host apps peuvent implémenter leurs propres backends.

Interface

from typing import Any, Optional
from catnip._rs import CacheEntry, CacheKey

class MyCustomCache:
    def get(self, key: CacheKey) -> Optional[CacheEntry]:
        """Retrieve entry from cache."""
        ...

    def set(self, key: CacheKey, value: Any, metadata: dict = None) -> None:
        """Store entry in cache."""
        ...

    def delete(self, key: CacheKey) -> bool:
        """Delete entry. Returns True if existed."""
        ...

    def clear(self) -> None:
        """Clear entire cache."""
        ...

    def exists(self, key: CacheKey) -> bool:
        """Check if key exists."""
        ...

    def stats(self) -> dict:
        """Return statistics."""
        return {
            'backend': 'custom',
            'size': 0,
            'hits': 0,
            'misses': 0,
        }

Exemple : Redis backend

import redis
import pickle
from catnip._rs import CacheEntry, CacheKey, CatnipCache

class RedisCache:
    def __init__(self, host='localhost', port=6379, db=0, ttl=3600):
        self.redis = redis.Redis(host=host, port=port, db=db)
        self.ttl = ttl
        self.hits = 0
        self.misses = 0

    def get(self, key: CacheKey) -> Optional[CacheEntry]:
        key_str = key.to_string()
        data = self.redis.get(key_str)
        if data:
            self.hits += 1
            return pickle.loads(data)
        self.misses += 1
        return None

    def set(self, key: CacheKey, value: Any, metadata: dict = None) -> None:
        key_str = key.to_string()
        entry = CacheEntry(key_str, value, key.cache_type, metadata or {})
        self.redis.setex(key_str, self.ttl, pickle.dumps(entry))

    def delete(self, key: CacheKey) -> bool:
        key_str = key.to_string()
        return bool(self.redis.delete(key_str))

    def clear(self) -> None:
        self.redis.flushdb()
        self.hits = 0
        self.misses = 0

    def exists(self, key: CacheKey) -> bool:
        key_str = key.to_string()
        return bool(self.redis.exists(key_str))

    def stats(self) -> dict:
        total = self.hits + self.misses
        hit_rate = (self.hits / total * 100) if total > 0 else 0
        return {
            'backend': 'redis',
            'size': self.redis.dbsize(),
            'hits': self.hits,
            'misses': self.misses,
            'hit_rate': f'{hit_rate:.1f}%',
        }

# Utilisation
redis_cache = RedisCache(ttl=7200)
cache = CatnipCache(backend=redis_cache)

JIT Cache

Cache unifié pour les traces JIT et les stencils Cranelift, colocalisé avec le cache de compilation.

Module : catnip_core/src/jit/trace_cache.rs

Emplacement : ~/.cache/catnip/ (fichiers plats, même répertoire que le DiskCache)

Fichiers :

  • jit_v{V}_{HASH}_{OFFSET} -- traces (postcard), clé = FNV-1a du bytecode + offset de boucle
  • jit_nv{V}_cl{CRANELIFT}_{SHA256} -- stencils Cranelift natifs, clé = hash du IR + triple ISA + flags CPU

V = VMOpCode::MAX + COMPILER_SALT dans les deux cas. Les traces persistent les enregistrements JIT pour éliminer le warm-up (100+ itérations) et sont recompilées par le Cranelift courant à la lecture (donc Cranelift-indépendantes). Les stencils persistent le code machine non relocaté pour éliminer la recompilation Cranelift.

L'invalidation se fait par version (anciennes entrées nettoyées au démarrage). Pour les stencils, le préfixe inclut en plus cl{CRANELIFT} = cranelift_codegen::VERSION : le hash que Cranelift calcule pour le nom de fichier n'encode pas sa propre version (VersionMarker est un type unité, sa Hash ne hashe rien -- la version n'est vérifiée qu'à la désérialisation du blob), si bien que deux releases de Cranelift produisent des noms de fichiers en collision pour le même IR. Les stencils n'étant pas portables entre releases, sans ce segment un binaire lié à un Cranelift plus récent chargerait du code machine émis par un plus ancien (miscompilation silencieuse). cleanup_stale purge alors les artefacts d'une autre version, et la lecture ne regarde que le préfixe courant.

Les écritures sont atomiques (temp + rename), ce qui permet l'utilisation concurrente par les workers ND sans lock.

Le DiskCache (catnip_*) ne prune que ses propres fichiers ; les fichiers JIT ne sont pas soumis au TTL du DiskCache.

Voir docs/dev/JIT.md pour les détails du mécanisme.

CacheKey et invalidation

Les clés incluent automatiquement :

  • Version du langage (__lang_id__)
  • Version Catnip (__version__)
  • Build date
  • Contenu (hash xxhash)
  • Options de compilation (optimize, tco_enabled)

Le cache s'invalide automatiquement quand :

  • La version de Catnip change
  • La date de build change
  • Les options de compilation changent

CLI

# Statistiques
catnip cache stats

# Nettoyage (remove expired)
catnip cache prune

# Clear complet
catnip cache clear

# Configuration
catnip config set cache_max_size_mb 500
catnip config set cache_ttl_seconds 7200
catnip config show --debug

Tests

pytest tests/ -k cache -v

Couverture : CacheKey, MemoryCache FIFO, DiskCache TTL/max_size, CatnipCache adapter, mémoïsation, itération CacheType.