Skip to content

Commit

Permalink
[POR-1794] Smart opt v2 (#3679)
Browse files Browse the repository at this point in the history
Co-authored-by: Feroze Mohideen <[email protected]>
  • Loading branch information
sdess09 and Feroze Mohideen authored Oct 3, 2023
1 parent a916d38 commit 531a250
Show file tree
Hide file tree
Showing 14 changed files with 729 additions and 210 deletions.
144 changes: 144 additions & 0 deletions dashboard/src/lib/hooks/useClusterResourceLimits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { AWS_INSTANCE_LIMITS } from "main/home/app-dashboard/validate-apply/services-settings/tabs/utils";
import { useEffect, useState } from "react";
import convert from "convert";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import api from "shared/api";

const clusterDataValidator = z.object({
labels: z.object({
"beta.kubernetes.io/instance-type": z.string().nullish(),
}).optional(),
}).transform((data) => {
const defaultResources = {
maxCPU: AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"],
maxRAM: AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"],
};
if (!data.labels) {
return defaultResources;
}
const instanceType = data.labels["beta.kubernetes.io/instance-type"];
const res = z.tuple([z.string(), z.string()]).safeParse(instanceType)
if (!res.success) {
return defaultResources;
}
const [instanceClass, instanceSize] = res.data;
if (AWS_INSTANCE_LIMITS[instanceClass] && AWS_INSTANCE_LIMITS[instanceClass][instanceSize]) {
const { vCPU, RAM } = AWS_INSTANCE_LIMITS[instanceClass][instanceSize];
return {
maxCPU: vCPU,
maxRAM: RAM,
};
}
return defaultResources;
});
export const useClusterResourceLimits = (
{
projectId,
clusterId,
}: {
projectId: number | undefined,
clusterId: number | undefined,
}
) => {
const UPPER_BOUND = 0.75;

const [maxCPU, setMaxCPU] = useState(
AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * UPPER_BOUND
); //default is set to a t3 medium
const [maxRAM, setMaxRAM] = useState(
// round to nearest 100
Math.round(
convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
UPPER_BOUND / 100
) * 100
); //default is set to a t3 medium

const { data } = useQuery(
["getClusterNodes", projectId, clusterId],
async () => {
if (!projectId || !clusterId) {
return Promise.resolve([]);
}

const res = await api.getClusterNodes(
"<token>",
{},
{
project_id: projectId,
cluster_id: clusterId,
}
)

return await z.array(clusterDataValidator).parseAsync(res.data);
},
{
enabled: !!projectId && !!clusterId,
refetchOnWindowFocus: false,
}
);

useEffect(() => {
if (data) {
// this logic handles CPU and RAM independently - we might want to change this later
const maxCPU = data.reduce((acc, curr) => {
return Math.max(acc, curr.maxCPU);
}, 0);
const maxRAM = data.reduce((acc, curr) => {
return Math.max(acc, curr.maxRAM);
}, 0);
// if the instance type has more than 4 GB ram, we use 90% of the ram/cpu
// otherwise, we use 75%
if (maxRAM > 4) {
// round down to nearest 0.5 cores
setMaxCPU(Math.floor(maxCPU * 0.9 * 2) / 2);
setMaxRAM(
Math.round(
convert(maxRAM, "GiB").to("MB") * 0.9 / 100
) * 100
);
} else {
setMaxCPU(Math.floor(maxCPU * UPPER_BOUND * 2) / 2);
setMaxRAM(
Math.round(
convert(maxRAM, "GiB").to("MB") * UPPER_BOUND / 100
) * 100
);
}
}
}, [data])


return {
maxCPU,
maxRAM
}
}

// this function returns the fraction which the resource sliders 'snap' to when the user turns on smart optimization
export const lowestClosestResourceMultipler = (min: number, max: number, value: number): number => {
const fractions = [0.5, 0.25, 0.125];

for (const fraction of fractions) {
const newValue = fraction * (max - min) + min;
if (newValue <= value) {
return fraction;
}
}

return 0.125; // Return 0 if no fraction rounds down
}

// this function is used to snap both resource sliders in unison when one is changed
export const closestMultiplier = (min: number, max: number, value: number): number => {
const fractions = [0.5, 0.25, 0.125];
let closestFraction = 0.125;
for (const fraction of fractions) {
const newValue = fraction * (max - min) + min;
if (Math.abs(newValue - value) < Math.abs(closestFraction * (max - min) + min - value)) {
closestFraction = fraction;
}
}

return closestFraction;
}
20 changes: 11 additions & 9 deletions dashboard/src/lib/porter-apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,9 @@ export function clientAppToProto(data: PorterAppFormData): PorterApp {
const { app, source } = data;

const services = app.services.reduce((acc: Record<string, Service>, svc) => {
acc[svc.name.value] = serviceProto(serializeService(svc));
const serialized = serializeService(svc)
const proto = serviceProto(serialized)
acc[svc.name.value] = proto
return acc;
}, {});

Expand Down Expand Up @@ -363,15 +365,15 @@ export function clientAppFromProto({
const predeployOverrides = serializeService(overrides.predeploy);
const predeploy = proto.predeploy
? [
deserializeService({
service: serializedServiceFromProto({
name: "pre-deploy",
service: proto.predeploy,
isPredeploy: true,
}),
override: predeployOverrides,
deserializeService({
service: serializedServiceFromProto({
name: "pre-deploy",
service: proto.predeploy,
isPredeploy: true,
}),
]
override: predeployOverrides,
}),
]
: undefined;

return {
Expand Down
108 changes: 58 additions & 50 deletions dashboard/src/lib/porter-apps/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const serviceValidator = z.object({
port: serviceNumberValidator,
cpuCores: serviceNumberValidator,
ramMegabytes: serviceNumberValidator,
smartOptimization: serviceBooleanValidator.optional(),
config: z.discriminatedUnion("type", [
webConfigValidator,
workerConfigValidator,
Expand All @@ -95,30 +96,31 @@ export type SerializedService = {
port: number;
cpuCores: number;
ramMegabytes: number;
smartOptimization?: boolean;
config:
| {
type: "web";
domains: {
name: string;
}[];
autoscaling?: SerializedAutoscaling;
healthCheck?: SerializedHealthcheck;
private?: boolean;
}
| {
type: "worker";
autoscaling?: SerializedAutoscaling;
}
| {
type: "job";
allowConcurrent?: boolean;
cron: string;
suspendCron?: boolean;
timeoutSeconds: number;
}
| {
type: "predeploy";
};
| {
type: "web";
domains: {
name: string;
}[];
autoscaling?: SerializedAutoscaling;
healthCheck?: SerializedHealthcheck;
private?: boolean;
}
| {
type: "worker";
autoscaling?: SerializedAutoscaling;
}
| {
type: "job";
allowConcurrent?: boolean;
cron: string;
suspendCron?: boolean;
timeoutSeconds: number;
}
| {
type: "predeploy";
};
};

export function isPredeployService(service: SerializedService | ClientService) {
Expand Down Expand Up @@ -146,6 +148,7 @@ export function defaultSerialized({
port: 3000,
cpuCores: 0.1,
ramMegabytes: 256,
smartOptimization: true,
};

const defaultAutoscaling: SerializedAutoscaling = {
Expand Down Expand Up @@ -211,6 +214,7 @@ export function serializeService(service: ClientService): SerializedService {
port: service.port.value,
cpuCores: service.cpuCores.value,
ramMegabytes: service.ramMegabytes.value,
smartOptimization: service.smartOptimization?.value,
config: {
type: "web" as const,
autoscaling: serializeAutoscaling({
Expand All @@ -232,6 +236,7 @@ export function serializeService(service: ClientService): SerializedService {
port: service.port.value,
cpuCores: service.cpuCores.value,
ramMegabytes: service.ramMegabytes.value,
smartOptimization: service.smartOptimization?.value,
config: {
type: "worker" as const,
autoscaling: serializeAutoscaling({
Expand All @@ -248,6 +253,7 @@ export function serializeService(service: ClientService): SerializedService {
port: service.port.value,
cpuCores: service.cpuCores.value,
ramMegabytes: service.ramMegabytes.value,
smartOptimization: service.smartOptimization?.value,
config: {
type: "job" as const,
allowConcurrent: config.allowConcurrent?.value,
Expand All @@ -264,6 +270,7 @@ export function serializeService(service: ClientService): SerializedService {
instances: service.instances.value,
port: service.port.value,
cpuCores: service.cpuCores.value,
smartOptimization: service.smartOptimization?.value,
ramMegabytes: service.ramMegabytes.value,
config: {
type: "predeploy" as const,
Expand Down Expand Up @@ -296,6 +303,7 @@ export function deserializeService({
service.ramMegabytes,
override?.ramMegabytes
),
smartOptimization: ServiceField.boolean(service.smartOptimization, override?.smartOptimization),
domainDeletions: [],
};

Expand Down Expand Up @@ -334,7 +342,7 @@ export function deserializeService({
})),
private:
typeof config.private === "boolean" ||
typeof overrideWebConfig?.private === "boolean"
typeof overrideWebConfig?.private === "boolean"
? ServiceField.boolean(config.private, overrideWebConfig?.private)
: undefined,
},
Expand Down Expand Up @@ -365,28 +373,28 @@ export function deserializeService({
type: "job" as const,
allowConcurrent:
typeof config.allowConcurrent === "boolean" ||
typeof overrideJobConfig?.allowConcurrent === "boolean"
typeof overrideJobConfig?.allowConcurrent === "boolean"
? ServiceField.boolean(
config.allowConcurrent,
overrideJobConfig?.allowConcurrent
)
config.allowConcurrent,
overrideJobConfig?.allowConcurrent
)
: ServiceField.boolean(false, undefined),
cron: ServiceField.string(config.cron, overrideJobConfig?.cron),
suspendCron:
typeof config.suspendCron === "boolean" ||
typeof overrideJobConfig?.suspendCron === "boolean"
typeof overrideJobConfig?.suspendCron === "boolean"
? ServiceField.boolean(
config.suspendCron,
overrideJobConfig?.suspendCron
)
config.suspendCron,
overrideJobConfig?.suspendCron
)
: ServiceField.boolean(false, undefined),
timeoutSeconds:
config.timeoutSeconds == 0
? ServiceField.number(3600, overrideJobConfig?.timeoutSeconds)
: ServiceField.number(
config.timeoutSeconds,
overrideJobConfig?.timeoutSeconds
),
config.timeoutSeconds,
overrideJobConfig?.timeoutSeconds
),
},
};
})
Expand Down Expand Up @@ -511,22 +519,22 @@ export function serializedServiceFromProto({
.with({ case: "jobConfig" }, ({ value }) =>
isPredeploy
? {
...service,
name,
config: {
type: "predeploy" as const,
},
}
...service,
name,
config: {
type: "predeploy" as const,
},
}
: {
...service,
name,
config: {
type: "job" as const,
...value,
allowConcurrent: value.allowConcurrentOptional,
timeoutSeconds: Number(value.timeoutSeconds),
},
}
...service,
name,
config: {
type: "job" as const,
...value,
allowConcurrent: value.allowConcurrentOptional,
timeoutSeconds: Number(value.timeoutSeconds),
},
}
)
.exhaustive();
}
Loading

0 comments on commit 531a250

Please sign in to comment.