Skip to content

v0.88.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 23:42
· 11 commits to main since this release

Fixed

  • The dashboard walked the whole durable mirror index once per memcache miss, thousands of times per poll. This is what actually spent the 200-290 seconds; the three releases before it each removed a real cost, but none of them the dominant one. Performance_CI_Node::stats_stores() arms every reader store against flame-stats:partition so an evicted bucket still answers, and a miss resolves through Partition_Node::locate_by() — which has no early stop for a key that is ABSENT from the index, so it reads and splits every .idx line in the partition before it can say "not here". Measured on eve: 192ms per full pass over 150,000 index lines. Measured against the shipped read paths with a cold cache and four partitions: overview issues 108 such batches, urls 1,728, and urls with a search term 3,456 — sixteen shards by four partitions by twenty-five bucket chunks, a geometry the per-shard fold (decision 14) requires for memory and so cannot shrink. 3,456 passes at 192ms is 663 seconds of index walking inside one 15-second poll, which is why the tail was violently bimodal (p50 187ms warm, p90 162.5s cold) and why cutting the URL count 6.7x changed nothing: the cost is the index's size times the batch COUNT, and neither is the URL count.
    Two bounds, both symmetric with what the write path already does:

    • Performance_CI_Node::dispatch() starts each VERB with a fresh stats_mirror_read_budget_ms (new config key, default 1500, 0 off) and arm_stats_reader() stops consulting the mirror once it is spent, answering from memcache alone for the rest of that verb. The class docblock already held every other disk scan to a bound — scan_floor() by time, MAX_INDEX_ENTRIES by count — and the mirror read was the one with none. dispatch() rather than fill() because Mcp_Controller calls it straight, with no Message behind it. What a spent budget costs is the EVICTED buckets of the partitions the fold had not reached yet — the outer loop is per store, so it drops later partitions whole rather than trimming each one's oldest — and memcache answers the rest, so the next poll is complete. The WORKER's seam is unbudgeted: it is restoring its own state, not answering a poll.
    • rehydrate_seam() no longer looks for a namespace STATS_MIRROR_TOPN caps at zero. buffer_mirror_write() refuses to store urls_h, urlnames_h and lb_h, so a read of one could only ever walk the index in full and find nothing — 256 of those 3,456 passes were guaranteed-futile.
    • Measured, one urls --search against a 150,000-line mirror on one partition: 149,877 ms unbounded, 1,661 ms budgeted. A whole-window locator PREFETCH was built and rejected on measurement — it makes the answer complete rather than clamped, but end-to-end it is 1,708 ms, because Log_Manager::url_hash() is a hand-rolled FNV-1a run twice per key and hashing a window costs ~740 ms across four partitions. The spec records the numbers so it is not re-derived.
  • The leaderboard read 288 buckets across four partitions on every poll, and each one carries a category per hook the site fires. 1,198 of them on a production hub, each with its own entry map — 1,152 keys and tens of millions of array elements unserialized and merged, per 15-second poll. That is the read that takes overview past any answering deadline. lb now has the coarse hourly tier urls has had since decision 17, folded by the same roll_up_hours() pass into the same shape a fine bucket holds, so one build_leaderboard() fold serves both tiers: 1,152 keys → 144, with each hour merged once at write time instead of 288 times per read. An hour the fold has not reached is still answered from its twelve fine buckets, so a fresh deploy and a cold-start backfill stay self-healing. Nothing is capped. The per-SERVER board keeps the fine path — a shard count is a constant the schema chooses, but the servers present in an hour cannot be enumerated from the keyspace.