Skip to content
VLSI Mentor

awk Part IV · Module 13 · Real-World AWK Engineering Projects

Project: Synthesis Area Analyzer

The fourth script compares two synthesis runs and lists what grew, which sounds like subtraction and is not. Two areas that print identically can differ at the twelfth decimal, so grew needs a threshold — and this chapter establishes the threshold the honest way, by measuring how often an exact comparison would have worked. On this pair of reports it works perfectly; on 58.3% of comparable value sets it does not. That is a better argument for a tolerance than any single failure would be, because it says the exactness you observe is luck.

Advanced18 min readawkPart IVProjectSynthesisFloating Point

Part IV · Module 13 · Chapter 13.4 · Project: Synthesis Area Analyzer

1. What the Script Has to Do

Four things, and only the first is arithmetic:

  1. Per-module area and cell counts from each of two reports.
  2. A delta per module, with a defensible definition of "grew".
  3. Modules present in one report and absent from the other — added or removed, not treated as zero.
  4. A reconciliation against each report's own TOTAL, because a dropped row changes every conclusion.

2. Two Runs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN {
  n = split("u_regfile:466:4980.000|u_alu:178:1310.000|u_add:420:2840.000|u_mul:642:5210.000|u_shift:190:1130.000|u_ctrl:170:1240.000|u_io:100:810.000|u_dma:83:690.000|u_fpu:27:1950.000|u_debug:18:183.000", R, "|")
  print "****************************************"; print "Report : area -hierarchy"; print "Design : soc_top rev1"
  print "****************************************"; print ""
  print "Module               Cells  Cell Area"; print "-------------------------------------------"
  for (i = 1; i <= n; i++) { split(R[i], f, ":"); printf "%-20s %5d %10.3f\n", f[1], f[2], f[3]; c += f[2]; a += f[3] }
  print "-------------------------------------------"; printf "%-20s %5d %10.3f\n", "TOTAL", c, a
}' > area_old.rpt
awk 'BEGIN {
  n = split("u_regfile:466:4980.000|u_alu:178:1310.000|u_add:468:3122.500|u_mul:642:5210.000|u_shift:190:1130.000|u_ctrl:214:1476.000|u_io:100:810.000|u_dma:83:690.000|u_fpu:27:1950.000|u_debug:18:183.000", R, "|")
  print "****************************************"; print "Report : area -hierarchy"; print "Design : soc_top rev2"
  print "****************************************"; print ""
  print "Module               Cells  Cell Area"; print "-------------------------------------------"
  for (i = 1; i <= n; i++) { split(R[i], f, ":"); printf "%-20s %5d %10.3f\n", f[1], f[2], f[3]; c += f[2]; a += f[3] }
  print "-------------------------------------------"; printf "%-20s %5d %10.3f\n", "TOTAL", c, a
}' > area_new.rpt
awk '$1 == "TOTAL" { printf "  %s: %d cells, %.3f area\n", FILENAME, $2, $3 }' area_old.rpt area_new.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  area_old.rpt: 2294 cells, 20343.000 area
  area_new.rpt: 2386 cells, 20861.500 area

Ten modules in each. Two of them changed.

3. What "Grew" Means

The obvious test is new > old. Whether that is safe depends on arithmetic nobody checks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; if ($1 == "TOTAL") ota = $3; next }
 /^[a-z_]/ && NF == 3 && $1 != "TOTAL" { da += $3 - oa[$1] } $1 == "TOTAL" { nta = $3 }
 END { printf "  total area delta      %.20f\n", nta - ota
       printf "  sum of module deltas  %.20f\n", da
       printf "  equal with ==         %s\n", (nta - ota == da ? "yes" : "NO") }' area_old.rpt area_new.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  total area delta      518.50000000000000000000
  sum of module deltas  518.50000000000000000000
  equal with ==         yes

Exact. On this pair of reports the reconciliation holds with == and no tolerance is needed.

