Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Buildroot/board/FOG/FOS/rootfs_overlay/bin/fog
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ if [[ -n $mode && $mode != +(*debug*) && -z $type ]]; then
enrollsbforce)
fog.enrollsbforce
;;
perftest)
fog.perftest
;;
*)
handleError "Fatal Error: Unknown mode :: $mode ($0)\n Args Passed: $*"
;;
Expand Down
161 changes: 161 additions & 0 deletions Buildroot/board/FOG/FOS/rootfs_overlay/bin/fog.perftest
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/bin/bash
#
# Performance Test task (fos#55): times the three stages a deploy runs
# through -- disk, network, CPU -- names the slowest, shows the result and
# reports it to the server so it stays on the host's record. Nothing is
# written to the disk and nothing is written to the storage node: the disk
# test only reads, and /images is mounted read-only.
. /usr/share/fog/lib/funcs.sh
. /usr/share/fog/lib/perftest-funcs.sh
clearScreen
displayBanner

# How much each test moves. Large enough to get past caches and ramp-up, small
# enough that the whole task takes a minute or two on slow hardware. The
# decompression sample is held in RAM (/tmp is tmpfs), so it stays modest.
DISK_MIB=2048
NET_MIB=1024
CPU_MIB=256

dots "Locating disks"
disks=""
disk=""
hd=""
getHardDisk
if [[ -z $hd ]]; then
echo "Failed"
debugPause
handleError "No hard disks found! ($0)\n Args Passed: $*"
fi
echo "Done"
debugPause

dots "Attempting to checkin"
res=""
count=0
sysuuid=$(dmidecode -s system-uuid)
sysuuid=${sysuuid,,}
while [[ $res != "##@GO" ]]; do
callServer "${web}service/Pre_Stage1.php" "sysuuid=${sysuuid}&mac=$mac"
res="$serverBody"
if [[ $count -ge 10 ]]; then
echo "Failed"
debugPause
handleError "Could not checkin: ${serverReason:-last reply was \"$res\"} ($0)\n Args Passed: $*"
fi
let count+=1
usleep 5000000
done
echo "Done"
debugPause

# Disk: a direct (uncached) sequential read of the target disk.
dots "Testing disk read ($hd)"
diskmbps=0
if perfReadTest "$hd" "$DISK_MIB" 1; then
diskmbps=$(perfRate "$PERF_BYTES" "$PERF_NS")
echo "Done"
else
echo "Failed"
fi
debugPause

# Network and CPU both need a captured image to read. fog.mount is not used:
# it mounts only for type=up/down, and this mount must be read-only.
netmbps=0
cpumbps=0
ratio100=100
fmt=""
sample=""
[[ ! -d /images ]] && mkdir -p /images >/dev/null 2>&1
umount /images >/dev/null 2>&1
# The server sends a non-imaging task the capture share, <node path>/dev/
# (BootMenuBase::getTasking). The stored images a deploy reads are its parent.
imgroot=$(perfImageRoot "$storage")
dots "Mounting storage read-only"
# -t nfs is load-bearing, see the comment in fog.mount.
if mount -t nfs -o ro,nolock,proto=tcp,rsize=32768,intr,noatime "$imgroot" /images >/tmp/mount-output 2>&1; then
echo "Done"
sample=$(perfPickSample /images)
else
echo "Failed"
echo " $(tr -d \\0 </tmp/mount-output)"
fi
debugPause

if [[ -n $sample ]]; then
fmt=$(perfFormat "$sample")
dots "Testing network read (${sample#/images/})"
if perfReadTest "$sample" "$NET_MIB" 0; then
netcompressed=$(perfRate "$PERF_BYTES" "$PERF_NS")
echo "Done"
else
netcompressed=0
echo "Failed"
fi
debugPause
if [[ $fmt != unknown ]]; then
dots "Testing decompression ($fmt)"
# Taken from the NFS client's cache after the read above, so this
# copy costs little and the timing below is the CPU alone.
dd if="$sample" of=/tmp/perf.sample bs=1M count="$CPU_MIB" >/dev/null 2>&1
if perfDecompressTest /tmp/perf.sample "$fmt"; then
cpumbps=$(perfRate "$PERF_OUT" "$PERF_NS")
ratio100=$(perfRatio100 "$PERF_IN" "$PERF_OUT")
echo "Done"
else
echo "Failed"
fi
rm -f /tmp/perf.sample
debugPause
fi
# The network moves compressed bytes; scale to what they unpack to.
netmbps=$(perfScale "$netcompressed" "$ratio100")
fi
umount /images >/dev/null 2>&1

bottleneck=$(perfBottleneck "$diskmbps" "$netmbps" "$cpumbps")

