Sliding window with panes, and floating-point drift
Measure your machineNeeds Python
Problem
A 24-hour window that slides every minute overlaps 1,440 other windows, so storing each record in every window it belongs to does 1,440 updates per record. Panes fold each record once into its 1-minute slice and combine 1,440 slices when a window is emitted: the same answers, with the per-record work divided by 1,440. For sums there is a cheaper trick, add the new value and subtract the expiring one, which is exact for integers and not for floating point: each addition and subtraction rounds, and the errors accumulate.
- Implement a 24-hour sliding count per key, refreshed every minute, in two ways: store each record in 1,440 windows, or fold it once into a 1-minute pane and sum 1,440 panes at emit time. Measure memory and CPU time for 100,000 records. 2. Implement a running sliding sum of floating-point values using add-new, subtract-old. Compare it with the exact recomputed sum (
math.fsum) after 10 million updates with values of very different magnitudes.
The question. How much work do panes save, and how far does the add-new, subtract-old sum drift?
Predict
The pane version uses far less compute (about the 1,440 factor on the per-record work) and the same results. The add/subtract running sum drifts from the exact result, sometimes by a visible amount, without any error message.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
sliding-window-with-panes-and-floating.py # /// script # requires-python = ">=3.10" # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Sliding windows with panes, and floating-point drift. (1) A 24-hour sliding count per key refreshed every minute, kept two ways: each record added to all 1,440 windows it belongs to, or folded once into a 1-minute pane with 1,440 panes summed at emit time. Work and memory for 100,000 records. (2) A running sliding sum of floats kept by add-new, subtract-old, against the exact sum (math.fsum) after 10 million updates. """ import argparse, json, math, random, time, tracemalloc from collections import deque, defaultdict p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--records", type=int, default=100_000, help="Records for part 1.") p.add_argument("--keys", type=int, default=100, help="Keys for part 1.") p.add_argument("--updates", type=int, default=10_000_000, help="Updates for part 2.") p.add_argument("--window", type=int, default=1000, help="Part 2: values in the sliding window.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() random.seed(args.seed) W = 1440 recs = [(random.randrange(args.keys), random.randrange(3 * W)) for _ in range(args.records)] # (key, minute) def per_window(): tracemalloc.start(); t = time.perf_counter(); win = defaultdict(int); ops = 0 for k, m in recs: for w in range(m, m + W): win[(k, w)] += 1; ops += 1 # the record lives in 1,440 windows el = time.perf_counter() - t; mem = tracemalloc.get_traced_memory()[0]; tracemalloc.stop() return el, mem, ops, win def panes(): tracemalloc.start(); t = time.perf_counter(); pane = defaultdict(int); ops = 0 for k, m in recs: pane[(k, m)] += 1; ops += 1 # fold once into its 1-minute pane out = {} for k in range(args.keys): for w in range(W - 1, 3 * W, 60): # emit one window per hour, from 1,440 panes out[(k, w)] = sum(pane.get((k, m), 0) for m in range(w - W + 1, w + 1)); ops += W el = time.perf_counter() - t; mem = tracemalloc.get_traced_memory()[0]; tracemalloc.stop() return el, mem, ops, out a_t, a_m, a_ops, a = per_window(); b_t, b_m, b_ops, b = panes() same = all(a.get(k, 0) == v for k, v in b.items()) part1 = [{"method": "record in every window", "seconds": a_t, "memory_mb": a_m / 1e6, "per_record_ops": a_ops / args.records}, {"method": "1-minute panes", "seconds": b_t, "memory_mb": b_m / 1e6, "per_record_ops": 1}] xs = deque(); running = 0.0; exact = 0.0; checkpoints = [] for i in range(args.updates): v = random.uniform(-1, 1) * 10 ** random.randint(-3, 9) xs.append(v); running += v if len(xs) > args.window: running -= xs.popleft() # add-new, subtract-old if (i + 1) % (args.updates // 10) == 0: exact = math.fsum(xs); checkpoints.append({"updates": i + 1, "running": running, "exact": exact, "drift": running - exact}) summary = {"same_counts": "yes" if same else "no", "final_drift": checkpoints[-1]["drift"], "final_exact": checkpoints[-1]["exact"], "max_abs_drift": max(abs(c["drift"]) for c in checkpoints)} for r in part1: print(f"{r['method']:<24} {r['seconds']:6.2f} s {r['memory_mb']:7.1f} MB {r['per_record_ops']:6.0f} updates per record") print(f"same counts at the emitted windows: {summary['same_counts']}") for c in checkpoints[-3:]: print(f"after {c['updates']:>10,} updates: running sum {c['running']:.6f}, exact {c['exact']:.6f}, drift {c['drift']:.3g}") if args.json: with open(args.json, "w") as f: json.dump({"summary": summary, "tables": {"panes": part1, "drift": checkpoints}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run sliding-window-with-panes-and-floating.py uv run sliding-window-with-panes-and-floating.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --records100,000 | Records for part 1. |
| --keys100 | Keys for part 1. |
| --updates10,000,000 | Updates for part 2. |
| --window1000 | Part 2: values in the sliding window. |
| --seed0 | Random seed. |
Our run
Sliding count, 100,000 records
| Method | Time | Memory (MB) | Updates per record |
|---|---|---|---|
| record in every window | 130 s | 82.1 | 1,440 |
| 1-minute panes | 3.19 s | 11 | 1 |
record in every window 130.49 s 82.1 MB 1440 updates per record
1-minute panes 3.19 s 11.0 MB 1 updates per record
same counts at the emitted windows: yes
after 8,000,000 updates: running sum -3314113514.157259, exact -3314113514.157441, drift 0.000181
after 9,000,000 updates: running sum 4656823220.153055, exact 4656823220.153465, drift -0.00041
after 10,000,000 updates: running sum 3893087633.081982, exact 3893087633.082731, drift -0.00075What we found
Storing each record in every window took 130 s and 82.1 MB; panes took 3.19 s and 11 MB, with the same counts (yes). After 10 million updates the running float sum was off by -0.001 from the exact sum of about 3.89B, and the error grew as the run went on, with no warning.
Read it
Panes are the general answer to sliding windows: do the per-record work once and pay the overlap at emit time. Add-new, subtract-old is fine for integers and counts; for floats, recompute periodically from the panes, or keep exact partial sums, so that rounding error cannot accumulate across millions of updates.