Files
RustProxy/scripts/performance/simulate_cache_034.py
T
2026-10-17 22:33:00 +02:00

113 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Deterministic analytical/synthetic cache-retention scenario, not a RustProxy load test.
Model: 20 vhosts with 100 HOT + 100 COLD uniformly requested objects each.
A new configuration is saved 1,000 times. Eighty percent of saves edit only
keepalive/LB operational controls, and 20% edit content policy on one vhost.
Every save is followed by 500 uniform GETs; every object is public/cacheable and
has infinite TTL. A miss immediately inserts the object in its original tier.
Capacity is assumed adequate and there is no origin latency or concurrent IO.
The baseline flushes both tiers globally on every save, as in 0.33.8.
The candidate changes only the affected vhost after content edits.
"""
import argparse
import json
import random
def simulate(vhosts=20, objects_per_tier=100, saves=1000, requests_per_save=500, seed=3400):
if min(vhosts, objects_per_tier, saves, requests_per_save) < 1:
raise ValueError("all counts must be positive")
rng = random.Random(seed)
objects = vhosts * objects_per_tier * 2
old = set(range(objects))
new = set(range(objects))
old_hits = new_hits = old_misses = new_misses = 0
old_evicted = new_evicted = 0
edits_operational = edits_content = 0
for step in range(saves):
changed_vhost = rng.randrange(vhosts)
content_edit = step % 5 == 0
if content_edit:
edits_content += 1
else:
edits_operational += 1
old_evicted += len(old)
old.clear()
if content_edit:
low = changed_vhost * objects_per_tier * 2
high = low + objects_per_tier * 2
removed = set(range(low, high)) & new
new_evicted += len(removed)
new.difference_update(removed)
for _ in range(requests_per_save):
object_id = rng.randrange(objects)
if object_id in old:
old_hits += 1
else:
old_misses += 1
old.add(object_id)
if object_id in new:
new_hits += 1
else:
new_misses += 1
new.add(object_id)
total = saves * requests_per_save
assert old_hits + old_misses == total
assert new_hits + new_misses == total
assert new_evicted <= old_evicted
assert new_misses <= old_misses
return {
"scenario": {
"vhosts": vhosts,
"hot_objects_per_vhost": objects_per_tier,
"cold_objects_per_vhost": objects_per_tier,
"saves": saves,
"operational_saves": edits_operational,
"single_vhost_content_saves": edits_content,
"gets_per_save": requests_per_save,
"total_gets": total,
"seed": seed,
"assumptions": "All objects public/cacheable, infinite TTL, instantly refilled, uniform GETs; no bandwidth/CPU/origin timings",
},
"legacy_global_purge": {
"invalidated_objects": old_evicted,
"misses": old_misses,
"hits": old_hits,
"hit_ratio_percent": round(100 * old_hits / total, 3),
},
"scoped_cache": {
"invalidated_objects": new_evicted,
"misses": new_misses,
"hits": new_hits,
"hit_ratio_percent": round(100 * new_hits / total, 3),
},
"improvement": {
"fewer_invalidations_percent": round(100 * (1 - new_evicted / old_evicted), 3) if old_evicted else 0,
"fewer_cache_misses_percent": round(100 * (1 - new_misses / old_misses), 3) if old_misses else 0,
"hit_ratio_gain_percentage_points": round(100 * (new_hits - old_hits) / total, 3),
"observed_rps_gain_percent": None,
},
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vhosts", type=int, default=20)
parser.add_argument("--objects-per-tier", type=int, default=100)
parser.add_argument("--saves", type=int, default=1000)
parser.add_argument("--gets-per-save", type=int, default=500)
parser.add_argument("--seed", type=int, default=3400)
parser.add_argument("--output", help="optional JSON results file")
args = parser.parse_args()
result = simulate(args.vhosts, args.objects_per_tier, args.saves, args.gets_per_save, args.seed)
encoded = json.dumps(result, indent=2, sort_keys=True)
if args.output:
from pathlib import Path
Path(args.output).write_text(encoded + "\n")
print(encoded)
if __name__ == "__main__":
main()