@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 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
- - Include a new alternative, "Green Byte Model" with the figures after speaking to folks at the IEA and other places.
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 1bye model will give different numbers now. It's mentioned in `#fixed` but it's worth repeating.
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
- - Update README
65
- - Update the emissions figured for green energy after further research on methodology with @@JamieBeevor
66
- - Incorproate class based CO2 models from @soulgalore
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
- Add the (currently unused) green byte model.
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 to generate, there is an environmental cost to every upload and download we make over the internet.
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
- It does this by implementing 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], to return a number for the estimated CO2 emissions for the corresponding number of bytes sent over the wire.
12
+ ### How it works
13
13
 
14
- It is currently used in the web performance tool [sitespeed.io][], to help developers build greener, more planet friendly digital services.
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, 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].
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, like Node.js, that support this.
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.8.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
- KWH_PER_BYTE_IN_DC,
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", function () {
6
- describe("perByte", function () {
7
- it.only("returns a simple average of the different networks", function () {
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(oneByte.KWH_PER_BYTE_FOR_NETWORK.toFixed(12)).toBe(expected_val);
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 oneByte = require("./1byte.js");
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
- perByte(bytes, green) {
25
- // return a CO2 figure for energy used to shift the corresponding
26
- // the data transfer.
10
+ // default model
11
+ this.model = new onebyte.OneByte();
27
12
 
28
- if (bytes < 1) {
29
- return 0;
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
- const KwHPerByte = KWH_PER_BYTE_IN_DC + KWH_PER_BYTE_FOR_NETWORK;
45
- return bytes * KwHPerByte * CO2_PER_KWH_IN_DC_GREY;
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", function () {
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("perByte", function () {
30
- it("returns a CO2 number for data transfer using 'grey' power", function () {
31
- expect(co2.perByte(MILLION).toPrecision(5)).toBe(
32
- MILLION_GREY.toPrecision(5)
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
- it("returns a lower CO2 number for data transfer from domains using entirely 'green' power", function () {
37
- expect(co2.perByte(MILLION, false).toPrecision(5)).toBe(
38
- MILLION_GREY.toPrecision(5)
39
- );
40
- expect(co2.perByte(MILLION, true).toPrecision(5)).toBe(
41
- MILLION_GREEN.toPrecision(5)
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
- describe("perPage", function () {
47
- it("returns CO2 for total transfer for page", function () {
48
- const pages = pagexray.convert(har);
49
- const pageXrayRun = pages[0];
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
- expect(co2.perPage(pageXrayRun).toPrecision(5)).toBe(
52
- TGWF_GREY_VALUE.toPrecision(5)
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
- it("returns lower CO2 for page served from green site", function () {
56
- const pages = pagexray.convert(har);
57
- const pageXrayRun = pages[0];
58
- let green = [
59
- "www.thegreenwebfoundation.org",
60
- "fonts.googleapis.com",
61
- "ajax.googleapis.com",
62
- "assets.digitalclimatestrike.net",
63
- "cdnjs.cloudflare.com",
64
- "graphite.thegreenwebfoundation.org",
65
- "analytics.thegreenwebfoundation.org",
66
- "fonts.gstatic.com",
67
- "api.thegreenwebfoundation.org",
68
- ];
69
- expect(co2.perPage(pageXrayRun, green)).toBeLessThan(TGWF_GREY_VALUE);
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
- it("returns a lower CO2 number where *some* domains use green power", function () {
72
- const pages = pagexray.convert(har);
73
- const pageXrayRun = pages[0];
74
- // green can be true, or a array containing entries
75
- let green = [
76
- "www.thegreenwebfoundation.org",
77
- "fonts.googleapis.com",
78
- "ajax.googleapis.com",
79
- "assets.digitalclimatestrike.net",
80
- "cdnjs.cloudflare.com",
81
- "graphite.thegreenwebfoundation.org",
82
- "analytics.thegreenwebfoundation.org",
83
- "fonts.gstatic.com",
84
- "api.thegreenwebfoundation.org",
85
- ];
86
- expect(co2.perPage(pageXrayRun, green).toPrecision(5)).toBe(
87
- TGWF_MIXED_VALUE.toPrecision(5)
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
- describe("perDomain", function () {
92
- it("shows object listing Co2 for each domain", function () {
93
- const pages = pagexray.convert(har);
94
- const pageXrayRun = pages[0];
95
- const res = co2.perDomain(pageXrayRun);
96
-
97
- const domains = [
98
- "thegreenwebfoundation.org",
99
- "www.thegreenwebfoundation.org",
100
- "maxcdn.bootstrapcdn.com",
101
- "fonts.googleapis.com",
102
- "ajax.googleapis.com",
103
- "assets.digitalclimatestrike.net",
104
- "cdnjs.cloudflare.com",
105
- "graphite.thegreenwebfoundation.org",
106
- "analytics.thegreenwebfoundation.org",
107
- "fonts.gstatic.com",
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
- it("shows lower Co2 for green domains", function () {
117
- const pages = pagexray.convert(har);
118
- const pageXrayRun = pages[0];
119
-
120
- const greenDomains = [
121
- "www.thegreenwebfoundation.org",
122
- "fonts.googleapis.com",
123
- "ajax.googleapis.com",
124
- "assets.digitalclimatestrike.net",
125
- "cdnjs.cloudflare.com",
126
- "graphite.thegreenwebfoundation.org",
127
- "analytics.thegreenwebfoundation.org",
128
- "fonts.gstatic.com",
129
- "api.thegreenwebfoundation.org",
130
- ];
131
- const res = co2.perDomain(pageXrayRun);
132
- const resWithGreen = co2.perDomain(pageXrayRun, greenDomains);
133
-
134
- for (let obj of res) {
135
- expect(typeof obj.co2).toBe("number");
136
- }
137
- for (let obj of greenDomains) {
138
- let index = 0;
139
- expect(resWithGreen[index].co2).toBeLessThan(res[index].co2);
140
- index++;
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
  });
@@ -0,0 +1,5 @@
1
+ const GIGABYTE = 1024 * 1024 * 1024;
2
+
3
+ module.exports = {
4
+ GIGABYTE,
5
+ };
@@ -0,0 +1,3 @@
1
+ const fileSize = require("./file-size");
2
+
3
+ module.exports = { fileSize };
@@ -0,0 +1,5 @@
1
+ const formatNumber = (num) => parseFloat(num.toFixed(2));
2
+
3
+ module.exports = {
4
+ formatNumber,
5
+ };
@@ -3,9 +3,9 @@
3
3
  const hosting = require("./hosting-api");
4
4
  const nock = require("nock");
5
5
 
6
- describe("hostingAPI", function () {
7
- describe("checking a single domain with #check", function () {
8
- it("using the API", async function () {
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", function () {
20
- it("using the API", async function () {
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", function () {
16
- describe("checking a single domain with #check", function () {
17
- test("tries to use a local database if available ", async function () {
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", function () {
23
- test("tries to use a local database if available", async function () {
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
@@ -3,7 +3,7 @@
3
3
  const hosting = require("./hosting-json");
4
4
  const path = require("path");
5
5
 
6
- describe("hostingJSON", function () {
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", function () {
22
- test("against the list of domains as JSON", async function () {
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", function () {
29
- test("against the list of domains as JSON loaded from a gzipped JSON", async function () {
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", function () {
36
- test("against the list of domains as JSON", async function () {
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
 
@@ -14,9 +14,9 @@ const jsonPath = path.resolve(
14
14
  "url2green.test.json"
15
15
  );
16
16
 
17
- describe("hosting", function () {
17
+ describe("hosting", () => {
18
18
  let har;
19
- beforeEach(function () {
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 ", function () {
28
- it("it returns a list of green domains, when passed a page object", async function () {
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", function () {
57
- it("use the API instead", async function () {
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", function () {
64
- it("Use the API", async function () {
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", function () {
72
- it("use the API", async function () {
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
+ ![swd model](../images/swd-energy-usage.png)
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
+ });