Created
July 28, 2026 03:48
-
-
Save fzakaria/17c72f0eddc0f10469e008e67e1385cc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env nix-shell | |
| #! nix-shell -i python3 --pure | |
| #! nix-shell -p "python3.withPackages(ps: with ps; [ plotnine pandas numpy scipy ])" | |
| #! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/72841a4a8761d1aed92ef6169a636872c986c76d.tar.gz | |
| """ | |
| Generate the figures for the "look at your data" post. | |
| Self-contained: the shebang above pins nixpkgs to an exact commit and pulls in | |
| plotnine + friends, so `./latency_charts.py` reproduces every figure on any | |
| machine with Nix installed -- no virtualenv, no pip, no system packages. | |
| One synthetic dataset, one fixed seed. A service rolls out a new caching tier | |
| over seven days. Cache *hits* are much faster; cache *misses* pay an extra hop | |
| and are much slower. The rollout fraction climbs 0 -> 100%, so the population | |
| morphs from one hump (all baseline) into two (fast hits + a slow miss tail). | |
| Every chart below is a different lens on the SAME numbers. | |
| chmod +x latency_charts.py && ./latency_charts.py # writes SVGs beside it | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| import pandas as pd | |
| from scipy.stats import gaussian_kde | |
| from plotnine import * | |
| from plotnine import options as p9options | |
| # ---------------------------------------------------------------------------- | |
| # Site palette. Charts are transparent line-art on the page's #ebe6da matte, so | |
| # nothing here paints a background. Ink + one accent, echoing the site itself. | |
| # ---------------------------------------------------------------------------- | |
| INK = "#1a1815" # near-black warm ink -> "before" / cache hit | |
| INK_MUTED = "#565046" | |
| INK_FAINT = "#6f685b" # muted neutral -> baseline reference | |
| ACCENT = "#9e3413" # burnt vermillion -> "after" / cache miss | |
| RULE = "#dcd5c5" # hairline gridlines | |
| RULE_STRONG= "#c2b9a6" | |
| MATTE = "#ebe6da" | |
| # sequential ramp (one hue, light -> dark) for the rollout magnitude | |
| RAMP7 = ["#e7c9ba", "#d8a888", "#c8855c", "#b56336", "#9e3413", "#7c2810", "#5a1c0b"] | |
| SERIF = "Newsreader, Georgia, 'Times New Roman', serif" | |
| MONO = "JetBrains Mono, ui-monospace, Menlo, monospace" | |
| OUT = "." | |
| W, H = 7.2, 4.3 # inches; ~720px, matches the 700px prose column | |
| # keep text as real <text> (tiny SVG, selectable, sharp in both themes) | |
| import matplotlib | |
| matplotlib.use("Agg") # headless: render straight to file, no display needed | |
| matplotlib.rcParams["svg.fonttype"] = "none" | |
| p9options.figure_size = (W, H) | |
| p9options.dpi = 100 | |
| def base_theme(h=H): | |
| return ( | |
| theme_minimal() | |
| + theme( | |
| figure_size=(W, h), | |
| text=element_text(family=SERIF, color=INK), | |
| axis_text=element_text(family=MONO, color=INK_MUTED, size=8), | |
| axis_title=element_text(family=SERIF, color=INK, size=11), | |
| plot_title=element_text(family=SERIF, color=INK, size=13, weight="bold", ha="left"), | |
| plot_subtitle=element_text(family=SERIF, color=INK_MUTED, size=10, ha="left"), | |
| plot_caption=element_text(family=MONO, color=INK_FAINT, size=7, ha="right"), | |
| panel_grid_major=element_line(color=RULE, size=0.4), | |
| panel_grid_minor=element_blank(), | |
| panel_background=element_rect(fill="none", color="none"), | |
| plot_background=element_rect(fill="none", color="none"), | |
| legend_background=element_rect(fill="none", color="none"), | |
| legend_key=element_rect(fill="none", color="none"), | |
| axis_ticks=element_line(color=RULE_STRONG, size=0.3), | |
| axis_line=element_line(color=RULE_STRONG, size=0.4), | |
| legend_position="none", | |
| plot_margin=0.022, # keep right-aligned captions off the edge | |
| ) | |
| ) | |
| def save(p, name): | |
| path = f"{OUT}/{name}.svg" | |
| p.save(path, verbose=False) | |
| print(f" wrote {path}") | |
| # ---------------------------------------------------------------------------- | |
| # 1. The data. Lognormal latencies; tuned so the mean drifts UP a little, the | |
| # median drops a lot, and the tail blows up -- so every summary statistic | |
| # ends up telling a different story. | |
| # ---------------------------------------------------------------------------- | |
| def generate(): | |
| rng = np.random.default_rng(42) | |
| # (median_ms, sigma) for each path. Tuned so the two after-populations are | |
| # far enough apart (and the miss share big enough) to read as two peaks. | |
| BASE = (100.0, 0.50) # day-0 world: one path | |
| HIT = (45.0, 0.40) # new tier, cache hit -> much faster, tight | |
| MISS = (300.0, 0.48) # new tier, cache miss -> extra hop, slower | |
| # Every request also has a response size. The cache holds small hot objects, | |
| # so the chance of a *miss* climbs with size (logistic in log-size). This is | |
| # what makes the slow tail: the misses are the big objects. | |
| SIZE_MED, SIZE_SIG = 18.0, 0.95 # KB, lognormal | |
| MISS_MID, MISS_WIDTH = 45.0, 0.60 # KB midpoint, logistic width (ln units) | |
| frac = [0.0, 0.15, 0.30, 0.50, 0.70, 0.85, 1.0] # rollout % by day | |
| N = 12_000 | |
| def lognorm(median, sigma, n): | |
| return rng.lognormal(mean=np.log(median), sigma=sigma, size=n) | |
| rows = [] | |
| for day, f in enumerate(frac): | |
| size = lognorm(SIZE_MED, SIZE_SIG, N) | |
| on_new = rng.random(N) < f | |
| p_miss = 1.0 / (1.0 + np.exp(-(np.log(size) - np.log(MISS_MID)) / MISS_WIDTH)) | |
| is_miss = rng.random(N) < p_miss | |
| lat = np.empty(N) | |
| m_base = ~on_new | |
| m_miss = on_new & is_miss | |
| m_hit = on_new & ~is_miss | |
| lat[m_base] = lognorm(*BASE, m_base.sum()) | |
| lat[m_hit] = lognorm(*HIT, m_hit.sum()) | |
| lat[m_miss] = lognorm(*MISS, m_miss.sum()) | |
| path = np.where(m_base, "baseline", np.where(m_hit, "hit", "miss")) | |
| rows.append(pd.DataFrame({"day": day, "rollout": f, "latency": lat, | |
| "size_kb": size, "path": path})) | |
| return pd.concat(rows, ignore_index=True) | |
| df = generate() | |
| before = df[df.day == 0].latency.to_numpy() # 0% rolled out | |
| after = df[df.day == 6].latency.to_numpy() # 100% rolled out | |
| def summary(x): | |
| return dict(mean=x.mean(), p50=np.percentile(x, 50), | |
| p95=np.percentile(x, 95), p99=np.percentile(x, 99)) | |
| sb, sa = summary(before), summary(after) | |
| print("\n stat before after delta") | |
| for k in ("mean", "p50", "p95", "p99"): | |
| d = (sa[k] - sb[k]) / sb[k] * 100 | |
| print(f" {k:>5} {sb[k]:7.0f} {sa[k]:7.0f} {d:+5.0f}%") | |
| # crossing point of the two ECDFs | |
| grid = np.linspace(1, 800, 4000) | |
| Fb = np.searchsorted(np.sort(before), grid) / before.size | |
| Fa = np.searchsorted(np.sort(after), grid) / after.size | |
| xcross = grid[np.argmin(np.abs(Fa - Fb) + (grid < 40) * 9)] # ignore the origin tie | |
| pcross = np.interp(xcross, grid, Fb) | |
| print(f"\n ECDFs cross near x={xcross:.0f} ms (p={pcross:.2f})\n") | |
| XCAP = 750 | |
| paired = pd.DataFrame({ | |
| "latency": np.concatenate([before, after]), | |
| "period": ["before"] * before.size + ["after"] * after.size, | |
| }) | |
| PERIOD_C = {"before": INK, "after": ACCENT} | |
| # ---------------------------------------------------------------------------- | |
| # FIG 1 -- the trap: the dashboard shows one number (the mean), and it moved | |
| # the wrong way. | |
| # ---------------------------------------------------------------------------- | |
| def fig_mean(): | |
| d = pd.DataFrame({"period": ["before", "after"], | |
| "mean": [sb["mean"], sa["mean"]]}) | |
| d["period"] = pd.Categorical(d["period"], ["before", "after"]) | |
| # 95% CI of the mean (tiny at N=12k) -- makes "no significant change" explicit | |
| def ci(x): | |
| return 1.96 * x.std(ddof=1) / np.sqrt(x.size) | |
| d["ci"] = [ci(before), ci(after)] | |
| p = ( | |
| ggplot(d, aes("period", "mean", fill="period")) | |
| + geom_col(width=0.55, show_legend=False) | |
| + geom_errorbar(aes(ymin="mean-ci", ymax="mean+ci"), width=0.14, | |
| color=INK, size=0.5) | |
| + geom_text(aes(label="mean.round(0).astype(int).astype(str) + ' ms'"), | |
| nudge_y=6, family=MONO, size=9, color=INK) | |
| + scale_fill_manual(values=PERIOD_C) | |
| + scale_y_continuous(expand=(0, 0, 0.12, 0)) | |
| + labs(title="The dashboard says you made it worse", | |
| subtitle="Mean request latency before vs. after the rollout (± 95% CI).", | |
| x="", y="mean latency (ms)", caption="synthetic · N=12,000 each") | |
| + base_theme(3.6) | |
| + theme(panel_grid_major_x=element_blank()) | |
| ) | |
| save(p, "latency_1_mean") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 2 -- density: the "after" is not one population, it is two. | |
| # ---------------------------------------------------------------------------- | |
| def fig_density(): | |
| d = paired[paired.latency <= XCAP] | |
| p = ( | |
| ggplot(d, aes("latency", fill="period", color="period")) | |
| + geom_density(aes(y=after_stat("density")), alpha=0.35, size=0.9) | |
| + scale_fill_manual(values=PERIOD_C) | |
| + scale_color_manual(values=PERIOD_C) | |
| + scale_x_continuous(expand=(0, 0)) | |
| + scale_y_continuous(expand=(0, 0, 0.05, 0)) | |
| + annotate("text", x=70, y=0.0122, label="after", color=ACCENT, | |
| family=SERIF, size=11, ha="left") | |
| + annotate("text", x=150, y=0.0075, label="before", color=INK, | |
| family=SERIF, size=11, ha="left") | |
| + annotate("text", x=300, y=0.0022, label="a second hump\nappears", | |
| color=ACCENT, family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + labs(title="Same average, a very different shape", | |
| subtitle="Distribution of latency. The rollout split the traffic in two.", | |
| x="latency (ms)", y="density", | |
| caption=f"tail beyond {XCAP} ms not shown") | |
| + base_theme() | |
| ) | |
| save(p, "latency_2_density") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 3 -- the hero: two ECDFs that CROSS. Read every percentile off one panel. | |
| # ---------------------------------------------------------------------------- | |
| def ecdf(x): | |
| xs = np.sort(x) | |
| ys = np.arange(1, xs.size + 1) / xs.size | |
| return xs, ys | |
| def fig_cdf(): | |
| parts = [] | |
| for name in ("before", "after"): | |
| xs, ys = ecdf(before if name == "before" else after) | |
| parts.append(pd.DataFrame({"latency": xs, "F": ys, "period": name})) | |
| d = pd.concat(parts) | |
| d = d[d.latency <= XCAP] | |
| p = ( | |
| ggplot(d, aes("latency", "F", color="period")) | |
| + geom_vline(xintercept=xcross, color=INK_FAINT, size=0.4, linetype="dotted") | |
| + geom_step(size=1.0) | |
| + scale_color_manual(values=PERIOD_C) | |
| + scale_x_continuous(expand=(0, 0)) | |
| + scale_y_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b], | |
| expand=(0.01, 0)) | |
| + annotate("text", x=88, y=0.94, label="after", color=ACCENT, | |
| family=SERIF, size=11, ha="left") | |
| + annotate("text", x=300, y=0.58, label="before", color=INK, | |
| family=SERIF, size=11, ha="left") | |
| + annotate("text", x=xcross + 14, y=0.18, | |
| label=f"they cross at\n~{xcross:.0f} ms (p{pcross*100:.0f})", | |
| color=INK_MUTED, family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + labs(title="One panel, every percentile", | |
| subtitle="Empirical CDF. Left of the crossing the change wins; right of it, it loses.", | |
| x="latency (ms)", y="share of requests ≤ x", | |
| caption=f"tail beyond {XCAP} ms not shown") | |
| + base_theme(4.6) | |
| ) | |
| save(p, "latency_3_cdf") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 4 -- the shift function: Q_after(p) - Q_before(p) at every quantile, with | |
| # a bootstrap 95% band. Quantifies "who won, and by how much, where." | |
| # ---------------------------------------------------------------------------- | |
| def fig_shift(): | |
| ps = np.linspace(0.02, 0.99, 98) | |
| delta = np.percentile(after, ps * 100) - np.percentile(before, ps * 100) | |
| rng = np.random.default_rng(7) | |
| B = 400 | |
| boot = np.empty((B, ps.size)) | |
| for i in range(B): | |
| ba = rng.choice(after, after.size, replace=True) | |
| bb = rng.choice(before, before.size, replace=True) | |
| boot[i] = np.percentile(ba, ps * 100) - np.percentile(bb, ps * 100) | |
| lo, hi = np.percentile(boot, [2.5, 97.5], axis=0) | |
| d = pd.DataFrame({"p": ps, "delta": delta, "lo": lo, "hi": hi}) | |
| d["sign"] = np.where(d.delta <= 0, "faster", "slower") | |
| zc = float(np.interp(0, delta, ps)) # p where delta crosses 0 | |
| p = ( | |
| ggplot(d, aes("p", "delta")) | |
| + geom_hline(yintercept=0, color=INK, size=0.5) | |
| + geom_ribbon(aes(ymin="lo", ymax="hi"), fill=INK_FAINT, alpha=0.18) | |
| + geom_line(size=1.1, color=ACCENT) | |
| + geom_point(aes(x=zc, y=0), color=INK, size=2.2) | |
| + scale_x_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b], | |
| expand=(0.01, 0)) | |
| + annotate("text", x=0.06, y=-30, label="faster\n(below zero)", color=INK_MUTED, | |
| family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + annotate("text", x=0.97, y=d.hi.max() * 0.7, label="slower\n(above zero)", | |
| color=ACCENT, family=MONO, size=7.5, ha="right", lineheight=0.9) | |
| + annotate("text", x=zc + 0.01, y=28, label=f"breaks even\nat p{zc*100:.0f}", | |
| color=INK, family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + labs(title="Who won, and by how much, at every percentile", | |
| subtitle="Shift function: after(p) − before(p) in ms, with a 95% bootstrap band.", | |
| x="percentile", y="change in latency (ms)", | |
| caption="400 bootstrap resamples") | |
| + base_theme(4.4) | |
| ) | |
| save(p, "latency_4_shift") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 5 -- ridgeline: the miss-hump being BORN as the rollout ramps day by day. | |
| # ---------------------------------------------------------------------------- | |
| def fig_ridge(): | |
| # Latency is lognormal, so we estimate and draw on a LOG axis: two lognormal | |
| # humps become two clearly separated bumps instead of one tall spike beside | |
| # an invisible shoulder. This is the honest way to look at latency. | |
| days = sorted(df.day.unique()) | |
| LO, HI = 25, XCAP | |
| glog = np.linspace(np.log10(LO), np.log10(HI), 400) | |
| spacing = 0.9 | |
| scale = 2.0 | |
| rib, crest = [], [] | |
| bases, ylabels = [], [] | |
| for day in days: | |
| x = df[(df.day == day) & (df.latency.between(LO, HI))].latency.to_numpy() | |
| kde = gaussian_kde(np.log10(x), bw_method=0.16) | |
| dens = kde(glog) | |
| dens = dens / dens.max() * scale | |
| base = (len(days) - 1 - day) * spacing # day 0 on top | |
| f = df[df.day == day].rollout.iloc[0] | |
| rib.append(pd.DataFrame({"x": 10 ** glog, "ymin": base, "ymax": base + dens, | |
| "day": day})) | |
| crest.append(pd.DataFrame({"x": 10 ** glog, "y": base + dens, "day": day})) | |
| bases.append(base) | |
| ylabels.append(f"day {day}\n{int(f*100)}%") | |
| R = pd.concat(rib) | |
| C = pd.concat(crest) | |
| R["day"] = pd.Categorical(R.day, days) | |
| fill_map = {d: RAMP7[d] for d in days} | |
| p = ( | |
| ggplot() | |
| + geom_ribbon(R, aes("x", ymin="ymin", ymax="ymax", fill="day", group="day"), | |
| alpha=0.92) | |
| + geom_line(C, aes("x", "y", group="day"), color=INK, size=0.35) | |
| + scale_fill_manual(values=fill_map) | |
| + scale_x_log10(breaks=[30, 60, 100, 200, 400, 700], expand=(0, 0)) | |
| + scale_y_continuous(breaks=bases, labels=ylabels, | |
| expand=(0.01, 0, 0.04, 0)) | |
| + labs(title="Watch the second peak be born", | |
| subtitle="Each day's latency (log scale) as the new tier ramps 0 → 100%.", | |
| x="latency (ms, log scale)", y="", | |
| caption="log x · tails outside 25–750 ms not shown") | |
| + base_theme(5.6) | |
| + theme(panel_grid_major_y=element_blank(), | |
| axis_text_y=element_text(family=MONO, color=INK_MUTED, size=7, | |
| lineheight=0.9), | |
| panel_grid_major_x=element_line(color=RULE, size=0.3)) | |
| ) | |
| save(p, "latency_5_ridge") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 5b -- heatmap: rollout day x latency, colour = per-day density. The second | |
| # population appears as a distinct band splitting off, day by day. | |
| # ---------------------------------------------------------------------------- | |
| def fig_heatmap(): | |
| days = sorted(df.day.unique()) | |
| LO, HI = 25, XCAP | |
| # smooth per-day density in log space (not raw histograms -> no striping) | |
| glog = np.linspace(np.log10(LO), np.log10(HI), 140) | |
| edges10 = 10 ** np.concatenate([[glog[0] - (glog[1] - glog[0]) / 2], | |
| (glog[:-1] + glog[1:]) / 2, | |
| [glog[-1] + (glog[1] - glog[0]) / 2]]) | |
| cells = [] | |
| for day in days: | |
| x = df[(df.day == day) & (df.latency.between(LO, HI))].latency.to_numpy() | |
| dens = gaussian_kde(np.log10(x), bw_method=0.16)(glog) | |
| dens = (dens / dens.max()) ** 0.7 # per-column peak, gamma-lifted | |
| f = df[df.day == day].rollout.iloc[0] | |
| cells.append(pd.DataFrame({ | |
| "day": day, "rollout": f, "dens": dens, | |
| "ymin": edges10[:-1], "ymax": edges10[1:], | |
| "xmin": day - 0.5, "xmax": day + 0.5, | |
| })) | |
| C = pd.concat(cells) | |
| xlabels = [f"day {d}\n{int(df[df.day==d].rollout.iloc[0]*100)}%" for d in days] | |
| p = ( | |
| ggplot(C) | |
| + geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", | |
| fill="dens")) | |
| + scale_fill_gradientn( | |
| colors=["#efe7db", "#e0b49a", "#c8855c", "#9e3413", "#5a1c0b"], | |
| guide=None) | |
| + scale_y_log10(breaks=[30, 60, 100, 200, 400, 700], expand=(0, 0)) | |
| + scale_x_continuous(breaks=days, labels=xlabels, expand=(0, 0)) | |
| + labs(title="Watch the second band appear", | |
| subtitle="Latency density per rollout day (each column scaled to its own peak).", | |
| x="", y="latency (ms, log scale)", | |
| caption="log y · tails outside 25–750 ms not shown") | |
| + base_theme(4.8) | |
| + theme(panel_grid_major=element_blank(), | |
| axis_text_x=element_text(family=MONO, color=INK_MUTED, size=7, | |
| lineheight=0.9)) | |
| ) | |
| save(p, "latency_5b_heatmap") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 6 -- filtered CDF: split the mystery by cache path and it resolves into | |
| # two clean, unimodal CDFs. The bimodality had a cause all along. | |
| # ---------------------------------------------------------------------------- | |
| def fig_filtered(): | |
| parts = [] | |
| # baseline reference from day 0 | |
| xs, ys = ecdf(before) | |
| parts.append(pd.DataFrame({"latency": xs, "F": ys, "grp": "baseline (before)"})) | |
| for path in ("hit", "miss"): | |
| x = df[(df.day == 6) & (df.path == path)].latency.to_numpy() | |
| xs, ys = ecdf(x) | |
| parts.append(pd.DataFrame({"latency": xs, "F": ys, | |
| "grp": f"cache {path}"})) | |
| d = pd.concat(parts) | |
| d = d[d.latency <= XCAP] | |
| cmap = {"cache hit": INK, "cache miss": ACCENT, "baseline (before)": INK_FAINT} | |
| lmap = {"cache hit": "solid", "cache miss": "solid", "baseline (before)": "dashed"} | |
| d["grp"] = pd.Categorical(d.grp, ["cache hit", "baseline (before)", "cache miss"]) | |
| d6 = df[df.day == 6] | |
| hit_pct = round((d6.path == "hit").mean() * 100) | |
| p = ( | |
| ggplot(d, aes("latency", "F", color="grp", linetype="grp")) | |
| + geom_step(size=1.0) | |
| + scale_color_manual(values=cmap) | |
| + scale_linetype_manual(values=lmap) | |
| + scale_x_continuous(expand=(0, 0)) | |
| + scale_y_continuous(labels=lambda b: [f"{int(v*100)}%" for v in b], | |
| expand=(0.01, 0)) | |
| + annotate("text", x=62, y=0.55, label=f"cache hit\n({hit_pct:.0f}% of traffic)", | |
| color=INK, family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + annotate("text", x=330, y=0.45, label="cache miss\n(the slow tail)", | |
| color=ACCENT, family=MONO, size=7.5, ha="left", lineheight=0.9) | |
| + annotate("text", x=175, y=0.90, label="baseline", color=INK_FAINT, | |
| family=MONO, size=7.5, ha="left") | |
| + labs(title="The bimodality had a cause", | |
| subtitle="The same 'after' requests, split by cache outcome: two clean populations.", | |
| x="latency (ms)", y="share of requests ≤ x", | |
| caption=f"tail beyond {XCAP} ms not shown") | |
| + base_theme(4.6) | |
| ) | |
| save(p, "latency_6_filtered") | |
| # ---------------------------------------------------------------------------- | |
| # FIG 7 -- the jointplot: WHY the slow requests are slow. Latency vs response | |
| # size, coloured by cache outcome, with a density on each margin. The | |
| # misses are the big objects; that is the whole mechanism. | |
| # Built in bare matplotlib because a jointplot needs marginal axes, | |
| # which the grammar of graphics has no vocabulary for. | |
| # ---------------------------------------------------------------------------- | |
| def fig_joint(): | |
| import matplotlib.pyplot as plt | |
| from matplotlib.gridspec import GridSpec | |
| d6 = df[(df.day == 6) & (df.latency <= XCAP)] | |
| rng = np.random.default_rng(11) | |
| grp = {"hit": d6[d6.path == "hit"], "miss": d6[d6.path == "miss"]} | |
| col = {"hit": INK, "miss": ACCENT} | |
| fig = plt.figure(figsize=(W, 5.4)) | |
| gs = GridSpec(2, 2, width_ratios=[5, 1], height_ratios=[1, 4], | |
| hspace=0.04, wspace=0.04, | |
| left=0.10, right=0.975, top=0.86, bottom=0.10) | |
| ax = fig.add_subplot(gs[1, 0]) | |
| axt = fig.add_subplot(gs[0, 0], sharex=ax) | |
| axr = fig.add_subplot(gs[1, 1], sharey=ax) | |
| for a in (fig.gca(), ax, axt, axr): | |
| a.set_facecolor("none") | |
| fig.patch.set_alpha(0) | |
| for name, g in grp.items(): | |
| s = g.sample(min(2500, len(g)), random_state=rng.integers(1 << 30)) | |
| ax.scatter(s.size_kb, s.latency, s=5, c=col[name], alpha=0.16, | |
| edgecolors="none", rasterized=True) | |
| # marginal densities in log space, drawn as light fills | |
| def marg(a, values, color, horizontal): | |
| lo, hi = np.log10(values.min()), np.log10(values.max()) | |
| grid = np.linspace(lo, hi, 200) | |
| dens = gaussian_kde(np.log10(values), bw_method=0.2)(grid) | |
| dens = dens / dens.max() | |
| x10 = 10 ** grid | |
| if horizontal: | |
| a.fill_betweenx(x10, 0, dens, color=color, alpha=0.35, lw=0) | |
| a.plot(dens, x10, color=color, lw=1.0) | |
| else: | |
| a.fill_between(x10, 0, dens, color=color, alpha=0.35, lw=0) | |
| a.plot(x10, dens, color=color, lw=1.0) | |
| for name, g in grp.items(): | |
| marg(axt, g.size_kb, col[name], horizontal=False) | |
| marg(axr, g.latency, col[name], horizontal=True) | |
| ax.set_xscale("log"); ax.set_yscale("log") | |
| ax.set_xlim(2, d6.size_kb.quantile(0.995)) | |
| ax.set_ylim(20, XCAP) | |
| ax.set_xticks([2, 5, 10, 20, 50, 100, 200]) | |
| ax.set_yticks([30, 60, 100, 200, 400]) | |
| from matplotlib.ticker import ScalarFormatter | |
| for axis in (ax.xaxis, ax.yaxis): | |
| axis.set_major_formatter(ScalarFormatter()) | |
| ax.set_xlabel("response size (KB, log scale)", family=SERIF, color=INK, size=11) | |
| ax.set_ylabel("latency (ms, log scale)", family=SERIF, color=INK, size=11) | |
| ax.tick_params(labelsize=8, colors=INK_MUTED) | |
| for lbl in ax.get_xticklabels() + ax.get_yticklabels(): | |
| lbl.set_family(MONO) | |
| ax.grid(True, color=RULE, lw=0.4) | |
| ax.text(0.04, 0.10, "cache hit", transform=ax.transAxes, color=INK, | |
| family=SERIF, size=12, ha="left") | |
| ax.text(0.78, 0.88, "cache miss", transform=ax.transAxes, color=ACCENT, | |
| family=SERIF, size=12, ha="left") | |
| for a in (axt, axr): | |
| for spine in a.spines.values(): | |
| spine.set_visible(False) | |
| a.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False) | |
| axt.margins(y=0); axr.margins(x=0) | |
| axt.set_ylim(0, 1.08); axr.set_xlim(0, 1.08) | |
| fig.text(0.10, 0.955, "The slow requests are the big ones", | |
| family=SERIF, color=INK, size=13, weight="bold", ha="left") | |
| fig.text(0.10, 0.905, | |
| "Latency vs. response size (after traffic). Each margin is a density.", | |
| family=SERIF, color=INK_MUTED, size=10, ha="left") | |
| fig.text(0.975, 0.012, "day 6 · sampled points · tail beyond 750 ms not shown", | |
| family=MONO, color=INK_FAINT, size=7, ha="right") | |
| fig.savefig("latency_7_joint.svg", transparent=True) | |
| plt.close(fig) | |
| print(" wrote ./latency_7_joint.svg") | |
| if __name__ == "__main__": | |
| print("generating figures...") | |
| fig_mean() | |
| fig_density() | |
| fig_cdf() | |
| fig_shift() | |
| fig_ridge() | |
| fig_heatmap() | |
| fig_filtered() | |
| fig_joint() | |
| print("done.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment