fix(controller,apiserver,xcecli): expose job CPU peak and efficiency to the apiserver and xcecli (bug-1736)
The controller computed run usage (CPU time, memory, CPU peak and efficiency, LSF bjobs -l definitions) but the capnp Job struct had no field for it, so the job pushed to the apiserver never carried it, and snapshots (the same struct) dropped it: a job restored from a checkpoint lost its accumulated usage. - controller.capnp: Job.runUsage @13 (JobRunusage); old entries decode with no run usage. - apiserver: map run_usage into JobInfo; max_mem, avg_mem, used_mem, cpu_time, peak_cpu, peak_cpu_time, peak_cpu_duration, mem_efficiency, avg_cpu_efficiency, peak_cpu_efficiency become queryable columns. - xcecli: -o fields MAX_MEM, AVG_MEM, MEM_EFFICIENCY, CPU_PEAK, CPU_PEAK_DURATION, CPU_EFFICIENCY, CPU_PEAK_EFFICIENCY (CPU_TIME now renders too), and a MEMORY USAGE / CPU USAGE block in jobs list -l. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2472aa73c
commit
1cedd7f791
9 changed files with 466 additions and 56 deletions
|
|
@ -103,6 +103,10 @@ func (p *JobsPagePrinter) initColumnWidths(jobs *models.JobsResponse) {
|
|||
width = len(fmt.Sprintf("%f", job.CoreLimit))
|
||||
case models.HEADER_JOB_MEM_LIMIT:
|
||||
width = len(fmt.Sprintf("%f", job.MemLimit))
|
||||
default:
|
||||
if v, ok := jobRunUsageColumn(format.name, job); ok {
|
||||
width = len(v)
|
||||
}
|
||||
}
|
||||
|
||||
if !p.wideFormat && width > 15 {
|
||||
|
|
@ -237,7 +241,11 @@ func (p *JobsPagePrinter) formatRows(jobs *models.JobsResponse, truncatFlag bool
|
|||
case models.HEADER_JOB_MEM_LIMIT:
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, formatSizeString(job.MemLimit)), "-")
|
||||
default:
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*v", width, "-"), "-")
|
||||
v, ok := jobRunUsageColumn(format.name, job)
|
||||
if !ok {
|
||||
v = "-"
|
||||
}
|
||||
mainRow = appendOrDefault(mainRow, fmt.Sprintf("%"+align+"*s", width, v), "-")
|
||||
}
|
||||
}
|
||||
rows = append(rows, mainRow)
|
||||
|
|
@ -409,6 +417,9 @@ func printJobsVerboseOutput(jobs *models.JobsResponse, filter []string) {
|
|||
|
||||
}
|
||||
PrintWrappedLine(w, endLine, 80, 18)
|
||||
for _, l := range jobResourceUsageLines(job) {
|
||||
fmt.Fprintln(w, l)
|
||||
}
|
||||
fmt.Println("")
|
||||
// print the scheduling parameters table
|
||||
fmt.Printf("SCHEDULING PARAMETERS\n")
|
||||
|
|
@ -443,3 +454,58 @@ func printJobsVerboseOutput(jobs *models.JobsResponse, filter []string) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// jobResourceUsageLines renders a started job's memory and CPU usage in the
|
||||
// LSF bjobs -l shape (bug-1736): MAX/AVG MEM in Mbytes and MEM EFFICIENCY
|
||||
// against the rusage[mem] request; CPU PEAK in cores with the interval that
|
||||
// set it; average and peak CPU efficiency against the allocated slots. A job
|
||||
// that has not started (PEND/PSUSP) has nothing to report.
|
||||
func jobResourceUsageLines(job models.QueryJob) []string {
|
||||
if job.JobStatus == "" || job.JobStatus == "PEND" || job.JobStatus == "PSUSP" {
|
||||
return nil
|
||||
}
|
||||
return []string{
|
||||
"",
|
||||
" MEMORY USAGE:",
|
||||
fmt.Sprintf(" MAX MEM: %.0f Mbytes; AVG MEM: %.0f Mbytes; MEM EFFICIENCY: %.2f%%",
|
||||
job.MaxMem, job.AvgMem, job.MemEfficiency),
|
||||
"",
|
||||
" CPU USAGE:",
|
||||
fmt.Sprintf(" CPU PEAK: %.2f ; CPU PEAK DURATION: %.0f second(s)",
|
||||
job.PeakCPU, job.PeakCPUDuration),
|
||||
fmt.Sprintf(" CPU AVERAGE EFFICIENCY: %.2f%% ; CPU PEAK EFFICIENCY: %.2f%%",
|
||||
job.AvgCPUEfficiency, job.PeakCPUEfficiency),
|
||||
}
|
||||
}
|
||||
|
||||
// jobRunUsageColumn formats one run-usage column of `jobs list -o` (bug-1736),
|
||||
// in the units of the -l view: memory in Mbytes, CPU peak in cores, durations
|
||||
// in seconds, efficiencies in percent. ok is false for any other column. A
|
||||
// job that has not started shows "-".
|
||||
func jobRunUsageColumn(header string, job models.QueryJob) (string, bool) {
|
||||
var v string
|
||||
switch header {
|
||||
case models.HEADER_JOB_CPU_TIME:
|
||||
v = fmt.Sprintf("%d", job.CPUTime)
|
||||
case models.HEADER_JOB_MAX_MEM:
|
||||
v = fmt.Sprintf("%.0f", job.MaxMem)
|
||||
case models.HEADER_JOB_AVG_MEM:
|
||||
v = fmt.Sprintf("%.0f", job.AvgMem)
|
||||
case models.HEADER_JOB_MEM_EFFICIENCY:
|
||||
v = fmt.Sprintf("%.2f%%", job.MemEfficiency)
|
||||
case models.HEADER_JOB_CPU_PEAK:
|
||||
v = fmt.Sprintf("%.2f", job.PeakCPU)
|
||||
case models.HEADER_JOB_CPU_PEAK_DURATION:
|
||||
v = fmt.Sprintf("%.0f", job.PeakCPUDuration)
|
||||
case models.HEADER_JOB_CPU_EFFICIENCY:
|
||||
v = fmt.Sprintf("%.2f%%", job.AvgCPUEfficiency)
|
||||
case models.HEADER_JOB_CPU_PEAK_EFFICIENCY:
|
||||
v = fmt.Sprintf("%.2f%%", job.PeakCPUEfficiency)
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
if job.JobStatus == "PEND" || job.JobStatus == "PSUSP" {
|
||||
v = "-"
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
|
|
|||
90
src/frontend/xcecli/common/output/page_print_jobs_test.go
Normal file
90
src/frontend/xcecli/common/output/page_print_jobs_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"xcecli/models"
|
||||
)
|
||||
|
||||
// bug-1736: `xcecli jobs list -l` shows the job's memory and CPU usage in the
|
||||
// LSF bjobs -l shape once the job has started.
|
||||
func TestJobResourceUsageLines_StartedJob(t *testing.T) {
|
||||
job := models.QueryJob{
|
||||
JobStatus: "RUN",
|
||||
MaxMem: 512,
|
||||
AvgMem: 300.4,
|
||||
MemEfficiency: 80.5,
|
||||
PeakCPU: 3.75,
|
||||
PeakCPUDuration: 30,
|
||||
AvgCPUEfficiency: 62.25,
|
||||
PeakCPUEfficiency: 95,
|
||||
}
|
||||
got := strings.Join(jobResourceUsageLines(job), "\n")
|
||||
for _, want := range []string{
|
||||
"MEMORY USAGE:",
|
||||
"MAX MEM: 512 Mbytes; AVG MEM: 300 Mbytes; MEM EFFICIENCY: 80.50%",
|
||||
"CPU USAGE:",
|
||||
"CPU PEAK: 3.75 ; CPU PEAK DURATION: 30 second(s)",
|
||||
"CPU AVERAGE EFFICIENCY: 62.25% ; CPU PEAK EFFICIENCY: 95.00%",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("missing %q in:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A job that has not started has no usage to report.
|
||||
func TestJobResourceUsageLines_PendingJob(t *testing.T) {
|
||||
for _, st := range []string{"PEND", "PSUSP"} {
|
||||
if lines := jobResourceUsageLines(models.QueryJob{JobStatus: st}); len(lines) != 0 {
|
||||
t.Errorf("%s job: expected no usage lines, got %q", st, lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1736: the usage fields can be selected with -o and are requested by -l.
|
||||
func TestJobFieldsMapIncludesRunUsage(t *testing.T) {
|
||||
have := map[string]bool{}
|
||||
for _, f := range models.JobFieldsMap {
|
||||
have[f] = true
|
||||
}
|
||||
for _, f := range []string{"max_mem", "avg_mem", "mem_efficiency",
|
||||
"peak_cpu", "peak_cpu_duration", "avg_cpu_efficiency",
|
||||
"peak_cpu_efficiency"} {
|
||||
if !have[f] {
|
||||
t.Errorf("JobFieldsMap has no field %q", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1736: the run-usage columns render in `jobs list -o` (they printed "-").
|
||||
func TestJobRunUsageColumn(t *testing.T) {
|
||||
job := models.QueryJob{
|
||||
JobStatus: "RUN", CPUTime: 1234, MaxMem: 512, AvgMem: 300.4,
|
||||
MemEfficiency: 80.5, PeakCPU: 3.75, PeakCPUDuration: 30,
|
||||
AvgCPUEfficiency: 62.25, PeakCPUEfficiency: 95,
|
||||
}
|
||||
cases := map[string]string{
|
||||
models.HEADER_JOB_CPU_TIME: "1234",
|
||||
models.HEADER_JOB_MAX_MEM: "512",
|
||||
models.HEADER_JOB_AVG_MEM: "300",
|
||||
models.HEADER_JOB_MEM_EFFICIENCY: "80.50%",
|
||||
models.HEADER_JOB_CPU_PEAK: "3.75",
|
||||
models.HEADER_JOB_CPU_PEAK_DURATION: "30",
|
||||
models.HEADER_JOB_CPU_EFFICIENCY: "62.25%",
|
||||
models.HEADER_JOB_CPU_PEAK_EFFICIENCY: "95.00%",
|
||||
}
|
||||
for h, want := range cases {
|
||||
got, ok := jobRunUsageColumn(h, job)
|
||||
if !ok || got != want {
|
||||
t.Errorf("%s: got %q (ok=%v), want %q", h, got, ok, want)
|
||||
}
|
||||
}
|
||||
if _, ok := jobRunUsageColumn(models.HEADER_JOB_USER, job); ok {
|
||||
t.Errorf("USER is not a run-usage column")
|
||||
}
|
||||
// a job that has not started shows "-", not zeros
|
||||
if got, _ := jobRunUsageColumn(models.HEADER_JOB_CPU_PEAK, models.QueryJob{JobStatus: "PEND"}); got != "-" {
|
||||
t.Errorf("PEND job CPU_PEAK: got %q, want -", got)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue