Every Monday the verification lead plots the bugs found per week. For the first weeks the line climbs as the testbench comes up; then it peaks; for the last two months it drifts down: 4, 4, 2, 3, 1, 0, 3. Management asks whether a falling curve means the design is nearly clean. It might. It might also mean the tests have stopped finding what is there (lesson 12.3). The curve needs a model to be read, and the model has error bars.
What does a flattening bug curve mean, and how many bugs are left?
The obvious reading is "fewer bugs found means fewer bugs left". A falling curve has three possible causes: the design really has fewer bugs left, the current techniques have exhausted what they can find, or effort fell (people moved, the regression broke). A growth model can estimate the first under the assumption that the testing stays the same; it cannot tell the three apart by itself.
The Goel-Okumoto model (fact M7) assumes a pool of a bugs findable by this testing, each found at a constant rate b. Fitting a and b to the weekly counts estimates the pool and so the bugs still to find, assuming the testing does not change.
A simulated project (true pool 80 bugs, rate 0.12 per week) is fitted at week 8 and week 16. Then 300 more projects with the same true curve show how much estimates vary from project to project.
"""Bug discovery curves: fitting a reliability-growth model to weekly bug counts.
The Goel-Okumoto model says the expected number of bugs found by time t is
m(t) = a (1 - exp(-b t)): a is the total number of bugs that testing of this kind will find,
b how fast they are found (fact M7). A project (illustrative, a = 80, b = 0.12 per week) finds
bugs week by week as Poisson counts. At week 8 and at week 16 the model is fitted to the counts
so far by maximum likelihood, giving an estimate of a and so of the bugs still to find.
Then 300 more simulated projects with the same true curve show how much such estimates spread.
"""
import math
import random
rng = random.Random(21)
A_TRUE, B_TRUE, WEEKS = 80, 0.12, 16
def poisson(lam):
k, p, l = 0, 1.0, math.exp(-lam)
while True:
p *= rng.random()
if p < l:
return k
k += 1
m = lambda a, b, t: a * (1 - math.exp(-b * t))
weekly = [poisson(m(A_TRUE, B_TRUE, w) - m(A_TRUE, B_TRUE, w - 1)) for w in range(1, WEEKS + 1)]
def fit(counts):
"""Maximum likelihood for grouped Poisson counts; a has a closed form for each b."""
T, n = len(counts), sum(counts)
best = None
for i in range(1, 1000):
b = i / 2000
a = n / (1 - math.exp(-b * T))
ll = 0.0
for w, c in enumerate(counts, start=1):
lam = m(a, b, w) - m(a, b, w - 1)
ll += c * math.log(lam) - lam
if best is None or ll > best[0]:
best = (ll, a, b)
return best[1], best[2]
print("week " + " ".join(f"{w:3d}" for w in range(1, WEEKS + 1)))
print("bugs " + " ".join(f"{c:3d}" for c in weekly))
for upto in (8, 16):
a, b = fit(weekly[:upto])
found = sum(weekly[:upto])
print(f"fit at week {upto:2d}: a = {a:5.1f}, b = {b:.3f}; found {found}, estimated still to find {a - found:5.1f}")
a16, b16 = fit(weekly)
fitted = [m(a16, b16, w) - m(a16, b16, w - 1) for w in range(1, WEEKS + 1)]
print("fit " + " ".join(f"{f:3.0f}" for f in fitted))
def spread(upto, runs=300):
est = []
for _ in range(runs):
counts = [poisson(m(A_TRUE, B_TRUE, w) - m(A_TRUE, B_TRUE, w - 1)) for w in range(1, upto + 1)]
est.append(fit(counts)[0])
est.sort()
return est[runs // 10], est[runs // 2], est[9 * runs // 10]
s8, s16 = spread(8), spread(16)
print(f"300 projects, estimated total at week 8: median {s8[1]:5.1f}, 10-90% range {s8[0]:5.1f} to {s8[2]:5.1f}")
print(f"300 projects, estimated total at week 16: median {s16[1]:5.1f}, 10-90% range {s16[0]:5.1f} to {s16[2]:5.1f}")
assert (s16[2] - s16[0]) < (s8[2] - s8[0]) and abs(s16[1] - A_TRUE) < 10
print(f"PASS: estimates centre near the true {A_TRUE} but spread widely; the range narrows from week 8 to week 16")It prints:
week 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
bugs 10 6 7 7 3 10 6 4 4 4 2 3 1 0 3 6
fit at week 8: a = 127.7, b = 0.067; found 53, estimated still to find 74.7
fit at week 16: a = 100.4, b = 0.088; found 76, estimated still to find 24.4
fit 8 8 7 7 6 5 5 5 4 4 4 3 3 3 2 2
300 projects, estimated total at week 8: median 84.5, 10-90% range 57.1 to 200.3
300 projects, estimated total at week 16: median 81.8, 10-90% range 68.4 to 97.7
PASS: estimates centre near the true 80 but spread widely; the range narrows from week 8 to week 16How to read it: this project's week-16 fit estimates about 100 bugs in total, so about 24 still to find, when the truth is 80 with 4 left. The counts are noisy (a week with 6 bugs at the end pulls the estimate up). Across 300 projects the estimate is right on average, centring near 82, but the 10–90% range at week 16 is 68 to 98; at week 8 it is 57 to 200. A growth model gives a useful order of magnitude and a trend, not a number to sign off on.
Pattern | Possible meanings | Check |
|---|---|---|
Rising | Environment coming up; new features enabled | Expected early |
Peak then steady decline | Pool being exhausted for current techniques | Fit a growth model; compare effort |
Sudden drop | Effort fell, regression broken, or a big fix | Look at runs and effort that week |
Late spike | New feature, new technique, or a change introduced bugs | Look at what changed (lesson 9.7) |
Long flat tail near zero | Technique exhausted, or design clean | Try a different technique (lesson 12.3); estimate with capture-recapture (lesson 12.6) |
Practice | Purpose |
|---|---|
Weekly bug counts by feature and by technique | Where bugs still come from |
Growth-model fits with ranges, not single numbers | Honest estimates of what is left |
Curves plotted with effort and changes | Falling rates interpreted correctly |
Discovery-curve criteria in sign-off (lesson 14.2) | A rate below a threshold for several weeks, with effort sustained |
A team (illustrative) signed off when its bug curve reached zero for two weeks. Those two weeks were the weeks the regression farm was moved to a new data centre and ran a fraction of its usual tests. A curve read without the effort beside it measured the move, not the design.
Weekly counts are random; a late week with 6 bugs makes the curve look less exhausted than it is, pulling the fitted total up.
That the testing stays the same; a new technique, new features or a change in effort breaks the assumption.
Discovery curves rise, peak and tail; a falling curve has several possible causes.
Growth models estimate the pool of bugs findable by current testing, with wide uncertainty early.
Read curves with effort and changes beside them, and report estimates as ranges.