Skip to content
About
Main content

Sliding window with panes, and floating-point drift

Measure your machineNeeds Python

Problem

A 24-hour 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. 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.

  1. 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

How many times faster are 1-minute panes than storing each record in every window?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    sliding-window-with-panes-and-floating.py64 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat 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

Sliding count, 100,000 records
MethodTimeMemory (MB)Updates per record
record in every window130 s82.11,440
1-minute panes3.19 s111
Reference runApple M2 · 8 cores · 8 GB
Output · uv run sliding-window-with-panes-and-floating.py
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.00075
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What 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 : 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.