. "$BST_LIB" # BST-TIMEOUT: 900 # eight smem invocations, each a python3 process start on # an interpreter that is one 40 MB static binary, plus one # smemcap walk of all of /proc. The 60s default is for # suites whose subject is a single small exec. # smem 1.5 (98273ce331bb, meta-openembedded/meta-oe dynamic-layers/meta-python, # with the recipe's own --source python3 patch and a meta-brintos bbappend) -- # the proportional-set-size reporter. It divides each shared page among the # processes mapping it, so that per-process figures sum to something meaningful # rather than counting every library once per user. Three packages: `smem' (the # python script), `smemcap' (the C helper that tars /proc up for later # analysis), and `smem-doc' (the man page). # # THE PROCESS-START WALL, WHICH DECIDES HOW THIS SUITE IS WRITTEN. # smem is a python3 script, so every invocation is a fresh start of a 40 MB # static interpreter, and this guest does not return a process's memory when it # exits (the runner's own header records the same wall for whole-image sweeps). # MEASURED, twice, on the batch-1871 image: the TWELFTH smem process of a boot # never returns and the guest then grows the host without bound -- 7.6 GiB/min, # to 8.7 GiB, which the harness kills as a runaway (brintos/linux#21, # brintos/hardwarejs#20). The first eleven complete normally, and the twelfth # wedges whether it reads live /proc or a fixture, so it is the process start # and not the read pattern. An xtrace of the suite put it at exactly the same # ordinal both times. # # So the invocation count is a budget, not an incidental. Every smem run below # happens ONCE, into a file, and every case reads that file; nothing re-runs a # mode to re-derive a second figure from it. Eight runs is the whole of it, # which leaves three of the measured eleven in hand for the load a shared image # puts on the guest before this suite starts. What the budget cannot buy is # recorded as an attributed skip naming this wall, never dropped silently. # # WHAT THIS PLATFORM CAN AND CANNOT ANSWER, WHICH DECIDES THE REST OF THE SHAPE. # smem's whole per-process input is /proc//smaps, and on wasm32-hwjs no # user process except pid 1 has a kernel-tracked mm: user processes run in # their own Worker over a SharedArrayBuffer, so their pages never become VMAs # the kernel accounts. /proc//smaps is therefore EMPTY for everything this # suite could start (measured on target for BRINTOS-1044, and tracked as # brintos/linux#49). smem does not report those processes as zero -- it drops # them: processtotals() discards any pid whose mapping count is 0, so the # per-process table simply has no rows for them. # # So the ticket's per-process requirement -- "at least one data row for a # process the suite itself started" -- is not merely unmeasurable here, it is # structurally impossible, and is recorded as an attributed skip below rather # than quietly dropped. # # WHAT IS ASSERTED INSTEAD, AND WHY IT IS NOT A CLIMBDOWN. smem takes its /proc # from `-S', either a directory prefix or an smemcap tar, so the reporting # contract -- the modes the budget affords, their columns, the PSS/USS/RSS # arithmetic and the USS <= PSS <= RSS invariant -- is driven ON TARGET from a # fixed capture (data/smem-proc.tar, built by src/smem_mkfixture.py, every # number in it chosen so the expected report can be computed on paper). That # path is not a simulation of the tool: it is the same showtable() over the same # parser, and it is the ONLY thing that exercises the recipe's own patch, which # fixes tardata's bytes-vs-str handling under python3. # # THE SYSTEM-WIDE MODE IS THE ONE THAT WORKS FULLY ON LIVE /proc. `smem -w' # reads /proc/meminfo and nothing else, and this kernel fills that in correctly. # It is cross-checked against a FROZEN COPY of /proc/meminfo rather than against # the live file -- MemFree moves between two reads, and a cross-check that raced # it would be a permanent flake rather than an assertion. # # DO NOT WRAP smem IN `timeout'. coreutils timeout arms a SIGALRM and waits on # the child, and that signal/wait path hangs on this runtime (brintos/linux#9); # the same rule 85-procps.test and 85-smemstat.test record. smem walks all of # /proc, which is the read pattern #9 makes risky, so every invocation below is # bare. require smem smem/present require python3 smem/python3-present t=${TMPDIR:-/tmp}/bst-smem.$$ rm -rf "$t"; mkdir -p "$t" || { bst_notok smem/scratch "cannot create $t"; exit 0; } cd "$t" || { bst_notok smem/scratch "cannot cd $t"; exit 0; } # --- 1. the packaging ----------------------------------------------------- # Asserted against dpkg's own file lists rather than by stat: a payload that # reached the image by some other route would satisfy a stat and would not # satisfy the claim being made. No smem process is spent here. check smem/install-path test -x /usr/bin/smem check_eq smem/which "/usr/bin/smem" "$(command -v smem)" check_out smem/pkg-script "/usr/bin/smem" cat /var/lib/dpkg/info/smem.list # PACKAGE_BEFORE_PN splits the C helper into its OWN package, so it is not in # smem.list at all. Both halves are asserted: absent there, present here. The # negative is the one that catches a packaging change silently folding them. check smem/pkg-helper-split sh -c '! grep -qx /usr/bin/smemcap /var/lib/dpkg/info/smem.list' check smemcap/install-path test -x /usr/bin/smemcap check_out smemcap/pkg-helper "/usr/bin/smemcap" cat /var/lib/dpkg/info/smemcap.list # The man page goes to smem-doc by the default FILES split, so its absence from # the runtime package is the packaging working, not a missing file. check smem/pkg-man-split sh -c '! grep -q smem.8 /var/lib/dpkg/info/smem.list' # The recipe rewrites the upstream `#!/usr/bin/env python' shebang. Both halves # of that matter on this image: the line itself, and the fact that the /usr/bin/env # it names is actually there -- a script whose interpreter cannot be found fails # with a message about the SCRIPT, not about env. check_eq smem/shebang "#!/usr/bin/env python3" "$(head -1 /usr/bin/smem)" check smem/env-present test -x /usr/bin/env # --- 2. the platform gap, and the live per-process report ----------------- # The child is started BEFORE the one live default-mode run, so that run is both # the mode's own assertion and the gap's: one process, two jobs, which is what # the budget above requires. check smem/proc-has-pids test "$(ls /proc | grep -c '^[0-9]')" -gt 5 sleep 120 & kid=$! check smem/child-alive test -d "/proc/$kid" # `test -s' is useless on procfs -- every file there reports st_size 0 -- so the # content is what is read. smem's iskernel() classifies an empty cmdline as a # kernel thread and drops the pid before smaps is ever opened, which would make # the cases below pass for the wrong reason. check smem/child-cmdline-nonempty sh -c '[ -n "$(cat /proc/'"$kid"'/cmdline)" ]' # MEASURED, and asserted so a fix is loud: the child exists, has a cmdline (so # smem does not classify it as a kernel thread), and has an EMPTY smaps -- which # is exactly why processtotals() drops it. When brintOS starts accounting user # memory these cases go red together, which is the signal to come back and turn # the skip below into a real assertion. check_eq smem/child-smaps-empty-today "0" \ "$(wc -c < "/proc/$kid/smaps" 2>/dev/null | tr -d ' ')" # RUN 1 of 8 -- live default mode. smem > live.out 2>/dev/null; live_rc=$? kill "$kid" 2>/dev/null check_eq smem/live-rc 0 "$live_rc" # The header is pinned with the column padding squeezed out: the PID and Command # widths are derived from the data, so a byte compare would be asserting this # boot's process table rather than smem's column contract. check_eq smem/live-columns "PID User Command Swap USS PSS RSS" \ "$(sed -n 1p live.out | tr -s ' ' | sed 's/^ //;s/ $//')" check_eq smem/child-absent-from-report-today "" \ "$(sed 1d live.out | awk -v p="$kid" '$1==p{print $1}')" bst_skip smem/child-row \ "the ticket asks for a data row for a process the suite itself started; no user process except pid 1 has a kernel-tracked mm on wasm32-hwjs, so its /proc//smaps is empty and smem drops it from the table before printing (brintos/linux#49) -- the per-process contract is asserted from data/smem-proc.tar instead" # RUN 2 of 8 -- live system-wide mode. smem -w > livew.out 2>/dev/null; livew_rc=$? check_eq smem/live-system-rc 0 "$livew_rc" check_eq smem/live-system-columns "Area Used Cache Noncache" \ "$(sed -n 1p livew.out | tr -s ' ' | sed 's/^ //;s/ $//')" # The five areas are a fixed list in a fixed order -- smem's default sort for # this mode is the hierarchical `order' field, not a value -- so unlike the # numbers they are assertable against live /proc. check_eq smem/live-system-areas \ "firmware/hardware|kernel image|kernel dynamic memory|userspace memory|free memory" \ "$(sed -e 1d -e 's/ *[0-9].*//' livew.out | tr '\n' '|' | sed 's/|$//')" bst_skip smem/live-users-mode \ "the -u and -m modes on LIVE /proc would each cost one more smem process, and the twelfth process start of a boot wedges this guest (measured twice on batch-1871: 7.6 GiB/min host growth, brintos/linux#21, brintos/hardwarejs#20). Both modes are asserted in full against the fixture below, where the values are computable on paper; on live /proc they could only have asserted their column headers" bst_skip smem/live-maps-mode \ "see smem/live-users-mode -- the same process-start budget, and -m is asserted against the fixture below" # --- 3. -w cross-checked against /proc/meminfo, field by field ------------ # Against a FROZEN copy, so the two sides describe the same instant. `-S ' # makes smem read /proc/meminfo; tarfile.open() on a directory raises, so # smem falls through to its plain-directory reader. mkdir -p "$t/snap/proc" cp /proc/meminfo "$t/snap/proc/meminfo" check smem/frozen-snapshot test -s "$t/snap/proc/meminfo" # RUN 3 of 8 -- the frozen snapshot. Its rc, its five rows, the ticket's sum # identity and the Noncache identity all come off THIS one file. smem -w -S "$t/snap" > sys.out 2>/dev/null; frozen_rc=$? check_eq smem/frozen-rc 0 "$frozen_rc" # The five Used/Cache pairs, each computed here from the same file smem read. # This is the whole of showsystem()'s arithmetic restated independently: # firmware/hardware = max(RAM - MemTotal, 0), and with no -R the two are equal # kernel image = 0 without -K # userspace = AnonPages + Mapped, of which Mapped is cache # kernel dynamic = MemTotal - MemFree - userspace # kernel cache = Buffers + SReclaimable + (Cached - Mapped) # free = MemFree, all of it reclaimable eval "$(awk '/^MemTotal:/{print "mt="$2} /^MemFree:/{print "mf="$2} /^Buffers:/{print "bu="$2} /^Cached:/{print "ca="$2} /^AnonPages:/{print "ap="$2} /^Mapped:/{print "mp="$2} /^SReclaimable:/{print "sr="$2}' "$t/snap/proc/meminfo")" # `|' as the s/// delimiter, not `/': one area is named "firmware/hardware". row() { grep "^$1" sys.out | tr -s ' ' | sed "s|^$1 *||;s| *$||"; } check_eq smem/system-firmware "0 0 0" "$(row 'firmware/hardware')" check_eq smem/system-kernel-img "0 0 0" "$(row 'kernel image')" check_eq smem/system-userspace \ "$((ap + mp)) $mp $((ap + mp - mp))" "$(row 'userspace memory')" check_eq smem/system-kernel-dyn \ "$((mt - mf - ap - mp)) $((bu + sr + ca - mp)) $((mt - mf - ap - mp - (bu + sr + ca - mp)))" \ "$(row 'kernel dynamic memory')" check_eq smem/system-free "$mf $mf 0" "$(row 'free memory')" # THE TICKET'S CROSS-CHECK, stated as the identity it is: the five Used figures # partition MemTotal exactly, because kernel-dynamic is defined as the # remainder. A single mis-parsed meminfo field breaks this sum. Read off the # file above rather than from a second smem run -- same report, no process. check_eq smem/system-used-sums-to-memtotal "$mt" \ "$(sed 1d sys.out | awk '{s+=$(NF-2)} END{print s+0}')" # Noncache is Used - Cache on every row, which is the one column smem derives # rather than reads. check_eq smem/system-noncache-identity "ok" \ "$(sed 1d sys.out | awk '{if ($(NF)!=$(NF-2)-$(NF-1)) bad=1} END{print bad?"bad":"ok"}')" # --- 4. the per-process contract, from the fixture ------------------------ # Staged on tmpfs rather than read off the virtiofs root: python's tarfile does # many small reads, and $BST_DATA is the rootfs. fix=$(bst_local "$BST_DATA/smem-proc.tar") check smem/fixture-present test -s "$fix" # RUN 4 of 8 -- the fixture's default report. stdout and stderr go to separate # files so the traceback case reads the one and the table cases the other; a # second run to see stderr is exactly what the budget forbids. # # THE RECIPE'S PATCH IS WHAT MAKES THIS WORK AT ALL. Unpatched, tardata._read # returns bytes and the first regex match against /proc/meminfo dies with # `TypeError: cannot use a string pattern on a bytes-like object'. So a clean # exit here IS the assertion that the patch is applied and effective. smem -S "$fix" > f.out 2> f.err; f_rc=$? check_eq smem/fixture-rc 0 "$f_rc" check smem/fixture-no-traceback sh -c '! grep -q Traceback '"$t"'/f.err' # Three processes, every figure computable on paper: # gamma half of a 100/80/40 shared lib + a 20 kB private-clean anon # beta the other half + a 40 kB private-dirty anon, 4 kB swapped # alpha private only -- the USS == PSS == RSS edge # Rows come out sorted by PSS ascending, which is smem's documented default. check_eq smem/fixture-report \ "300 tester /usr/bin/gamma 0 20 60 100|200 tester /usr/bin/beta -v 4 40 80 120|100 root /usr/bin/alpha 8 96 96 96" \ "$(sed 1d f.out | tr -s ' ' | sed 's/^ //;s/ $//' | tr '\n' '|' | sed 's/|$//')" # THE INVARIANT THE TICKET ASKS FOR, over every row rather than over one: the # three figures are non-negative integers and USS <= PSS <= RSS. alpha is the # case that makes the <= rather than < correct -- a process with no shared pages # has all three equal. check_eq smem/fixture-uss-le-pss-le-rss "3 ok" \ "$(sed 1d f.out | awk ' { u=$(NF-2); p=$(NF-1); r=$NF if (u ~ /^[0-9]+$/ && p ~ /^[0-9]+$/ && r ~ /^[0-9]+$/ && u<=p && p<=r) n++ else bad++ } END { print n+0, bad?"bad":"ok" }')" # PSS is strictly between USS and RSS wherever pages ARE shared, which is the # property smem exists for. Asserting only <= would pass on a build that # reported RSS three times. check_eq smem/fixture-pss-strictly-between "2" \ "$(sed 1d f.out | awk '$(NF-2)<$(NF-1) && $(NF-1)<$NF' | grep -c .)" # --- 5. the other folding modes, from the fixture ------------------------ # RUN 5 of 8 -- -u folds by uid: root is alpha alone, tester is beta+gamma # summed. The names come from the archive's own uname fields, which is the only # place a tar source can get them. smem -u -S "$fix" > fu.out 2>/dev/null check_eq smem/fixture-users \ "root 1 8 96 96 96|tester 2 4 60 140 220" \ "$(sed 1d fu.out | tr -s ' ' | sed 's/^ //;s/ $//' | tr '\n' '|' | sed 's/|$//')" # RUN 6 of 8 -- -m folds by mapping name. AVGPSS is PSS/PIDs truncated -- # 124/3 = 41, not 41.3 -- which is what pins the arithmetic rather than the # layout. smem -m -S "$fix" > fm.out 2>/dev/null check_eq smem/fixture-maps \ "/usr/bin/alpha 1 32 32|/usr/lib/libshared.so.1 2 40 80| 3 41 124" \ "$(sed 1d fm.out | tr -s ' ' | sed 's/^ //;s/ $//' | tr '\n' '|' | sed 's/|$//')" # --- 6. -k: the unit suffix, and the ordering it must not touch ---------- # RUN 7 of 8 -- -k switches every size to an abbreviated unit. The ticket asks # for exactly two things about it, and they are asserted separately. smem -k -S "$fix" > fk.out 2>/dev/null; fk_rc=$? check_eq smem/fixture-k-rc 0 "$fk_rc" check_eq smem/fixture-k-suffix \ "300 tester /usr/bin/gamma 0 20.0K 60.0K 100.0K|200 tester /usr/bin/beta -v 4.0K 40.0K 80.0K 120.0K|100 root /usr/bin/alpha 8.0K 96.0K 96.0K 96.0K" \ "$(sed 1d fk.out | tr -s ' ' | sed 's/^ //;s/ $//' | tr '\n' '|' | sed 's/|$//')" # A zero renders as a bare "0" with no suffix -- units() returns early -- so the # gamma row above is also the assertion that -k does not invent a unit for it. # The comparison is between the two files already on disk, not between two fresh # runs. check_eq smem/fixture-k-order-unchanged \ "$(sed 1d f.out | awk '{print $1}' | tr '\n' ' ')" \ "$(sed 1d fk.out | awk '{print $1}' | tr '\n' ' ')" # RUN 8 of 8 -- the same on the system report, where the magnitudes reach M and # the rounding is visible: 450000 kB is 439.5M, not 450.0M. This is also the # fixture's system-mode arithmetic: the numbers below are the plain-unit table # of section 3's formulae, expressed in the units -k prints. smem -w -k -S "$fix" > fwk.out 2>/dev/null check_eq smem/fixture-k-system \ "firmware/hardware 0 0 0|kernel image 0 0 0|kernel dynamic memory 439.5M 302.7M 136.7M|userspace memory 341.8M 97.7M 244.1M|free memory 195.3M 195.3M 0" \ "$(sed 1d fwk.out | tr -s ' ' | sed 's/^ //;s/ $//' | tr '\n' '|' | sed 's/|$//')" # --- CONTROL -------------------------------------------------------------- # The fixture comparisons above must be capable of failing: a smem that printed # nothing, or an awk that always said "ok", would sweep most of this suite. Two # reports already on disk, so the control costs no process. check_eq smem/control-fixture-differs "differ" \ "$([ "$(cat f.out)" = "$(cat fu.out)" ] && echo same || echo differ)" # --- 7. smemcap, and what the budget leaves of the round trip ------------ # The C half, built for wasm32 from the recipe's own do_compile. It tars /proc # up on stdout; it is not python, so it costs nothing against the budget above. smemcap > cap.tar 2>/dev/null check smemcap/capture-rc test -s cap.tar check smemcap/capture-has-meminfo sh -c 'tar tf '"$t"'/cap.tar 2>/dev/null | grep -qx meminfo' bst_skip smemcap/round-trip \ "reading smemcap's own tar back with smem -w -S cap.tar would be a ninth smem process, and the twelfth process start of a boot wedges this guest (measured twice on batch-1871, brintos/linux#21, brintos/hardwarejs#20). The tar-source READER is asserted in full against data/smem-proc.tar above -- it is the same tardata path, and it is what exercises the recipe's patch; what stays unasserted here is only that smemcap's WRITER agrees with it, and the capture's own contents are asserted above" bst_skip smem/option-surface \ "-t (totals row), -c (column selection, including the unknown-field exit 255 and its diagnostic), -p (percentages), -H (header suppression) and --pie (the matplotlib refusal the bbappend's dropped RRECOMMENDS makes certain) are each one more smem process, and the budget recorded at the head of this file is eight. They are option plumbing over the same showtable() the fixture cases above assert end to end; the modes that carry the port's own arithmetic were kept in preference (brintos/linux#21, brintos/hardwarejs#20)" cd /; rm -rf "$t"