Partition Python cache hot storage
This commit is contained in:
parent
432e5443e4
commit
31cf0377bc
3 changed files with 57 additions and 32 deletions
|
|
@ -184,6 +184,8 @@ CultCache, CultNet, and CultMesh. It is evidence, not confetti.
|
|||
benchmarks with the same operation names and reports median Python-to-C# ratios
|
||||
from three samples by default. It also emits per-metric threshold checks and an
|
||||
overall parity status so performance claims have an explicit evidence gate.
|
||||
The Python in-memory record owner is type-partitioned so public `get(...)`
|
||||
reads avoid per-call composite-key allocation on the hot path.
|
||||
|
||||
## Local Python Gates
|
||||
|
||||
|
|
@ -206,9 +208,10 @@ node --test packages\cultnet-ts\dist-test\test\interop\cultnet-interop.test.js
|
|||
|
||||
## Still Not Claimed
|
||||
|
||||
- Python does not claim to match the C# runtime for raw throughput. It has a
|
||||
measured local baseline plus a shared public `CultCache` upsert/get comparison
|
||||
harness with threshold status, not a performance victory certificate.
|
||||
- Python does not claim to match the C# runtime for every raw-throughput
|
||||
workload. It has a measured local baseline plus a shared public `CultCache`
|
||||
upsert/get comparison harness with threshold status, not a universal
|
||||
performance victory certificate.
|
||||
- Python does not own LiteNetLib security/session behavior; it speaks the
|
||||
schema-v0 interop lane used by the package harness.
|
||||
- Python does not implement the full C# daemon/server stack. Its current role is
|
||||
|
|
|
|||
|
|
@ -418,5 +418,7 @@ registered cache, and public `CultCache` upsert/get throughput. The C# compare
|
|||
command also runs `packages/cultcache-py/tools/GameCult.Caching.Benchmark` and
|
||||
reports median Python-to-C# ratios for the shared public cache operations. It
|
||||
uses three samples by default; pass `--samples` when you need a different
|
||||
evidence shape. Treat those numbers as a local baseline, not a claim that
|
||||
Python matches the C# reference in every workload.
|
||||
evidence shape. The public in-memory cache owner stores records in
|
||||
type-partitioned dictionaries so hot `get(...)` calls avoid per-call
|
||||
composite-key allocation. Treat the benchmark numbers as a local baseline, not
|
||||
a claim that Python matches the C# reference in every workload.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ class CultCacheError(RuntimeError):
|
|||
@dataclass
|
||||
class _State:
|
||||
documents: dict[str, DocumentDefinition[Any]] = field(default_factory=dict)
|
||||
values: dict[tuple[str, str], Any] = field(default_factory=dict)
|
||||
envelopes: dict[tuple[str, str], CultCacheEnvelope] = field(default_factory=dict)
|
||||
values: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
envelopes: dict[str, dict[str, CultCacheEnvelope]] = field(default_factory=dict)
|
||||
stores_by_type: dict[str, list[BackingStore]] = field(default_factory=dict)
|
||||
generic_stores: list[BackingStore] = field(default_factory=list)
|
||||
name_extractors: dict[str, str | Any] = field(default_factory=dict)
|
||||
|
|
@ -113,13 +113,14 @@ class CultCache:
|
|||
raise CultCacheError(f"Duplicate global document for type: {envelope.type}")
|
||||
seen_globals.add(envelope.type)
|
||||
value = document.decode_payload(envelope.payload)
|
||||
self._state.values[(envelope.type, envelope.key)] = value
|
||||
self._state.envelopes[(envelope.type, envelope.key)] = envelope
|
||||
self._state.values.setdefault(envelope.type, {})[envelope.key] = value
|
||||
self._state.envelopes.setdefault(envelope.type, {})[envelope.key] = envelope
|
||||
self._rebuild_indexes()
|
||||
|
||||
def get(self, document: DocumentDefinition[T], key: str) -> T | None:
|
||||
self._assert_registered(document)
|
||||
return self._state.values.get((document.type, key))
|
||||
values = self._state.values.get(document.type)
|
||||
return None if values is None else values.get(key)
|
||||
|
||||
def get_required(self, document: DocumentDefinition[T], key: str) -> T:
|
||||
value = self.get(document, key)
|
||||
|
|
@ -129,11 +130,12 @@ class CultCache:
|
|||
|
||||
def get_all(self, document: DocumentDefinition[T]) -> list[T]:
|
||||
self._assert_registered(document)
|
||||
return [value for (type, _), value in self._state.values.items() if type == document.type]
|
||||
return list(self._state.values.get(document.type, {}).values())
|
||||
|
||||
def get_envelope(self, document: DocumentDefinition[Any], key: str) -> CultCacheEnvelope | None:
|
||||
self._assert_registered(document)
|
||||
return self._state.envelopes.get((document.type, key))
|
||||
envelopes = self._state.envelopes.get(document.type)
|
||||
return None if envelopes is None else envelopes.get(key)
|
||||
|
||||
def get_required_envelope(self, document: DocumentDefinition[Any], key: str) -> CultCacheEnvelope:
|
||||
envelope = self.get_envelope(document, key)
|
||||
|
|
@ -169,7 +171,9 @@ class CultCache:
|
|||
self._assert_registered(document)
|
||||
if document.global_document and key != GLOBAL_KEY:
|
||||
raise CultCacheError(f"Global document {document.type} must use key {GLOBAL_KEY}")
|
||||
old_value = self._state.values.get((document.type, key))
|
||||
values = self._state.values.setdefault(document.type, {})
|
||||
envelopes = self._state.envelopes.setdefault(document.type, {})
|
||||
old_value = values.get(key)
|
||||
catalog_entry = document.catalog_entry()
|
||||
envelope = CultCacheEnvelope.create(
|
||||
key=key,
|
||||
|
|
@ -183,8 +187,8 @@ class CultCache:
|
|||
store.push(envelope)
|
||||
if old_value is not None:
|
||||
self._remove_value_indexes(document.type, key, old_value)
|
||||
self._state.values[(document.type, key)] = value
|
||||
self._state.envelopes[(document.type, key)] = envelope
|
||||
values[key] = value
|
||||
envelopes[key] = envelope
|
||||
self._add_value_indexes(document.type, key, value)
|
||||
|
||||
def put_envelope(self, document: DocumentDefinition[T], envelope: CultCacheEnvelope) -> T:
|
||||
|
|
@ -193,14 +197,16 @@ class CultCache:
|
|||
raise CultCacheError(
|
||||
f"Envelope type {envelope.type} does not match document type {document.type}"
|
||||
)
|
||||
old_value = self._state.values.get((document.type, envelope.key))
|
||||
values = self._state.values.setdefault(document.type, {})
|
||||
envelopes = self._state.envelopes.setdefault(document.type, {})
|
||||
old_value = values.get(envelope.key)
|
||||
value = document.decode_payload(envelope.payload)
|
||||
for store in self._stores_for_type(document.type):
|
||||
store.push(envelope)
|
||||
if old_value is not None:
|
||||
self._remove_value_indexes(document.type, envelope.key, old_value)
|
||||
self._state.values[(document.type, envelope.key)] = value
|
||||
self._state.envelopes[(document.type, envelope.key)] = envelope
|
||||
values[envelope.key] = value
|
||||
envelopes[envelope.key] = envelope
|
||||
self._add_value_indexes(document.type, envelope.key, value)
|
||||
return value
|
||||
|
||||
|
|
@ -219,12 +225,14 @@ class CultCache:
|
|||
stores = self._stores_for_type(document.type)
|
||||
for store in stores:
|
||||
store.push_all(envelopes)
|
||||
values_by_key = self._state.values.setdefault(document.type, {})
|
||||
envelopes_by_key = self._state.envelopes.setdefault(document.type, {})
|
||||
for envelope, value in zip(envelopes, values):
|
||||
old_value = self._state.values.get((document.type, envelope.key))
|
||||
old_value = values_by_key.get(envelope.key)
|
||||
if old_value is not None:
|
||||
self._remove_value_indexes(document.type, envelope.key, old_value)
|
||||
self._state.values[(document.type, envelope.key)] = value
|
||||
self._state.envelopes[(document.type, envelope.key)] = envelope
|
||||
values_by_key[envelope.key] = value
|
||||
envelopes_by_key[envelope.key] = envelope
|
||||
self._add_value_indexes(document.type, envelope.key, value)
|
||||
return values
|
||||
|
||||
|
|
@ -246,8 +254,15 @@ class CultCache:
|
|||
self._assert_registered(document)
|
||||
for store in self._stores_for_type(document.type):
|
||||
store.delete(document.type, key)
|
||||
old_value = self._state.values.pop((document.type, key), None)
|
||||
self._state.envelopes.pop((document.type, key), None)
|
||||
values = self._state.values.get(document.type)
|
||||
old_value = None if values is None else values.pop(key, None)
|
||||
if values == {}:
|
||||
self._state.values.pop(document.type, None)
|
||||
envelopes = self._state.envelopes.get(document.type)
|
||||
if envelopes is not None:
|
||||
envelopes.pop(key, None)
|
||||
if envelopes == {}:
|
||||
self._state.envelopes.pop(document.type, None)
|
||||
if old_value is not None:
|
||||
self._remove_value_indexes(document.type, key, old_value)
|
||||
|
||||
|
|
@ -257,12 +272,16 @@ class CultCache:
|
|||
|
||||
def snapshot(self) -> dict[str, dict[str, Any]]:
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for (type, key), value in self._state.values.items():
|
||||
out.setdefault(type, {})[key] = value
|
||||
for type, values in self._state.values.items():
|
||||
out[type] = dict(values)
|
||||
return out
|
||||
|
||||
def snapshot_envelopes(self) -> list[CultCacheEnvelope]:
|
||||
return list(self._state.envelopes.values())
|
||||
return [
|
||||
envelope
|
||||
for envelopes in self._state.envelopes.values()
|
||||
for envelope in envelopes.values()
|
||||
]
|
||||
|
||||
def _stores_for_type(self, type: str) -> list[BackingStore]:
|
||||
specific = self._state.stores_by_type.get(type)
|
||||
|
|
@ -282,8 +301,9 @@ class CultCache:
|
|||
def _rebuild_indexes(self) -> None:
|
||||
self._state.names.clear()
|
||||
self._state.indexes.clear()
|
||||
for (type, key), value in self._state.values.items():
|
||||
self._add_value_indexes(type, key, value)
|
||||
for type, values in self._state.values.items():
|
||||
for key, value in values.items():
|
||||
self._add_value_indexes(type, key, value)
|
||||
|
||||
def _add_value_indexes(self, type: str, key: str, value: Any) -> None:
|
||||
name_extractor = self._state.name_extractors.get(type)
|
||||
|
|
@ -315,9 +335,9 @@ class CultCache:
|
|||
name_extractor = self._state.name_extractors.get(type)
|
||||
if name_extractor is None:
|
||||
return
|
||||
for (candidate_type, candidate_key), candidate in self._state.values.items():
|
||||
for candidate_key, candidate in self._state.values.get(type, {}).items():
|
||||
candidate_name = extract_value(candidate, name_extractor)
|
||||
if candidate_type == type and candidate_key != key and candidate_name is not None and str(candidate_name) == name:
|
||||
if candidate_key != key and candidate_name is not None and str(candidate_name) == name:
|
||||
self._state.names[lookup_key] = candidate_key
|
||||
|
||||
def _remove_secondary_index(self, type: str, key: str, index: str, value: str) -> None:
|
||||
|
|
@ -328,9 +348,9 @@ class CultCache:
|
|||
extractor = self._state.index_extractors.get(type, {}).get(index)
|
||||
if extractor is None:
|
||||
return
|
||||
for (candidate_type, candidate_key), candidate in self._state.values.items():
|
||||
for candidate_key, candidate in self._state.values.get(type, {}).items():
|
||||
candidate_value = extract_value(candidate, extractor)
|
||||
if candidate_type == type and candidate_key != key and candidate_value is not None and str(candidate_value) == value:
|
||||
if candidate_key != key and candidate_value is not None and str(candidate_value) == value:
|
||||
self._state.indexes[lookup_key] = candidate_key
|
||||
|
||||
def _assert_registered(self, document: DocumentDefinition[Any]) -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue