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/storage-costs.ts
Views: 687
1
/*
2
Estimating Google Cloud storage costs.
3
*/
4
5
import type { CloudFilesystemMetric } from "@cocalc/util/db-schema/cloud-filesystems";
6
import type { GoogleCloudData } from "@cocalc/util/compute/cloud/google-cloud/compute-cost";
7
import { GOOGLE_REGION_PREFIX_TO_LOCATION } from "@cocalc/util/db-schema/cloud-filesystems";
8
import { capitalize } from "@cocalc/util/misc";
9
import { commas, human_readable_size } from "@cocalc/util/misc";
10
11
const MS_IN_MONTH = 730 * 60 * 60 * 1000;
12
const GB = 1024 * 1024 * 1024;
13
14
interface Options {
15
metrics: CloudFilesystemMetric[];
16
priceData: GoogleCloudData;
17
}
18
19
export function estimateCost_bytes_used({ metrics, priceData }: Options): {
20
cost: number[];
21
rate_per_GB_per_month: number;
22
} {
23
let cost: number[] = [0];
24
if (metrics.length == 0) {
25
return { cost, rate_per_GB_per_month: 0 };
26
}
27
const { bucket_location, bucket_storage_class } = metrics[0];
28
const rate_per_GB_per_month = getAtRestPrice({
29
priceData,
30
bucket_storage_class,
31
bucket_location,
32
});
33
const rate_per_byte_per_ms = rate_per_GB_per_month / MS_IN_MONTH / GB;
34
let s = 0;
35
for (let i = 1; i < metrics.length; i++) {
36
const avg_bytes_used =
37
(metrics[i].bytes_used + metrics[i - 1].bytes_used) / 2;
38
const ms = metrics[i].timestamp - metrics[i - 1].timestamp;
39
s += avg_bytes_used * rate_per_byte_per_ms * ms;
40
cost.push(s);
41
}
42
return { cost, rate_per_GB_per_month };
43
}
44
45
// return at rest price per GB per month
46
function getAtRestPrice({ priceData, bucket_storage_class, bucket_location }) {
47
const cls = bucket_storage_class.includes("autoclass")
48
? "Standard"
49
: capitalize(bucket_storage_class);
50
const { atRest } = priceData.storage;
51
if (bucket_location.includes("-")) {
52
return atRest.regions[bucket_location][cls];
53
} else {
54
return atRest.multiRegions[bucket_location][cls];
55
}
56
}
57
58
function sortByServer(metrics: CloudFilesystemMetric[]) {
59
const byServer: { [id: number]: CloudFilesystemMetric[] } = {};
60
for (const metric of metrics) {
61
const id = metric.compute_server_id;
62
if (byServer[id] == null) {
63
byServer[id] = [metric];
64
} else {
65
byServer[id].push(metric);
66
}
67
}
68
return byServer;
69
}
70
71
export function estimateCost({
72
field,
73
metrics,
74
priceData,
75
}: {
76
metrics: CloudFilesystemMetric[];
77
priceData: GoogleCloudData;
78
field: string;
79
}): {
80
cost_min: number;
81
cost_max: number;
82
total: number;
83
desc: string;
84
} {
85
if (field == "bytes_put") {
86
return estimateCost_bytes_put({ metrics, priceData });
87
} else if (field == "bytes_get") {
88
return estimateCost_bytes_get({ metrics, priceData });
89
} else if (field == "objects_put") {
90
return estimateCost_objects_put({ metrics, priceData });
91
} else if (field == "objects_get") {
92
return estimateCost_objects_get({ metrics, priceData });
93
}
94
return { cost_min: 0, cost_max: 0, total: 0, desc: "" };
95
}
96
97
function estimateCost_bytes_put({ metrics, priceData }: Options): {
98
cost_min: number;
99
cost_max: number;
100
total: number;
101
desc: string;
102
} {
103
const x = estimateCost_field({
104
metrics,
105
priceData,
106
field: "bytes_put",
107
getPrice: (opts) => {
108
const { min, max } = getUploadPrice(opts);
109
return { min: min / GB, max: max / GB };
110
},
111
});
112
const desc = `to upload ${human_readable_size(x.total)} data`;
113
return { ...x, desc };
114
}
115
116
function estimateCost_bytes_get({ metrics, priceData }: Options): {
117
cost_min: number;
118
cost_max: number;
119
total: number;
120
desc: string;
121
} {
122
const x = estimateCost_field({
123
metrics,
124
priceData,
125
field: "bytes_get",
126
getPrice: (opts) => {
127
const { min, max } = getDownloadPrice(opts);
128
return { min: min / GB, max: max / GB };
129
},
130
});
131
const desc = `to download ${human_readable_size(x.total)} data`;
132
return { ...x, desc };
133
}
134
135
function estimateCost_objects_put({ metrics, priceData }: Options): {
136
cost_min: number;
137
cost_max: number;
138
total: number;
139
desc: string;
140
} {
141
const x = estimateCost_field({
142
metrics,
143
priceData,
144
field: "objects_put",
145
getPrice: (opts) => {
146
const cost = getClassA1000Price(opts);
147
return { min: cost / 1000, max: cost / 1000 };
148
},
149
});
150
const desc = `to upload ${commas(x.total)} objects (class A operations)`;
151
return { ...x, desc };
152
}
153
154
function estimateCost_objects_get({ metrics, priceData }: Options): {
155
cost_min: number;
156
cost_max: number;
157
total: number;
158
desc: string;
159
} {
160
const x = estimateCost_field({
161
metrics,
162
priceData,
163
field: "objects_get",
164
getPrice: (opts) => {
165
const cost = getClassB1000Price(opts);
166
return { min: cost / 1000, max: cost / 1000 };
167
},
168
});
169
const desc = `to download ${commas(x.total)} objects (class B operations)`;
170
return { ...x, desc };
171
}
172
173
function estimateCost_field({ metrics, priceData, field, getPrice }): {
174
cost_min: number;
175
cost_max: number;
176
total: number;
177
} {
178
if (metrics.length == 0) {
179
return { cost_min: 0, cost_max: 0, total: 0 };
180
}
181
// divide up by compute server id
182
const byServer = sortByServer(metrics);
183
// compute the data
184
let cost_min = 0,
185
cost_max = 0,
186
total = 0;
187
for (const id in byServer) {
188
const metrics = byServer[id];
189
const { bucket_location, compute_server_location, bucket_storage_class } =
190
metrics[0];
191
const { min, max } = getPrice({
192
priceData,
193
bucket_location,
194
bucket_storage_class,
195
compute_server_location,
196
});
197
let value = 0;
198
let process_uptime = 0;
199
for (let i = 1; i < metrics.length; i++) {
200
if (metrics[i].process_uptime > process_uptime) {
201
value += (metrics[i][field] ?? 0) - (metrics[i - 1][field] ?? 0);
202
}
203
process_uptime = metrics[i].process_uptime;
204
}
205
total += value;
206
cost_min += value * min;
207
cost_max += value * max;
208
}
209
return { cost_min, cost_max, total };
210
}
211
212
// price per GB to upload data
213
function getUploadPrice({
214
priceData,
215
bucket_location,
216
compute_server_location,
217
}): { min: number; max: number } {
218
if (compute_server_location == "world") {
219
return { min: 0, max: 0 };
220
}
221
if (compute_server_location == "unknown" || !compute_server_location) {
222
return { min: 0, max: 0 };
223
}
224
if (bucket_location == compute_server_location) {
225
return { min: 0, max: 0 };
226
}
227
const bucketLoc =
228
GOOGLE_REGION_PREFIX_TO_LOCATION[bucket_location.split("-")[0]];
229
const computeLoc =
230
GOOGLE_REGION_PREFIX_TO_LOCATION[compute_server_location.split("-")[0]];
231
const s =
232
priceData.storage.dataTransferInsideGoogleCloud[computeLoc][bucketLoc];
233
return { min: s, max: s };
234
}
235
236
function getDownloadPrice({
237
priceData,
238
bucket_location,
239
compute_server_location,
240
}): { min: number; max: number } {
241
if (compute_server_location == "world") {
242
return { min: 0.12, max: 0.12 };
243
}
244
if (compute_server_location == "unknown" || !compute_server_location) {
245
// not in google cloud -- it's 0.12 or more in some edge cases.
246
return { min: 0.12, max: 0.23 };
247
}
248
if (bucket_location == compute_server_location) {
249
return { min: 0, max: 0 };
250
}
251
const bucketLoc =
252
GOOGLE_REGION_PREFIX_TO_LOCATION[bucket_location.split("-")[0]];
253
const computeLoc =
254
GOOGLE_REGION_PREFIX_TO_LOCATION[compute_server_location.split("-")[0]];
255
const s =
256
priceData.storage.dataTransferInsideGoogleCloud[computeLoc][bucketLoc];
257
return { min: s, max: s };
258
}
259
260
function getClassA1000Price({
261
priceData,
262
bucket_location,
263
bucket_storage_class,
264
}): number {
265
let cls;
266
if (bucket_storage_class.includes("auto")) {
267
cls = "standard";
268
} else {
269
cls = bucket_storage_class;
270
}
271
if (bucket_location.includes("-")) {
272
// single region
273
return priceData.storage.singleRegionOperations[cls].classA1000;
274
} else {
275
return priceData.storage.multiRegionOperations[cls].classA1000;
276
}
277
}
278
279
function getClassB1000Price({
280
priceData,
281
bucket_location,
282
bucket_storage_class,
283
}): number {
284
let cls;
285
if (bucket_storage_class.includes("auto")) {
286
cls = "standard";
287
} else {
288
cls = bucket_storage_class;
289
}
290
if (bucket_location.includes("-")) {
291
// single region
292
return priceData.storage.singleRegionOperations[cls].classB1000;
293
} else {
294
return priceData.storage.multiRegionOperations[cls].classB1000;
295
}
296
}
297
298