# BST-TIMEOUT: 300 # # The bound is here rather than at the 60s default because nearly all of this # suite's time is ONE process: an 8.5 MB probe module instantiated once, which # then walks every request size from 1 to 256 KiB four times (est, monotone, # nallocx, distinct-count) -- ~1M MallocExtension calls -- and runs the heap # profiler. That is a single indivisible cost on a host having a bad day, not a # hundred small ones that average out. . "$BST_LIB" # gperftools 2.15 (meta-oe recipes-support, SRCREV 365060c4) -- Google's # performance tools: the thread-caching allocator tcmalloc, the heap profiler, # and pprof, the Perl report generator. Tenth port of the C++ round # (BRINTOS-1094), after abseil-cpp, re2, gflags, glog, fmt, double-conversion, # snappy, leveldb and highway. # # --------------------------------------------------------------------------- # WHAT THIS ARM DOES NOT HAVE, STATED UP FRONT, BECAUSE THE SUITE ASSERTS EACH # ABSENCE RATHER THAN LEAVING IT TO BE DISCOVERED # # NO CPU PROFILER. configure prints `Could not find the PC. Will not try to # compile libprofiler` and builds no libprofiler.a: gperftools recovers the # sampled program counter out of a signal ucontext, and there is no register # file in a ucontext on wasm32 to recover it from. Even with one there would # be nothing to sample -- the CPU profiler needs SIGPROF from setitimer # delivered onto a running stack. `gperftools/no-libprofiler` asserts the # archive's ABSENCE, so a future toolchain that starts building it shows up # as a red here rather than as a silent change. # # NO STACK UNWINDER, SO HEAP SAMPLES CARRY NO CALL SITES. stacktrace.cc ends # its chain of backends in impl__null, whose GetStackTrace returns 0 frames. # Every other backend is ruled out on this arm: libunwind is off, glibc here # does not declare backtrace(), the frame-pointer walker is gated on # x86/arm/riscv, and _Unwind_Backtrace is DECLARED by clang's own # while nothing on this arm defines it -- which is a link-time phantom import # and is exactly what the recipe's perftools_cv_have_unwind_backtrace=no # answers. The profiler still records every allocation and its totals; it # cannot say where they came from. `gperftools/stack-*` assert that directly. # # ONE THREAD. The pthread TSD API links here and tcmalloc uses it for its # per-thread cache, so there is exactly one thread cache and it is correct -- # but pthread_create does not work on this target, so nothing in this suite # exercises contention. A concurrency defect in tcmalloc is out of reach of # this image, not covered by this file, and that is a property of the target. # # WHERE THE EXPECTED VALUES COME FROM, WHICH IS NOT FROM A TARGET RUN. # # data/gperftools-native.txt is the same probe, built by the SAME RECIPE from # the SAME SRCREV with the SAME configure answers (BBCLASSEXTEND = "native"), # run on x86-64. tcmalloc's size-class table is computed by SizeMap::Init() # from four constants in src/common.h -- kAlignment 8, kMinAlign 16, # kPageShift 13, kMaxSize 256K -- none of which depends on the pointer size, # the libc or the target. So the table is a claim about tcmalloc's arithmetic # that both arms must answer identically, and `gperftools/oracle-match` # compares the whole of it byte for byte. # # The probe's own invariants are stronger than the table: it checks # est >= request, monotonicity, and nallocx agreement over EVERY request size # from 1 to 256 KiB, so a build that returned a self-consistent but wrong # table disagrees with itself before it disagrees with the oracle. # --------------------------------------------------------------------------- probe=/usr/lib/brintos-tests/data/gperftools-probe oracle="$BST_DATA/gperftools-native.txt" stage=${TMPDIR:-/tmp}/bst-gperftools.$$ mkdir -p "$stage" # ----------------------------------------------------------------- presence --- # check_rc with `test` is an ash builtin and forks nothing, unlike check/check_out # whose command substitution does. On an arm where a fork is a fresh wasm module # instantiation that difference is the whole reason the presence half is cheap. check_rc gperftools/bin-pprof 0 test -x /usr/bin/pprof check_rc gperftools/bin-pprof-symbolize 0 test -x /usr/bin/pprof-symbolize check_rc gperftools/lib-tcmalloc 0 test -f /usr/lib/libtcmalloc.a check_rc gperftools/lib-tcmalloc-minimal 0 test -f /usr/lib/libtcmalloc_minimal.a check_rc gperftools/lib-tcmalloc-debug 0 test -f /usr/lib/libtcmalloc_debug.a check_rc gperftools/lib-tcmalloc-minimal-debug 0 test -f /usr/lib/libtcmalloc_minimal_debug.a check_rc gperftools/header-tcmalloc 0 test -f /usr/include/gperftools/tcmalloc.h check_rc gperftools/header-heap-profiler 0 test -f /usr/include/gperftools/heap-profiler.h check_rc gperftools/header-malloc-extension 0 test -f /usr/include/gperftools/malloc_extension.h check_rc gperftools/header-nallocx 0 test -f /usr/include/gperftools/nallocx.h check_rc gperftools/header-stacktrace 0 test -f /usr/include/gperftools/stacktrace.h # The google/ headers are upstream's compatibility shims for pre-rename callers; # they are part of what the package promises and cost nothing to check. check_rc gperftools/header-compat-google 0 test -f /usr/include/google/tcmalloc.h check_rc gperftools/pc-tcmalloc 0 test -f /usr/lib/pkgconfig/libtcmalloc.pc check_rc gperftools/pc-tcmalloc-minimal 0 test -f /usr/lib/pkgconfig/libtcmalloc_minimal.pc check_rc gperftools/payload-probe 0 test -f "$probe" check_rc gperftools/oracle-present 0 test -f "$oracle" # ------------------------------------------------ absences, with the reason --- # No libprofiler.a: the CPU profiler is not built here (see the header above). # Asserted as an absence so a toolchain change that starts building it is a red # rather than a silent difference. if [ -e /usr/lib/libprofiler.a ] || [ -e /usr/lib/libprofiler.so ]; then bst_notok gperftools/no-libprofiler "libprofiler is present; the CPU profiler was not supposed to build on this arm" else bst_ok gperftools/no-libprofiler fi # Shape, not absence: this arm converts to shared libraries per recipe # (BRINTOS-1740), so a .so here is a conversion, not a policy breach. bst_lib_shape gperftools/static-only "/usr/lib/libtcmalloc.so" "/usr/lib/libtcmalloc_minimal.so" "/usr/lib/libtcmalloc.so.4" # ---------------------------------------------------------------- the probe --- # Copied to a tmpfs and made executable there: brintos-selftests stages every # data/ payload 0644 and the deployed rootfs is read-only at browser runtime, so # a suite that ran its payload in place would be untested against how the payload # actually arrives. cp "$probe" "$stage/probe" 2>/dev/null && chmod +x "$stage/probe" out=$stage/out.txt err=$stage/err.txt if [ -x "$stage/probe" ]; then GPERFTOOLS_PROBE_HEAP_PREFIX=$stage/heap TCMALLOC_STACKTRACE_METHOD_VERBOSE=1 \ "$stage/probe" > "$out" 2> "$err" rc=$? check_eq gperftools/probe-exit 0 "$rc" else bst_notok gperftools/probe-exit "probe payload missing or not executable" : > "$out"; : > "$err" fi # The probe prints ==GPT-END== last. Without this a probe that died half way # through would have every case it DID reach counted as a pass, which is the # same shape as a suite reporting a short clean run. if grep -qxF '==GPT-END==' "$out"; then bst_ok gperftools/probe-complete else bst_notok gperftools/probe-complete "no ==GPT-END== — the probe did not run to the end" fi if grep -qxF 'failures 0' "$out"; then bst_ok gperftools/probe-no-failures else bst_notok gperftools/probe-no-failures "$(grep -E '^(FAIL|failures) ' "$out" | head -3 | tr '\n' ';')" fi check_out gperftools/probe-version "probe-pv 2.15" grep -F 'probe-pv ' "$out" check_out gperftools/tc-version "tc-version 2.15" grep -F 'tc-version ' "$out" # The arm witness: this target is 32-bit, and the size-class table below is # identical to the 64-bit oracle's anyway. That is the whole point of comparing # it -- the table is arithmetic, not layout. check_out gperftools/arm-32bit "sizeof-void-p 4" grep -F 'sizeof-void-p' "$out" # ---------------------------------------------------------- the oracle match --- # The strong claim, made once over the whole segment: classes, invariants, # properties and the GetStats() shape, byte for byte against the x86-64 control. sed -n '/^==GPT-CLASSES==$/,/^==GPT-STACK==$/p' "$out" | sed '$d' > "$stage/seg.txt" if [ -s "$stage/seg.txt" ] && cmp -s "$stage/seg.txt" "$oracle"; then bst_ok gperftools/oracle-match else bst_notok gperftools/oracle-match "$(_bst_1line "$(diff "$oracle" "$stage/seg.txt" 2>&1 | head -6)")" fi # ------------------------------------------------ the same claim, case by case --- # The byte-compare above is one red for any difference anywhere. These name the # individual claims so a reader of the report knows WHICH one moved: the two ends # of the table, both alignment runs, the page-size steps, and the first request # past kMaxSize, where tcmalloc stops using size classes at all. for spec in \ 'class-1:class 1 8 8' \ 'class-9:class 9 16 16' \ 'class-17:class 17 32 32' \ 'class-129:class 129 144 144' \ 'class-1024:class 1024 1024 1024' \ 'class-8192:class 8192 8192 8192' \ 'class-65536:class 65536 65536 65536' \ 'class-131072:class 131072 131072 131072' \ 'class-kmaxsize:class 262144 262144 262144' \ 'class-past-kmaxsize:class 262145 270336 270336' \ 'classes-distinct:classes-distinct-1-to-262144 85' \ ; do id=${spec%%:*}; want=${spec#*:} if grep -qxF "$want" "$out"; then bst_ok "gperftools/$id" else bst_notok "gperftools/$id" "expected line: $want" fi done # Each of these is a PASS/FAIL line the probe emitted. They are re-reported here # one per case because "the probe failed" is not a finding a reader can act on. for c in \ inv/est-ge-request inv/est-monotone inv/nallocx-agrees \ inv/allocated-size-matches-class inv/alignment inv/content-roundtrip \ inv/current-allocated-delta \ prop/unknown-rejected stats/nonempty stats/has-in-use stats/has-pageheap \ heap/not-running-before heap/running-after-start heap/profile-nonempty \ heap/profile-header heap/profile-has-sample-lines \ heap/profile-has-mapped-libraries heap/not-running-after-stop \ ; do id=$(echo "$c" | tr '/' '-') if grep -qxF "PASS $c" "$out"; then bst_ok "gperftools/$id" else bst_notok "gperftools/$id" "$(_bst_1line "$(grep -F "$c" "$out" | head -1)")" fi done # MallocExtension has to ANSWER for each documented property. A stub instance -- # which is what a consumer gets if tcmalloc's initialiser never ran -- answers # false for every one of them, and every value it would have returned is live # state that differs per run, so presence is the whole assertion. for p in \ generic.current_allocated_bytes generic.heap_size \ tcmalloc.pageheap_free_bytes tcmalloc.pageheap_unmapped_bytes \ tcmalloc.slack_bytes tcmalloc.max_total_thread_cache_bytes \ tcmalloc.current_total_thread_cache_bytes tcmalloc.transfer_cache_free_bytes \ tcmalloc.thread_cache_free_bytes tcmalloc.aggressive_memory_decommit \ ; do id=$(echo "$p" | tr '.' '-') if grep -qxF "prop $p answered" "$out"; then bst_ok "gperftools/prop-$id" else bst_notok "gperftools/prop-$id" "MallocExtension did not answer for $p" fi done # ------------------------------------------------------ the stacktrace arm --- # Three ways of asking the same question, because it is the one thing about this # port a reader is most likely to be surprised by. The x86-64 control answers # `generic_fp` and 1 frame to the first two. if grep -qxF 'stack-frames 0' "$out"; then bst_ok gperftools/stack-frames-zero else bst_notok gperftools/stack-frames-zero "$(_bst_1line "$(grep -F 'stack-frames' "$out" | head -1)")" fi check_out gperftools/stack-backend-null "Chosen stacktrace method is null" cat "$err" if grep -qE '^\* (libgcc|libunwind|generic|generic_fp|arm|ppc|instrument)$' "$err"; then bst_notok gperftools/stack-only-null "$(_bst_1line "$(grep '^\* ' "$err" | tr '\n' ' ')")" else bst_ok gperftools/stack-only-null fi # --------------------------------------------------- the heap profile on disk --- # GetHeapProfile() is asserted inside the probe; this is the other half -- the # file HeapProfilerDump wrote, which is what pprof would be handed. hp=$(ls "$stage"/heap.*.heap 2>/dev/null | head -1) if [ -n "$hp" ] && [ -s "$hp" ]; then bst_ok gperftools/heap-file-written check_out gperftools/heap-file-header "heap profile:" head -1 "$hp" check_out gperftools/heap-file-mapped "MAPPED_LIBRARIES" cat "$hp" else bst_notok gperftools/heap-file-written "HeapProfilerDump wrote no profile under $stage" bst_notok gperftools/heap-file-header "no profile to read" bst_notok gperftools/heap-file-mapped "no profile to read" fi # ------------------------------------------------------------------- pprof --- # THESE THREE ARE THE PORT'S ATTRIBUTED REDS, AND THEY ARE NOT gperftools'. # # pprof is a Perl script -- the only program this package installs -- and on this # image EVERY substantial Perl PROGRAM aborts inside glibc's malloc consistency # checks before it prints anything. Measured on one boot, five programs from four # unrelated packages: # # automake --version -> double free or corruption (!prev) rc=134 # aclocal --version -> mremap_chunk(): invalid pointer rc=134 # dpkg-source --version -> mremap_chunk(): invalid pointer rc=134 # gnu-configize --version -> free(): invalid size rc=134 # pprof --version -> mremap_chunk(): invalid pointer rc=134 # # while the interpreter is healthy under far more memory than any of them use: # `perl -e` builds a 40 MB string and a 20000-element array of arrayrefs, rc=0. # So it is neither a size limit nor this package's defect -- it is the # module-loading path corrupting glibc's heap. Filed as # brintos/linux-in-the-browser#176 (the same class as the closed #73, whose own # reproducer `perl -e 'print "ok"'` now passes). # # THEY STAY RED RATHER THAN SKIPPED OR DELETED. pprof IS installed and IS this # package's only program: a `skip` would read as "not shipped", and removing the # cases would lose the only place this defect is visible from a package port. # Each carries the issue in its own detail string, so the report names it without # a reader having to come here -- and each will go green on its own the day #176 # closes, which is what makes them worth keeping. pprof_blocked="brintos/linux-in-the-browser#176 -- every multi-module Perl program on this image aborts in glibc malloc (automake, aclocal, dpkg-source, gnu-configize do the same)" if [ -x /usr/bin/pprof ]; then # It reports the gperftools version its own source pins ("2.0"), not the # package version -- an upstream quirk, asserted as it is rather than as it # ought to be, for the day this runs. pout=$(pprof --version 2>&1); prc=$? case "$pout" in *"part of gperftools 2.0"*) bst_ok gperftools/pprof-version ;; *) bst_notok gperftools/pprof-version "$pprof_blocked -- rc=$prc $(_bst_1line "$pout")" ;; esac pout=$(pprof --help 2>&1); prc=$? case "$pout" in *"Usage:"*) bst_ok gperftools/pprof-usage ;; *) bst_notok gperftools/pprof-usage "$pprof_blocked -- rc=$prc $(_bst_1line "$pout")" ;; esac # The question this one would answer if it ran: can pprof read a profile this # image produced? It shells out to nm/objdump/addr2line for symbols, and on # this arm those are absent under the names it looks for and could not read a # WebAssembly module if they were there -- so the expected best case is an # address-only report, not symbols. Recorded rather than assumed. if [ -n "$hp" ] && [ -s "$hp" ]; then pout=$(pprof --text "$stage/probe" "$hp" 2>&1); prc=$? if [ "$prc" -eq 0 ]; then bst_ok gperftools/pprof-text else bst_notok gperftools/pprof-text "$pprof_blocked -- rc=$prc $(_bst_1line "$pout")" fi else bst_notok gperftools/pprof-text "no heap profile to report on" fi else bst_notok gperftools/pprof-version "pprof not installed" bst_notok gperftools/pprof-usage "pprof not installed" bst_notok gperftools/pprof-text "pprof not installed" fi rm -rf "$stage"