Skip to content

Commit d0e33c8

Browse files
authored
Merge pull request #383 from JosuaCarl/fix-session-memory
Fix floating point error and refactor Bytes for value types
2 parents dacf37b + f995776 commit d0e33c8

27 files changed

Lines changed: 205 additions & 132 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
## Bug Fixes:
33
- CPU power model now applied correctly
44
- Unintended report value removal
5+
- Fixed floating point error for numerical conversion division
56

67
## Misc:
78
- Improved workflow reporting form extension/CLI by deriving and injecting workflow metadata from the provided trace file
89
- Added full integration test
910
- Adapted to Nextflow 26
11+
- Reworked `Bytes`, such that it can take binary and decimal-based values
1012

1113
## Features:
1214
- Transformation of data file to provenance file with schema.org / bioschemas.org type annotation in JSON-LD data format

docs/usage/FAQ.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@
3131
- Set the TDP via `powerdrawCpuDefault = <TDP per core>` and then ignore the warning with `ignoreCpuModel = true`.
3232

3333
For more information see our documentation on [power draw parameters](./parameters.md#hardware-power-draw). You can additionally
34-
report the model with the ["Missing chip" GitHub issue](https://github.com/nextflow-io/nf-co2footprint/issues/new?template=missing_chip.yaml).
34+
report the model with the ["Missing chip" GitHub issue](https://github.com/nextflow-io/nf-co2footprint/issues/new?template=missing_chip.yaml).
35+
36+
37+
<a id="differing-memory"></a>
38+
??? faq "Differing memory values"
39+
40+
The conversion of `Byte`-type units, such as `memory` is done in accordance with [SI standard, decimal-based naming](https://en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes), rather than [Nextflows binary-based system](https://en.wikipedia.org/wiki/Byte#Multiple-byte_units).
41+
In essence, we use: 1 GB = 1 000 000 000 bytes (1 * 1000^3), while Nextflow uses: 1 GB = 1 073 741 824 bytes (1 * 1024^3), which should be 1 GiB (Gibibyte).
42+
43+
Example: <br>
44+
&emsp; -> You request `process.memory = 12 GB` <br>
45+
&emsp; -> Nextflow converts it to 12884901888 bytes and passes the restriction onto the executor <br>
46+
&emsp;&emsp; => 12.88 GB = 12 GiB are allocated for the process <br>
47+
&emsp; -> The plugin uses the the former to proceed with calculations and reporting, while Nextflow (incorrectly) reports using the latter <br>

src/main/nextflow/co2footprint/CO2FootprintCalculator.groovy

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ class CO2FootprintCalculator {
8585

8686
/* ===== Memory Information ===== */
8787

88-
final Long requestedMemory = trace.get('memory') as Long // [bytes]
89-
final Long maxRequiredMemory = trace.get('peak_rss') as Long // [bytes]
88+
final BigInteger requestedMemory = trace.get('memory') as BigInteger // [bytes]
89+
final BigInteger maxRequiredMemory = trace.get('peak_rss') as BigInteger // [bytes]
9090

9191
// Assign the final memory value
9292
final BigDecimal memory
@@ -120,7 +120,7 @@ class CO2FootprintCalculator {
120120
final BigDecimal ci = timeCiRecords.getCi(trace)
121121

122122
// Personal energy mix based carbon intensity
123-
final Double ciMarket = config.ciMarket
123+
final BigDecimal ciMarket = config.ciMarket
124124

125125

126126
/* ===== Energy & Emission Calculation ===== */
@@ -147,9 +147,9 @@ class CO2FootprintCalculator {
147147
co2eMarket,
148148
ci,
149149
cpuUsage,
150-
memory as Long,
150+
memory,
151151
runtime_h,
152-
numberOfCores as Integer,
152+
numberOfCores,
153153
powerdrawPerCore,
154154
config.ignoreCpuModel ? 'Custom value' : cpuModel,
155155
rawEnergyProcessor,

src/main/nextflow/co2footprint/Metrics/Bytes.groovy

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
package nextflow.co2footprint.Metrics
22

3+
/**
4+
* Class to handle Byte metrics
5+
*/
36
class Bytes extends Quantity {
47
/**
58
* Creator of Bytes, a Quantity which represents a number in bytes
69
*
710
* @param value The numeric value.
8-
* @param scale The unit scale (e.g. '', 'K', 'M'). Defaults to ''.
11+
* @param scale The unit scale (e.g. '', 'K', 'M' or 'Ki, 'Gi',...). Defaults to ''.
912
* @param type The datatype label. Defaults to 'Bytes'.
1013
* @param description Optional human-readable description of the value.
1114
*/
1215
Bytes(Object value, String scale='', String type='Bytes', String description = null) {
1316
super(value, scale, 'B', type, description)
14-
scalingFactor = 1024
17+
scalingLists.add(['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi', 'Ri', 'Qi'])
18+
scalingFactors.add(1024)
1519
}
1620

1721
/**
1822
* Creates a {@link Metric} or a {@link Bytes}, if the value is numerical.
1923
*
2024
* @param value The raw value.
21-
* @param scale The unit scale (e.g. '', 'K', 'M'). Defaults to ''.
25+
* @param scale The unit scale (e.g. '', 'K', 'M' or 'Ki, 'Gi',...). Defaults to ''.
2226
* @param type The type label for the metric. Defaults to 'Bytes'.
2327
* @param description Optional human-readable description of the metric.
2428
* @return A Bytes or Metric object depending on the input value.
@@ -31,5 +35,4 @@ class Bytes extends Quantity {
3135
return new Metric(value, type, "${scale}B", description)
3236
}
3337
}
34-
3538
}

src/main/nextflow/co2footprint/Metrics/Quantity.groovy

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ class Quantity extends Metric<BigDecimal> {
1212
String scale
1313
boolean integerType
1414
String separator = ' '
15-
int scalingFactor = 1000
15+
16+
// Units: pico, nano, micro, milli, 0, Kilo, Mega, Giga, Tera, Peta, Exa (decimal 1000 scale)
17+
List<List<String>> scalingLists = [['p', 'n', 'u', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E']]
18+
List<Integer> scalingFactors = [1000]
1619

1720
/**
1821
* Creator of a Quantity, combining the tracking and reporting of a number, associated with a unit.
@@ -83,15 +86,16 @@ class Quantity extends Metric<BigDecimal> {
8386
* Converts a numeric value to the closest or given scale with SI prefixes.
8487
* Scales the value up or down by a given factor and adjusts the scale prefix accordingly.
8588
*
89+
* @param value The value that is to be converted
90+
* @param currentScale The current scale
91+
* @param scales The scales that are available
92+
* @param scalingFactor The scaling factor between the units
8693
* @param targetScale The scale that should be converted to (e.g. G), default of null (optional)
87-
* @return Converted quantity with appropriate scale
94+
* @return Converted value
8895
*/
89-
Quantity scale(String targetScale=null) {
96+
static ScaledValue applyScale(BigDecimal value, String currentScale, List<String> scales, int scalingFactor, String targetScale=null) {
9097
if (value) {
91-
92-
final List<String> scales = ['p', 'n', 'u', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E']
93-
// Units: pico, nano, micro, milli, 0, Kilo, Mega, Giga, Tera, Peta, Exa
94-
int scaleIndex = getIdx(scale, scales)
98+
int scaleIndex = getIdx(currentScale, scales)
9599

96100
int targetScaleIndex
97101
int difference
@@ -108,11 +112,45 @@ class Quantity extends Metric<BigDecimal> {
108112
))
109113
targetScaleIndex = scaleIndex + difference
110114
}
111-
value = (value / scalingFactor**difference) as BigDecimal
115+
value = value.divide(scalingFactor**difference)
112116
targetScale = scales[targetScaleIndex]
113117
}
114118

115-
scale = targetScale
119+
return new ScaledValue(value, targetScale)
120+
}
121+
122+
/**
123+
* Converts a numeric value to the closest or given scale with SI prefixes.
124+
* Scales the value up or down by a given factor and adjusts the scale prefix accordingly.
125+
*
126+
* @param targetScale The scale that should be converted to (e.g. G), default of null (optional)
127+
* @return Converted quantity with appropriate scale
128+
*/
129+
Quantity scale(String targetScale=null) {
130+
// Search for scales in the available lists
131+
int scalePos = scalingLists.findIndexOf { List<String> scalingList -> scalingList.contains(scale) }
132+
int targetScalePos = scalingLists.findIndexOf { List<String> scalingList -> scalingList.contains(targetScale) }
133+
134+
// Define current scaling list and factor
135+
List<String> scalingList = scalingLists[scalePos]
136+
Integer scalingFactor = scalingFactors[scalePos]
137+
138+
ScaledValue scaledValue
139+
if(targetScale == null || scalePos == targetScalePos) {
140+
scaledValue = applyScale(value, scale, scalingList, scalingFactor, targetScale)
141+
}
142+
else {
143+
// Define target scaling list and factor
144+
List<String> targetScalingList = scalingLists[targetScalePos]
145+
Integer targetScalingFactor = scalingFactors[targetScalePos]
146+
147+
// Convert to base value and then to target scale
148+
scaledValue = applyScale(value, scale, scalingList, scalingFactor, '')
149+
scaledValue = applyScale(scaledValue.value, scaledValue.scale, targetScalingList, targetScalingFactor, targetScale)
150+
}
151+
152+
value = scaledValue.value
153+
scale = scaledValue.scale
116154
return this
117155
}
118156

@@ -184,4 +222,17 @@ class Quantity extends Metric<BigDecimal> {
184222
map['value'] = returnValue()
185223
return map
186224
}
225+
226+
/**
227+
* Container class for a scaled value
228+
*/
229+
static class ScaledValue {
230+
BigDecimal value
231+
String scale
232+
233+
ScaledValue(BigDecimal value, String scale) {
234+
this.value = value
235+
this.scale = scale
236+
}
237+
}
187238
}

src/main/nextflow/co2footprint/Parsers/TraceFileParser.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class TraceFileParser {
4343
value = switch (TraceRecord.FIELDS.get(key)) {
4444
case 'mem' -> if (value.endsWith('B')) {
4545
List<String> split = value.split(' ')
46-
Metric<BigDecimal> bytes = Bytes.of(split[0].toDouble(), split[1].dropRight(1)).scale('')
46+
Metric<BigDecimal> bytes = Bytes.of(split[0] as BigDecimal, split[1].dropRight(1)).scale('')
4747
bytes.value.toLong()
4848
} else {
4949
value.toLong()

src/main/nextflow/co2footprint/Recorders/SessionTraceRecorder.groovy

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,14 @@ class SessionTraceRecorder {
131131
)
132132

133133
if (samples) {
134+
List<Long> rss = samples.collect({ MemorySample sample -> sample.rssBytes})
135+
List<Long> vmem = samples.collect({ MemorySample sample -> sample.virtualMemoryBytes})
134136
sessionRecord.putAll(
135137
[
136-
rss: samples.collect({ MemorySample sample -> sample.rssBytes}).average() as Long,
137-
vmem: samples.collect({ MemorySample sample -> sample.virtualMemoryBytes}).average() as Long,
138-
peak_rss: samples.collect({ MemorySample sample -> sample.rssBytes}).max() as Long,
139-
peak_vmem: samples.collect({ MemorySample sample -> sample.virtualMemoryBytes}).max() as Long,
138+
rss: rss.average(),
139+
vmem: vmem.average(),
140+
peak_rss: rss.max(),
141+
peak_vmem: vmem.max(),
140142
]
141143
)
142144
}

src/main/nextflow/co2footprint/Records/CO2Record.groovy

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ class CO2Record extends TraceRecord {
8080
* @param rawEnergyMemory Memory-specific energy consumed by the task (kWh)
8181
*/
8282
CO2Record(
83-
TraceRecord traceRecord, Double energy, Double co2e, Double co2eMarket, Double ci,
84-
Double cpuUsage, Long memory, Double time, Integer cpus, Double powerdrawCPU,
85-
String cpu_model, Double rawEnergyProcessor, Double rawEnergyMemory
83+
TraceRecord traceRecord, BigDecimal energy, BigDecimal co2e, BigDecimal co2eMarket, BigDecimal ci,
84+
BigDecimal cpuUsage, BigDecimal memory, BigDecimal time, Integer cpus, BigDecimal powerdrawCPU,
85+
String cpu_model, BigDecimal rawEnergyProcessor, BigDecimal rawEnergyMemory
8686
) {
8787
// Add trace Record values
8888
traceKeys = traceRecord.store.keySet() as List<String>
@@ -240,7 +240,7 @@ class CO2Record extends TraceRecord {
240240
case 'carbon_intensity' -> new Quantity(value, '', 'gCO₂e/kWh').toReadable()
241241
case 'powerdraw_cpu' -> new Quantity(value, '', 'W').toReadable()
242242
case '%cpu' -> new Percentage(value).toReadable()
243-
case 'memory' -> new Bytes(value, 'G', 'B').toReadable()
243+
case 'memory' -> new Bytes(value, 'G').toReadable()
244244
case 'raw_energy_processor' -> new Quantity(value, 'k', 'Wh').toReadable()
245245
case 'raw_energy_memory' -> new Quantity(value, 'k', 'Wh').toReadable()
246246
default -> getFmtStr(key)

src/test/nextflow/co2footprint/CO2FootprintCalculatorTest.groovy

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class CO2FootprintCalculatorTest extends Specification{
4040
traceRecord.cpus = 2
4141
traceRecord.cpu_model = cpuModel
4242
traceRecord.'%cpu' = 100.0
43-
traceRecord.memory = (7 as Long) * (1024**3 as Long)
43+
traceRecord.memory = (7 as Long) * (1000**3 as Long)
4444

4545
CO2FootprintConfig config = new CO2FootprintConfig(configMap, tdpDataMatrix, ciDataMatrix, [:])
4646
CO2FootprintCalculator co2FootprintComputer = new CO2FootprintCalculator(tdpDataMatrix, config)
@@ -101,28 +101,28 @@ class CO2FootprintCalculatorTest extends Specification{
101101

102102
when:
103103
// Try to compute the CO2 footprint, catching any exceptions
104-
def result = null
105-
def caught = null
104+
CO2Record result = null
105+
Exception caught = null
106106
try {
107107
result = co2FootprintComputer.computeTaskCO2footprint(traceRecord, timeCiRecordCollector)
108-
} catch (Exception e) {
108+
} catch (MissingValueException e) {
109109
caught = e
110110
}
111111

112112
then:
113113
// If we expect an exception, assert it was thrown
114-
if (expectException) {
115-
assert caught instanceof MissingValueException
114+
if (caught) {
115+
assert expectException
116116
} else {
117117
// Otherwise, check that the computed memory matches the expected value (in GB)
118118
assert result.store.memory == expectedMemory
119119
}
120120

121121
where:
122122
memory | peak_rss | expectException | expectedMemory
123-
8L*1024**3 | 4L*1024**3 | false | 8L // requested memory used
124-
null | 4L*1024**3 | false | 4L // peak_rss used (requested null)
125-
4L*1024**3 | null | false | 4L // requested used (required null)
123+
8L*1000**3 | 4L*1000**3 | false | 8L // requested memory used
124+
null | 4L*1000**3 | false | 4L // peak_rss used (requested null)
125+
4L*1000**3 | null | false | 4L // requested used (required null)
126126
null | null | true | null // throws error (both null)
127127
}
128128

@@ -134,7 +134,7 @@ class CO2FootprintCalculatorTest extends Specification{
134134
traceRecord.cpus = cpus
135135
traceRecord.cpu_model = "Some model"
136136
traceRecord.'%cpu' = pCpu
137-
traceRecord.memory = (7 as Long) * (1024**3 as Long)
137+
traceRecord.memory = (7 as Long) * (1000**3 as Long)
138138

139139
CO2FootprintConfig config = new CO2FootprintConfig([:], tdpDataMatrix, ciDataMatrix, [:])
140140
CO2FootprintCalculator co2FootprintComputer = new CO2FootprintCalculator(tdpDataMatrix, config)

src/test/nextflow/co2footprint/CO2FootprintObserverTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class CO2FootprintObserverTest extends Specification{
4141
'cpus': 1,
4242
'cpu_model': "Unknown model",
4343
'%cpu': 100.0,
44-
'memory': (7 as Long) * (1024**3 as Long), // 7 GB
44+
'memory': (7 as Long) * (1000**3 as Long), // 7 GB
4545
'status': 'COMPLETED'
4646
]
4747
)

0 commit comments

Comments
 (0)