113 lines
4.5 KiB
Python
Executable File
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()
|