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