That is not a licence to use ==, and the reason is measurable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'BEGIN {
  srand(7); n = 20000
  for (k = 1; k <= n; k++) {
    a1 = int(rand()*900000)/1000; a2 = int(rand()*900000)/1000
    b1 = int(rand()*900000)/1000; b2 = int(rand()*900000)/1000
    if ((a2 + b2) - (a1 + b1) == (a2 - a1) + (b2 - b1)) exact++
  }
  printf "  %d of %d random 3-decimal pairs reconcile EXACTLY with == (%.1f%%)\n", exact, n, 100*exact/n
  printf "  so %d of %d (%.1f%%) do NOT\n", n - exact, n, 100*(n-exact)/n
}'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  8347 of 20000 random 3-decimal pairs reconcile EXACTLY with == (41.7%)
  so 11653 of 20000 (58.3%) do NOT

And a second threshold, which is an engineering decision rather than an arithmetic one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; next }
 /^[a-z_]/ && NF == 3 && $1 != "TOTAL" {
   d = $3 - oa[$1]
   if (d > 1e-6) printf "  %-11s %+9.3f absolute, %+6.2f%% relative\n", $1, d, 100*d/oa[$1] }' area_old.rpt area_new.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_add        +282.500 absolute,  +9.95% relative
  u_ctrl       +236.000 absolute, +19.03% relative

u_add grew more in absolute terms and u_ctrl grew more as a fraction of itself. Both are reported, because a large module growing 0.05% and a small one growing 20% are different events — Chapter 11.2 section 7's point about ranking by the quantity you will act on.

4. Modules That Appear or Disappear

A module in one report and not the other is not a delta of zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -v '^u_debug' area_old.rpt | awk '$1 == "TOTAL" { printf "%-20s %5d %10.3f\n", "TOTAL", 2294-18, 20343.000-183.000; next } { print }' > old2.rpt
awk '$1 == "u_fpu" { print; printf "%-20s %5d %10.3f\n", "u_pcie", 310, 2400.000; next } $1 == "TOTAL" { printf "%-20s %5d %10.3f\n", "TOTAL", 2386+310, 20861.500+2400.000; next } { print }' area_new.rpt > new2.rpt
awk 'FNR == 1 { n++ } /^[a-z_]/ && NF == 3 && $1 != "TOTAL" { if (n == 1) o[$1] = 1; else nw[$1] = 1 }
 END { for (m in o) if (!(m in nw)) r = r " " m
       for (m in nw) if (!(m in o)) a = a " " m
       printf "  removed:%s\n  added:%s\n", (r == "" ? " none" : r), (a == "" ? " none" : a) }' old2.rpt new2.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  removed: none
  added: u_debug u_pcie

Two modules appear in the new report and none disappears — u_debug was removed from the old one for this demonstration, so from the comparison's point of view it is new. The for (m in …) loops here accumulate into a string whose content does not depend on order, but the script uses a file-order array for its own output, because two added modules would otherwise print in an undefined sequence.

5. The Complete Script

Azvya Education Pvt. Ltd.VLSI Mentor
areadiff.awk — synthetic teaching script.
# areadiff.awk — synthesis area analyzer.
#
# Usage:  awk -f areadiff.awk area_old.rpt area_new.rpt
#         awk -v tol=1e-6 -v rel=0.1 -f areadiff.awk area_old.rpt area_new.rpt
# Exit:   0 nothing grew beyond the tolerance
#         1 at least one module grew
#         2 the per-module rows do not reconcile with a report's TOTAL row
#
# Design notes, each from a measured failure in Module 11:
#   - Compares floating-point areas with a TOLERANCE, never ==. On this pair of
#     reports == happens to be exact; on 58.3% of random 3-decimal value sets
#     it is not, so exactness here is luck.                          (11.1)
#   - Integer cell counts DO compare with ==; the exemption runs the other
#     way.                                                           (11.1)
#   - Reconciles the per-module rows against each report's own TOTAL -- the
#     only check that detects a dropped or misparsed row.            (11.8)
#   - Reports a module present in one report and absent from the other, rather
#     than treating a missing key as zero.                           (9.5)
#   - Reports both an absolute and a relative threshold, because a large module
#     growing 0.05% and a small one doubling are different events.    (11.2)

