We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Capstones step 5 of 5
Capstone: five defects, and which tool would have found each one
Real performance work is never one clean bottleneck. It is a mix of CPU, memory and object-lifetime problems whose relative importance is unknown until you measure — and two of the five defects here are invisible to a CPU profiler.
pipeline.py is an order-enrichment module. Its behaviour is correct. Keep it
correct, and fix all five.
The five defects
1. A quadratic join. if order.customer_id in active where active is a
list. Every order scans the whole collection. The fix is one line and it is the
single most common accidental-quadratic in Python.
The grader does not time this. active is a Collection that counts every
membership test it is asked to perform, and the assertion is that the count is
zero: a correct implementation hashes the collection once and never asks it
again. Deterministic, and it fails the same way on a fast machine and a slow one.
2. asdict() in the serialisation hot path. dataclasses.asdict does a
recursive deep copy: every nested dataclass becomes a new dict, every tuple a
new list, all the way down. (One thing that is not true any more: Decimal
leaves are no longer copied — Decimal is in copy‘s atomic-types set since
3.12. The cost is the containers, not the leaves.) Your fields are frozen and
therefore safe to share, so a hand-written shallow projection passes the nested
tuple through by reference. The grader checks exactly that identity.
3. An @lru_cache-decorated method. The cache key is (self, customer_id),
so the cache pins the directory instance and every Customer it has ever
returned, forever, at module scope. The grader looks up 20,000 customers,
registers them in a WeakSet, drops its own references, calls gc.collect()
and counts survivors. A WeakValueDictionary gives you the cache without the
ownership.
One detail that will stop you dead: a @dataclass(slots=True) class is not
weak-referenceable. __weakref__ is only added when you pass
weakref_slot=True. Without it the grader’s WeakSet raises
TypeError: cannot create weak reference.
4. An accumulator built with self.buffer += chunk. CPython has a fast path
that mutates a string or bytes object in place when its refcount is 1 — but
self.buffer is an attribute, so there is always a second reference and the
fast path never fires. Every add() allocates and copies the whole buffer:
quadratic in the number of chunks, and completely invisible in a profiler as
anything other than “a lot of time in add“.
Collect the parts and b"".join(...) once. And note what the grader checks
besides throughput: it takes an alias of the accumulated value, adds another
chunk, and requires the alias to be unchanged. That is a guard against the
tempting over-fix — an in-place bytearray whose previously-returned value
mutates under its caller’s feet.
5. A function that materialises the whole scan before filtering. 120,000
rows built into a list, then filtered down to twelve. A generator pipeline plus
islice does the identical work with a peak footprint of one row. The grader
wraps the call in tracemalloc and bounds the peak to a multiple of the
result size rather than the scan size.
The written half
For each defect, know which tool would have found it — and know why two of them
would not show up in cProfile at all:
| defect | what finds it |
|---|---|
| 1 quadratic join | any CPU profiler; a scaling test at n and 2n confirms the shape |
2 asdict |
CPU profiler — the time is genuinely spent in copy |
| 3 cache leak |
not a CPU profiler. tracemalloc snapshots over time, gc.get_objects(), memray, or a WeakSet canary |
4 += accumulator |
CPU profiler shows the symptom (“lots of time in add“); only reading the code, or a scaling test, shows the cause |
| 5 scan-then-filter |
not a CPU profiler. The work is the same; only the memory differs. tracemalloc peak, or an OOM at 3 a.m. |
That table is the actual lesson. A profiler answers “where is the time”, and two
of the five most expensive things a service does wrong are not about time at
all. The 3.15 sampling profiler (python -m profiling.sampling) changes where
you can run the CPU profiler — attaching to a live production process with no
restart — but it does not change what a CPU profiler is blind to.
Constraints
The driver is fixed; every behavioural result it reports must stay identical.
mypy --strict clean — which the module you are given is not, so that is part
of the work too.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.