Local context packing stole the agent clock today. The remote model sat behind a fat snapshot. Comment threads still grade models like racehorses. I kept a stacked bar instead.
Why trust a vibe when wall time has names? I split one coding pass into four stages. Walk the tree. Pack the prompt. Ride the wire. Apply the patch.
The graph I saved was rude. Bytes moved first. Tokens waited second. That order embarrassed my earlier hunches.
This is a lab method, not a trophy benchmark. I did not crown a model. I timed my own laptop.
The debate this week sounds loud on DEV. Some folks say AI already codes better. Others say vibe work is not engineering. Both skip the clock.
I care about the clock. An agent loop is a pipeline. Pipelines hide fat stages under a single spinner.
Here is the analogy I use. You do not blame the chef for a slow dinner. You check the grocery run first. Context packing is that grocery run.
I wanted a graph I could keep. Not a screenshot of a chat. A CSV I can diff after I change ignore rules.
The artifact is a small profiler. It walks a repo. It records bytes and milliseconds. It prints a stacked bar you can paste.
I label every number as sample output. Your tree will disagree. That disagreement is the point.
Start with a dirty fixture. Copy a tiny service. Leave node_modules in place on purpose. Then run the walker twice.
Run it naive. Run it with ignores. Naive walks feel fast in your head. They are not.
Stat calls add up like gravel in a shoe. One extra directory is nothing. A thousand extra files become the story.
I used Python because the stdlib is enough. No new agent framework. No fake distributed trace.
Save this as pack_profile.py. Point it at a real tree. Read the bar before you blame the model.
#!/usr/bin/env python3
"""Lab profiler for agent context packing. Sample timings only."""
from __future__ import annotations
import csv
import os
import time
from dataclasses import dataclass, field
from pathlib import Path
SKIP_DIR_NAMES = {".git", "node_modules", "dist", ".venv", "__pycache__"}
TEXT_SUFFIXES = {".py", ".ts", ".js", ".md", ".json", ".toml", ".yml", ".yaml"}
@dataclass
class Stage:
name: str
ms: float = 0.0
bytes: int = 0
@dataclass
class Report:
stages: list[Stage] = field(default_factory=list)
def add(self, name: str, ms: float, nbytes: int = 0) -> None:
self.stages.append(Stage(name, ms, nbytes))
def should_skip_dir(name: str, use_ignores: bool) -> bool:
if not use_ignores:
return name in {".git"}
return name in SKIP_DIR_NAMES
def walk_and_pack(root: Path, use_ignores: bool, cap_bytes: int) -> tuple[int, int, float]:
t0 = time.perf_counter()
total = 0
files = 0
chunks: list[bytes] = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if not should_skip_dir(d, use_ignores)]
for name in filenames:
path = Path(dirpath) / name
if path.suffix.lower() not in TEXT_SUFFIXES:
continue
try:
data = path.read_bytes()
except OSError:
continue
files += 1
if total >= cap_bytes:
continue
take = data[: max(0, cap_bytes - total)]
chunks.append(take)
total += len(take)
packed = b"\n".join(chunks)
ms = (time.perf_counter() - t0) * 1000
return len(packed), files, ms
def wire_stub(payload_bytes: int) -> float:
"""Local serialization stand-in. Not a model measurement."""
t0 = time.perf_counter()
blob = b"x" * min(payload_bytes, 1_000_000)
_ = hash(blob)
return (time.perf_counter() - t0) * 1000
def apply_stub(root: Path) -> tuple[int, float]:
t0 = time.perf_counter()
patch = b"# lab patch marker\n"
target = root / ".pack_profile_apply.txt"
target.write_bytes(patch)
ms = (time.perf_counter() - t0) * 1000
return len(patch), ms
def bar(ms: float, total: float, width: int = 24) -> str:
if total <= 0:
return " " * width
n = int(round(width * (ms / total)))
n = min(width, max(0, n))
return "#" * n + "." * (width - n)
def write_csv(path: Path, report: Report) -> None:
with path.open("w", newline="") as fh:
w = csv.writer(fh)
w.writerow(["stage", "ms", "bytes"])
for s in report.stages:
w.writerow([s.name, f"{s.ms:.3f}", s.bytes])
def main() -> None:
root = Path(os.environ.get("PACK_ROOT", ".")).resolve()
use_ignores = os.environ.get("PACK_IGNORES", "1") != "0"
cap = int(os.environ.get("PACK_CAP", "750000"))
packed, files, walk_ms = walk_and_pack(root, use_ignores, cap)
report = Report()
report.add("walk_pack", walk_ms, packed)
report.add("wire_local_stub", wire_stub(packed), packed)
apply_bytes, apply_ms = apply_stub(root)
report.add("apply_stub", apply_ms, apply_bytes)
total = sum(s.ms for s in report.stages) or 1.0
print(f"root={root}")
print(f"files_touched={files} packed_bytes={packed} ignores={use_ignores}")
print("SAMPLE GRAPH ONLY. This is not a model benchmark.")
for s in report.stages:
print(f"{s.name:16} {s.ms:8.2f} ms {bar(s.ms, total)} {s.bytes} B")
out = Path("pack_graph.csv")
write_csv(out, report)
print(f"wrote {out}")
if __name__ == "__main__":
main()
Run the naive pass first. Watch the file count jump. Then turn ignores back on.
python3 pack_profile.py
PACK_ROOT=. PACK_IGNORES=0 PACK_CAP=750000 python3 pack_profile.py
PACK_ROOT=. PACK_IGNORES=1 PACK_CAP=750000 python3 pack_profile.py
cp pack_graph.csv pack_graph_ignores.csv
Want CPU names, not just bars? Keep a profile too. cProfile is enough for this lab.
python3 -m cProfile -o pack.prof pack_profile.py
python3 -c "import pstats; p=pstats.Stats('pack.prof'); p.sort_stats('cumtime').print_stats(20)"
If py-spy is on the box, record a picture. I still keep the CSV. Pictures rot. Rows diff.
py-spy record -o pack.svg -- python3 pack_profile.py
Sample output looks like this on my fixture. Treat it as a shape, not a score. Your milliseconds will move.
SAMPLE GRAPH ONLY. This is not a model benchmark.
walk_pack 412.17 ms ################.... 750000 B
wire_local_stub 6.02 ms .................... 750000 B
apply_stub 1.14 ms .................... 18 B
See the punchline? The walk ate the bar. The stub wire was a footnote. Apply was a shrug.
That graph is what I kept. Not a model leaderboard. Not a chat screenshot. A stacked bar that survived the debate.
People ask the wrong first question. They ask which model is smarter. I ask which stage owns the seconds.
If packing owns the seconds, a fancier model will still wait. It waits on your snapshot. It waits on your ignores.
Cap bytes on purpose. Unlimited packing is a fantasy. Agents choke on whole monorepos the way pockets choke on bricks.
I cap in the script because production prompts cap too. The cap is a flashlight. It shows which files won the lottery.
Change one ignore rule. Diff the CSV. Did walk_pack shrink? Did packed bytes drop? That is the experiment.
diff -u pack_graph.csv pack_graph_ignores.csv || true
Do not stop at local stubs forever. A stub cannot catch queue delay. It cannot catch TLS setup. It cannot catch a cold server.
When I needed a real wire stage, I used a throwaway endpoint. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, which I treated as a lab lane for that slice only.
I did not use it to grade intelligence. I used it to give the wire a heartbeat. Packing still had to win or lose on my disk.
Hook the wire later if you want. Time connect, headers, and first byte yourself. Keep those rows next to walk_pack. Then the bar stays honest.
A fair test plan is boring. That is why it works. Same fixture. Same cap. One changed ignore. One changed endpoint.
Warm the disk once. Discard that run. Record the next three. Median the milliseconds. Do not publish the warm-up.
Refuse to mix variables. Do not change the model and the ignore file together. You will not know which lever moved.
If walk_pack still dominates, stop tuning prompts. Fix the grocery run. Add ignores. Narrow suffixes. Cap harder.
If the live wire dominates, then talk servers. Until then, the model is a suspect with an alibi.
I have watched teams invert this. They swap models weekly. They never weigh the snapshot. The spinner stays. The story changes clothes.
The stacked bar does not care about clothes. It cares about milliseconds. It cares about bytes.
Limitations are not fine print. They are the method. This script does not measure token quality. It does not measure correctness.
The wire stub is local hashing. It is not network truth. The apply stub writes one marker file. It is not git apply.
Binary files are skipped. Generated folders are skipped only when ignores are on. Symlink storms can still lie.
Who should not use this approach? Anyone selling a latency SLA from these rows. Anyone profiling a huge monorepo with ignores off. Anyone who needs a courtroom trace of a vendor model.
Also skip it if you will not keep the CSV. A one-off print is a vibe. A saved bar is evidence.
I still like arguments about engineering taste. Taste does not replace a clock. The clock does not replace taste either.
Keep both. Start with the grocery run. Save the stacked bar. Then decide whether the model even got a turn.
If you want a disposable lane for the live wire later, the free model access and free server option was enough for that lab slice. Pack the tree first anyway.
Top comments (0)