BEGIN { if (tol == "") tol = 1e-6; if (rel == "") rel = 0.1 }

FNR == 1 { nfile++ }

/^[a-z_]/ && NF == 3 && $1 != "TOTAL" {
    if (nfile == 1) { oc[$1] = $2 + 0; oa[$1] = $3 + 0; osum_c += $2; osum_a += $3
                      if (!($1 in oseen)) { oseen[$1] = 1; order[++nmod] = $1 } }
    else            { nc[$1] = $2 + 0; na[$1] = $3 + 0; nsum_c += $2; nsum_a += $3
                      if (!($1 in nseen)) { nseen[$1] = 1; norder[++nnew] = $1 } }
}

$1 == "TOTAL" { if (nfile == 1) { otc = $2 + 0; ota = $3 + 0 } else { ntc = $2 + 0; nta = $3 + 0 } }

function near(x, y) { return (x - y < tol && y - x < tol) }

END {
    if (nfile < 2) { print "ERROR: need two reports" > "/dev/stderr"; exit 2 }

    # reconcile each report against its own TOTAL before comparing them
    if (!near(osum_a, ota) || osum_c != otc) {
        printf "ERROR: old rows sum to %d cells / %.3f area, its TOTAL says %d / %.3f\n", osum_c, osum_a, otc, ota > "/dev/stderr"; exit 2 }
    if (!near(nsum_a, nta) || nsum_c != ntc) {
        printf "ERROR: new rows sum to %d cells / %.3f area, its TOTAL says %d / %.3f\n", nsum_c, nsum_a, ntc, nta > "/dev/stderr"; exit 2 }

    printf "%-12s %7s %7s %7s %11s %11s %10s %8s\n", "module", "cells-", "cells+", "dcells", "area-", "area+", "darea", "drel%"
    for (i = 1; i <= nmod; i++) {
        m = order[i]
        if (!(m in nseen)) { printf "%-12s %7d %7s %7s %11.3f %11s %10s %8s  REMOVED\n", m, oc[m], "-", "-", oa[m], "-", "-", "-"; gone++; continue }
        dc = nc[m] - oc[m]; da = na[m] - oa[m]
        dr = (oa[m] > 0 ? 100 * da / oa[m] : 0)
        note = "[same]"
        if (da > tol)       { note = "GREW"; ngrew++ }
        else if (da < -tol) { note = "shrank" }
        if (da > tol && dr > rel) note = note " (" sprintf("%.2f%%", dr) " > " rel "%)"
        printf "%-12s %7d %7d %+7d %11.3f %11.3f %+10.3f %+8.2f  %s\n", m, oc[m], nc[m], dc, oa[m], na[m], da, dr, note
    }
    for (i = 1; i <= nnew; i++) { m = norder[i]
        if (!(m in oseen)) { printf "%-12s %7s %7d %7s %11s %11.3f %10s %8s  ADDED\n", m, "-", nc[m], "-", "-", na[m], "-", "-"; added++ } }

    print ""
    printf "TOTALS  cells %d -> %d (%+d)   area %.3f -> %.3f (%+.3f, %+.2f%%)\n",
           otc, ntc, ntc - otc, ota, nta, nta - ota, 100 * (nta - ota) / ota
    printf "#\n# %d module(s); %d grew beyond %g; %d added; %d removed\n", nmod, ngrew + 0, tol, added + 0, gone + 0
    printf "# area deltas compared with a tolerance of %g, cell counts with == (integers are exact)\n", tol
    if (ngrew) exit 1
}

function near(x, y) is two one-sided comparisons rather than an absolute value, because POSIX awk has no abs() — Chapter 11.1 section 8. And both order[] and norder[] exist so that the main table and the ADDED rows each print in file order rather than in whatever sequence for (m in …) returns.

