{
  "id": "P135",
  "slug": "percentiles",
  "key": "P135-percentiles",
  "title": "Percentiles",
  "summary": "Three shops' response times: exact percentiles, why their p95s cannot be averaged while their histograms can be added, which aggregates survive being computed by shop, and how strict backends must be when a request waits for N of them.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P135 - Percentiles\n\nResponse times from three shops, their exact percentiles, and what can\nand cannot be put together from per-shop numbers.\n\n- `main.eml` - the menu, the shops, the percentiles, putting the p95\n  together, aggregates by shop, and a request that fans out\n- `pct.eml` - the response times, nearest-rank percentiles, histograms on\n  fixed buckets, and the fan-out numbers\n\nHow each part works:\n\n- Each shop has mostly quick requests and a share of slow ones: 3% of\n  A's 600, 5% of B's 300, 10% of C's 100. A percentile is the smallest\n  time with at least that share at or below it.\n- The p95 of all 1000 is 185 ms. The shops' own p95s give 252.7 as a\n  mean, 120.1 as a mean by requests, and 610 as the largest: none of\n  them is the p95 of the whole. Histograms on fixed buckets add exactly,\n  and the bucket the p95 falls in gives 200, an upper bound, never below.\n- As in the corpus case `aggregates-that-do-not-decompose`, a sum, a\n  count, a maximum and a minimum put together by shop; a distinct count\n  adds the times two shops share twice, 181 against 171, and the median\n  of the medians is 71 where the median is 44.\n- As in the corpus case `percentile-composition`, a request that waits\n  for N backends is as slow as the slowest. Every backend within its p99\n  happens 0.99^N of the time: 90.44% for 10. To hold the request at p99,\n  each backend must be held at the quantile q with q^N = 0.99: 99.900%\n  for 10, near the p99.9. Both are worked out in whole numbers.\n\nWhat is checked: menu choices 1 to 6.\n\nSessions: `sessions/basic.in` gives every screen once, and\n`sessions/bad-input.in` gives menu choices 0, x, 7, an empty one and 12,\nthen the shops.\n\nBuilt on the verified corpus cases `aggregates-that-do-not-decompose`\n(what computing per shard and merging keeps and loses) and\n`percentile-composition` (the slowest of N backends sets the request's\ntime).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P135 percentiles: three shops' response times, their exact percentiles,\n# the wrong and the right ways to put them together, aggregates by shop,\n# and a request that fans out to several backends.\nimport pct\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef all_times(sh):\n    [] => out\n    for s in sh:\n        out + s[1] => out\n    return out\n\ndef show(sh):\n    for s in sh:\n        (\"  shop \" + s[0] + \": \" + str(len(s[1])) + \" requests, the first five \" + str(s[1][0]) + \", \" + str(s[1][1]) + \", \" + str(s[1][2]) + \", \" + str(s[1][3]) + \" and \" + str(s[1][4]) + \" ms\") ^0\n    \"Each shop has mostly quick requests and a share of slow ones: 3% at A, 5% at B, 10% at C.\" ^0\n\ndef percentiles(sh):\n    \"Exact percentiles, by nearest rank (ms):\" ^0\n    \"  shop   requests    p50    p95    p99\" ^0\n    for s in sh:\n        (\"  \" + pad(s[0], 5) + lpad(str(len(s[1])), 10) + lpad(str(pct.percentile(s[1], 50)), 7) + lpad(str(pct.percentile(s[1], 95)), 7) + lpad(str(pct.percentile(s[1], 99)), 7)) ^0\n    all_times(sh) => t\n    (\"  \" + pad(\"all\", 5) + lpad(str(len(t)), 10) + lpad(str(pct.percentile(t, 50)), 7) + lpad(str(pct.percentile(t, 95)), 7) + lpad(str(pct.percentile(t, 99)), 7)) ^0\n\ndef merging(sh):\n    all_times(sh) => t\n    pct.percentile(t, 95) => truth\n    0 => total\n    0 => weighted\n    0 => most\n    for s in sh:\n        pct.percentile(s[1], 95) => p\n        total + p => total\n        weighted + p * len(s[1]) => weighted\n        if p > most:\n            p => most\n    (\"The p95 of all \" + str(len(t)) + \" requests is \" + str(truth) + \" ms. From the shops' own p95s:\") ^0\n    (\"  their mean:                \" + mean_text(total, len(sh))) ^0\n    (\"  their mean, by requests:   \" + mean_text(weighted, len(t))) ^0\n    (\"  the largest:               \" + str(most)) ^0\n    [] => h\n    for k in [0:len(pct.buckets()) - 1]:\n        h + [0] => h\n    for s in sh:\n        pct.histogram(s[1]) => hs\n        for k in [0:len(h) - 1]:\n            h[k] + hs[k] => h[k]\n    pct.from_histogram(h, 95) => hb\n    (\"  the shops' histograms added: at most \" + str(hb) + \" (bucket edges at 25, 50, 100, 200, 300, 500, 800 and 1000)\") ^0\n    \"None of the first three is the p95 of the whole; the histograms add exactly, and their answer is the edge of the bucket the p95 falls in, never below it.\" ^0\n\ndef mean_text(s, n):\n    # s / n to one decimal, rounded half up\n    int((20 * s + n) / (2 * n)) => t\n    while t * 2 * n > 20 * s + n:\n        t - 1 => t\n    while (t + 1) * 2 * n <= 20 * s + n:\n        t + 1 => t\n    return str(int(t / 10)) + \".\" + str(t % 10)\n\ndef aggregates(sh):\n    all_times(sh) => t\n    0 => s_sum\n    0 => s_count\n    0 => s_max\n    100000 => s_min\n    0 => s_distinct\n    [] => meds\n    for s in sh:\n        0 => x\n        for v in s[1]:\n            x + v => x\n        s_sum + x => s_sum\n        s_count + len(s[1]) => s_count\n        for v in s[1]:\n            if v > s_max:\n                v => s_max\n            if v < s_min:\n                v => s_min\n        s_distinct + pct.distinct(s[1]) => s_distinct\n        meds + [pct.median_low(s[1])] => meds\n    0 => w_sum\n    0 => w_max\n    100000 => w_min\n    for v in t:\n        w_sum + v => w_sum\n        if v > w_max:\n            v => w_max\n        if v < w_min:\n            v => w_min\n    \"Computed for each shop and put together, against computed on all requests at once:\" ^0\n    \"  aggregate           by shop   at once\" ^0\n    row(\"sum, added\", s_sum, w_sum)\n    row(\"count, added\", s_count, len(t))\n    row(\"max of the maxes\", s_max, w_max)\n    row(\"min of the mins\", s_min, w_min)\n    row(\"distinct, added\", s_distinct, pct.distinct(t))\n    row(\"median of medians\", pct.median_low(meds), pct.median_low(t))\n    \"A sum, a count, a maximum and a minimum put together; a distinct count adds the times two shops share twice, and a median of medians is not the median.\" ^0\n\ndef row(name, a, b):\n    \"\" => mark\n    if a != b:\n        \"   differs\" => mark\n    (\"  \" + pad(name, 18) + lpad(str(a), 9) + lpad(str(b), 10) + mark) ^0\n\ndef fan_out():\n    \"A request that waits for N backends is as slow as the slowest of them.\" ^0\n    \"  N    within every backend's p99   backend quantile needed for the request's p99\" ^0\n    for n in [1, 2, 5, 10, 20]:\n        pct.within(n) => w\n        pct.needed(n) => q\n        (\"  \" + pad(str(n), 5) + lpad(str(int(w / 100)) + \".\" + two(w % 100) + \"%\", 14) + lpad(str(int(q / 1000)) + \".\" + three(q % 1000) + \"%\", 38)) ^0\n    \"Holding a request over ten backends to p99 needs each backend held near its p99.9, not its p99.\" ^0\n\ndef two(x):\n    if x < 10:\n        return \"0\" + str(x)\n    return str(x)\n\ndef three(x):\n    if x < 10:\n        return \"00\" + str(x)\n    if x < 100:\n        return \"0\" + str(x)\n    return str(x)\n\npct.shops() => sh\n\n\"== Percentiles ==\" ^0\n\"Response times from three shops: their percentiles, and what can be put together from per-shop numbers.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(sh)\n    elif choice == \"2\":\n        percentiles(sh)\n    elif choice == \"3\":\n        merging(sh)\n    elif choice == \"4\":\n        aggregates(sh)\n    elif choice == \"5\":\n        fan_out()\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import pct\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef all_times(sh):\n    out = []\n    for s in sh:\n        out = out + s[1]\n    return out\n\ndef show(sh):\n    for s in sh:\n        print(\"  shop \" + s[0] + \": \" + str(len(s[1])) + \" requests, the first five \" + str(s[1][0]) + \", \" + str(s[1][1]) + \", \" + str(s[1][2]) + \", \" + str(s[1][3]) + \" and \" + str(s[1][4]) + \" ms\")\n    print(\"Each shop has mostly quick requests and a share of slow ones: 3% at A, 5% at B, 10% at C.\")\n\ndef percentiles(sh):\n    print(\"Exact percentiles, by nearest rank (ms):\")\n    print(\"  shop   requests    p50    p95    p99\")\n    for s in sh:\n        print(\"  \" + pad(s[0], 5) + lpad(str(len(s[1])), 10) + lpad(str(pct.percentile(s[1], 50)), 7) + lpad(str(pct.percentile(s[1], 95)), 7) + lpad(str(pct.percentile(s[1], 99)), 7))\n    t = all_times(sh)\n    print(\"  \" + pad(\"all\", 5) + lpad(str(len(t)), 10) + lpad(str(pct.percentile(t, 50)), 7) + lpad(str(pct.percentile(t, 95)), 7) + lpad(str(pct.percentile(t, 99)), 7))\n\ndef merging(sh):\n    t = all_times(sh)\n    truth = pct.percentile(t, 95)\n    total = 0\n    weighted = 0\n    most = 0\n    for s in sh:\n        p = pct.percentile(s[1], 95)\n        total = total + p\n        weighted = weighted + p * len(s[1])\n        if p > most:\n            most = p\n    print(\"The p95 of all \" + str(len(t)) + \" requests is \" + str(truth) + \" ms. From the shops' own p95s:\")\n    print(\"  their mean:                \" + mean_text(total, len(sh)))\n    print(\"  their mean, by requests:   \" + mean_text(weighted, len(t)))\n    print(\"  the largest:               \" + str(most))\n    h = []\n    for k in range(0, len(pct.buckets())):\n        h = h + [0]\n    for s in sh:\n        hs = pct.histogram(s[1])\n        for k in range(0, len(h)):\n            h[k] = h[k] + hs[k]\n    hb = pct.from_histogram(h, 95)\n    print(\"  the shops' histograms added: at most \" + str(hb) + \" (bucket edges at 25, 50, 100, 200, 300, 500, 800 and 1000)\")\n    print(\"None of the first three is the p95 of the whole; the histograms add exactly, and their answer is the edge of the bucket the p95 falls in, never below it.\")\n\ndef mean_text(s, n):\n    t = int((20 * s + n) / (2 * n))\n    while t * 2 * n > 20 * s + n:\n        t = t - 1\n    while (t + 1) * 2 * n <= 20 * s + n:\n        t = t + 1\n    return str(int(t / 10)) + \".\" + str(t % 10)\n\ndef aggregates(sh):\n    t = all_times(sh)\n    s_sum = 0\n    s_count = 0\n    s_max = 0\n    s_min = 100000\n    s_distinct = 0\n    meds = []\n    for s in sh:\n        x = 0\n        for v in s[1]:\n            x = x + v\n        s_sum = s_sum + x\n        s_count = s_count + len(s[1])\n        for v in s[1]:\n            if v > s_max:\n                s_max = v\n            if v < s_min:\n                s_min = v\n        s_distinct = s_distinct + pct.distinct(s[1])\n        meds = meds + [pct.median_low(s[1])]\n    w_sum = 0\n    w_max = 0\n    w_min = 100000\n    for v in t:\n        w_sum = w_sum + v\n        if v > w_max:\n            w_max = v\n        if v < w_min:\n            w_min = v\n    print(\"Computed for each shop and put together, against computed on all requests at once:\")\n    print(\"  aggregate           by shop   at once\")\n    row(\"sum, added\", s_sum, w_sum)\n    row(\"count, added\", s_count, len(t))\n    row(\"max of the maxes\", s_max, w_max)\n    row(\"min of the mins\", s_min, w_min)\n    row(\"distinct, added\", s_distinct, pct.distinct(t))\n    row(\"median of medians\", pct.median_low(meds), pct.median_low(t))\n    print(\"A sum, a count, a maximum and a minimum put together; a distinct count adds the times two shops share twice, and a median of medians is not the median.\")\n\ndef row(name, a, b):\n    mark = \"\"\n    if a != b:\n        mark = \"   differs\"\n    print(\"  \" + pad(name, 18) + lpad(str(a), 9) + lpad(str(b), 10) + mark)\n\ndef fan_out():\n    print(\"A request that waits for N backends is as slow as the slowest of them.\")\n    print(\"  N    within every backend's p99   backend quantile needed for the request's p99\")\n    for n in [1, 2, 5, 10, 20]:\n        w = pct.within(n)\n        q = pct.needed(n)\n        print(\"  \" + pad(str(n), 5) + lpad(str(int(w / 100)) + \".\" + two(w % 100) + \"%\", 14) + lpad(str(int(q / 1000)) + \".\" + three(q % 1000) + \"%\", 38))\n    print(\"Holding a request over ten backends to p99 needs each backend held near its p99.9, not its p99.\")\n\ndef two(x):\n    if x < 10:\n        return \"0\" + str(x)\n    return str(x)\n\ndef three(x):\n    if x < 10:\n        return \"00\" + str(x)\n    if x < 100:\n        return \"0\" + str(x)\n    return str(x)\n\nsh = pct.shops()\nprint(\"== Percentiles ==\")\nprint(\"Response times from three shops: their percentiles, and what can be put together from per-shop numbers.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(sh)\n    elif choice == \"2\":\n        percentiles(sh)\n    elif choice == \"3\":\n        merging(sh)\n    elif choice == \"4\":\n        aggregates(sh)\n    elif choice == \"5\":\n        fan_out()\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "pct.eml",
      "eml": "# P135 percentiles - response times from three shops, and what can and\n# cannot be put together from per-shop numbers. As in the corpus case\n# aggregates-that-do-not-decompose, a sum, a count, a maximum and a minimum\n# merge; a median, a distinct count and a percentile do not, while a\n# histogram on fixed buckets does, and gives an upper bound. As in\n# percentile-composition, a request that waits for N backends is as slow as\n# the slowest, so it meets the backends' p99 only when all N do.\n\ndef mix(i, salt):\n    i * 7919 + salt * 104729 + 12345 => x\n    for k in [1:3]:\n        (x * x + 7) % 1000000007 => x\n    return x % 1000000\n\ndef shops():\n    # [name, response times in ms]\n    [[\"A\", 600, 20, 30, 3, 200], [\"B\", 300, 40, 60, 5, 300], [\"C\", 100, 100, 100, 10, 500]] => spec\n    [] => out\n    for s in spec:\n        [] => times\n        for i in [1:s[1]]:\n            if mix(i, ord_of(s[0])) % 100 < s[4]:\n                times + [s[5] + mix(i, ord_of(s[0]) + 7) % 300] => times\n            else:\n                times + [s[2] + mix(i, ord_of(s[0]) + 3) % s[3]] => times\n        out + [[s[0], times]] => out\n    return out\n\ndef ord_of(name):\n    if name == \"A\":\n        return 11\n    if name == \"B\":\n        return 12\n    return 13\n\ndef counts(times):\n    # {time: how many}\n    {} => c\n    for t in times:\n        if t in c:\n            c[t] + 1 => c[t]\n        else:\n            1 => c[t]\n    return c\n\ndef percentile(times, p):\n    # nearest rank: the smallest time with at least p percent at or below it\n    counts(times) => c\n    0 => most\n    for t in times:\n        if t > most:\n            t => most\n    0 => run\n    for v in [0:most]:\n        if v in c:\n            run + c[v] => run\n            if run * 100 >= p * len(times):\n                return v\n    return most\n\ndef buckets():\n    return [25, 50, 100, 200, 300, 500, 800, 1000]\n\ndef histogram(times):\n    # how many times fall at or below each bucket's edge and above the last\n    [] => h\n    for b in buckets():\n        h + [0] => h\n    for t in times:\n        False => placed\n        for k in [0:len(buckets()) - 1]:\n            if not placed and t <= buckets()[k]:\n                h[k] + 1 => h[k]\n                True => placed\n    return h\n\ndef from_histogram(h, p):\n    # the edge of the bucket where p percent is reached: an upper bound\n    0 => n\n    for x in h:\n        n + x => n\n    0 => run\n    for k in [0:len(h) - 1]:\n        run + h[k] => run\n        if run * 100 >= p * n:\n            return buckets()[k]\n    return buckets()[len(h) - 1]\n\ndef distinct(times):\n    return len(counts(times))\n\ndef median_low(times):\n    return percentile(times, 50)\n\ndef within(n):\n    # [whole percent, hundredths]: the share of requests over n backends\n    # that are all within each backend's p99, 0.99 ^ n, rounded half up\n    1 => num\n    1 => den\n    for k in [1:n]:\n        num * 99 => num\n        den * 100 => den\n    # in hundredths of a percent\n    int((2 * 10000 * num + den) / (2 * den)) => t\n    while t * 2 * den > 2 * 10000 * num + den:\n        t - 1 => t\n    while (t + 1) * 2 * den <= 2 * 10000 * num + den:\n        t + 1 => t\n    return t\n\ndef needed(n):\n    # the smallest backend quantile, in thousandths of a percent, that\n    # keeps the request within it 99 times in 100: k / 100000 with\n    # (k / 100000) ^ n >= 0.99, found by bisection on whole numbers\n    0 => lo\n    100000 => hi\n    while lo < hi:\n        int((lo + hi) / 2) => mid\n        1 => a\n        1 => b\n        for k in [1:n]:\n            a * mid => a\n            b * 100000 => b\n        if a * 100 >= 99 * b:\n            mid => hi\n        else:\n            mid + 1 => lo\n    return lo\n",
      "python": "def mix(i, salt):\n    x = i * 7919 + salt * 104729 + 12345\n    for k in range(1, 4):\n        x = (x * x + 7) % 1000000007\n    return x % 1000000\n\ndef shops():\n    spec = [[\"A\", 600, 20, 30, 3, 200], [\"B\", 300, 40, 60, 5, 300], [\"C\", 100, 100, 100, 10, 500]]\n    out = []\n    for s in spec:\n        times = []\n        for i in range(1, s[1]+1):\n            if mix(i, ord_of(s[0])) % 100 < s[4]:\n                times = times + [s[5] + mix(i, ord_of(s[0]) + 7) % 300]\n            else:\n                times = times + [s[2] + mix(i, ord_of(s[0]) + 3) % s[3]]\n        out = out + [[s[0], times]]\n    return out\n\ndef ord_of(name):\n    if name == \"A\":\n        return 11\n    if name == \"B\":\n        return 12\n    return 13\n\ndef counts(times):\n    c = {}\n    for t in times:\n        if t in c:\n            c[t] = c[t] + 1\n        else:\n            c[t] = 1\n    return c\n\ndef percentile(times, p):\n    c = counts(times)\n    most = 0\n    for t in times:\n        if t > most:\n            most = t\n    run = 0\n    for v in range(0, most+1):\n        if v in c:\n            run = run + c[v]\n            if run * 100 >= p * len(times):\n                return v\n    return most\n\ndef buckets():\n    return [25, 50, 100, 200, 300, 500, 800, 1000]\n\ndef histogram(times):\n    h = []\n    for b in buckets():\n        h = h + [0]\n    for t in times:\n        placed = False\n        for k in range(0, len(buckets())):\n            if not placed and t <= buckets()[k]:\n                h[k] = h[k] + 1\n                placed = True\n    return h\n\ndef from_histogram(h, p):\n    n = 0\n    for x in h:\n        n = n + x\n    run = 0\n    for k in range(0, len(h)):\n        run = run + h[k]\n        if run * 100 >= p * n:\n            return buckets()[k]\n    return buckets()[len(h) - 1]\n\ndef distinct(times):\n    return len(counts(times))\n\ndef median_low(times):\n    return percentile(times, 50)\n\ndef within(n):\n    num = 1\n    den = 1\n    for k in range(1, n+1):\n        num = num * 99\n        den = den * 100\n    t = int((2 * 10000 * num + den) / (2 * den))\n    while t * 2 * den > 2 * 10000 * num + den:\n        t = t - 1\n    while (t + 1) * 2 * den <= 2 * 10000 * num + den:\n        t = t + 1\n    return t\n\ndef needed(n):\n    lo = 0\n    hi = 100000\n    while lo < hi:\n        mid = int((lo + hi) / 2)\n        a = 1\n        b = 1\n        for k in range(1, n+1):\n            a = a * mid\n            b = b * 100000\n        if a * 100 >= 99 * b:\n            hi = mid\n        else:\n            lo = mid + 1\n    return lo\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n7\n\n12\n1\n6\n",
      "screen": "== Percentiles ==\nResponse times from three shops: their percentiles, and what can be put together from per-shop numbers.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 7\nPick a number from 1 to 6.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> \nPick a number from 1 to 6.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 12\nPick a number from 1 to 6.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 1\n  shop A: 600 requests, the first five 23, 200, 37, 35 and 26 ms\n  shop B: 300 requests, the first five 44, 84, 50, 70 and 52 ms\n  shop C: 100 requests, the first five 154, 777, 111, 143 and 187 ms\nEach shop has mostly quick requests and a share of slow ones: 3% at A, 5% at B, 10% at C.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n5\n6\n",
      "screen": "== Percentiles ==\nResponse times from three shops: their percentiles, and what can be put together from per-shop numbers.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 1\n  shop A: 600 requests, the first five 23, 200, 37, 35 and 26 ms\n  shop B: 300 requests, the first five 44, 84, 50, 70 and 52 ms\n  shop C: 100 requests, the first five 154, 777, 111, 143 and 187 ms\nEach shop has mostly quick requests and a share of slow ones: 3% at A, 5% at B, 10% at C.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 2\nExact percentiles, by nearest rank (ms):\n  shop   requests    p50    p95    p99\n  A           600     35     49    410\n  B           300     71     99    518\n  C           100    154    610    771\n  all        1000     44    185    523\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 3\nThe p95 of all 1000 requests is 185 ms. From the shops' own p95s:\n  their mean:                252.7\n  their mean, by requests:   120.1\n  the largest:               610\n  the shops' histograms added: at most 200 (bucket edges at 25, 50, 100, 200, 300, 500, 800 and 1000)\nNone of the first three is the p95 of the whole; the histograms add exactly, and their answer is the edge of the bucket the p95 falls in, never below it.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 4\nComputed for each shop and put together, against computed on all requests at once:\n  aggregate           by shop   at once\n  sum, added            71132     71132\n  count, added           1000      1000\n  max of the maxes        777       777\n  min of the mins          20        20\n  distinct, added         181       171   differs\n  median of medians        71        44   differs\nA sum, a count, a maximum and a minimum put together; a distinct count adds the times two shops share twice, and a median of medians is not the median.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 5\nA request that waits for N backends is as slow as the slowest of them.\n  N    within every backend's p99   backend quantile needed for the request's p99\n  1            99.00%                               99.000%\n  2            98.01%                               99.499%\n  5            95.10%                               99.800%\n  10           90.44%                               99.900%\n  20           81.79%                               99.950%\nHolding a request over ten backends to p99 needs each backend held near its p99.9, not its p99.\n\n1) the shops  2) percentiles  3) putting p95 together  4) aggregates by shop  5) fan-out  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "aggregates-that-do-not-decompose",
      "caseId": "307-aggregates-that-do-not-decompose",
      "title": "Aggregates that do not decompose — four survived partitioning, three did not, and the call sites are identical"
    },
    {
      "slug": "percentile-composition",
      "caseId": "267-percentile-composition",
      "title": "Percentile composition — every backend meets its p99, one request in ten is slow"
    }
  ],
  "updated": "2026-10-17"
}
