From 8e662457906e6a488e17cdfae78bfda5f8b88dc6 Mon Sep 17 00:00:00 2001 From: Simon Fishel Date: Sat, 23 Mar 2024 21:51:37 -0700 Subject: [PATCH] fix tests, clean up comments, update per-visit types --- __mocks__/https.js | 1 + src/co2.js | 12 ++++----- src/helpers/index.js | 1 + src/hosting-json.node.js | 10 ++++---- src/sustainable-web-design.js | 48 +++++++++++++++++++++++++---------- src/types.js | 28 +++++++++++++++----- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/__mocks__/https.js b/__mocks__/https.js index a4cee94..c2fbe98 100644 --- a/__mocks__/https.js +++ b/__mocks__/https.js @@ -3,6 +3,7 @@ const https = jest.createMockFromModule("https"); import { Stream } from "stream"; const stream = new Stream(); +stream.statusCode = 200; https.get.mockImplementation((url, options, callback) => { url, { headers: getApiRequestHeaders("TestRunner") }, callback(stream); diff --git a/src/co2.js b/src/co2.js index ecd0ced..001f53a 100644 --- a/src/co2.js +++ b/src/co2.js @@ -12,8 +12,8 @@ import { parseOptions, toTotalCO2 } from "./helpers/index.js"; class CO2 { /** * @param {object} options - * @param {'1byte' | 'swd'=} options.model - * @param {'segment'=} options.results + * @param {'1byte' | 'swd'=} options.model The model to use (OneByte or Sustainable Web Design) + * @param {'segment'=} options.results Optional. Whether to return segment-level emissions estimates. */ constructor(options = {}) { this.model = new SustainableWebDesign(); @@ -53,7 +53,7 @@ class CO2 { * * @param {number} bytes * @param {boolean} green - * @return {number | CO2ByComponentWithTotal} the amount of CO2 in grammes + * @return {number | AdjustedCO2ByComponentWithTotal} the amount of CO2 in grammes */ perVisit(bytes, green = false) { if ("perVisit" in this.model) { @@ -126,9 +126,7 @@ class CO2 { } return { - co2: toTotalCO2( - this.model.perVisit(bytes, green, this._segment, adjustments) - ), + co2: this.model.perVisit(bytes, green, this._segment, adjustments), green, variables: { description: @@ -221,6 +219,7 @@ class CO2 { /** @type {Record>} */ const co2PerContentType = {}; for (let asset of pageXray.assets) { + // TODO (simon) check that this `domain` -> `host` conversion is correct const domain = new URL(asset.url).host; const transferSize = asset.transferSize; const co2ForTransfer = toTotalCO2( @@ -276,7 +275,6 @@ class CO2 { /** * @param {PageXRay} pageXray * @param {string[]=} greenDomains - * @returns */ perParty(pageXray, greenDomains) { let firstParty = 0; diff --git a/src/helpers/index.js b/src/helpers/index.js index d6a05d6..9f85508 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -39,6 +39,7 @@ function parseOptions(options) { } adjustments.gridIntensity["device"] = { country: device.country, + // TODO (simon) check that parseFloat can be safely removed here value: averageIntensity.data[device.country?.toUpperCase()], }; } else if (typeof device === "number") { diff --git a/src/hosting-json.node.js b/src/hosting-json.node.js index d0166b2..3a19e1b 100644 --- a/src/hosting-json.node.js +++ b/src/hosting-json.node.js @@ -10,17 +10,17 @@ const gunzip = promisify(zlib.gunzip); /** * Converts a readable stream to a string. * @param {fs.ReadStream} stream - The readable stream to convert. - * @returns {Promise} A promise that resolves to the string representation of the stream. + * @returns {Promise} A promise that resolves to the string representation of the stream. */ -async function streamToString(stream) { +async function streamToBuffer(stream) { return new Promise((resolve, reject) => { /** @type {Buffer[]} */ const chunks = []; stream.on("error", reject); stream.on("data", (chunk) => - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)) ); - stream.on("end", () => resolve(Buffer.concat(chunks).toString())); + stream.on("end", () => resolve(Buffer.concat(chunks))); }); } @@ -31,7 +31,7 @@ async function streamToString(stream) { */ async function getGzippedFileAsJson(jsonPath) { const readStream = fs.createReadStream(jsonPath); - const text = await streamToString(readStream); + const text = await streamToBuffer(readStream); const unzipped = await gunzip(text); return unzipped.toString(); } diff --git a/src/sustainable-web-design.js b/src/sustainable-web-design.js index 70d20be..4642366 100644 --- a/src/sustainable-web-design.js +++ b/src/sustainable-web-design.js @@ -48,10 +48,13 @@ class SustainableWebDesign { * Accept an object keys by the different system components, and * return an object with the co2 figures key by the each component * - * @param {EnergyByComponent} energyByComponent - energy grouped by the four system components + * @template {AdjustedEnergyByComponent | EnergyByComponent} EnergyObject + * @template [CO2Object=EnergyObject extends AdjustedEnergyByComponent ? AdjustedCO2ByComponent : CO2ByComponent] + * @param {EnergyObject} energyByComponent - energy grouped by the four system components + * // TODO (simon) check on this type for carbonIntensity * @param {(number | boolean)=} carbonIntensity - carbon intensity to apply to the datacentre values * @param {ModelAdjustments=} options - carbon intensity to apply to the datacentre values - * @return {CO2ByComponent} the total number in grams of CO2 equivalent emissions + * @return {CO2Object} the total number in grams of CO2 equivalent emissions */ co2byComponent( energyByComponent, @@ -84,14 +87,28 @@ class SustainableWebDesign { dataCenterCarbonIntensity = RENEWABLES_GRID_INTENSITY; } - return { - dataCenterCO2: - energyByComponent.dataCenterEnergy * dataCenterCarbonIntensity, - consumerDeviceCO2: - energyByComponent.consumerDeviceEnergy * deviceCarbonIntensity, - networkCO2: energyByComponent.networkEnergy * networkCarbonIntensity, - productionCO2: energyByComponent.productionEnergy * globalEmissions, - }; + /** @type {Record} */ + const returnCO2ByComponent = {}; + for (const [key, value] of Object.entries(energyByComponent)) { + // we update the datacentre, as that's what we have information + // about. + if (key.startsWith("dataCenterEnergy")) { + returnCO2ByComponent[key.replace("Energy", "CO2")] = + value * dataCenterCarbonIntensity; + } else if (key.startsWith("consumerDeviceEnergy")) { + returnCO2ByComponent[key.replace("Energy", "CO2")] = + value * deviceCarbonIntensity; + } else if (key.startsWith("networkEnergy")) { + returnCO2ByComponent[key.replace("Energy", "CO2")] = + value * networkCarbonIntensity; + } else { + // Use the global intensity for the remaining segments + returnCO2ByComponent[key.replace("Energy", "CO2")] = + value * globalEmissions; + } + } + + return /** @type {CO2Object} */ (returnCO2ByComponent); } /** @@ -152,7 +169,7 @@ class SustainableWebDesign { * @param {boolean} carbonIntensity - a boolean indicating whether the data center is green or not * @param {boolean} segmentResults - a boolean indicating whether to return the results broken down by component * @param {ModelAdjustments=} options - an object containing the grid intensity and first/return visitor values - * @return {number | CO2ByComponentWithTotal} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component + * @return {number | AdjustedCO2ByComponentWithTotal} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component */ perVisit( bytes, @@ -160,7 +177,8 @@ class SustainableWebDesign { segmentResults = false, options = undefined ) { - const energyBycomponent = this.energyPerByteByComponent(bytes); + // TODO (simon) figure out if this method call is correct + const energyBycomponent = this.energyPerVisitByComponent(bytes, options); if (typeof carbonIntensity !== "boolean") { // otherwise when faced with non numeric values throw an error @@ -221,7 +239,7 @@ class SustainableWebDesign { * @param {number=} returnView - what percentage of visits are loading this page for subsequent times * @param {number=} dataReloadRatio - what percentage of a page is reloaded on each subsequent page view * - * @return {AdjustedEnergyComponent} Object containing the energy in kilowatt hours, keyed by system component + * @return {AdjustedEnergyByComponent} Object containing the energy in kilowatt hours, keyed by system component */ energyPerVisitByComponent( bytes, @@ -259,7 +277,9 @@ class SustainableWebDesign { value * returnView * dataReloadRatio; } - return /** @type {AdjustedEnergyComponent} */ (cacheAdjustedSegmentEnergy); + return /** @type {AdjustedEnergyByComponent} */ ( + cacheAdjustedSegmentEnergy + ); } /** diff --git a/src/types.js b/src/types.js index 5f9a162..d9badb7 100644 --- a/src/types.js +++ b/src/types.js @@ -14,10 +14,10 @@ * Or, an object, which contains a key of country and a value that is an Alpha-3 ISO country code. * * @typedef ModelOptions - * @property {ModelOptionsGridIntensity=} gridIntensity - * @property {number=} dataReloadRatio - * @property {number=} returnVisitPercentage - * @property {number=} firstVisitPercentage + * @property {ModelOptionsGridIntensity=} gridIntensity Segment-level description of the grid carbon intensity. + * @property {number=} dataReloadRatio A number between 0 and 1 representing the percentage of data that is downloaded by return visitors. + * @property {number=} returnVisitPercentage A number between 0 and 1 representing the percentage of returning visitors. + * @property {number=} firstVisitPercentage A number between 0 and 1 representing the percentage of new visitors. * * @typedef ModelAdjustmentSegment * @property {number} value @@ -55,7 +55,7 @@ * @property {TraceResultVariables} variables - The variables used to calculate the CO2 estimate * * @typedef CO2EstimateTraceResultPerVisit - * @property {number} co2 - The CO2 estimate in grams/kilowatt-hour + * @property {number | AdjustedCO2ByComponentWithTotal} co2 - The CO2 estimate in grams/kilowatt-hour * @property {boolean} green - Whether the domain is green or not * @property {TraceResultVariables} variables - The variables used to calculate the CO2 estimate * @@ -81,8 +81,12 @@ * @property {number} productionEnergy * @property {number} dataCenterEnergy * - * @typedef {Object} AdjustedEnergyComponent - * @type {{ [K in keyof EnergyByComponent as `${K} - first`]: EnergyByComponent[K] } & { [K in keyof EnergyByComponent as `${K} - subsequest`]: EnergyByComponent[K] }} + * @typedef {Object} AdjustedEnergyByComponent + * @type {{ + * [K in keyof EnergyByComponent as `${K} - first`]: EnergyByComponent[K] + * } & { + * [K in keyof EnergyByComponent as `${K} - subsequest`]: EnergyByComponent[K] + * }} * * @typedef CO2ByComponent * @property {number} consumerDeviceCO2 @@ -90,6 +94,13 @@ * @property {number} productionCO2 * @property {number} dataCenterCO2 * + * @typedef {Object} AdjustedCO2ByComponent + * @type {{ + * [K in keyof CO2ByComponent as `${K} - first`]: CO2ByComponent[K] + * } & { + * [K in keyof CO2ByComponent as `${K} - subsequest`]: CO2ByComponent[K] + * }} + * * @typedef CO2ByComponentWithTotal * @property {number} consumerDeviceCO2 * @property {number} networkCO2 @@ -97,6 +108,9 @@ * @property {number} dataCenterCO2 * @property {number} total * + * @typedef {Object} AdjustedCO2ByComponentWithTotal + * @type {AdjustedCO2ByComponent & { total: number }} + * * @typedef PageXRayDomain * @property {number} transferSize *