Files
pikasTech-unidesk/scripts/src/gc-remote-growth.py
T
2026-07-05 04:18:22 +00:00

293 lines
15 KiB
Python

def registry_growth_snapshot():
summary = {
"path": REGISTRY_ROOT,
"sizeBytes": du_size(REGISTRY_ROOT, 60) or 0,
}
summary["sizeHuman"] = fmt_bytes(summary["sizeBytes"])
if OPTIONS.get("hwlabRegistry", False):
plan = plan_registry_retention()
retention = dict(plan.get("summary") or {})
for key in ["registrySizeBytes", "estimatedReclaimBytes"]:
if key in retention:
retention[key.replace("Bytes", "Human")] = fmt_bytes(retention.get(key) or 0)
summary["retentionPlan"] = retention
else:
summary["retentionPlan"] = {
"skipped": True,
"reason": "rerun snapshot with --include-hwlab-registry to compute tag/revision retention counters",
}
summary["cadence"] = {
"dryRun": "daily or before/after every v0.2 CI/CD burst",
"maintenanceRun": "weekly, or when root >=80%, or when registry growth exceeds the agreed daily threshold",
"planCommand": "bun scripts/cli.ts gc remote %s plan --target-use-percent 70 --include-hwlab-registry --limit 50" % PROVIDER_ID,
"snapshotCommand": "bun scripts/cli.ts gc remote %s snapshot --include-hwlab-registry --history-limit 12" % PROVIDER_ID,
"runCommand": "bun scripts/cli.ts gc remote %s run --confirm --include-hwlab-registry --target-use-percent 70 --limit 50" % PROVIDER_ID,
"defaultRetention": {
"keepPerRepo": int(OPTIONS.get("registryKeepPerRepo") or 20),
"minAgeHours": float(OPTIONS.get("registryMinAgeHours") or 48),
"protects": ["current workload refs", "digest closure", "protected tags", "recent tags", "newest N tags per repo"],
},
}
return summary
def growth_watermark_policy(root_disk):
use_percent = root_disk.get("usePercent") if isinstance(root_disk, dict) else None
if use_percent is None:
state = "unknown"
action = "collect-snapshot"
elif use_percent < 75:
state = "healthy"
action = "observe-trend"
elif use_percent < 80:
state = "watch"
action = "run-dry-run-plan"
elif use_percent < 85:
state = "maintenance"
action = "schedule-owner-aware-retention"
else:
state = "emergency"
action = "restore-runtime-then-file-evidence"
return {
"state": state,
"recommendedAction": action,
"watermarks": [
{"range": "<75%", "action": "trend only"},
{"range": "75%-80%", "action": "run dry-run plan and identify source"},
{"range": "80%-85%", "action": "small owner-aware retention run"},
{"range": ">=85%", "action": "runtime recovery first, then root-cause growth source"},
],
"growthThresholdPolicy": "If bytes/day remains high for consecutive snapshots, act before 80%; exact threshold should be set from the first week of saved snapshots.",
}
def snapshot_metric_map(snapshot):
metrics = {}
root = snapshot.get("rootDisk") or {}
if isinstance(root, dict) and root.get("usedBytes") is not None:
metrics["root.usedBytes"] = {"value": safe_int(root.get("usedBytes")), "unit": "bytes", "label": "root used bytes"}
for item in snapshot.get("sources") or []:
if not isinstance(item, dict) or item.get("sizeBytes") is None:
continue
key = "source.%s.sizeBytes" % item.get("id")
metrics[key] = {"value": safe_int(item.get("sizeBytes")), "unit": "bytes", "label": item.get("label") or item.get("id")}
storage = ((snapshot.get("ciStorage") or {}).get("byOwnerGroup") or {})
if not storage:
storage = ((snapshot.get("pvcAttribution") or {}).get("byOwnerGroup") or {})
for owner, value in storage.items():
metrics["ciStorage.%s.estimatedBytes" % owner] = {"value": safe_int((value or {}).get("estimatedBytes")), "unit": "bytes", "label": "CI storage %s" % owner}
memory = snapshot.get("memoryPressure") or {}
memory_summary = memory.get("summary") or {}
if memory_summary.get("matchedRssBytes") is not None:
metrics["memoryPressure.matchedRssBytes"] = {"value": safe_int(memory_summary.get("matchedRssBytes")), "unit": "bytes", "label": "matched observer/chrome RSS"}
if memory_summary.get("observeStateBytes") is not None:
metrics["memoryPressure.observeStateBytes"] = {"value": safe_int(memory_summary.get("observeStateBytes")), "unit": "bytes", "label": "web observe state bytes"}
for key in ["matchedProcessCount", "activeObserverSignals", "staleObserverSignals"]:
if memory_summary.get(key) is not None:
metrics["memoryPressure.%s" % key] = {"value": safe_int(memory_summary.get(key)), "unit": "count", "label": "memory pressure %s" % key}
registry = snapshot.get("registry") or {}
retention = registry.get("retentionPlan") or {}
for key in ["totalTags", "totalRevisions", "deleteTags", "deleteRevisions", "estimatedReclaimBytes"]:
if key in retention and retention.get(key) is not None:
unit = "bytes" if key.endswith("Bytes") else "count"
metrics["registry.%s" % key] = {"value": safe_int(retention.get(key)), "unit": unit, "label": "registry %s" % key}
return metrics
def delta_metric_rows(before, after):
before_metrics = snapshot_metric_map(before)
after_metrics = snapshot_metric_map(after)
before_ts = iso_to_epoch(before.get("observedAt"))
after_ts = iso_to_epoch(after.get("observedAt"))
seconds = (after_ts - before_ts) if before_ts is not None and after_ts is not None else None
rows = []
for key in sorted(set(before_metrics.keys()) | set(after_metrics.keys())):
old = before_metrics.get(key, {"value": 0, "unit": (after_metrics.get(key) or {}).get("unit"), "label": key})
new = after_metrics.get(key, {"value": 0, "unit": old.get("unit"), "label": old.get("label")})
delta = safe_int(new.get("value")) - safe_int(old.get("value"))
row = {
"key": key,
"label": new.get("label") or old.get("label") or key,
"unit": new.get("unit") or old.get("unit"),
"before": old.get("value"),
"after": new.get("value"),
"delta": delta,
}
if row["unit"] == "bytes":
row["beforeHuman"] = fmt_bytes(row["before"] or 0)
row["afterHuman"] = fmt_bytes(row["after"] or 0)
row["deltaHuman"] = ("-" if delta < 0 else "") + fmt_bytes(abs(delta))
if seconds and seconds > 0:
per_day = int(delta * 86400 / seconds)
row["perDayBytes"] = per_day
row["perDayHuman"] = ("-" if per_day < 0 else "") + fmt_bytes(abs(per_day)) + "/day"
rows.append(row)
rows.sort(key=lambda item: safe_int(item.get("delta")), reverse=True)
return {"durationSeconds": seconds, "metrics": rows}
def growth_trend_payload(points):
points = [point for point in points if isinstance(point, dict)]
if len(points) < 2:
return {
"pointCount": len(points),
"state": "insufficient-history",
"message": "Run snapshot at least twice to compute deltas.",
}
latest_delta = delta_metric_rows(points[-2], points[-1])
window_delta = delta_metric_rows(points[0], points[-1])
def rate_warning(delta):
seconds = delta.get("durationSeconds")
if seconds is not None and seconds < 3600:
return {
"code": "short-window-rate-noisy",
"message": "Per-day rates from windows shorter than 1 hour are directional only; use daily snapshots for governance decisions.",
"durationSeconds": seconds,
}
return None
return {
"pointCount": len(points),
"oldestAt": points[0].get("observedAt"),
"latestAt": points[-1].get("observedAt"),
"latestDelta": {
"durationSeconds": latest_delta.get("durationSeconds"),
"rateWarning": rate_warning(latest_delta),
"topGrowingBytes": [row for row in latest_delta.get("metrics", []) if row.get("unit") == "bytes" and safe_int(row.get("delta")) > 0][:10],
"topShrinkingBytes": [row for row in reversed(latest_delta.get("metrics", [])) if row.get("unit") == "bytes" and safe_int(row.get("delta")) < 0][:10],
"registryCounters": [row for row in latest_delta.get("metrics", []) if str(row.get("key", "")).startswith("registry.") and row.get("unit") == "count"],
},
"windowDelta": {
"durationSeconds": window_delta.get("durationSeconds"),
"rateWarning": rate_warning(window_delta),
"topGrowingBytes": [row for row in window_delta.get("metrics", []) if row.get("unit") == "bytes" and safe_int(row.get("delta")) > 0][:10],
"topShrinkingBytes": [row for row in reversed(window_delta.get("metrics", [])) if row.get("unit") == "bytes" and safe_int(row.get("delta")) < 0][:10],
"registryCounters": [row for row in window_delta.get("metrics", []) if str(row.get("key", "")).startswith("registry.") and row.get("unit") == "count"],
},
}
def compact_metric_rows(rows, limit=3):
compact = []
for row in (rows or [])[:limit]:
compact.append({
"key": row.get("key"),
"label": row.get("label"),
"unit": row.get("unit"),
"delta": row.get("delta"),
"deltaHuman": row.get("deltaHuman"),
"perDayHuman": row.get("perDayHuman"),
})
return compact
def compact_trend_payload(payload):
if payload.get("state") == "insufficient-history":
return payload
latest = payload.get("latestDelta") or {}
window = payload.get("windowDelta") or {}
return {
"pointCount": payload.get("pointCount"),
"oldestAt": payload.get("oldestAt"),
"latestAt": payload.get("latestAt"),
"latestDelta": {
"durationSeconds": latest.get("durationSeconds"),
"rateWarning": latest.get("rateWarning"),
"topGrowingBytes": compact_metric_rows(latest.get("topGrowingBytes") or [], 1),
"topShrinkingBytes": compact_metric_rows(latest.get("topShrinkingBytes") or [], 1),
"registryCounters": compact_metric_rows(latest.get("registryCounters") or [], 1),
},
"windowDelta": {
"durationSeconds": window.get("durationSeconds"),
"rateWarning": window.get("rateWarning"),
"topGrowingBytes": compact_metric_rows(window.get("topGrowingBytes") or [], 1),
"topShrinkingBytes": compact_metric_rows(window.get("topShrinkingBytes") or [], 1),
"registryCounters": compact_metric_rows(window.get("registryCounters") or [], 1),
},
"fullDisclosure": "rerun trend --full for all metric rows",
}
def compact_growth_point(item):
registry = item.get("registry") or {}
retention = registry.get("retentionPlan") or {}
ci_storage = item.get("ciStorage") or {}
containerd = item.get("containerd") or {}
memory = item.get("memoryPressure") or {}
memory_summary = memory.get("summary") or {}
observe = (memory.get("webObserve") or {})
return {
"observedAt": item.get("observedAt"),
"rootDisk": item.get("rootDisk"),
"sourceCount": len(item.get("sources") or []),
"registry": {
"sizeBytes": registry.get("sizeBytes"),
"sizeHuman": registry.get("sizeHuman"),
"totalTags": retention.get("totalTags"),
"totalRevisions": retention.get("totalRevisions"),
"deleteTags": retention.get("deleteTags"),
"deleteRevisions": retention.get("deleteRevisions"),
"estimatedReclaimBytes": retention.get("estimatedReclaimBytes"),
"estimatedReclaimHuman": retention.get("estimatedReclaimHuman"),
},
"ciStorage": {
"pvcCount": ci_storage.get("pvcCount"),
"estimatedBytes": ci_storage.get("estimatedBytes"),
"estimatedHuman": ci_storage.get("estimatedHuman"),
"byOwnerGroup": ci_storage.get("byOwnerGroup"),
},
"containerd": {
"state": containerd.get("state"),
"cleanupSupported": containerd.get("cleanupSupported"),
},
"memoryPressure": {
"matchedProcessCount": memory_summary.get("matchedProcessCount"),
"matchedRssBytes": memory_summary.get("matchedRssBytes"),
"matchedRssHuman": memory_summary.get("matchedRssHuman"),
"activeObserverSignals": memory_summary.get("activeObserverSignals"),
"staleObserverSignals": memory_summary.get("staleObserverSignals"),
"observeStateBytes": memory_summary.get("observeStateBytes"),
"observeStateHuman": memory_summary.get("observeStateHuman"),
"webObserveRootCount": observe.get("rootCount"),
},
}
def collect_growth_snapshot(observed_at, preflight):
root_disk = df_snapshot()
sources = disk_source_snapshot()
ci_storage = ci_storage_snapshot()
memory_pressure = collect_memory_pressure()
compact_pvc = compact_pvc_attribution(ci_storage)
if bool(OPTIONS.get("full")):
public_pvc = ci_storage
public_memory = memory_pressure
else:
public_pvc = compact_ci_storage_summary(ci_storage)
public_memory = compact_memory_summary(memory_pressure)
registry = registry_growth_snapshot()
containerd = containerd_breakdown_snapshot()
commands = {
"snapshot": "bun scripts/cli.ts gc remote %s snapshot --include-hwlab-registry --history-limit %s" % (PROVIDER_ID, int(OPTIONS.get("historyLimit") or 12)),
"trend": "bun scripts/cli.ts gc remote %s trend --history-limit %s" % (PROVIDER_ID, int(OPTIONS.get("historyLimit") or 12)),
"registryPlan": "bun scripts/cli.ts gc remote %s plan --target-use-percent 70 --include-hwlab-registry --limit 50" % PROVIDER_ID,
"hwlabCiRetention": ((ci_storage.get("handoff") or {}).get("hwlab") or {}).get("dryRun"),
"agentrunRetention": ((ci_storage.get("handoff") or {}).get("agentrun") or {}).get("dryRun"),
"remotePolicy": "bun scripts/cli.ts gc remote %s policy plan" % PROVIDER_ID,
}
if not bool(OPTIONS.get("full")):
commands = {
"trend": "bun scripts/cli.ts gc remote %s trend --history-limit %s" % (PROVIDER_ID, int(OPTIONS.get("historyLimit") or 12)),
"status": "bun scripts/cli.ts gc remote %s status --limit %s" % (PROVIDER_ID, int(OPTIONS.get("limit") or 50)),
"full": "bun scripts/cli.ts gc remote %s snapshot --full --no-save" % PROVIDER_ID,
}
return {
"ok": True,
"action": "gc remote snapshot",
"providerId": PROVIDER_ID,
"dryRun": True,
"mutation": False,
"diagnosticStateMutation": bool(OPTIONS.get("saveSnapshot", True)),
"observedAt": observed_at,
"rootDisk": root_disk,
"clusterPreflight": preflight,
"sources": sources,
"registry": registry,
"pvcAttribution": public_pvc,
"memoryPressure": public_memory,
"containerd": containerd,
"policy": growth_watermark_policy(root_disk or {}),
"commands": commands,
}