6. Running It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f areadiff.awk area_old.rpt area_new.rpt
echo "status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module        cells-  cells+  dcells       area-       area+      darea    drel%
u_regfile        466     466      +0    4980.000    4980.000     +0.000    +0.00  [same]
u_alu            178     178      +0    1310.000    1310.000     +0.000    +0.00  [same]
u_add            420     468     +48    2840.000    3122.500   +282.500    +9.95  GREW (9.95% > 0.1%)
u_mul            642     642      +0    5210.000    5210.000     +0.000    +0.00  [same]
u_shift          190     190      +0    1130.000    1130.000     +0.000    +0.00  [same]
u_ctrl           170     214     +44    1240.000    1476.000   +236.000   +19.03  GREW (19.03% > 0.1%)
u_io             100     100      +0     810.000     810.000     +0.000    +0.00  [same]
u_dma             83      83      +0     690.000     690.000     +0.000    +0.00  [same]
u_fpu             27      27      +0    1950.000    1950.000     +0.000    +0.00  [same]
u_debug           18      18      +0     183.000     183.000     +0.000    +0.00  [same]

TOTALS  cells 2294 -> 2386 (+92)   area 20343.000 -> 20861.500 (+518.500, +2.55%)
#
# 10 module(s); 2 grew beyond 1e-06; 0 added; 0 removed
# area deltas compared with a tolerance of 1e-06, cell counts with == (integers are exact)
status: 1

Two modules grew, eight are unchanged, and the footer states the comparison method — which is Chapter 11.8's rule that an aggregation decision belongs in the output. [same] is printed rather than left blank so that no row ends in whitespace and so that "unchanged" is an explicit verdict rather than an absence.

7. The Refusals

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '$1 == "TOTAL" { sub(/20343.000/, "99999.000") } { print }' area_old.rpt > badtot.rpt
awk -f areadiff.awk badtot.rpt area_new.rpt 2>&1 >/dev/null
echo "status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: old rows sum to 2294 cells / 20343.000 area, its TOTAL says 2294 / 99999.000
status: 2

The guard fires on the report whose rows do not add up to its own total — and it refuses before printing a single delta, because a comparison against a report with a dropped row is meaningless.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f areadiff.awk old2.rpt new2.rpt 2>/dev/null | grep -E "ADDED|^# [0-9]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_pcie             -     310       -           -    2400.000          -        -  ADDED
u_debug            -      18       -           -     183.000          -        -  ADDED
# 9 module(s); 2 grew beyond 1e-06; 2 added; 0 removed

Two added modules, in file order. An earlier version of the script iterated for (m in nseen) here and the two rows came out in an undefined sequence — the fix is the norder[] array, and the reason to care is that a diff of two nights' reports must not show spurious changes.

8. Every Decision and Its Source

decisionalternative that failsmeasured in
tolerance for area comparisons== is exact here and fails on 58.3% of comparable data11.1
== for cell countsintegers are exact; a tolerance would mask a real ±111.1
near() as two one-sided testsPOSIX awk has no abs()11.1
reconcile each report against its own TOTALa dropped row changes every delta11.8
refuse before emitting any rowa partial diff invites use10.2
report added and removed modulesa missing key is not a delta of zero9.5
absolute and relative thresholdsa small module doubling and a large one drifting differ11.2
file-order arrays for both tablesfor (m in …) has no defined order10.4
[same] rather than a blankno trailing whitespace, and unchanged is a verdict—
state the comparison method in the footera decision that changes the answer is part of it11.8

9. Common Mistakes

== on two areas. Exact here, and wrong on a majority of comparable inputs — section 3.

A tolerance on cell counts. It would hide a genuine difference of one cell.

abs(). POSIX awk has none; write two comparisons.

Only an absolute threshold. A 0.05% drift on a large module swamps a 20% jump on a small one.

Treating a missing module as zero. It was added or removed, which is a different fact — section 4.

