@tgwf/co2 0.8.0 → 0.9.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -6
- package/README.md +22 -25
- package/images/swd-energy-usage.png +0 -0
- package/package.json +2 -1
- package/src/1byte.js +39 -4
- package/src/1byte.test.js +5 -4
- package/src/co2.js +19 -34
- package/src/co2.test.js +265 -126
- package/src/constants/file-size.js +5 -0
- package/src/constants/index.js +3 -0
- package/src/helpers/index.js +5 -0
- package/src/hosting-api.test.js +5 -5
- package/src/hosting-database.test.js +5 -5
- package/src/hosting-json.test.js +7 -7
- package/src/hosting.test.js +10 -10
- package/src/readme.md +66 -0
- package/src/sustainable-web-design.js +224 -0
- package/src/sustainable-web-design.test.js +81 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# [0.9.0] - 2022-03-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added newly implemented Sustainable Web Design model [thanks @dryden!]
|
|
18
|
+
- Added new readme page for using both emissions models
|
|
19
|
+
- Added new source of data to the Sustainable Web Design model from Ember Climate.
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Changed the CO2 class to accept either the One Byte model or the Sustainable Web Design model
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Fixed various typos.
|
|
12
26
|
|
|
13
27
|
# [0.8.0] - 2021-11-28
|
|
14
28
|
|
|
@@ -23,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
23
37
|
|
|
24
38
|
### Changed
|
|
25
39
|
|
|
26
|
-
- The
|
|
40
|
+
- The 1byte model will give different numbers now. It's mentioned in `#fixed` but it's worth repeating.
|
|
27
41
|
|
|
28
42
|
## [0.7.0] - 2021-11-28
|
|
29
43
|
|
|
@@ -61,9 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
61
75
|
|
|
62
76
|
### Changed
|
|
63
77
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
78
|
+
- Updated README
|
|
79
|
+
- Updated the emissions figured for green energy after further research on methodology with @@JamieBeevor
|
|
80
|
+
- Incorporated class based CO2 models from @soulgalore
|
|
67
81
|
- Credit contributors
|
|
68
82
|
|
|
69
83
|
|
|
@@ -83,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
83
97
|
|
|
84
98
|
### Added
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
Added the (currently unused) green byte model.
|
|
87
101
|
|
|
88
102
|
### Changed
|
|
89
103
|
|
package/README.md
CHANGED
|
@@ -3,24 +3,37 @@
|
|
|
3
3
|
<img src="https://github.com/thegreenwebfoundation/co2.js/actions/workflows/unittests.yml/badge.svg" />
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
We know computers use electricity, and because most of the electricity we use comes from burning fossil fuels
|
|
6
|
+
We know computers use electricity, and because most of the electricity we use comes from burning fossil fuels, there is an environmental cost to every upload and download we make over the internet.
|
|
7
7
|
|
|
8
8
|
We can do something about this though. The same way we use performance budgets to make apps and websites faster and cheaper to run, we can use carbon budgets to make them faster, cheaper and _greener_.
|
|
9
9
|
|
|
10
10
|
The CO2 package from [The Green Web Foundation][tgwf] lets you quickly estimate these emissions, to make measurable improvements as part of your workflow.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### How it works
|
|
13
13
|
|
|
14
|
-
It
|
|
14
|
+
It does this by implementing various models for converting the measurable usage of digital services into a figure for the estimated CO2 emissions.
|
|
15
|
+
|
|
16
|
+
This defaults to the 1byte model as used by the Shift Project, as introduced in their report on CO2 emissions from digital infrastructure, [Lean ICT: for a sober digital][soberDigital], with the [Sustainable Web Design model][swd] as a popular alternative..
|
|
17
|
+
|
|
18
|
+
For more information, see the documentation [for when to use the different models, with code samples to start you off](./src/readme.md).
|
|
19
|
+
|
|
20
|
+
### Who uses it
|
|
21
|
+
|
|
22
|
+
It is currently used in the web performance tool [sitespeed.io][], [ecoping][], [Websitecarbon.com](websitecarbon), and [ecograder][] to help developers build greener, more planet friendly digital services.
|
|
15
23
|
|
|
16
24
|
If you want to build this kind of environmental information into your own software, and want some advice, we'd be happy to hear from you - please open an issue, remembering to link to your project.
|
|
17
25
|
|
|
18
|
-
This is open source software, with all the guarantees associated
|
|
26
|
+
This is open source software, with all the guarantees associated. So if you want professional advice, to a deadline, and you have a budget please see the services offered by the [Green Web Foundation][tgwf-services].
|
|
19
27
|
|
|
28
|
+
|
|
29
|
+
[sitespeed.io]: https://sitespeed.io
|
|
30
|
+
[ecoping]: https://ecoping.earth
|
|
31
|
+
[ecograder]: https://ecograder.com
|
|
32
|
+
[websitecarbon]: https://www.websitecarbon.com
|
|
33
|
+
[tgwf]: https://www.thegreenwebfoundation.org
|
|
34
|
+
[tgwf-services]: https://www.thegreenwebfoundation.org/services
|
|
35
|
+
[swd]: https://sustainablewebdesign.org/calculating-digital-emissions
|
|
20
36
|
[soberDigital]: https://theshiftproject.org/en/lean-ict-2/
|
|
21
|
-
[sitespeed.io]: https://sitespeed.io/
|
|
22
|
-
[tgwf]: https://www.thegreenwebfoundation.org/
|
|
23
|
-
[tgwf-services]: https://www.thegreenwebfoundation.org/services/
|
|
24
37
|
|
|
25
38
|
|
|
26
39
|
## Usage
|
|
@@ -29,7 +42,7 @@ This is open source software, with all the guarantees associated, so if you want
|
|
|
29
42
|
|
|
30
43
|
#### Server-side
|
|
31
44
|
|
|
32
|
-
This approach relies on the `fs` module and so can only be used on platforms
|
|
45
|
+
This approach relies on the `fs` module and so can only be used on platforms like Node.js, that support this.
|
|
33
46
|
|
|
34
47
|
```js
|
|
35
48
|
|
|
@@ -54,7 +67,7 @@ const co2Emission = new CO2();
|
|
|
54
67
|
estimatedCO2 = co2Emission.perByte(bytesSent)
|
|
55
68
|
|
|
56
69
|
console.log(`Sending a gigabyte, had a carbon footprint of ${estimatedCO2.toFixed(3)} grams of CO2`)
|
|
57
|
-
|
|
70
|
+
****
|
|
58
71
|
```
|
|
59
72
|
|
|
60
73
|
### Checking for green power
|
|
@@ -76,22 +89,6 @@ greencheck.checkPage(["google.com"])
|
|
|
76
89
|
|
|
77
90
|
```
|
|
78
91
|
|
|
79
|
-
### Notes
|
|
80
|
-
|
|
81
|
-
Please note, we currently look at just the carbon cost of _generating_ the electricity, similar to how the [International Energy Agency (IEA)] does, not the full life cycle cost of the energy.
|
|
82
|
-
|
|
83
|
-
Doing this would include things like:
|
|
84
|
-
|
|
85
|
-
- the carbon emitted when carrying out activity associated with digging up the fuel
|
|
86
|
-
- the carbon associated with mining the materials to _build_ the power stations, datacentres, and so on
|
|
87
|
-
- the end of life costs
|
|
88
|
-
- the maintenance costs over the life of the datacentres, power generation and end user devices, and the rest of the internet
|
|
89
|
-
|
|
90
|
-
Life cycle figures do exist, but they are very difficult to do well. If you're interested in contributing to this, we'd love to hear from you.
|
|
91
|
-
|
|
92
|
-
|
|
93
92
|
# Licenses
|
|
94
93
|
|
|
95
94
|
Apache 2.0
|
|
96
|
-
|
|
97
|
-
[International Energy Agency (IEA)]: https://www.iea.org/
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tgwf/co2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-0",
|
|
4
4
|
"description": "Work out the co2 of your digital services",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest",
|
|
8
|
+
"test:watch": "jest --watch",
|
|
8
9
|
"lint": "eslint .",
|
|
9
10
|
"lint:fix": "eslint . --fix",
|
|
10
11
|
"travis": "npm run lint && jest"
|
package/src/1byte.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// Use the 1byte model for now from the Shift Project, and assume a US grid mix figure they use of around 519 g co2 for the time being. It's lower for Europe, and in particular, France, but for v1, we don't include this
|
|
2
2
|
const CO2_PER_KWH_IN_DC_GREY = 519;
|
|
3
3
|
|
|
4
|
+
// this figure is from the IEA's 2018 report for a global average:
|
|
5
|
+
const CO2_PER_KWH_NETWORK_GREY = 475;
|
|
6
|
+
|
|
7
|
+
// TODO - these figures need to be updated, as the figures for green
|
|
8
|
+
// shouldn't really be zero now we know that carbon intensity figures
|
|
9
|
+
// for renewables still usually include the life cycle emissions
|
|
10
|
+
const CO2_PER_KWH_IN_DC_GREEN = 0;
|
|
11
|
+
|
|
4
12
|
// the 1 byte model gives figures for energy usage for:
|
|
5
13
|
|
|
6
14
|
// datacentres
|
|
@@ -28,9 +36,36 @@ const KWH_PER_BYTE_FOR_NETWORK =
|
|
|
28
36
|
(FIXED_NETWORK_WIRED + FIXED_NETWORK_WIFI + FOUR_G_MOBILE) / 3;
|
|
29
37
|
|
|
30
38
|
const KWH_PER_BYTE_FOR_DEVICES = 1.3e-10;
|
|
39
|
+
|
|
40
|
+
class OneByte {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
|
|
44
|
+
this.KWH_PER_BYTE_FOR_NETWORK = KWH_PER_BYTE_FOR_NETWORK;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
perByte(bytes, green) {
|
|
48
|
+
if (bytes < 1) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (green) {
|
|
53
|
+
// if we have a green datacentre, use the lower figure for renewable energy
|
|
54
|
+
const Co2ForDC = bytes * KWH_PER_BYTE_IN_DC * CO2_PER_KWH_IN_DC_GREEN;
|
|
55
|
+
|
|
56
|
+
// but for the worest of the internet, we can't easily check, so assume
|
|
57
|
+
// grey for now
|
|
58
|
+
const Co2forNetwork =
|
|
59
|
+
bytes * KWH_PER_BYTE_FOR_NETWORK * CO2_PER_KWH_NETWORK_GREY;
|
|
60
|
+
|
|
61
|
+
return Co2ForDC + Co2forNetwork;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const KwHPerByte = KWH_PER_BYTE_IN_DC + KWH_PER_BYTE_FOR_NETWORK;
|
|
65
|
+
return bytes * KwHPerByte * CO2_PER_KWH_IN_DC_GREY;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
module.exports = {
|
|
32
|
-
|
|
33
|
-
KWH_PER_BYTE_FOR_NETWORK,
|
|
34
|
-
KWH_PER_BYTE_FOR_DEVICES,
|
|
35
|
-
CO2_PER_KWH_IN_DC_GREY,
|
|
70
|
+
OneByte,
|
|
36
71
|
};
|
package/src/1byte.test.js
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
const oneByte = require("./1byte");
|
|
4
4
|
|
|
5
|
-
describe("onebyte",
|
|
6
|
-
describe("perByte",
|
|
7
|
-
it.only("returns a simple average of the different networks",
|
|
5
|
+
describe("onebyte", () => {
|
|
6
|
+
describe("perByte", () => {
|
|
7
|
+
it.only("returns a simple average of the different networks", () => {
|
|
8
8
|
// we limit this to 12 figures with toFixed(12), because
|
|
9
9
|
// we have a recurring 333333 afterwards
|
|
10
10
|
// 4.88e-10 is the same as 0.000000000488
|
|
11
11
|
const expected_val = (0.000000000488).toFixed(12);
|
|
12
|
+
const instance = new oneByte.OneByte();
|
|
12
13
|
|
|
13
|
-
expect(
|
|
14
|
+
expect(instance.KWH_PER_BYTE_FOR_NETWORK.toFixed(12)).toBe(expected_val);
|
|
14
15
|
});
|
|
15
16
|
});
|
|
16
17
|
});
|
package/src/co2.js
CHANGED
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const url = require("url");
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
const KWH_PER_BYTE_IN_DC = oneByte.KWH_PER_BYTE_IN_DC;
|
|
7
|
-
const KWH_PER_BYTE_FOR_NETWORK = oneByte.KWH_PER_BYTE_FOR_NETWORK;
|
|
8
|
-
const CO2_PER_KWH_IN_DC_GREY = oneByte.CO2_PER_KWH_IN_DC_GREY;
|
|
9
|
-
|
|
10
|
-
// this figure is from the IEA's 2018 report for a global average:
|
|
11
|
-
const CO2_PER_KWH_NETWORK_GREY = 475;
|
|
12
|
-
|
|
13
|
-
// The IEA figures cover electricity but as far as I can tell, it does not
|
|
14
|
-
// cover life cycle emissions, and the 1byte models appears to do the same
|
|
15
|
-
// so, we use zero emissions for green infra in the DC
|
|
16
|
-
// https://github.com/thegreenwebfoundation/co2.js/issues/2
|
|
17
|
-
const CO2_PER_KWH_IN_DC_GREEN = 0;
|
|
4
|
+
const onebyte = require("./1byte.js");
|
|
18
5
|
|
|
19
6
|
class CO2 {
|
|
20
7
|
constructor(options) {
|
|
21
8
|
this.options = options;
|
|
22
|
-
}
|
|
23
9
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// the data transfer.
|
|
10
|
+
// default model
|
|
11
|
+
this.model = new onebyte.OneByte();
|
|
27
12
|
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (green) {
|
|
33
|
-
// if we have a green datacentre, use the lower figure for renewable energy
|
|
34
|
-
const Co2ForDC = bytes * KWH_PER_BYTE_IN_DC * CO2_PER_KWH_IN_DC_GREEN;
|
|
35
|
-
|
|
36
|
-
// but for the rest of the internet, we can't easily check, so assume
|
|
37
|
-
// grey for now
|
|
38
|
-
const Co2forNetwork =
|
|
39
|
-
bytes * KWH_PER_BYTE_FOR_NETWORK * CO2_PER_KWH_NETWORK_GREY;
|
|
40
|
-
|
|
41
|
-
return Co2ForDC + Co2forNetwork;
|
|
13
|
+
if (options) {
|
|
14
|
+
this.model = new options.model();
|
|
42
15
|
}
|
|
16
|
+
}
|
|
43
17
|
|
|
44
|
-
|
|
45
|
-
|
|
18
|
+
//
|
|
19
|
+
//
|
|
20
|
+
/**
|
|
21
|
+
* Accept a figure in bytes for data transfer, and a boolean for whether
|
|
22
|
+
* the domain shows as 'green', and return a CO2 figure for energy used to shift the corresponding
|
|
23
|
+
* the data transfer.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} bytes
|
|
26
|
+
* @param {boolean} green
|
|
27
|
+
* @return {number} the amount of CO2 in grammes
|
|
28
|
+
*/
|
|
29
|
+
perByte(bytes, green) {
|
|
30
|
+
return this.model.perByte(bytes, green);
|
|
46
31
|
}
|
|
47
32
|
|
|
48
33
|
perDomain(pageXray, greenDomains) {
|
package/src/co2.test.js
CHANGED
|
@@ -4,147 +4,286 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
6
|
const CO2 = require("./co2");
|
|
7
|
+
const swd = require("./sustainable-web-design");
|
|
7
8
|
const pagexray = require("pagexray");
|
|
8
9
|
|
|
9
|
-
describe("co2",
|
|
10
|
+
describe("co2", () => {
|
|
10
11
|
let har, co2;
|
|
11
|
-
const TGWF_GREY_VALUE = 0.20497;
|
|
12
|
-
const TGWF_GREEN_VALUE = 0.54704;
|
|
13
|
-
const TGWF_MIXED_VALUE = 0.16718;
|
|
14
|
-
|
|
15
|
-
const MILLION = 1000000;
|
|
16
|
-
const MILLION_GREY = 0.29081;
|
|
17
|
-
const MILLION_GREEN = 0.23196;
|
|
18
|
-
|
|
19
|
-
beforeEach(function () {
|
|
20
|
-
co2 = new CO2();
|
|
21
|
-
har = JSON.parse(
|
|
22
|
-
fs.readFileSync(
|
|
23
|
-
path.resolve(__dirname, "../data/fixtures/tgwf.har"),
|
|
24
|
-
"utf8"
|
|
25
|
-
)
|
|
26
|
-
);
|
|
27
|
-
});
|
|
28
12
|
|
|
29
|
-
describe("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
describe("1 byte model", () => {
|
|
14
|
+
const TGWF_GREY_VALUE = 0.20497;
|
|
15
|
+
const TGWF_GREEN_VALUE = 0.54704;
|
|
16
|
+
const TGWF_MIXED_VALUE = 0.16718;
|
|
17
|
+
|
|
18
|
+
const MILLION = 1000000;
|
|
19
|
+
const MILLION_GREY = 0.29081;
|
|
20
|
+
const MILLION_GREEN = 0.23196;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
co2 = new CO2();
|
|
24
|
+
har = JSON.parse(
|
|
25
|
+
fs.readFileSync(
|
|
26
|
+
path.resolve(__dirname, "../data/fixtures/tgwf.har"),
|
|
27
|
+
"utf8"
|
|
28
|
+
)
|
|
33
29
|
);
|
|
34
30
|
});
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
describe("perByte", () => {
|
|
33
|
+
it("returns a CO2 number for data transfer using 'grey' power", () => {
|
|
34
|
+
expect(co2.perByte(MILLION).toPrecision(5)).toBe(
|
|
35
|
+
MILLION_GREY.toPrecision(5)
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns a lower CO2 number for data transfer from domains using entirely 'green' power", () => {
|
|
40
|
+
expect(co2.perByte(MILLION).toPrecision(5)).toBe(
|
|
41
|
+
MILLION_GREY.toPrecision(5)
|
|
42
|
+
);
|
|
43
|
+
expect(co2.perByte(MILLION, true).toPrecision(5)).toBe(
|
|
44
|
+
MILLION_GREEN.toPrecision(5)
|
|
45
|
+
);
|
|
46
|
+
});
|
|
43
47
|
});
|
|
44
|
-
});
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
describe("perPage", () => {
|
|
50
|
+
it("returns CO2 for total transfer for page", () => {
|
|
51
|
+
const pages = pagexray.convert(har);
|
|
52
|
+
const pageXrayRun = pages[0];
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
expect(co2.perPage(pageXrayRun).toPrecision(5)).toBe(
|
|
55
|
+
TGWF_GREY_VALUE.toPrecision(5)
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
it("returns lower CO2 for page served from green site", () => {
|
|
59
|
+
const pages = pagexray.convert(har);
|
|
60
|
+
const pageXrayRun = pages[0];
|
|
61
|
+
let green = [
|
|
62
|
+
"www.thegreenwebfoundation.org",
|
|
63
|
+
"fonts.googleapis.com",
|
|
64
|
+
"ajax.googleapis.com",
|
|
65
|
+
"assets.digitalclimatestrike.net",
|
|
66
|
+
"cdnjs.cloudflare.com",
|
|
67
|
+
"graphite.thegreenwebfoundation.org",
|
|
68
|
+
"analytics.thegreenwebfoundation.org",
|
|
69
|
+
"fonts.gstatic.com",
|
|
70
|
+
"api.thegreenwebfoundation.org",
|
|
71
|
+
];
|
|
72
|
+
expect(co2.perPage(pageXrayRun, green)).toBeLessThan(TGWF_GREY_VALUE);
|
|
73
|
+
});
|
|
74
|
+
it("returns a lower CO2 number where *some* domains use green power", () => {
|
|
75
|
+
const pages = pagexray.convert(har);
|
|
76
|
+
const pageXrayRun = pages[0];
|
|
77
|
+
// green can be true, or a array containing entries
|
|
78
|
+
let green = [
|
|
79
|
+
"www.thegreenwebfoundation.org",
|
|
80
|
+
"fonts.googleapis.com",
|
|
81
|
+
"ajax.googleapis.com",
|
|
82
|
+
"assets.digitalclimatestrike.net",
|
|
83
|
+
"cdnjs.cloudflare.com",
|
|
84
|
+
"graphite.thegreenwebfoundation.org",
|
|
85
|
+
"analytics.thegreenwebfoundation.org",
|
|
86
|
+
"fonts.gstatic.com",
|
|
87
|
+
"api.thegreenwebfoundation.org",
|
|
88
|
+
];
|
|
89
|
+
expect(co2.perPage(pageXrayRun, green).toPrecision(5)).toBe(
|
|
90
|
+
TGWF_MIXED_VALUE.toPrecision(5)
|
|
91
|
+
);
|
|
92
|
+
});
|
|
54
93
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
describe("perDomain", () => {
|
|
95
|
+
it("shows object listing Co2 for each domain", () => {
|
|
96
|
+
const pages = pagexray.convert(har);
|
|
97
|
+
const pageXrayRun = pages[0];
|
|
98
|
+
const res = co2.perDomain(pageXrayRun);
|
|
99
|
+
|
|
100
|
+
const domains = [
|
|
101
|
+
"thegreenwebfoundation.org",
|
|
102
|
+
"www.thegreenwebfoundation.org",
|
|
103
|
+
"maxcdn.bootstrapcdn.com",
|
|
104
|
+
"fonts.googleapis.com",
|
|
105
|
+
"ajax.googleapis.com",
|
|
106
|
+
"assets.digitalclimatestrike.net",
|
|
107
|
+
"cdnjs.cloudflare.com",
|
|
108
|
+
"graphite.thegreenwebfoundation.org",
|
|
109
|
+
"analytics.thegreenwebfoundation.org",
|
|
110
|
+
"fonts.gstatic.com",
|
|
111
|
+
"api.thegreenwebfoundation.org",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (let obj of res) {
|
|
115
|
+
expect(domains.indexOf(obj.domain)).toBeGreaterThan(-1);
|
|
116
|
+
expect(typeof obj.co2).toBe("number");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
it("shows lower Co2 for green domains", () => {
|
|
120
|
+
const pages = pagexray.convert(har);
|
|
121
|
+
const pageXrayRun = pages[0];
|
|
122
|
+
|
|
123
|
+
const greenDomains = [
|
|
124
|
+
"www.thegreenwebfoundation.org",
|
|
125
|
+
"fonts.googleapis.com",
|
|
126
|
+
"ajax.googleapis.com",
|
|
127
|
+
"assets.digitalclimatestrike.net",
|
|
128
|
+
"cdnjs.cloudflare.com",
|
|
129
|
+
"graphite.thegreenwebfoundation.org",
|
|
130
|
+
"analytics.thegreenwebfoundation.org",
|
|
131
|
+
"fonts.gstatic.com",
|
|
132
|
+
"api.thegreenwebfoundation.org",
|
|
133
|
+
];
|
|
134
|
+
const res = co2.perDomain(pageXrayRun);
|
|
135
|
+
const resWithGreen = co2.perDomain(pageXrayRun, greenDomains);
|
|
136
|
+
|
|
137
|
+
for (let obj of res) {
|
|
138
|
+
expect(typeof obj.co2).toBe("number");
|
|
139
|
+
}
|
|
140
|
+
for (let obj of greenDomains) {
|
|
141
|
+
let index = 0;
|
|
142
|
+
expect(resWithGreen[index].co2).toBeLessThan(res[index].co2);
|
|
143
|
+
index++;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
70
146
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Sustainable Web Design model", () => {
|
|
150
|
+
// the SWD model should have slightly higher values as
|
|
151
|
+
// we include more of the system in calculations for the
|
|
152
|
+
// same levels of data transfer
|
|
153
|
+
const MILLION = 1000000;
|
|
154
|
+
const MILLION_GREY = 0.33343;
|
|
155
|
+
const MILLION_GREEN = 0.28908;
|
|
156
|
+
|
|
157
|
+
const TGWF_GREY_VALUE = 0.23501;
|
|
158
|
+
const TGWF_GREEN_VALUE = 0.54704;
|
|
159
|
+
const TGWF_MIXED_VALUE = 0.20652;
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
co2 = new CO2({ model: swd });
|
|
163
|
+
har = JSON.parse(
|
|
164
|
+
fs.readFileSync(
|
|
165
|
+
path.resolve(__dirname, "../data/fixtures/tgwf.har"),
|
|
166
|
+
"utf8"
|
|
167
|
+
)
|
|
88
168
|
);
|
|
89
169
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"api.thegreenwebfoundation.org",
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
for (let obj of res) {
|
|
112
|
-
expect(domains.indexOf(obj.domain)).toBeGreaterThan(-1);
|
|
113
|
-
expect(typeof obj.co2).toBe("number");
|
|
114
|
-
}
|
|
170
|
+
|
|
171
|
+
describe("perByte", () => {
|
|
172
|
+
it("returns a CO2 number for data transfer", () => {
|
|
173
|
+
co2.perByte(MILLION);
|
|
174
|
+
expect(co2.perByte(MILLION).toPrecision(5)).toBe(
|
|
175
|
+
MILLION_GREY.toPrecision(5)
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns a lower CO2 number for data transfer from domains using entirely 'green' power", () => {
|
|
180
|
+
expect(co2.perByte(MILLION, false).toPrecision(5)).toBe(
|
|
181
|
+
MILLION_GREY.toPrecision(5)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(co2.perByte(MILLION, true).toPrecision(5)).toBe(
|
|
185
|
+
MILLION_GREEN.toPrecision(5)
|
|
186
|
+
);
|
|
187
|
+
});
|
|
115
188
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
189
|
+
|
|
190
|
+
describe("perPage", () => {
|
|
191
|
+
it("returns CO2 for total transfer for page", () => {
|
|
192
|
+
const pages = pagexray.convert(har);
|
|
193
|
+
const pageXrayRun = pages[0];
|
|
194
|
+
|
|
195
|
+
expect(co2.perPage(pageXrayRun).toPrecision(5)).toBe(
|
|
196
|
+
TGWF_GREY_VALUE.toPrecision(5)
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
it("returns lower CO2 for page served from green site", () => {
|
|
200
|
+
const pages = pagexray.convert(har);
|
|
201
|
+
const pageXrayRun = pages[0];
|
|
202
|
+
let green = [
|
|
203
|
+
"www.thegreenwebfoundation.org",
|
|
204
|
+
"fonts.googleapis.com",
|
|
205
|
+
"ajax.googleapis.com",
|
|
206
|
+
"assets.digitalclimatestrike.net",
|
|
207
|
+
"cdnjs.cloudflare.com",
|
|
208
|
+
"graphite.thegreenwebfoundation.org",
|
|
209
|
+
"analytics.thegreenwebfoundation.org",
|
|
210
|
+
"fonts.gstatic.com",
|
|
211
|
+
"api.thegreenwebfoundation.org",
|
|
212
|
+
];
|
|
213
|
+
expect(co2.perPage(pageXrayRun, green)).toBeLessThan(TGWF_GREY_VALUE);
|
|
214
|
+
});
|
|
215
|
+
it("returns a lower CO2 number where *some* domains use green power", () => {
|
|
216
|
+
const pages = pagexray.convert(har);
|
|
217
|
+
const pageXrayRun = pages[0];
|
|
218
|
+
// green can be true, or a array containing entries
|
|
219
|
+
let green = [
|
|
220
|
+
"www.thegreenwebfoundation.org",
|
|
221
|
+
"fonts.googleapis.com",
|
|
222
|
+
"ajax.googleapis.com",
|
|
223
|
+
"assets.digitalclimatestrike.net",
|
|
224
|
+
"cdnjs.cloudflare.com",
|
|
225
|
+
"graphite.thegreenwebfoundation.org",
|
|
226
|
+
"analytics.thegreenwebfoundation.org",
|
|
227
|
+
"fonts.gstatic.com",
|
|
228
|
+
"api.thegreenwebfoundation.org",
|
|
229
|
+
];
|
|
230
|
+
expect(co2.perPage(pageXrayRun, green).toPrecision(5)).toBe(
|
|
231
|
+
TGWF_MIXED_VALUE.toPrecision(5)
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe("perDomain", () => {
|
|
236
|
+
it("shows object listing Co2 for each domain", () => {
|
|
237
|
+
const pages = pagexray.convert(har);
|
|
238
|
+
const pageXrayRun = pages[0];
|
|
239
|
+
const res = co2.perDomain(pageXrayRun);
|
|
240
|
+
|
|
241
|
+
const domains = [
|
|
242
|
+
"thegreenwebfoundation.org",
|
|
243
|
+
"www.thegreenwebfoundation.org",
|
|
244
|
+
"maxcdn.bootstrapcdn.com",
|
|
245
|
+
"fonts.googleapis.com",
|
|
246
|
+
"ajax.googleapis.com",
|
|
247
|
+
"assets.digitalclimatestrike.net",
|
|
248
|
+
"cdnjs.cloudflare.com",
|
|
249
|
+
"graphite.thegreenwebfoundation.org",
|
|
250
|
+
"analytics.thegreenwebfoundation.org",
|
|
251
|
+
"fonts.gstatic.com",
|
|
252
|
+
"api.thegreenwebfoundation.org",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
for (let obj of res) {
|
|
256
|
+
expect(domains.indexOf(obj.domain)).toBeGreaterThan(-1);
|
|
257
|
+
expect(typeof obj.co2).toBe("number");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
it("shows lower Co2 for green domains", () => {
|
|
261
|
+
const pages = pagexray.convert(har);
|
|
262
|
+
const pageXrayRun = pages[0];
|
|
263
|
+
|
|
264
|
+
const greenDomains = [
|
|
265
|
+
"www.thegreenwebfoundation.org",
|
|
266
|
+
"fonts.googleapis.com",
|
|
267
|
+
"ajax.googleapis.com",
|
|
268
|
+
"assets.digitalclimatestrike.net",
|
|
269
|
+
"cdnjs.cloudflare.com",
|
|
270
|
+
"graphite.thegreenwebfoundation.org",
|
|
271
|
+
"analytics.thegreenwebfoundation.org",
|
|
272
|
+
"fonts.gstatic.com",
|
|
273
|
+
"api.thegreenwebfoundation.org",
|
|
274
|
+
];
|
|
275
|
+
const res = co2.perDomain(pageXrayRun);
|
|
276
|
+
const resWithGreen = co2.perDomain(pageXrayRun, greenDomains);
|
|
277
|
+
|
|
278
|
+
for (let obj of res) {
|
|
279
|
+
expect(typeof obj.co2).toBe("number");
|
|
280
|
+
}
|
|
281
|
+
for (let obj of greenDomains) {
|
|
282
|
+
let index = 0;
|
|
283
|
+
expect(resWithGreen[index].co2).toBeLessThan(res[index].co2);
|
|
284
|
+
index++;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
142
287
|
});
|
|
143
288
|
});
|
|
144
|
-
// describe('perContentType', function () {
|
|
145
|
-
// test.skip('shows a breakdown of emissions by content type');
|
|
146
|
-
// });
|
|
147
|
-
// describe('dirtiestResources', function () {
|
|
148
|
-
// it.skip('shows the top 10 resources by CO2 emissions');
|
|
149
|
-
// });
|
|
150
289
|
});
|
package/src/hosting-api.test.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
const hosting = require("./hosting-api");
|
|
4
4
|
const nock = require("nock");
|
|
5
5
|
|
|
6
|
-
describe("hostingAPI",
|
|
7
|
-
describe("checking a single domain with #check",
|
|
8
|
-
it("using the API", async
|
|
6
|
+
describe("hostingAPI", () => {
|
|
7
|
+
describe("checking a single domain with #check", () => {
|
|
8
|
+
it("using the API", async () => {
|
|
9
9
|
const scope = nock("https://api.thegreenwebfoundation.org/")
|
|
10
10
|
.get("/greencheck/google.com")
|
|
11
11
|
.reply(200, {
|
|
@@ -16,8 +16,8 @@ describe("hostingAPI", function () {
|
|
|
16
16
|
expect(res).toEqual(true);
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
describe("implicitly checking multiple domains with #check",
|
|
20
|
-
it("using the API", async
|
|
19
|
+
describe("implicitly checking multiple domains with #check", () => {
|
|
20
|
+
it("using the API", async () => {
|
|
21
21
|
const scope = nock("https://api.thegreenwebfoundation.org/")
|
|
22
22
|
.get("/v2/greencheckmulti/[%22google.com%22,%22kochindustries.com%22]")
|
|
23
23
|
.reply(200, {
|
|
@@ -12,15 +12,15 @@ const dbPath = path.resolve(
|
|
|
12
12
|
"url2green.test.db"
|
|
13
13
|
);
|
|
14
14
|
|
|
15
|
-
describe("hostingDatabase",
|
|
16
|
-
describe("checking a single domain with #check",
|
|
17
|
-
test("tries to use a local database if available ", async
|
|
15
|
+
describe("hostingDatabase", () => {
|
|
16
|
+
describe("checking a single domain with #check", () => {
|
|
17
|
+
test("tries to use a local database if available ", async () => {
|
|
18
18
|
const res = await hosting.check("google.com", dbPath);
|
|
19
19
|
expect(res).toEqual(true);
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
|
-
describe("implicitly checking multiple domains with #check",
|
|
23
|
-
test("tries to use a local database if available", async
|
|
22
|
+
describe("implicitly checking multiple domains with #check", () => {
|
|
23
|
+
test("tries to use a local database if available", async () => {
|
|
24
24
|
const res = await hosting.check(
|
|
25
25
|
["google.com", "kochindustries.com"],
|
|
26
26
|
dbPath
|
package/src/hosting-json.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const hosting = require("./hosting-json");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
-
describe("hostingJSON",
|
|
6
|
+
describe("hostingJSON", () => {
|
|
7
7
|
const jsonPath = path.resolve(
|
|
8
8
|
__dirname,
|
|
9
9
|
"..",
|
|
@@ -18,22 +18,22 @@ describe("hostingJSON", function () {
|
|
|
18
18
|
"fixtures",
|
|
19
19
|
"url2green.test.json.gz"
|
|
20
20
|
);
|
|
21
|
-
describe("checking a single domain with #check",
|
|
22
|
-
test("against the list of domains as JSON", async
|
|
21
|
+
describe("checking a single domain with #check", () => {
|
|
22
|
+
test("against the list of domains as JSON", async () => {
|
|
23
23
|
const db = await hosting.loadJSON(jsonPath);
|
|
24
24
|
const res = await hosting.check("google.com", db);
|
|
25
25
|
expect(res).toEqual(true);
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
-
describe("checking a single domain with #check",
|
|
29
|
-
test("against the list of domains as JSON loaded from a gzipped JSON", async
|
|
28
|
+
describe("checking a single domain with #check", () => {
|
|
29
|
+
test("against the list of domains as JSON loaded from a gzipped JSON", async () => {
|
|
30
30
|
const db = await hosting.loadJSON(jsonPathGz);
|
|
31
31
|
const res = await hosting.check("google.com", db);
|
|
32
32
|
expect(res).toEqual(true);
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
|
-
describe("implicitly checking multiple domains with #check",
|
|
36
|
-
test("against the list of domains as JSON", async
|
|
35
|
+
describe("implicitly checking multiple domains with #check", () => {
|
|
36
|
+
test("against the list of domains as JSON", async () => {
|
|
37
37
|
const db = await hosting.loadJSON(jsonPath);
|
|
38
38
|
const domains = ["google.com", "kochindustries.com"];
|
|
39
39
|
|
package/src/hosting.test.js
CHANGED
|
@@ -14,9 +14,9 @@ const jsonPath = path.resolve(
|
|
|
14
14
|
"url2green.test.json"
|
|
15
15
|
);
|
|
16
16
|
|
|
17
|
-
describe("hosting",
|
|
17
|
+
describe("hosting", () => {
|
|
18
18
|
let har;
|
|
19
|
-
beforeEach(
|
|
19
|
+
beforeEach(() => {
|
|
20
20
|
har = JSON.parse(
|
|
21
21
|
fs.readFileSync(
|
|
22
22
|
path.resolve(__dirname, "../data/fixtures/tgwf.har"),
|
|
@@ -24,8 +24,8 @@ describe("hosting", function () {
|
|
|
24
24
|
)
|
|
25
25
|
);
|
|
26
26
|
});
|
|
27
|
-
describe("checking all domains on a page object with #checkPage ",
|
|
28
|
-
it("it returns a list of green domains, when passed a page object", async
|
|
27
|
+
describe("checking all domains on a page object with #checkPage ", () => {
|
|
28
|
+
it("it returns a list of green domains, when passed a page object", async () => {
|
|
29
29
|
const pages = pagexray.convert(har);
|
|
30
30
|
const pageXrayRun = pages[0];
|
|
31
31
|
const db = await hosting.loadJSON(jsonPath);
|
|
@@ -53,23 +53,23 @@ describe("hosting", function () {
|
|
|
53
53
|
// 'it returns an empty list, when passed a page object with no green domains'
|
|
54
54
|
// );
|
|
55
55
|
});
|
|
56
|
-
describe("checking a single domain with #check",
|
|
57
|
-
it("use the API instead", async
|
|
56
|
+
describe("checking a single domain with #check", () => {
|
|
57
|
+
it("use the API instead", async () => {
|
|
58
58
|
const db = await hosting.loadJSON(jsonPath);
|
|
59
59
|
const res = await hosting.check("google.com", db);
|
|
60
60
|
expect(res).toEqual(true);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
-
describe("implicitly checking multiple domains with #check",
|
|
64
|
-
it("Use the API", async
|
|
63
|
+
describe("implicitly checking multiple domains with #check", () => {
|
|
64
|
+
it("Use the API", async () => {
|
|
65
65
|
const db = await hosting.loadJSON(jsonPath);
|
|
66
66
|
|
|
67
67
|
const res = await hosting.check(["google.com", "kochindustries.com"], db);
|
|
68
68
|
expect(res).toContain("google.com");
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
|
-
describe("explicitly checking multiple domains with #checkMulti",
|
|
72
|
-
it("use the API", async
|
|
71
|
+
describe("explicitly checking multiple domains with #checkMulti", () => {
|
|
72
|
+
it("use the API", async () => {
|
|
73
73
|
const db = await hosting.loadJSON(jsonPath);
|
|
74
74
|
const res = await hosting.check(["google.com", "kochindustries.com"], db);
|
|
75
75
|
expect(res).toContain("google.com");
|
package/src/readme.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# How to use the different models in CO2.js
|
|
2
|
+
|
|
3
|
+
CO2js offers two models for understanding the environmental impact of compute - the 1byte model (`1byte.js`), and the Sustainable Web Design model (`swd.js`)
|
|
4
|
+
|
|
5
|
+
### The 1byte model
|
|
6
|
+
|
|
7
|
+
The default model in use is the 1byte model as used by the Shift Project, as introduced in their report on CO2 emissions from digital infrastructure, [Lean ICT: for a sober digital][soberDigital].
|
|
8
|
+
|
|
9
|
+
This returns a number for the estimated CO2 emissions for the corresponding number of bytes sent over the wire, and has been used for video streaming, file downloads and websites.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// assume you have imported or required the CO2 class from 1byte.js
|
|
13
|
+
// for your runtime - either a browser, nodejs, etc.
|
|
14
|
+
|
|
15
|
+
const bytesSent = (1024 * 1024 * 1024)
|
|
16
|
+
const co2Emission = new CO2();
|
|
17
|
+
const estimatedCO2 = co2Emission.perByte(bytesSent)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### The Sustainable Web Design model
|
|
22
|
+
|
|
23
|
+
As of version 0.9, CO2.js also provides the [Sustainable Web Design model][swd] for calculating emissions from digital services. As the name suggests, this has been designed for helping understand the environmental impact of websites. Further details are available on the [Sustainable Web Design website explaining the model](https://sustainablewebdesign.org/calculating-digital-emissions/), but for convenience, a short summary is below.
|
|
24
|
+
|
|
25
|
+
#### How the SWD works
|
|
26
|
+
|
|
27
|
+
This model uses data transfer as an proxy indicator for total resource usage, and uses this number to extrapolate energy usage numbers for your application as a fraction of the energy used by the total system comprised of:
|
|
28
|
+
|
|
29
|
+
1. the use-phase energy of datacentres serving content
|
|
30
|
+
2. the use-phase energy network transfering the data
|
|
31
|
+
3. the use-phase energy of user device an user is accessing content on
|
|
32
|
+
4. the total embodied energy used to create all of the above
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
It then converts these energy figures to carbon emissions, based on the carbon intensity of electricity from the [Ember annual global electricity review][Ember-annual-global-electricity-review].
|
|
37
|
+
|
|
38
|
+
The carbon intensity of electricity figures for the swd model include the the full lifecycle emissions including upstream methane, supply-chain and manufacturing emissions, and include all gases, converted into CO2 equivalent over a 100 year timescale.
|
|
39
|
+
|
|
40
|
+
This follows the approach used by the IPCC 5th Assessment Report Annex 3 (2014), for the carbon intensity of electricity.
|
|
41
|
+
|
|
42
|
+
[Ember's methodology notes][ember-methodology] detail where the rest of this data comes from in more detail, as well as any further assumptions made.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
[ember-methodology]: https://ember-climate.org/app/uploads/2022/03/GER22-Methodology.pdf
|
|
46
|
+
|
|
47
|
+
[Ember-annual-global-electricity-review]: https://ember-climate.org/insights/research/european-electricity-review-2022/
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Sample usage
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
// assume you have imported or required the CO2 class from swd.js
|
|
56
|
+
// for your runtime - either a browser, nodejs, etc.
|
|
57
|
+
|
|
58
|
+
// assume a 1 megabyte webpage
|
|
59
|
+
const bytesSent = (1024 * 1024 * 1024)
|
|
60
|
+
|
|
61
|
+
const co2Emission = new CO2();
|
|
62
|
+
const estimatedCO2ForTransfer = co2Emission.emissionsPerVisitInGrams(bytesSent)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
[soberDigital]: https://theshiftproject.org/en/lean-ict-2/
|
|
66
|
+
[swd]: https://sustainablewebdesign.org/calculating-digital-emissions
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sustainable Web Design
|
|
5
|
+
*
|
|
6
|
+
* Updated calculations and figures from
|
|
7
|
+
* https://sustainablewebdesign.org/calculating-digital-emissions/
|
|
8
|
+
*
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
const { fileSize } = require("./constants");
|
|
12
|
+
const { formatNumber } = require("./helpers");
|
|
13
|
+
|
|
14
|
+
// this refers to the estimated total energy use for the internet around 2000 TWh,
|
|
15
|
+
// divided by the total transfer it enables around 2500 exabytes
|
|
16
|
+
const KWH_PER_GB = 0.81;
|
|
17
|
+
|
|
18
|
+
// these constants outline how the energy is attributed to
|
|
19
|
+
// different parts of the system in the SWD model
|
|
20
|
+
const END_USER_DEVICE_ENERGY = 0.52;
|
|
21
|
+
const NETWORK_ENERGY = 0.14;
|
|
22
|
+
const DATACENTER_ENERGY = 0.15;
|
|
23
|
+
const PRODUCTION_ENERGY = 0.19;
|
|
24
|
+
|
|
25
|
+
// These carbon intensity figures https://ember-climate.org/data/data-explorer
|
|
26
|
+
// - Global carbon intensity for 2021
|
|
27
|
+
const GLOBAL_INTENSITY = 442;
|
|
28
|
+
const RENEWABLES_INTENSITY = 50;
|
|
29
|
+
|
|
30
|
+
// Taken from: https://gitlab.com/wholegrain/carbon-api-2-0/-/blob/master/includes/carbonapi.php
|
|
31
|
+
|
|
32
|
+
const FIRST_TIME_VIEWING_PERCENTAGE = 0.25;
|
|
33
|
+
const RETURNING_VISITOR_PERCENTAGE = 0.75;
|
|
34
|
+
const PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD = 0.02;
|
|
35
|
+
|
|
36
|
+
class SustainableWebDesign {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.options = options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Accept a figure for bytes transferred and return an object representing
|
|
43
|
+
* the share of the total enrgy use of the entire system, broken down
|
|
44
|
+
* by each corresponding system component
|
|
45
|
+
*
|
|
46
|
+
* @param {number} bytes - the data transferred in bytes
|
|
47
|
+
* @return {object} Object containing the energy in kilowatt hours, keyed by system component
|
|
48
|
+
*/
|
|
49
|
+
energyPerByteByComponent(bytes) {
|
|
50
|
+
const transferedBytesToGb = bytes / fileSize.GIGABYTE;
|
|
51
|
+
const energyUsage = transferedBytesToGb * KWH_PER_GB;
|
|
52
|
+
|
|
53
|
+
// return the total energy, with breakdown by component
|
|
54
|
+
return {
|
|
55
|
+
consumerDeviceEnergy: energyUsage * END_USER_DEVICE_ENERGY,
|
|
56
|
+
networkEnergy: energyUsage * NETWORK_ENERGY,
|
|
57
|
+
productionEnergy: energyUsage * PRODUCTION_ENERGY,
|
|
58
|
+
dataCenterEnergy: energyUsage * DATACENTER_ENERGY,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Accept an object keys by the different system components, and
|
|
63
|
+
* return an object with the co2 figures key by the each component
|
|
64
|
+
*
|
|
65
|
+
* @param {object} energyBycomponent - energy grouped by the four system components
|
|
66
|
+
* @param {number} [carbonIntensity] - carbon intensity to apply to the datacentre values
|
|
67
|
+
* @return {number} the total number in grams of CO2 equivalent emissions
|
|
68
|
+
*/
|
|
69
|
+
co2byComponent(energyBycomponent, carbonIntensity = GLOBAL_INTENSITY) {
|
|
70
|
+
const returnCO2ByComponent = {};
|
|
71
|
+
for (const [key, value] of Object.entries(energyBycomponent)) {
|
|
72
|
+
// we update the datacentre, as that's what we have information
|
|
73
|
+
// about.
|
|
74
|
+
if (key === "dataCenterEnergy") {
|
|
75
|
+
returnCO2ByComponent[key] = value * carbonIntensity;
|
|
76
|
+
} else {
|
|
77
|
+
// We don't have info about the device location,
|
|
78
|
+
// nor the network path used, nor the production emissions
|
|
79
|
+
// so we revert to global figures
|
|
80
|
+
returnCO2ByComponent[key] = value * GLOBAL_INTENSITY;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return returnCO2ByComponent;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Accept a figure for bytes transferred and return a single figure for CO2
|
|
88
|
+
* emissions. Where information exists about the origin data is being
|
|
89
|
+
* fetched from, a different carbon intensity figure
|
|
90
|
+
* is applied for the datacentre share of the carbon intensity.
|
|
91
|
+
*
|
|
92
|
+
* @param {number} bytes - the data transferred in bytes
|
|
93
|
+
* @param {number} `carbonIntensity` the carbon intensity for datacentre (average figures, not marginal ones)
|
|
94
|
+
* @return {number} the total number in grams of CO2 equivalent emissions
|
|
95
|
+
*/
|
|
96
|
+
perByte(bytes, carbonIntensity = GLOBAL_INTENSITY) {
|
|
97
|
+
const energyBycomponent = this.energyPerByteByComponent(bytes);
|
|
98
|
+
|
|
99
|
+
// when faced with falsy values, fallback to global intensity
|
|
100
|
+
if (Boolean(carbonIntensity) === false) {
|
|
101
|
+
carbonIntensity = GLOBAL_INTENSITY;
|
|
102
|
+
}
|
|
103
|
+
// if we have a boolean, we have a green result from the green web checker
|
|
104
|
+
// use the renewables intensity
|
|
105
|
+
if (carbonIntensity === true) {
|
|
106
|
+
carbonIntensity = RENEWABLES_INTENSITY;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// otherwise when faced with non numeric values throw an error
|
|
110
|
+
if (typeof carbonIntensity !== "number") {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`perByte expects a numeric value or boolean for the carbon intensity value. Received: ${carbonIntensity}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const co2ValuesbyComponent = this.co2byComponent(
|
|
117
|
+
energyBycomponent,
|
|
118
|
+
carbonIntensity
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// pull out our values…
|
|
122
|
+
const co2Values = Object.values(co2ValuesbyComponent);
|
|
123
|
+
|
|
124
|
+
// so we can return their sum
|
|
125
|
+
return co2Values.reduce(
|
|
126
|
+
(prevValue, currentValue) => prevValue + currentValue
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Accept a figure for bytes transferred and return the number of kilowatt hours used
|
|
132
|
+
* by the total system for this data transfer
|
|
133
|
+
*
|
|
134
|
+
* @param {number} bytes
|
|
135
|
+
* @return {number} the number of kilowatt hours used
|
|
136
|
+
*/
|
|
137
|
+
energyPerByte(bytes) {
|
|
138
|
+
const energyByComponent = this.energyPerByteByComponent(bytes);
|
|
139
|
+
|
|
140
|
+
// pull out our values…
|
|
141
|
+
const energyValues = Object.values(energyByComponent);
|
|
142
|
+
|
|
143
|
+
// so we can return their sum
|
|
144
|
+
return energyValues.reduce(
|
|
145
|
+
(prevValue, currentValue) => prevValue + currentValue
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Accept a figure for bytes transferred, and return an object containing figures
|
|
151
|
+
* per system component, with the caching assumptions applied. This tries to account
|
|
152
|
+
* for webpages being loaded from a cache by browsers, so if you had a thousand page views,
|
|
153
|
+
* and tried to work out the energy per visit, the numbers would reflect the reduced amounts
|
|
154
|
+
* of transfer.
|
|
155
|
+
*
|
|
156
|
+
* @param {number} bytes - the data transferred in bytes for loading a webpage
|
|
157
|
+
* @param {number} firstView - what percentage of visits are loading this page for the first time
|
|
158
|
+
* @param {number} returnView - what percentage of visits are loading this page for subsequent times
|
|
159
|
+
* @param {number} dataReloadRatio - what percentage of a page is reloaded on each subsequent page view
|
|
160
|
+
*
|
|
161
|
+
* @return {object} Object containing the energy in kilowatt hours, keyed by system component
|
|
162
|
+
*/
|
|
163
|
+
energyPerVisitByComponent(
|
|
164
|
+
bytes,
|
|
165
|
+
firstView = FIRST_TIME_VIEWING_PERCENTAGE,
|
|
166
|
+
returnView = RETURNING_VISITOR_PERCENTAGE,
|
|
167
|
+
dataReloadRatio = PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD
|
|
168
|
+
) {
|
|
169
|
+
const energyBycomponent = this.energyPerByteByComponent(bytes);
|
|
170
|
+
const cacheAdjustedSegmentEnergy = {};
|
|
171
|
+
|
|
172
|
+
for (const [key, value] of Object.entries(energyBycomponent)) {
|
|
173
|
+
// represent the first load
|
|
174
|
+
cacheAdjustedSegmentEnergy[key] = value * firstView;
|
|
175
|
+
|
|
176
|
+
// then represent the subsequent load
|
|
177
|
+
cacheAdjustedSegmentEnergy[key] += value * returnView * dataReloadRatio;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return cacheAdjustedSegmentEnergy;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Accept a figure for bytes, and return the total figure for energy per visit
|
|
185
|
+
* using the default caching assumptions for loading a single website
|
|
186
|
+
*
|
|
187
|
+
* @param {number} bytes
|
|
188
|
+
* @return {number} the total energy use for the visit, after applying the caching assumptions
|
|
189
|
+
*/
|
|
190
|
+
energyPerVisit(bytes) {
|
|
191
|
+
// fetch the values using the default caching assumptions
|
|
192
|
+
const energyValues = Object.values(this.energyPerVisitByComponent(bytes));
|
|
193
|
+
|
|
194
|
+
// return the summed of the values to return our single number
|
|
195
|
+
return energyValues.reduce(
|
|
196
|
+
(prevValue, currentValue) => prevValue + currentValue
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// TODO: this method looks like it applies the carbon intensity
|
|
201
|
+
// change to the *entire* system, not just the datacenter.
|
|
202
|
+
emissionsPerVisitInGrams(energyPerVisit, carbonintensity = GLOBAL_INTENSITY) {
|
|
203
|
+
return formatNumber(energyPerVisit * carbonintensity);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
annualEnergyInKwh(energyPerVisit, monthlyVisitors = 1000) {
|
|
207
|
+
return energyPerVisit * monthlyVisitors * 12;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
annualEmissionsInGrams(co2grams, monthlyVisitors = 1000) {
|
|
211
|
+
return co2grams * monthlyVisitors * 12;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
annualSegmentEnergy(annualEnergy) {
|
|
215
|
+
return {
|
|
216
|
+
consumerDeviceEnergy: formatNumber(annualEnergy * END_USER_DEVICE_ENERGY),
|
|
217
|
+
networkEnergy: formatNumber(annualEnergy * NETWORK_ENERGY),
|
|
218
|
+
dataCenterEnergy: formatNumber(annualEnergy * DATACENTER_ENERGY),
|
|
219
|
+
productionEnergy: formatNumber(annualEnergy * PRODUCTION_ENERGY),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = SustainableWebDesign;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const SustainableWebDesign = require("./sustainable-web-design");
|
|
2
|
+
|
|
3
|
+
describe("sustainable web design model", () => {
|
|
4
|
+
const swd = new SustainableWebDesign();
|
|
5
|
+
const averageWebsiteInBytes = 2257715.2;
|
|
6
|
+
|
|
7
|
+
describe("energyPerByteByComponent", () => {
|
|
8
|
+
it("should return a object with numbers for each system component", () => {
|
|
9
|
+
const groupedEnergy = swd.energyPerByteByComponent(averageWebsiteInBytes);
|
|
10
|
+
|
|
11
|
+
expect(groupedEnergy.consumerDeviceEnergy).toBeCloseTo(0.00088564, 8);
|
|
12
|
+
expect(groupedEnergy.networkEnergy).toBeCloseTo(0.00023844, 8);
|
|
13
|
+
expect(groupedEnergy.productionEnergy).toBeCloseTo(0.0003236, 8);
|
|
14
|
+
expect(groupedEnergy.dataCenterEnergy).toBeCloseTo(0.00025547, 8);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("energyPerByte", () => {
|
|
19
|
+
it("should return a number in kilowatt hours for the given data transfer in bytes", () => {
|
|
20
|
+
const energyForTransfer = swd.energyPerByte(averageWebsiteInBytes);
|
|
21
|
+
expect(energyForTransfer).toBeCloseTo(0.00170316, 7);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("perByte", () => {
|
|
26
|
+
it("should return a single number for CO2 emissions", () => {
|
|
27
|
+
expect(typeof swd.perByte(2257715.2)).toBe("number");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("energyPerVisit", function () {
|
|
32
|
+
it("should return a number", () => {
|
|
33
|
+
expect(typeof swd.energyPerVisit(averageWebsiteInBytes)).toBe("number");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should calculate the correct energy", () => {
|
|
37
|
+
expect(swd.energyPerVisit(averageWebsiteInBytes)).toBe(
|
|
38
|
+
0.0004513362121582032
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("emissionsPerVisitInGrams", function () {
|
|
44
|
+
it("should calculate the correct co2 per visit", () => {
|
|
45
|
+
const averageWebsiteInBytes = 2257715.2;
|
|
46
|
+
const energy = swd.energyPerVisit(averageWebsiteInBytes);
|
|
47
|
+
expect(swd.emissionsPerVisitInGrams(energy)).toEqual(0.2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should accept a dynamic KwH value", () => {
|
|
51
|
+
const averageWebsiteInBytes = 2257715.2;
|
|
52
|
+
const energy = swd.energyPerVisit(averageWebsiteInBytes);
|
|
53
|
+
expect(swd.emissionsPerVisitInGrams(energy, 245)).toEqual(0.11);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("annualEnergyInKwh", function () {
|
|
58
|
+
it("should calculate the correct energy in kWh", () => {
|
|
59
|
+
expect(swd.annualEnergyInKwh(averageWebsiteInBytes)).toBe(27092582400);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("annualEmissionsInGrams", function () {
|
|
64
|
+
it("should calculate the corrent energy in grams", () => {
|
|
65
|
+
expect(swd.annualEmissionsInGrams(averageWebsiteInBytes)).toBe(
|
|
66
|
+
27092582400
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("annualSegmentEnergy", function () {
|
|
72
|
+
it("should return the correct values", () => {
|
|
73
|
+
expect(swd.annualSegmentEnergy(averageWebsiteInBytes)).toEqual({
|
|
74
|
+
consumerDeviceEnergy: 1174011.9,
|
|
75
|
+
dataCenterEnergy: 338657.28,
|
|
76
|
+
networkEnergy: 316080.13,
|
|
77
|
+
productionEnergy: 428965.89,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|