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