v0.88.0
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 againstflame-stats:partitionso an evicted bucket still answers, and a miss resolves throughPartition_Node::locate_by()— which has no early stop for a key that is ABSENT from the index, so it reads and splits every.idxline 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:overviewissues 108 such batches,urls1,728, andurlswith 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 freshstats_mirror_read_budget_ms(new config key, default 1500,0off) andarm_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_ENTRIESby count — and the mirror read was the one with none.dispatch()rather thanfill()becauseMcp_Controllercalls 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 namespaceSTATS_MIRROR_TOPNcaps at zero.buffer_mirror_write()refuses to storeurls_h,urlnames_handlb_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 --searchagainst 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, becauseLog_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
overviewpast any answering deadline.lbnow has the coarse hourly tierurlshas had since decision 17, folded by the sameroll_up_hours()pass into the same shape a fine bucket holds, so onebuild_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.