perfLine() {
local label="$1" mbps="$2" note="$3"
if [[ $mbps -gt 0 ]]; then
printf " %-12s %6s MB/s %5s GB/min%s\n" "$label" "$mbps" "$(perfGBmin "$mbps")" "$note"
else
printf " %-12s %s\n" "$label" "not measured"
fi
}
echo
echo " +-----------------------------------------+"
echo " | Performance Test (uncompressed rates) |"
echo " +-----------------------------------------+"
perfLine "Disk read" "$diskmbps" " ($hd)"
perfLine "Network" "$netmbps"
perfLine "Decompress" "$cpumbps" "${fmt:+ ($fmt)}"
[[ -z $sample ]] && echo " No captured image on the storage node, so network and CPU were skipped."
if [[ $ratio100 -ne 100 ]]; then
echo " Compression ratio of the sample: $(( ratio100 / 100 )).$(( ratio100 % 100 / 10 )):1"
fi
if [[ -n $bottleneck ]]; then
echo
echo " A deploy to this machine is limited by: $bottleneck"
fi
echo

# Reported BEFORE the task is completed, so the server can still tie the
# result to this task. A failed report is a warning, not an abort: the
# measurement is on screen either way.
dots "Sending results to the FOG server"
sample64=$(echo -n "${sample#/images/}" | base64 | tr -d '\n')
kernel=$(uname -r)
initver=$(awk -F= '/^export initversion=/{print $2}' /usr/share/fog/lib/funcs.sh)
callServer "${web}service/perftest.php" "mac=${mac}&disk=${hd}&diskmbps=${diskmbps}&netmbps=${netmbps}&cpumbps=${cpumbps}&ratio=${ratio100}&format=${fmt}&sample=${sample64}&bottleneck=${bottleneck}&kernel=${kernel}&initversion=${initver}"
if [[ $serverBody == "##" ]]; then
echo "Done"
else
echo "Failed"
echo " ${serverReason:-$serverBody}"
fi
debugPause
echo " * Press [Enter] key to continue"
read -t 300 -p ""
. /bin/fog.nonimgcomplete
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/bin/bash
#
# Imaging performance test (fos#55). A deploy moves an image through three
# stages -- the network (NFS read from the storage node), the CPU (zstd or
# pigz decompression) and the target disk -- and runs no faster than the
# slowest one. These functions time each stage on its own so the slowest can
# be named. bin/fog.perftest runs them; tests/checks/perftest.sh exercises
# them off-target.
#
# Every rate is reported in UNCOMPRESSED megabytes per second, the unit
# partclone's own GB/min display uses. The network and CPU stages move
# compressed bytes, so they are scaled by the sample's compression ratio;
# without that, a 2:1 image would make the network look half as fast as it
# is next to the disk.

# Megabytes (10^6 bytes) per second from a byte count and a duration in
# nanoseconds. Integer only: bash has no floating point, and a whole MB/s is
# all the comparison needs. A zero duration yields 0 rather than dividing by
# zero.
perfRate() {
local bytes="$1" ns="$2"
[[ -z $bytes || -z $ns || $ns -le 0 ]] && { echo 0; return; }
echo $(( bytes * 1000 / ns ))
}

# GB/min from MB/s, to one decimal place, for the line people compare with
# partclone's display.
perfGBmin() {
local mbps="$1"
local tenths=$(( mbps * 60 / 100 ))
echo "$(( tenths / 10 )).$(( tenths % 10 ))"
}

# The NFS source of the stored images, from the storage= a non-imaging task
# receives. The server sends such a task <ip>:/<node path>/dev/, the capture
# share; the stored images are its parent. Anything not ending in dev/ is
# returned as it is.
perfImageRoot() {
local s="${1%/}"
[[ $s == */dev ]] && s="${s%/dev}"
echo "$s/"
}

# The largest captured image file under the mounted storage root, or nothing.
# Captured partitions are d<disk>p<part>.img (pigz, one file) or
# d<disk>p<part>.img.NNN (zstd, split into 200M pieces). Captures still in
# progress sit one level deeper, at dev/<mac>/, which the glob's depth never
# reaches: a half-written file says nothing about the images a deploy reads.
perfPickSample() {
local root="$1" f size best="" bestsize=0
# A glob, not find: the init carries busybox find, which has no -printf.
for f in "$root"/*/d*p*.img "$root"/*/d*p*.img.[0-9][0-9][0-9]; do
[[ -f $f ]] || continue
size=$(stat -c %s "$f" 2>/dev/null)
[[ -n $size && $size -gt $bestsize ]] && { best=$f; bestsize=$size; }
done
echo "$best"
}

