Skip to content

Commit ab8faf4

Browse files
committed
feat: implement utilisation monitoring for SLURM and surface in stats command
1 parent d0a6de7 commit ab8faf4

5 files changed

Lines changed: 369 additions & 20 deletions

File tree

src/main/groovy/bpipe/Command.groovy

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import groovy.util.logging.Log;
3030
import java.nio.file.Path
3131

3232
import bpipe.executor.CommandExecutor
33+
import bpipe.executor.CommandUtilisation
3334
import bpipe.executor.ProbeCommandExecutor;;
3435

3536
@Log
@@ -122,6 +123,13 @@ class Command implements Serializable {
122123

123124
transient List<CommandDependency> dependencies
124125

126+
/**
127+
* Actual resource utilisation captured after the command completed,
128+
* populated by executors implementing {@link bpipe.executor.UtilisationCapturingExecutor}.
129+
* Null when the executor does not support utilisation capture.
130+
*/
131+
CommandUtilisation utilisation
132+
125133
@CompileStatic
126134
Map getConfig(List<PipelineFile> inputs) {
127135

src/main/groovy/bpipe/CommandManager.groovy

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ package bpipe
2727
import groovy.transform.CompileStatic
2828
import groovy.util.logging.Log
2929
import bpipe.executor.CommandExecutor
30+
import bpipe.executor.CommandUtilisation
3031
import bpipe.executor.CustomCommandExecutor
3132
import bpipe.executor.LocalCommandExecutor;
3233
import bpipe.executor.ThrottledDelegatingCommandExecutor;
34+
import bpipe.executor.UtilisationCapturingExecutor;
3335

3436
/**
3537
* Manages execution, persistence and stopping of commands executed
@@ -299,7 +301,26 @@ class CommandManager {
299301

300302
if(e instanceof ThrottledDelegatingCommandExecutor)
301303
e = e.commandExecutor
302-
304+
305+
// Capture utilisation before cleanup, which may delete accounting data
306+
if(e instanceof UtilisationCapturingExecutor) {
307+
def utilisationConfig = Config.userConfig.utilisation
308+
if(utilisationConfig?.enabled != false) {
309+
try {
310+
cmd.utilisation = ((UtilisationCapturingExecutor)e).captureUtilisation()
311+
if(cmd.utilisation != null) {
312+
log.info "Captured utilisation for command ${cmd.id}: cores=${cmd.utilisation.coresUsed}, rss=${cmd.utilisation.maxRssBytes}, state=${cmd.utilisation.state}"
313+
}
314+
else {
315+
log.info "No utilisation data available for command ${cmd.id}"
316+
}
317+
}
318+
catch(Exception ex) {
319+
log.warning("Failed to capture utilisation for command ${cmd.id}: ${ex.message}")
320+
}
321+
}
322+
}
323+
303324
e.cleanup()
304325

305326
if(!commandIds.containsKey(e))

src/main/groovy/bpipe/Pipeline.groovy

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,27 @@ public class Pipeline implements ResourceRequestor {
11021102
procs cmd.rawProcessedConfig?.procs
11031103
memory cmd.rawProcessedConfig?.memory
11041104
}
1105+
if(cmd.utilisation != null) {
1106+
def u = cmd.utilisation
1107+
Map utilAttrs = [:]
1108+
if(u.source) utilAttrs.source = u.source
1109+
if(u.capturedAtMs) utilAttrs.capturedAtMs = u.capturedAtMs
1110+
utilisation(utilAttrs) {
1111+
if(u.elapsedSeconds != null) elapsedSeconds(u.elapsedSeconds)
1112+
if(u.cpuSecondsTotal != null) cpuSecondsTotal(u.cpuSecondsTotal)
1113+
if(u.coresUsed != null) coresUsed(u.coresUsed)
1114+
if(u.maxRssBytes != null) maxRssBytes(u.maxRssBytes)
1115+
if(u.maxVmemBytes != null) maxVmemBytes(u.maxVmemBytes)
1116+
if(u.state) state(u.state)
1117+
if(u.extras) {
1118+
extras {
1119+
u.extras.each { k, v ->
1120+
extra(key: k, value: v)
1121+
}
1122+
}
1123+
}
1124+
}
1125+
}
11051126
if(cmd.inputs) {
11061127
inputs {
11071128
cmd.inputs.each { PipelineFile pf ->

src/main/groovy/bpipe/cmd/StatsCommand.groovy

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,41 +142,55 @@ class StatsCommand extends BpipeCommand {
142142
List<List> stats = doms.collect { dom ->dom.commands.command }.sum().groupBy { cmdNode ->
143143
cmdNode.stage.text()
144144
}.collect { stage, cmds ->
145-
145+
146146
// Failed commands do not accurately reflect the time taken, so
147147
// only count commands that succeeded
148148
List succeeded = cmds.grep { cmd ->
149149
cmd.exitCode.text() == "0"
150150
}
151-
151+
152152
List valid = succeeded.grep { cmd ->
153153
// Bug where start times not initialised can have caused historical entries to have this
154154
!cmd.start.text().startsWith('1970')
155155
}
156-
156+
157157
if(valid.isEmpty()) {
158158
return null
159159
}
160160

161-
List<Long> times = valid.collect { cmd ->
162-
long startTimeMs = toDate(cmd.start).time
161+
List<Long> times = valid.collect { cmd ->
162+
long startTimeMs = toDate(cmd.start).time
163163
startTimeMs == 0 ? 0 : toDate(cmd.end).time - startTimeMs
164164
}
165-
165+
166166
List<Long> startTimes = valid.collect { cmd -> def t = toDate(cmd.start).time; return t; }
167167
long minStart = startTimes.min()
168168
long minStartRel = minStart != null ? minStart - pipelineStartTimeMs : -1
169-
long maxEnd = valid.collect { cmd ->
169+
long maxEnd = valid.collect { cmd ->
170170
long startTimeMs = toDate(cmd.start).time;
171171
return startTimeMs == 0 ? 0 : toDate(cmd.end).time
172-
}.max() - pipelineStartTimeMs
173-
172+
}.max() - pipelineStartTimeMs
173+
174174
double mean = times.isEmpty() ? 0 : times.sum() / times.size()
175175

176176
String timing = (minStartRel < 0) ? '' : formatTimingBar(minStartRel, maxEnd, pipelineTotalMs, 60)
177177

178178
def cores = valid.collect { cmd -> cmd.resources?.procs }.find { it }?: "-"
179179

180+
// Utilisation: mean cores used across instances
181+
List<Double> coresUsedVals = valid.collect { cmd ->
182+
String cu = cmd.utilisation?.coresUsed?.text()
183+
(cu && cu.isDouble()) ? cu.toDouble() : null
184+
}.findAll { it != null }
185+
String used = coresUsedVals ? String.format('%.1f', coresUsedVals.sum() / coresUsedVals.size()) : '-'
186+
187+
// Peak memory: max across instances
188+
List<Long> rssVals = valid.collect { cmd ->
189+
String rss = cmd.utilisation?.maxRssBytes?.text()
190+
(rss && rss.isLong()) ? rss.toLong() : null
191+
}.findAll { it != null }
192+
String peakMem = rssVals ? Utils.humanBytes(rssVals.max()) : '-'
193+
180194
List<Long> instanceBytes = valid.collect { sumInputBytes(it) }.findAll { it != null }
181195
String inputs = instanceBytes ? Utils.humanBytes((Long)instanceBytes.sum()) : '-'
182196

@@ -186,14 +200,16 @@ class StatsCommand extends BpipeCommand {
186200
formatTimeSpan(times.min()?.toLong()),
187201
formatTimeSpan(mean), formatTimeSpan(times.max()),
188202
cores,
203+
used,
204+
peakMem,
189205
((times.sum()?:0) / 1000.0).toLong(),
190206
inputs,
191207
timing
192208
]
193209
}
194210
.grep { it != null }
195211

196-
Utils.table(["Stage","Count","Min","Mean","Max", "Cores","Weight","Inputs","Timing"], stats, indent:1)
212+
Utils.table(["Stage","Count","Min","Mean","Max", "Cores","Used","Peak Mem","Weight","Inputs","Timing"], stats, indent:1)
197213

198214
out.println ""
199215
}
@@ -287,6 +303,13 @@ class StatsCommand extends BpipeCommand {
287303

288304
String branch = cmd.branch.text() ?: '-'
289305
String cores = cmd.resources?.procs?.text() ?: '-'
306+
307+
// Utilisation columns
308+
String cuText = cmd.utilisation?.coresUsed?.text()
309+
String used = (cuText && cuText.isDouble()) ? String.format('%.1f', cuText.toDouble()) : '-'
310+
String rssText = cmd.utilisation?.maxRssBytes?.text()
311+
String peakMem = (rssText && rssText.isLong()) ? Utils.humanBytes(rssText.toLong()) : '-'
312+
290313
String exit = inProgress ? '' : exitText
291314
String duration = inProgress ? (formatTimeSpan(durMs) + '+') : formatTimeSpan(durMs)
292315
Long instanceBytes = sumInputBytes(cmd)
@@ -299,13 +322,13 @@ class StatsCommand extends BpipeCommand {
299322

300323
String timing = formatTimingBar(startRel, endRel, pipelineTotalMs, 60)
301324

302-
return [branch, formatTimeSpan(startRel), duration, cores, inputs, exit, timing, commandPreview]
325+
return [branch, formatTimeSpan(startRel), duration, cores, used, peakMem, inputs, exit, timing, commandPreview]
303326
}
304327

305328
out.println ""
306329
out.println " Stage: ${stageName}${matching.size()} instance${matching.size() == 1 ? '' : 's'}"
307330
out.println ""
308331

309-
Utils.table(["Branch","Start","Duration","Cores","Inputs","Exit","Timing","Command"], rows, indent:1)
332+
Utils.table(["Branch","Start","Duration","Cores","Used","Peak Mem","Inputs","Exit","Timing","Command"], rows, indent:1)
310333
}
311334
}

0 commit comments

Comments
 (0)