Comparing without reconciling each report first. A dropped row silently changes every delta.

for (m in …) for output. The added-module rows came out in an undefined order.

Leaving an unchanged row blank. It trails whitespace and says nothing.

10. Exercises

Exercise 1 — Only what grew

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/GREW/ { printf "  %-11s %+10.3f  %+7.2f%%\n", $1, $7, $8 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_add         +282.500    +9.95%
  u_ctrl        +236.000   +19.03%

Exercise 2 — A stricter relative threshold

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -v rel=15 -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/GREW/ { print "  " $0 }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  u_add            420     468     +48    2840.000    3122.500   +282.500    +9.95  GREW
  u_ctrl           170     214     +44    1240.000    1476.000   +236.000   +19.03  GREW (19.03% > 15%)

Both still grew in absolute terms, and only u_ctrl clears a 15% relative bar — the annotation appears on exactly the row that passes both tests.

Exercise 3 — Debug a reconciliation that fails on good data

A comparison script reports that a report's modules do not sum to its total.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk '/^[a-z_]/ && NF == 3 && $1 != "TOTAL" { s += $3 } $1 == "TOTAL" { t = $3 }
 END { printf "  rows sum to %.20f\n  TOTAL says  %.20f\n  equal with == : %s\n  within 1e-6   : %s\n", s, t, (s == t ? "yes" : "NO"), (s - t < 1e-6 && t - s < 1e-6 ? "yes" : "no") }' area_old.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  rows sum to 20343.00000000000000000000
  TOTAL says  20343.00000000000000000000
  equal with == : yes
  within 1e-6   : yes

Here both agree. The point of the exercise is that this is not guaranteed — section 3 measured == holding on only 41.7% of comparable value sets, so a check written this way passes its test case and fails later on data that is equally correct.

Exercise 4 — Debug added modules in a changing order

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for r in 1 2 3; do printf "  run %d: " $r; awk -f areadiff.awk old2.rpt new2.rpt 2>/dev/null | awk '/ADDED/ { printf "[%s]", $1 } END { print "" }'; done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  run 1: [u_pcie][u_debug]
  run 2: [u_pcie][u_debug]
  run 3: [u_pcie][u_debug]

Stable across three runs, because the script walks norder[] — the order the modules appeared in the file. With for (m in nseen) the two rows would come out in whatever order the implementation chose, and a nightly diff of the analyzer's own output would show changes that are not changes.

Exercise 5 — Engineering application

Azvya Education Pvt. Ltd.VLSI Mentor
check_area.sh — synthetic teaching script.
#!/bin/sh
# check_area.sh — compare two area reports and route the outcome.

awk -v tol=1e-6 -v rel=5 -f areadiff.awk area_old.rpt area_new.rpt > area_report.txt 2> area_errors.txt
status=$?
# the footer's count is $4 and the TOTALS line's area delta is $11, both
# established by printing the fields rather than guessing -- Chapter 12.4
ngrew=$(awk '/grew beyond/ { print $4 }' area_report.txt)
dtot=$(awk '/^TOTALS/ { d = $11; sub(/^\(/, "", d); sub(/,$/, "", d); print d }' area_report.txt)

case $status in
  0) echo "AREA STABLE: nothing grew beyond the tolerance" ;;
  1) echo "AREA GREW: $ngrew module(s), total $dtot; see area_report.txt" ;;
  2) echo "AREA REPORT UNUSABLE:"; cat area_errors.txt ;;
  *) echo "ANALYZER FAILED (status $status)" ;;
esac
exit $status
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sh check_area.sh
echo "status: $?"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AREA GREW: 2 module(s), total +518.500; see area_report.txt
status: 1

Both values come out clean, and getting there took printing the fields first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/grew beyond/ { printf "NF=%d ", NF; for (i=1;i<=NF;i++) printf "[%d:%s]", i, $i; print "" }'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NF=11 [1:#][2:10][3:module(s);][4:2][5:grew][6:beyond][7:1e-06;][8:0][9:added;][10:0][11:removed]