# zstd, gzip or unknown, from the first four bytes. pigz writes ordinary gzip.
perfFormat() {
local magic
magic=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' \n')
case $magic in
28b52ffd) echo zstd ;;
1f8b*) echo gzip ;;
*) echo unknown ;;
esac
}

# The decompressor a deploy uses for a given format, matching writeImage():
# zstdmt -dc for zstd images, pigz -dc for gzip ones.
perfDecompressor() {
case $1 in
zstd) echo "zstdmt -dc" ;;
gzip) echo "pigz -dc" ;;
esac
}

# Reads up to <mib> MiB from <src> into /dev/null and sets PERF_BYTES and
# PERF_NS. <direct> = 1 adds iflag=direct, which bypasses the page cache so a
# disk read measures the disk and not RAM. Returns dd's status.
perfReadTest() {
local src="$1" mib="$2" direct="$3" flag="" start end out rc
[[ $direct == 1 ]] && flag="iflag=direct"
start=$(date +%s%N)
out=$(dd if="$src" of=/dev/null bs=1M count="$mib" $flag 2>&1)
rc=$?
end=$(date +%s%N)
PERF_NS=$(( end - start ))
# dd's summary line starts with the byte count it actually moved, which is
# less than <mib> MiB when the source is shorter.
PERF_BYTES=$(echo "$out" | awk '/bytes/ {print $1; exit}')
[[ -z $PERF_BYTES ]] && PERF_BYTES=0
return $rc
}

# Decompresses <sample> (a compressed fragment already in RAM) with the
# deploy's own decompressor and sets PERF_IN, PERF_OUT and PERF_NS. The
# fragment ends mid-stream, so the decompressor's exit status is expected to
# be non-zero and is ignored; what it produced before the cut is what is
# measured.
perfDecompressTest() {
local sample="$1" fmt="$2" cmd start end
cmd=$(perfDecompressor "$fmt")
PERF_IN=$(stat -c %s "$sample" 2>/dev/null)
[[ -z $cmd || -z $PERF_IN ]] && { PERF_OUT=0; PERF_NS=0; return 1; }
start=$(date +%s%N)
PERF_OUT=$($cmd < "$sample" 2>/dev/null | wc -c)
end=$(date +%s%N)
PERF_NS=$(( end - start ))
return 0
}

# Uncompressed output bytes per compressed input byte, times 100, so a 2.5:1
# image gives 250. 100 (no scaling) when either count is missing.
perfRatio100() {
local in="$1" out="$2"
[[ -z $in || -z $out || $in -le 0 || $out -le 0 ]] && { echo 100; return; }
echo $(( out * 100 / in ))
}

# A compressed-byte rate scaled to the uncompressed bytes it unpacks to.
perfScale() {
echo $(( $1 * $2 / 100 ))
}

# Names the slowest of the stages that were measured. Arguments are
# uncompressed MB/s for disk, network and cpu; an empty or 0 value means the
# stage was not measured and is left out.
perfBottleneck() {
local -a names=(disk network cpu)
local -a rates=("$1" "$2" "$3")
local i best="" bestrate=""
for i in 0 1 2; do
[[ -z ${rates[$i]} || ${rates[$i]} -le 0 ]] && continue
if [[ -z $bestrate || ${rates[$i]} -lt $bestrate ]]; then
best=${names[$i]}
bestrate=${rates[$i]}
fi
done
echo "$best"
}
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ tests/checks/mdraid-gate.sh # md arrays auto-assemble only with mdraid=
tests/checks/multicast-receiver.sh # udp-receiver gets --start-timeout from mcastMaxWait; an empty stream says no sender answered (fos#48)

tests/checks/fixparts-binary-present.sh # fs.patch builds fixparts, which runFixparts() calls (fos#194)
tests/checks/perftest.sh # Performance Test task: rates, sampled image, dev/ share, bottleneck (fos#55)

tests/checks/package-mirrors.sh # build.sh's package mirror fallback and hash enforcement
```
Expand Down
8 changes: 8 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ tests/checks/fixparts-binary-present.sh
# it. A symbol with no Kconfig definition is
# dropped silently, which is how every init from
# 2023-12 on shipped without it. See fos#194
tests/checks/perftest.sh
# fog.perftest (Performance Test task): MB/s and
# GB/min arithmetic, the dev/ share mapped to the
# stored images, the sampled image (never a
# capture in progress), real zstdmt/pigz on a
# stream cut mid-way, scaling compressed rates
# to uncompressed, and which stage it blames.
# See fos#55
tests/checks/package-mirrors.sh
# build.sh's package-mirror seeding: resolves each
# package's version/source/site out of its own .mk
Expand Down
Loading
Loading