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
29 changes: 28 additions & 1 deletion abs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ ABS v$VERSION — AskClaw VPS 跑分脚本
-z, --size SIZE fio 测试文件大小,如 512M、2G、8G
-t, --threads N CPU/内存测试线程数
-n, --no-install 不安装缺失工具
--install 显式允许用 sudo 安装缺失工具(默认即启用)
--strict 测试不完整时以非零状态退出
--lang zh|en 输出语言,默认 zh
--net-info 查询 IPv4/IPv6 和外部 IP/ASN
Expand Down Expand Up @@ -122,6 +123,7 @@ Options:
-z, --size SIZE fio test file size, e.g. 512M, 2G, 8G; default auto
-t, --threads N CPU/memory threads; default detected vCPU ($T)
-n, --no-install do not install missing packages
--install explicitly allow installing missing tools with sudo (default on)
--strict exit non-zero when the score is incomplete
--lang zh|en output language; default zh
--net-info check IPv4/IPv6 and external IP/ASN
Expand Down Expand Up @@ -165,6 +167,7 @@ while [ "$#" -gt 0 ]; do
shift; [ "$#" -gt 0 ] || die "缺少参数值:-t/--threads" "Missing value for -t/--threads"
T="$1" ;;
-n|--no-install) INSTALL=0 ;;
--install) INSTALL=1 ;;
--strict) STRICT=1 ;;
--lang)
shift; [ "$#" -gt 0 ] || die "缺少参数值:--lang" "Missing value for --lang"
Expand Down Expand Up @@ -446,7 +449,7 @@ install_tools() {

INSTALL_ATTEMPTED="1"
say "缺少工具:$missing" "Missing tools: $missing"
say "正在尝试安装;使用 -n/--no-install 可跳过安装。" "Installing missing tools when possible... use -n/--no-install to skip installation."
say "将通过系统包管理器用 sudo 安装:$missing(-n/--no-install 可跳过)" "Will install via your system package manager with sudo: $missing (-n/--no-install to skip)"

local curl_pkg="" iperf_pkg=""
if [ "$NETWORK" = "1" ] || [ "$NET_INFO" = "1" ]; then
Expand Down Expand Up @@ -507,6 +510,7 @@ zh_metric() {
suffix="${metric#Memory write (}"; suffix="${suffix% threads)}"; printf '内存写入(%s 线程)' "$suffix" ;;
"Memory read") printf '内存读取' ;;
"Memory write") printf '内存写入' ;;
"CPU steal (1s sample)") printf 'CPU steal(1 秒采样)' ;;
"sysbench CPU/memory") printf 'sysbench CPU/内存测试' ;;
"fio disk tests") printf 'fio 磁盘测试' ;;
"fio prepare") printf 'fio 测试文件准备' ;;
Expand Down Expand Up @@ -655,6 +659,7 @@ zh_result() {
text="${text//p95/P95}"
[[ "$text" == R\ * ]] && text="读 ${text#R }"
text="${text// \/ W / \/ 写 }"
text="${text//(informational; host contention)/(仅供参考;宿主机争用)}"
text="${text//(not scored)/(不计分)}"
text="${text//(buffered; not scored)/(缓存模式,不计分)}"
printf '%s' "$text"
Expand Down Expand Up @@ -1352,6 +1357,24 @@ run_iperf_cmd() {
abs_timeout "$budget" "$@"
}

cpu_steal_pct() {
# Context metric (not scored): steal% = vCPU time the hypervisor gave to
# other guests. Explains depressed CPU scores on contended/burstable hosts.
# Linux-only: /proc/stat has no steal column on macOS/BSD.
local stat="${1:-/proc/stat}" stat2="${2:-${1:-/proc/stat}}"
local s1 t1 s2 t2
read -r s1 t1 <<EOF
$(awk '/^cpu / {total=0; for(i=2;i<=NF;i++) total+=$i; print $9, total; exit}' "$stat" 2>/dev/null)
EOF
[ -n "${s1:-}" ] || return 1
sleep "${3:-1}"
read -r s2 t2 <<EOF
$(awk '/^cpu / {total=0; for(i=2;i<=NF;i++) total+=$i; print $9, total; exit}' "$stat2" 2>/dev/null)
EOF
[ -n "${s2:-}" ] || return 1
awk -v ds="$((s2 - s1))" -v dt="$((t2 - t1))" 'BEGIN { if (dt <= 0) exit 1; printf "%.1f", ds * 100 / dt }'
}

parse_iperf_bps() {
python3 - "$1" "$2" <<'PY'
import json, sys
Expand Down Expand Up @@ -1802,6 +1825,10 @@ if have sysbench; then
add "CPU all threads ($T)" "FAILED; see $LAST_LOG"
fi

if steal_pct="$(cpu_steal_pct /proc/stat /proc/stat 1)"; then
add "CPU steal (1s sample)" "${steal_pct}% (informational; host contention)"
fi

if run_log mem-read sysbench memory --threads="$T" --time="$D" --events=0 --percentile=95 --memory-scope=local --memory-block-size="$MEM_BLOCK" --memory-total-size=100T --memory-oper=read --memory-access-mode=seq run; then
mem_read="$(memspeed "$LAST_LOG")"
if [ -n "$mem_read" ]; then
Expand Down
25 changes: 25 additions & 0 deletions tests/test_abs.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,31 @@ def test_scoring_prefers_metrics_tsv_over_display_text(self) -> None:
)
self.assertTrue(proc.stdout.startswith("FULL 1000"), proc.stdout)

def test_cpu_steal_pct_from_proc_stat(self) -> None:
fn = extract_between(self.source, "cpu_steal_pct() {", "\n}\n\nparse_iperf_bps()") + "\n}\n"
with tempfile.TemporaryDirectory() as td:
root = Path(td)
before = root / "stat1"
after = root / "stat2"
# /proc/stat cpu fields: user nice system idle iowait irq softirq steal ...
before.write_text("cpu 100 0 100 700 0 0 0 0 0 0\n", encoding="utf-8")
after.write_text("cpu 100 0 100 750 0 0 0 50 0 0\n", encoding="utf-8")
proc = subprocess.run(
["bash", "-c", fn + f'\ncpu_steal_pct {before} {after} 0'],
text=True,
capture_output=True,
)
self.assertEqual(proc.stdout.strip(), "50.0")
# macOS-style /proc/stat absence (or no steal column) -> no output, rc != 0
missing = root / "stat3"
missing.write_text("cpu 100 0 100 700 0 0 0\n", encoding="utf-8")
proc = subprocess.run(
["bash", "-c", fn + f'\ncpu_steal_pct {missing} {missing} 0'],
text=True,
capture_output=True,
)
self.assertNotEqual(proc.returncode, 0)

def test_malformed_fio_json_is_failed_not_zero(self) -> None:
metric = extract_between(self.source, "fio_metric() {", "\n}\n\nabs_local_score()") + "\n}\n"
with tempfile.NamedTemporaryFile("w", suffix=".json") as bad:
Expand Down
Loading