perf(webui): make redaction containers copy-on-write
This commit is contained in:
1 parent
b7f30c6365
commit
86fe1a5746
2 files changed
+66
-13
No files matched your search
+14
-12
@@ -1035,23 +1035,25 @@ def _redact_value(v, *, _enabled: bool | None = None):
|
||||
if isinstance(v, str):
|
||||
return _redact_text(v, _enabled=_enabled)
|
||||
if isinstance(v, dict):
|
||||
changed = False
|
||||
out = {}
|
||||
out = None
|
||||
for key, value in v.items():
|
||||
redacted = _redact_value(value, _enabled=_enabled)
|
||||
if redacted is value:
|
||||
continue
|
||||
if out is None:
|
||||
out = dict(v)
|
||||
out[key] = redacted
|
||||
if redacted is not value:
|
||||
changed = True
|
||||
return out if changed else v
|
||||
return v if out is None else out
|
||||
if isinstance(v, list):
|
||||
changed = False
|
||||
out = []
|
||||
for item in v:
|
||||
out = None
|
||||
for index, item in enumerate(v):
|
||||
redacted = _redact_value(item, _enabled=_enabled)
|
||||
out.append(redacted)
|
||||
if redacted is not item:
|
||||
changed = True
|
||||
return out if changed else v
|
||||
if redacted is item:
|
||||
continue
|
||||
if out is None:
|
||||
out = list(v)
|
||||
out[index] = redacted
|
||||
return v if out is None else out
|
||||
return v
|
||||
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ GIL (json/allocations ne le liberent pas), ce qui serialise les onglets.
|
||||
|
||||
Contrat verifie ici:
|
||||
1. Quand rien n'est redige, la valeur RENVOYEE EST l'objet d'origine
|
||||
(identite, pas seulement egalite) -> zero allocation.
|
||||
(identite, pas seulement egalite) et aucun conteneur proportionnel au
|
||||
payload n'est alloue en chemin.
|
||||
2. Quand quelque chose est redige, un NOUVEL objet est renvoye et
|
||||
l'original n'est PAS mute (fail-closed: pas de fuite par aliasing).
|
||||
3. Le resultat reste egal a celui de l'ancienne implementation.
|
||||
"""
|
||||
import sys
|
||||
import tracemalloc
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
@@ -55,6 +57,32 @@ def test_clean_payload_is_returned_by_identity():
|
||||
assert out["meta"] is payload["meta"]
|
||||
|
||||
|
||||
def test_clean_payload_does_not_allocate_eager_container_copies():
|
||||
"""La voie propre doit rester O(1) en allocation de conteneurs temporaires."""
|
||||
payload = {
|
||||
"dict": {index: index for index in range(20_000)},
|
||||
"list": list(range(20_000)),
|
||||
}
|
||||
|
||||
was_tracing = tracemalloc.is_tracing()
|
||||
if not was_tracing:
|
||||
tracemalloc.start()
|
||||
before, _ = tracemalloc.get_traced_memory()
|
||||
tracemalloc.reset_peak()
|
||||
try:
|
||||
out = _redact_value(payload, _enabled=True)
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
finally:
|
||||
if not was_tracing:
|
||||
tracemalloc.stop()
|
||||
|
||||
assert out is payload
|
||||
# Une liste temporaire de 20 000 pointeurs depasse a elle seule 160 Ko.
|
||||
# Le seuil reste volontairement un ordre de grandeur en dessous, sans
|
||||
# dependre des details exacts de l'allocateur d'une version de CPython.
|
||||
assert peak - before < 16_384, f"copies temporaires detectees: {peak - before} octets"
|
||||
|
||||
|
||||
def test_sensitive_payload_is_copied_and_original_untouched():
|
||||
"""Quelque chose est redige -> nouvel objet, original intact."""
|
||||
inner = {"role": "user", "content": SECRET}
|
||||
@@ -78,6 +106,29 @@ def test_sensitive_payload_is_copied_and_original_untouched():
|
||||
assert out["meta"] is payload["meta"]
|
||||
|
||||
|
||||
def test_copy_on_first_change_preserves_order_types_and_clean_identity():
|
||||
"""Une redaction tardive copie seulement son chemin sans reordonner le JSON."""
|
||||
before = {"clean": ["avant", 1]}
|
||||
after = {"clean": ["apres", 2]}
|
||||
payload = {
|
||||
"before": before,
|
||||
"items": [before, {"secret": SECRET}, after],
|
||||
"after": after,
|
||||
}
|
||||
|
||||
out = _redact_value(payload, _enabled=True)
|
||||
|
||||
assert type(out) is dict
|
||||
assert type(out["items"]) is list
|
||||
assert list(out) == list(payload)
|
||||
assert out["before"] is before
|
||||
assert out["after"] is after
|
||||
assert out["items"][0] is before
|
||||
assert out["items"][2] is after
|
||||
assert out["items"][1] is not payload["items"][1]
|
||||
assert out["items"][1]["secret"] != SECRET
|
||||
|
||||
|
||||
def test_matches_reference_implementation():
|
||||
"""Le resultat doit rester identique a l'implementation d'origine."""
|
||||
payload = {
|
||||
|
||||
Reference in new issue
Block a user