CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/compute/cloud/google-cloud/compute-cost.ts
Views: 687
1
import type { GoogleCloudConfiguration } from "@cocalc/util/db-schema/compute-servers";
2
import debug from "debug";
3
import { DNS_COST_PER_HOUR } from "@cocalc/util/compute/dns";
4
5
const log = debug("cocalc:util:compute-cost");
6
7
// copy-pasted from my @cocalc/gcloud-pricing-calculator package to help with sanity in code below.
8
9
interface PriceData {
10
prices?: { [region: string]: number };
11
spot?: { [region: string]: number };
12
vcpu?: number;
13
memory?: number;
14
count?: number; // for gpu's only
15
max?: number; // for gpu's only
16
machineType?: string | { [count: number]: string[] }; // for gpu's only
17
}
18
19
interface ZoneData {
20
machineTypes: string; // ['e2','n1','n2', 't2d' ... ] -- array of machine type prefixes
21
location: string; // description of where it is
22
lowC02: boolean; // if true, low c02 emissions
23
gpus: boolean; // if true, has gpus
24
}
25
26
export interface BucketPricing {
27
Standard?: number;
28
Nearline?: number;
29
Coldline?: number;
30
Archive?: number;
31
}
32
33
export type GoogleWorldLocations =
34
| "APAC"
35
| "Europe"
36
| "Middle East"
37
| "North America"
38
| "South Africa"
39
| "South America";
40
41
interface GoogleWorldPrices {
42
APAC: number;
43
Europe: number;
44
"Middle East": number;
45
"North America": number;
46
"South Africa": number;
47
"South America": number;
48
}
49
50
export interface GoogleCloudData {
51
machineTypes: { [machineType: string]: PriceData };
52
disks: {
53
"pd-standard": { prices: { [zone: string]: number } };
54
"pd-ssd": { prices: { [zone: string]: number } };
55
"pd-balanced": { prices: { [zone: string]: number } };
56
"hyperdisk-balanced-capacity": { prices: { [zone: string]: number } };
57
"hyperdisk-balanced-iops": { prices: { [zone: string]: number } };
58
"hyperdisk-balanced-throughput": { prices: { [zone: string]: number } };
59
};
60
accelerators: { [acceleratorType: string]: PriceData };
61
zones: { [zone: string]: ZoneData };
62
// markup percentage: optionally include markup to always increase price by this amount,
63
// e.g., if markup is 42, then price will be multiplied by 1.42.
64
markup?: number;
65
storage: {
66
atRest: {
67
dualRegions: { [region: string]: BucketPricing };
68
multiRegions: {
69
asia: BucketPricing;
70
eu: BucketPricing;
71
us: BucketPricing;
72
};
73
regions: {
74
[region: string]: BucketPricing;
75
};
76
};
77
dataTransferInsideGoogleCloud: {
78
APAC: GoogleWorldPrices;
79
Europe: GoogleWorldPrices;
80
"Middle East": GoogleWorldPrices;
81
"North America": GoogleWorldPrices;
82
"South Africa": GoogleWorldPrices;
83
"South America": GoogleWorldPrices;
84
};
85
dataTransferOutsideGoogleCloud: {
86
worldwide: number;
87
china: number;
88
australia: number;
89
};
90
interRegionReplication: {
91
asia: number;
92
eu: number;
93
us: number;
94
};
95
retrieval: {
96
standard: number;
97
nearline: number;
98
coldline: number;
99
archive: number;
100
};
101
singleRegionOperations: {
102
standard: { classA1000: number; classB1000: number };
103
nearline: { classA1000: number; classB1000: number };
104
coldline: { classA1000: number; classB1000: number };
105
archive: { classA1000: number; classB1000: number };
106
};
107
};
108
}
109
110
interface Options {
111
configuration: GoogleCloudConfiguration;
112
// output of getData from this package -- https://www.npmjs.com/package/@cocalc/gcloud-pricing-calculator
113
// except that package is backend only (it caches to disk), so data is obtained via an api, then used here.
114
priceData: GoogleCloudData;
115
state?: "running" | "off" | "suspended";
116
}
117
118
/*
119
Returns the cost per hour in usd of a given Google Cloud vm configuration,
120
given the result of getData from @cocalc/gcloud-pricing-calculator.
121
*/
122
export default function computeCost({
123
configuration,
124
priceData,
125
state = "running",
126
}: Options): number {
127
if (state == "off") {
128
return computeOffCost({ configuration, priceData });
129
} else if (state == "suspended") {
130
return computeSuspendedCost({ configuration, priceData });
131
} else if (state == "running") {
132
return computeRunningCost({ configuration, priceData });
133
} else {
134
throw Error(`computing cost for state "${state}" not implemented`);
135
}
136
}
137
138
function computeRunningCost({ configuration, priceData }) {
139
const instanceCost = computeInstanceCost({ configuration, priceData });
140
const diskCost = computeDiskCost({ configuration, priceData });
141
const externalIpCost = computeExternalIpCost({ configuration, priceData });
142
const acceleratorCost = computeAcceleratorCost({ configuration, priceData });
143
const dnsCost = computeDnsCost({ configuration });
144
log("cost", {
145
instanceCost,
146
diskCost,
147
externalIpCost,
148
acceleratorCost,
149
dnsCost,
150
});
151
return instanceCost + diskCost + externalIpCost + acceleratorCost + dnsCost;
152
}
153
154
function computeDnsCost({ configuration }) {
155
return configuration.dns ? DNS_COST_PER_HOUR : 0;
156
}
157
158
export function computeInstanceCost({ configuration, priceData }) {
159
const data = priceData.machineTypes[configuration.machineType];
160
if (data == null) {
161
throw Error(
162
`unable to determine cost since machine type ${configuration.machineType} is unknown. Select a different machine type.`,
163
);
164
}
165
const cost =
166
data[configuration.spot ? "spot" : "prices"]?.[configuration.region];
167
if (cost == null) {
168
if (configuration.spot && Object.keys(data["spot"]).length == 0) {
169
throw Error(
170
`spot instance pricing for ${configuration.machineType} is not available`,
171
);
172
}
173
throw Error(
174
`unable to determine cost since machine type ${configuration.machineType} is not available in the region '${configuration.region}'. Select a different region.`,
175
);
176
}
177
return markup({ cost, priceData });
178
}
179
180
// Compute the total cost of disk for this configuration, including any markup.
181
182
// for now this is the only thing we support
183
export const DEFAULT_HYPERDISK_BALANCED_IOPS = 3000;
184
export const DEFAULT_HYPERDISK_BALANCED_THROUGHPUT = 140;
185
186
export function hyperdiskCostParams({ region, priceData }): {
187
capacity: number;
188
iops: number;
189
throughput: number;
190
} {
191
const diskType = "hyperdisk-balanced";
192
const capacity =
193
priceData.disks["hyperdisk-balanced-capacity"]?.prices[region];
194
if (!capacity) {
195
throw Error(
196
`Unable to determine ${diskType} capacity pricing in ${region}. Select a different region.`,
197
);
198
}
199
const iops = priceData.disks["hyperdisk-balanced-iops"]?.prices[region];
200
if (!iops) {
201
throw Error(
202
`Unable to determine ${diskType} iops pricing in ${region}. Select a different region.`,
203
);
204
}
205
const throughput =
206
priceData.disks["hyperdisk-balanced-throughput"]?.prices[region];
207
if (!throughput) {
208
throw Error(
209
`Unable to determine ${diskType} throughput pricing in ${region}. Select a different region.`,
210
);
211
}
212
return { capacity, iops, throughput };
213
}
214
215
export function computeDiskCost({ configuration, priceData }: Options): number {
216
const diskType = configuration.diskType ?? "pd-standard";
217
let cost;
218
if (diskType == "hyperdisk-balanced") {
219
// per hour pricing for hyperdisks is NOT "per GB". The pricing is per hour, but the
220
// formula is not as simple as "per GB", so we compute the cost per hour via
221
// the more complicated formula here.
222
const { capacity, iops, throughput } = hyperdiskCostParams({
223
priceData,
224
region: configuration.region,
225
});
226
cost =
227
(configuration.diskSizeGb ?? 10) * capacity +
228
(configuration.hyperdiskBalancedIops ?? DEFAULT_HYPERDISK_BALANCED_IOPS) *
229
iops +
230
(configuration.hyperdiskBalancedThroughput ??
231
DEFAULT_HYPERDISK_BALANCED_THROUGHPUT) *
232
throughput;
233
} else {
234
// per hour pricing for the rest of the disks is just "per GB" via the formula here.
235
const diskCostPerGB =
236
priceData.disks[diskType]?.prices[configuration.region];
237
log("disk cost per GB per hour", { diskCostPerGB });
238
if (!diskCostPerGB) {
239
throw Error(
240
`unable to determine cost since disk cost in region ${configuration.region} is unknown. Select a different region.`,
241
);
242
}
243
cost = diskCostPerGB * (configuration.diskSizeGb ?? 10);
244
}
245
return markup({ cost, priceData });
246
}
247
248
export function computeOffCost({ configuration, priceData }: Options): number {
249
const diskCost = computeDiskCost({ configuration, priceData });
250
const dnsCost = computeDnsCost({ configuration });
251
252
return diskCost + dnsCost;
253
}
254
255
export function computeSuspendedCost({
256
configuration,
257
priceData,
258
}: Options): number {
259
const diskCost = computeDiskCost({ configuration, priceData });
260
const memoryCost = computeSuspendedMemoryCost({ configuration, priceData });
261
const dnsCost = computeDnsCost({ configuration });
262
263
return diskCost + memoryCost + dnsCost;
264
}
265
266
export function computeSuspendedMemoryCost({ configuration, priceData }) {
267
// how much memory does it have?
268
const data = priceData.machineTypes[configuration.machineType];
269
if (data == null) {
270
throw Error(
271
`unable to determine cost since machine type ${configuration.machineType} is unknown. Select a different machine type.`,
272
);
273
}
274
const { memory } = data;
275
if (!memory) {
276
throw Error(
277
`cannot compute suspended cost without knowing memory of machine type '${configuration.machineType}'`,
278
);
279
}
280
// Pricing / GB of RAM / month is here -- https://cloud.google.com/compute/all-pricing#suspended_vm_instances
281
// It is really weird in the table, e.g., in some places it claims to be basically 0, and in Sao Paulo it is
282
// 0.25/GB/month, which seems to be the highest. Until I nail this down properly with SKU's, for cocalc
283
// we will just use 0.25 + the markup.
284
const cost = (memory * 0.25) / 730;
285
return markup({ cost, priceData });
286
}
287
288
// TODO: This could change and should be in pricing data --
289
// https://cloud.google.com/vpc/network-pricing#ipaddress
290
export const EXTERNAL_IP_COST = {
291
standard: 0.005,
292
spot: 0.0025,
293
};
294
295
export function computeExternalIpCost({ configuration, priceData }) {
296
if (!configuration.externalIp) {
297
return 0;
298
}
299
let cost;
300
if (configuration.spot) {
301
cost = EXTERNAL_IP_COST.spot;
302
} else {
303
cost = EXTERNAL_IP_COST.standard;
304
}
305
return markup({ cost, priceData });
306
}
307
308
export function computeAcceleratorCost({ configuration, priceData }) {
309
if (!configuration.acceleratorType) {
310
return 0;
311
}
312
// we have 1 or more GPUs:
313
const acceleratorCount = configuration.acceleratorCount ?? 1;
314
// sometimes google has "tesla-" in the name, sometimes they don't,
315
// but our pricing data doesn't.
316
const acceleratorData =
317
priceData.accelerators[configuration.acceleratorType] ??
318
priceData.accelerators[configuration.acceleratorType.replace("tesla-", "")];
319
if (acceleratorData == null) {
320
throw Error(`unknown GPU accelerator ${configuration.acceleratorType}`);
321
}
322
323
if (
324
typeof acceleratorData.machineType == "string" &&
325
!configuration.machineType.startsWith(acceleratorData.machineType)
326
) {
327
throw Error(
328
`machine type for ${configuration.acceleratorType} must be ${acceleratorData.machineType}. Change the machine type.`,
329
);
330
}
331
if (typeof acceleratorData.machineType == "object") {
332
let v: string[] = acceleratorData.machineType[acceleratorCount];
333
if (v == null) {
334
throw Error(`invalid number of GPUs`);
335
}
336
if (!v.includes(configuration.machineType)) {
337
throw Error(
338
`machine type for ${
339
configuration.acceleratorType
340
} with count ${acceleratorCount} must be one of ${v.join(", ")}`,
341
);
342
}
343
}
344
let costPer =
345
acceleratorData[configuration.spot ? "spot" : "prices"]?.[
346
configuration.zone
347
];
348
log("accelerator cost per", { costPer });
349
if (costPer == null) {
350
throw Error(
351
`GPU accelerator ${configuration.acceleratorType} not available in zone ${configuration.zone}. Select a different zone.`,
352
);
353
}
354
return markup({ cost: costPer * acceleratorCount, priceData });
355
}
356
357
export const DATA_TRANSFER_OUT_COST_PER_GiB = 0.15;
358
export function computeNetworkCost(dataTransferOutGiB: number): number {
359
// The worst possible case is 0.15
360
// https://cloud.google.com/vpc/network-pricing
361
// We might come up with a most sophisticated and affordable model if we
362
// can figure it out; however, it seems possibly extremely difficult.
363
// For now our solution will be to charge a flat 0.15 fee, and don't
364
// include any markup.
365
const cost = dataTransferOutGiB * DATA_TRANSFER_OUT_COST_PER_GiB;
366
return cost;
367
}
368
369
export function markup({ cost, priceData }) {
370
if (priceData.markup) {
371
return cost * (1 + priceData.markup / 100.0);
372
}
373
return cost;
374
}
375
376