diff --git a/docs/reference/gc.md b/docs/reference/gc.md index de24fbda..d20a14bc 100644 --- a/docs/reference/gc.md +++ b/docs/reference/gc.md @@ -83,6 +83,10 @@ NC01 的内存压力 cgroup 回收规则: - owning YAML 声明目标 cgroup、回收量、`minimumInactiveAnonBytes` 和 swap 保留线; - plan 与执行前复核 `inactive_anon` 不低于 YAML 下限; - 最坏情况下的换出量不会突破 YAML 声明的 `minimumSwapFreeBytesAfterReclaim`; +- cgroup `reclaimBytes` 只表示内核请求上界,不是预期收益: + - plan 必须显示 `estimateKind=kernel-request-upper-bound`; + - job 终态必须分别披露 cgroup 实际下降、`MemAvailable` 变化、`targetMet` 和剩余缺口; + - 内核请求成功但目标未达到时,结果必须为 `memory-target-not-met`,不得按请求量宣称已回收; - 按 `plan`、`run --confirm`、`status`、`memory` 的顺序复核实际结果。 默认 `/tmp` GC 只包含 allowlisted 诊断目录和已知低风险路径。非 allowlist 的 stale `/tmp` 一级子项必须显式 `--include-stale-tmp` 才能进入候选;扫描按 `--limit` 有界枚举候选,执行时仍只允许删除 `/tmp` 直接子项,并避开 X11/ICE/font socket、systemd private、tmux、ssh、vscode 等系统/session 前缀。该入口不能递归扩大成通用 `/tmp` 清空器,也不能为了估算全量临时目录而长时间阻塞。 diff --git a/scripts/src/gc-remote-runner.py b/scripts/src/gc-remote-runner.py index 0957edf4..9879ed29 100644 --- a/scripts/src/gc-remote-runner.py +++ b/scripts/src/gc-remote-runner.py @@ -1526,15 +1526,22 @@ def summarize(candidates, returned, disk=None): } def summarize_memory_candidates(candidates, returned): - total_memory = sum(safe_int(item.get("expectedMemoryRssBytes")) for item in candidates) - returned_memory = sum(safe_int(item.get("expectedMemoryRssBytes")) for item in returned) + total_memory = sum(safe_int(item.get("expectedMemoryRssBytes")) for item in candidates if item.get("kind") != "cgroup-memory-reclaim") + returned_memory = sum(safe_int(item.get("expectedMemoryRssBytes")) for item in returned if item.get("kind") != "cgroup-memory-reclaim") + requested_memory = sum(safe_int(item.get("reclaimBytes")) for item in candidates if item.get("kind") == "cgroup-memory-reclaim") + returned_requested_memory = sum(safe_int(item.get("reclaimBytes")) for item in returned if item.get("kind") == "cgroup-memory-reclaim") by_kind = {} for item in candidates: kind = item.get("kind") or "unknown" - bucket = by_kind.setdefault(kind, {"count": 0, "expectedMemoryRssBytes": 0}) + bucket = by_kind.setdefault(kind, {"count": 0}) bucket["count"] += 1 - bucket["expectedMemoryRssBytes"] += safe_int(item.get("expectedMemoryRssBytes")) - bucket["expectedMemoryRss"] = fmt_bytes(bucket["expectedMemoryRssBytes"]) + if kind == "cgroup-memory-reclaim": + bucket["requestedMemoryReclaimBytes"] = safe_int(bucket.get("requestedMemoryReclaimBytes")) + safe_int(item.get("reclaimBytes")) + bucket["requestedMemoryReclaim"] = fmt_bytes(bucket["requestedMemoryReclaimBytes"]) + bucket["estimateKind"] = "kernel-request-upper-bound" + else: + bucket["expectedMemoryRssBytes"] = safe_int(bucket.get("expectedMemoryRssBytes")) + safe_int(item.get("expectedMemoryRssBytes")) + bucket["expectedMemoryRss"] = fmt_bytes(bucket["expectedMemoryRssBytes"]) return { "scope": "memory-pressure-only", "candidateCount": len(candidates), @@ -1543,6 +1550,11 @@ def summarize_memory_candidates(candidates, returned): "expectedMemoryRss": fmt_bytes(total_memory), "returnedExpectedMemoryRssBytes": returned_memory, "returnedExpectedMemoryRss": fmt_bytes(returned_memory), + "requestedMemoryReclaimBytes": requested_memory, + "requestedMemoryReclaim": fmt_bytes(requested_memory), + "returnedRequestedMemoryReclaimBytes": returned_requested_memory, + "returnedRequestedMemoryReclaim": fmt_bytes(returned_requested_memory), + "cgroupEstimateKind": "kernel-request-upper-bound" if requested_memory > 0 else None, "estimatedDiskBytes": 0, "outcome": "memory-reclaim-candidates" if candidates else "no-memory-reclaim", "byKind": by_kind, @@ -2612,6 +2624,15 @@ def finish_memory_pressure_job(job_id, candidates, observed_at, worker_pid, work - safe_int((memory_before.get("memory") or {}).get("availableBytes")) ) if memory_before.get("ok") and memory_after.get("ok") else None, }) + target_available = safe_int((MEMORY_CONFIG.get("cgroupReclaim") or {}).get("targetMemAvailableBytes")) + after_available = safe_int((memory_after.get("memory") or {}).get("availableBytes")) + target_met = target_available > 0 and after_available > target_available + summary.update({ + "targetMemAvailableBytes": target_available, + "targetMet": target_met, + "targetGapBytes": max(0, target_available - after_available + 1) if target_available > 0 else None, + "outcome": "memory-target-met" if target_met else "memory-target-not-met", + }) payload = { "ok": len(failed) == 0, "action": "gc remote run", diff --git a/scripts/src/gc-remote-web-observe.py b/scripts/src/gc-remote-web-observe.py index d6aaa37e..23646366 100644 --- a/scripts/src/gc-remote-web-observe.py +++ b/scripts/src/gc-remote-web-observe.py @@ -455,6 +455,9 @@ def collect_cgroup_reclaim_candidates(): "memoryCurrentBytes": current, "memorySwapCurrentBytes": read_cgroup_integer(path, "memory.swap.current"), "memoryStat": memory_stat, + "requestedMemoryReclaimBytes": reclaim_bytes, + "requestedMemoryReclaim": fmt_bytes(reclaim_bytes), + "estimateKind": "kernel-request-upper-bound", "expectedMemoryRssBytes": reclaim_bytes, "expectedMemoryRss": fmt_bytes(reclaim_bytes), "estimatedDiskBytes": 0, @@ -728,9 +731,16 @@ def execute_cgroup_reclaim_candidate(candidate): cgroup_delta = max(0, cgroup_before - cgroup_after) if cgroup_before is not None and cgroup_after is not None else 0 host_delta = max(0, after_available - before_available) if after_available is not None else 0 succeeded = write_error is None or cgroup_delta > 0 or host_delta > 0 + partial = max(cgroup_delta, host_delta) < safe_int(configured.get("reclaimBytes")) + if write_error and succeeded: + outcome = "partial-reclaim-after-kernel-error" + elif succeeded and partial: + outcome = "partial-reclaim" + else: + outcome = "reclaim-requested" if succeeded else "reclaim-request-failed" return { "status": "succeeded" if succeeded else "failed", - "outcome": "partial-reclaim-after-kernel-error" if write_error and succeeded else ("reclaim-requested" if succeeded else "reclaim-request-failed"), + "outcome": outcome, "reclaimedMemoryBytes": max(cgroup_delta, host_delta), "hostMemAvailableDeltaBytes": (after_available - before_available) if after_available is not None else None, "cgroupMemoryDeltaBytes": cgroup_delta, diff --git a/scripts/src/gc-remote.ts b/scripts/src/gc-remote.ts index 220a6de7..2e155c90 100644 --- a/scripts/src/gc-remote.ts +++ b/scripts/src/gc-remote.ts @@ -400,8 +400,6 @@ function compactMemoryPressureCandidate(value: unknown): Record kind: candidate.kind, reason: candidate.reason, risk: candidate.risk, - expectedMemoryRssBytes: candidate.expectedMemoryRssBytes, - expectedMemoryRss: candidate.expectedMemoryRss, action: { op: action.op, allowlist: action.allowlist }, }; if (candidate.kind === "cgroup-memory-reclaim") { @@ -410,6 +408,9 @@ function compactMemoryPressureCandidate(value: unknown): Record configId: candidate.configId, path: candidate.path, reclaimBytes: candidate.reclaimBytes, + requestedMemoryReclaimBytes: candidate.requestedMemoryReclaimBytes, + requestedMemoryReclaim: candidate.requestedMemoryReclaim, + estimateKind: candidate.estimateKind, swappiness: candidate.swappiness, minimumInactiveAnonBytes: candidate.minimumInactiveAnonBytes, targetMemAvailableBytes: candidate.targetMemAvailableBytes, @@ -428,6 +429,8 @@ function compactMemoryPressureCandidate(value: unknown): Record && positiveInteger(identity.startTicks) !== null); return { ...base, + expectedMemoryRssBytes: candidate.expectedMemoryRssBytes, + expectedMemoryRss: candidate.expectedMemoryRss, rootPid: candidate.rootPid, sid: candidate.sid, rootStartTicks: candidate.rootStartTicks,