$3 is module(s); and $4 is the count — a first draft of this exercise used $3 and printed module(s); module(s). The TOTALS line needs $11 with its leading parenthesis and trailing comma stripped, for the same reason.

That is the fourth time in Part IV that an index-based re-parse of a formatted line has taken the wrong field, and the structural answer is the one Chapter 13.3 section 6 built: a delimited sidecar with a fixed field count. This script does not have one, which is a genuine gap — Chapter 13.5 is where the metrics row gets written properly.

Exercise 6 — Challenge: show that the tolerance cannot be chosen from this report

Section 3 chose 10⁻⁶ because the report prints three decimals. Show that no observation of these two reports could have determined it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
awk 'FNR == NR { if (/^[a-z_]/ && NF == 3 && $1 != "TOTAL") oa[$1] = $3; next }
 /^[a-z_]/ && NF == 3 && $1 != "TOTAL" { d = $3 - oa[$1]; if (d != 0) { n++; if (ab == "" || (d < 0 ? -d : d) < ab) ab = (d < 0 ? -d : d) } }
 END { printf "  %d non-zero delta(s); smallest magnitude %.3f\n", n, ab }' area_old.rpt area_new.rpt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  2 non-zero delta(s); smallest magnitude 236.000

The smallest non-zero delta in this comparison is 236.000. Any tolerance between 0 and 236 gives the same two GREW rows, so the data is consistent with a tolerance of 10⁻¹², 10⁻⁶, 1, or 100:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for t in 1e-12 1e-6 1 100 235 237; do
  printf "  tol=%-6s " "$t"
  awk -v tol=$t -f areadiff.awk area_old.rpt area_new.rpt 2>/dev/null | awk '/grew beyond/ { printf "%s module(s) grew\n", $4 }'
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  tol=1e-12  2 module(s) grew
  tol=1e-6   2 module(s) grew
  tol=1      2 module(s) grew
  tol=100    2 module(s) grew
  tol=235    2 module(s) grew
  tol=237    1 module(s) grew

Six tolerances spanning fourteen orders of magnitude and five of them give the identical answer. The report cannot distinguish them, so the choice has to come from outside it — from the fact that the report prints three decimals, which makes anything below 10⁻³ unexpressible and anything below 10⁻⁶ certainly noise.

That is the general shape of a threshold decision and it is worth stating because it is easy to get backwards: a threshold is not calibrated on the data it will be applied to. Chapter 11.7's unconstrained-slack cutoff of 1e29 was chosen the same way — from what the quantity physically is, not from the values in one report — and both are kept in a named BEGIN variable so that the choice is visible rather than buried in a comparison.

11. Summary

  • Four outputs: per-module counts, a delta with a defined threshold, added and removed modules, and a reconciliation.
  • == on areas is exact on these reports and fails on 58.3% of comparable value sets. The exactness you observe is luck.
  • Module 11.1's pair differs at the twelfth decimal and prints identically, including its difference as -0.000.
  • A tolerance is a policy, not a bug fix — the failure depends on the values, so testing does not reveal it.
  • Integer cell counts compare with ==. The exemption runs the opposite way from intuition.
  • near() is two one-sided comparisons; POSIX awk has no abs().
  • Report absolute and relative growth. u_add grew more in area and u_ctrl more as a fraction.
  • A module in one report and not the other is added or removed, not a delta of zero.
  • Reconcile each report against its own TOTAL before comparing, and refuse before emitting any row.
  • Walk file-order arrays for output. for (m in …) made the added rows appear in an undefined sequence.
  • The threshold cannot be calibrated on the data. Five tolerances spanning fourteen orders of magnitude give the identical answer here.

Chapter 13.5 — Project: Regression Metrics Exporter closes Module 13 by combining all four scripts into one dated row a dashboard can accumulate — and the design question becomes what a row must contain to still be comparable in six months.

Where this fits

Part of the awk curriculum.