Skip to content

Commit

Permalink
feat: show warning if no machine is running or there are not enough r…
Browse files Browse the repository at this point in the history
…esources (containers#227)

Signed-off-by: lstocchi <lstocchi@redhat.com>
  • Loading branch information
lstocchi committed Apr 10, 2024
1 parent 42609e0 commit 58d9d1c
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 1 deletion.
15 changes: 15 additions & 0 deletions packages/backend/src/studio-api-impl.ts
Expand Up @@ -42,6 +42,8 @@ import type { SnippetManager } from './managers/SnippetManager';
import type { Language } from 'postman-code-generators';
import type { ModelOptions } from '@shared/src/models/IModelOptions';
import type { CancellationTokenRegistry } from './registries/CancellationTokenRegistry';
import { checkContainerConnectionStatusAndResources, getPodmanConnection } from './utils/podman';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';

interface PortQuickPickItem extends podmanDesktopApi.QuickPickItem {
port: number;
Expand Down Expand Up @@ -235,6 +237,15 @@ export class StudioApiImpl implements StudioAPI {
return podmanDesktopApi.navigation.navigateToPod(pod.kind, pod.Name, pod.engineId);
}

async navigateToResources(): Promise<void> {
return podmanDesktopApi.navigation.navigateToResources();
}

async navigateToEditConnectionProvider(connectionName: string): Promise<void> {
const connection = getPodmanConnection(connectionName);
return podmanDesktopApi.navigation.navigateToEditProviderContainerConnection(connection);
}

async getApplicationsState(): Promise<ApplicationState[]> {
return this.applicationManager.getApplicationsState();
}
Expand Down Expand Up @@ -412,4 +423,8 @@ export class StudioApiImpl implements StudioAPI {
throw new Error(`Cancellation token with id ${tokenId} does not exist.`);
this.cancellationTokenRegistry.getCancellationTokenSource(tokenId).cancel();
}

async checkContainerConnectionStatusAndResources(memory: number): Promise<ContainerConnectionInfo> {
return checkContainerConnectionStatusAndResources(memory);
}
}
61 changes: 60 additions & 1 deletion packages/backend/src/utils/podman.ts
Expand Up @@ -16,7 +16,10 @@
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import type { ProviderContainerConnection } from '@podman-desktop/api';
import { configuration, env, process, provider } from '@podman-desktop/api';
import { configuration, containerEngine, env, process, provider } from '@podman-desktop/api';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';

export const MIN_CPUS_VALUE = 10;

export type MachineJSON = {
Name: string;
Expand Down Expand Up @@ -88,6 +91,17 @@ export function getFirstRunningPodmanConnection(): ProviderContainerConnection |
return engine;
}

export function getPodmanConnection(connectionName: string): ProviderContainerConnection {
const engine = provider
.getContainerConnections()
.filter(connection => connection.connection.type === 'podman')
.find(connection => connection.connection.name === connectionName);
if (!engine) {
throw new Error(`no podman connection found with name ${connectionName}`);
}
return engine;
}

async function getJSONMachineList(): Promise<string> {
const { stdout } = await process.exec(getPodmanCli(), ['machine', 'list', '--format', 'json']);
return stdout;
Expand Down Expand Up @@ -118,3 +132,48 @@ export async function isQEMUMachine(): Promise<boolean> {

return false;
}

export async function checkContainerConnectionStatusAndResources(
memoryNeeded: number,
): Promise<ContainerConnectionInfo> {
let connection: ProviderContainerConnection;
try {
connection = getFirstRunningPodmanConnection();
} catch (e) {
console.log(String(e));
}

if (!connection) {
return {
status: 'no-machine',
};
}

const engineInfo = await containerEngine.info(`${connection.providerId}.${connection.connection.name}`);
if (!engineInfo) {
return {
status: 'no-machine',
};
}

const hasCpus = engineInfo.cpus && engineInfo.cpus >= MIN_CPUS_VALUE;
const hasMemory =
engineInfo.memory && engineInfo.memoryUsed && engineInfo.memory - engineInfo.memoryUsed >= memoryNeeded;

if (!hasCpus || !hasMemory) {
return {
name: connection.connection.name,
cpus: engineInfo.cpus ?? 0,
memoryIdle: engineInfo.memory - engineInfo.memoryUsed,
cpusExpected: MIN_CPUS_VALUE,
memoryExpected: memoryNeeded,
status: 'not-enough-resources',
canEdit: !!connection.connection.lifecycle?.edit,
};
}

return {
name: engineInfo.engineName,
status: 'running',
};
}
@@ -0,0 +1,94 @@
<script lang="ts">
import Fa from 'svelte-fa';
import { faCircleExclamation } from '@fortawesome/free-solid-svg-icons';
import Button from '../button/Button.svelte';
import { filesize } from 'filesize';
import { studioClient } from '/@/utils/client';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';
export let connectionInfo: ContainerConnectionInfo;
type BannerBackgroundColor = 'light' | 'dark';
export let background: BannerBackgroundColor = 'light';
let title: string | undefined = '';
let description: string | undefined = '';
let actionName: string | undefined = '';
$: updateTitleDescription(connectionInfo);
function updateTitleDescription(connectionInfo: ContainerConnectionInfo) {
if (connectionInfo.status === 'no-machine') {
title = 'No Podman machine is running';
description = 'Please start a Podman Machine before proceeding further.';
actionName = 'Start now';
return;
}
if (connectionInfo.status === 'not-enough-resources') {
title = 'Upgrade your Podman machine for best AI performance';
const hasEnoughCPU = connectionInfo.cpus >= connectionInfo.cpusExpected;
const hasEnoughMemory = connectionInfo.memoryIdle > connectionInfo.memoryExpected;
let machineCurrentStateDescription = '';
let machinePreferredStateDescription = '';
if (hasEnoughCPU) {
machineCurrentStateDescription += `${connectionInfo.cpus} vCPUs`;
machinePreferredStateDescription += `${connectionInfo.cpusExpected} vCPUs`;
if (!hasEnoughMemory) {
machineCurrentStateDescription += ` and ${filesize(connectionInfo.memoryIdle, { base: 2 })} of memory available`;
machinePreferredStateDescription += ` and ${filesize(connectionInfo.memoryExpected, { base: 2 })} of memory`;
}
} else {
machineCurrentStateDescription += `${filesize(connectionInfo.memoryIdle, { base: 2 })} of memory available`;
machinePreferredStateDescription += `${filesize(connectionInfo.memoryExpected, { base: 2 })} of memory`;
}
const machineName = `${connectionInfo.name.includes('Podman Machine') ? connectionInfo.name : `Podman Machine ${connectionInfo.name}`}`;
description = `Your ${machineName} has ${machineCurrentStateDescription}.`;
if (connectionInfo?.canEdit) {
description += `We recommend upgrading your Podman machine with at least ${machinePreferredStateDescription} for better AI performance.`;
actionName = 'Upgrade now';
} else {
description += `We recommend freeing some resources on your Podman machine to have at least ${machinePreferredStateDescription} for better AI performance.`;
}
return;
}
title = undefined;
description = undefined;
actionName = undefined;
}
function executeCommand() {
if (connectionInfo.status === 'not-enough-resources' && connectionInfo.canEdit) {
studioClient.navigateToEditConnectionProvider(connectionInfo.name);
return;
}
if (connectionInfo.status == 'no-machine') {
studioClient.navigateToResources();
}
}
</script>

{#if title && description}
<div
class="w-full {background === 'light'
? 'bg-charcoal-500'
: 'bg-charcoal-800'} border-t-[3px] border-red-700 p-4 mt-5 shadow-inner">
<div class="flex flex-row space-x-3">
<div class="flex">
<Fa icon="{faCircleExclamation}" class="text-red-600" />
</div>
<div class="flex flex-col grow">
<span class="font-medium text-sm">{title}</span>
<span class="text-sm">{description}</span>
</div>
{#if actionName}
<div class="flex items-center">
<Button class="grow text-gray-500" on:click="{executeCommand}">{actionName}</Button>
</div>
{/if}
</div>
</div>
{/if}
17 changes: 17 additions & 0 deletions packages/frontend/src/pages/CreateService.svelte
Expand Up @@ -14,6 +14,9 @@ import { filterByLabel } from '/@/utils/taskUtils';
import TasksProgress from '/@/lib/progress/TasksProgress.svelte';
import ErrorMessage from '../lib/ErrorMessage.svelte';
import { inferenceServers } from '/@/stores/inferenceServers';
import ContainerConnectionStatusInfo from '../lib/notification/ContainerConnectionStatusInfo.svelte';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';
import { checkContainerConnectionStatus } from '../utils/connectionUtils';
// List of the models available locally
let localModels: ModelInfo[];
Expand All @@ -40,6 +43,13 @@ let error: boolean = false;
let containerId: string | undefined = undefined;
$: available = containerId && $inferenceServers.some(server => server.container.containerId);
let connectionInfo: ContainerConnectionInfo | undefined;
$: if (localModels && modelId) {
checkContainerConnectionStatus(localModels, modelId)
.then(value => (connectionInfo = value))
.catch((e: unknown) => console.log(String(e)));
}
const onContainerPortInput = (event: Event): void => {
const raw = (event.target as HTMLInputElement).value;
try {
Expand Down Expand Up @@ -123,6 +133,13 @@ onMount(async () => {
loading="{containerPort === undefined}">
<svelte:fragment slot="content">
<div class="flex flex-col w-full">
<!-- warning machine resources -->
{#if connectionInfo}
<div class="mx-5">
<ContainerConnectionStatusInfo connectionInfo="{connectionInfo}" />
</div>
{/if}

<!-- tasks tracked -->
{#if trackedTasks.length > 0}
<div class="mx-5 mt-5" role="status">
Expand Down
37 changes: 37 additions & 0 deletions packages/frontend/src/utils/connectionUtils.ts
@@ -0,0 +1,37 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { ModelInfo } from '@shared/src/models/IModelInfo';
import { studioClient } from './client';
import type { ContainerConnectionInfo } from '@shared/src/models/IContainerConnectionInfo';

export async function checkContainerConnectionStatus(
localModels: ModelInfo[],
modelId: string,
): Promise<ContainerConnectionInfo | undefined> {
const model: ModelInfo | undefined = localModels.find(model => model.id === modelId);
let connection: ContainerConnectionInfo | undefined;
if (model) {
try {
connection = await studioClient.checkContainerConnectionStatusAndResources(model.memory);
} catch (e) {
console.log(e);
}
}
return connection;
}
9 changes: 9 additions & 0 deletions packages/shared/src/StudioAPI.ts
Expand Up @@ -28,6 +28,7 @@ import type { Language } from 'postman-code-generators';
import type { CreationInferenceServerOptions } from './models/InferenceServerConfig';
import type { ModelOptions } from './models/IModelOptions';
import type { Conversation } from './models/IPlaygroundMessage';
import type { ContainerConnectionInfo } from './models/IContainerConnectionInfo';

export abstract class StudioAPI {
abstract ping(): Promise<string>;
Expand All @@ -48,6 +49,8 @@ export abstract class StudioAPI {

abstract navigateToContainer(containerId: string): Promise<void>;
abstract navigateToPod(podId: string): Promise<void>;
abstract navigateToResources(): Promise<void>;
abstract navigateToEditConnectionProvider(connectionName: string): Promise<void>;

abstract getApplicationsState(): Promise<ApplicationState[]>;
abstract requestRemoveApplication(recipeId: string, modelId: string): Promise<void>;
Expand Down Expand Up @@ -164,4 +167,10 @@ export abstract class StudioAPI {
* @param tokenId the id of the CancellationToken to cancel
*/
abstract requestCancelToken(tokenId: number): Promise<void>;

/**
* Check if the running podman machine is running and has enough resources to execute task
* @param memory amount of memory that must be idle
*/
abstract checkContainerConnectionStatusAndResources(memory: number): Promise<ContainerConnectionInfo>;
}
43 changes: 43 additions & 0 deletions packages/shared/src/models/IContainerConnectionInfo.ts
@@ -0,0 +1,43 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

export type ContainerConnectionInfo =
| RunningContainerConnection
| NotEnoughResourcesContainerConnection
| NoContainerConnection;

export type ContainerConnectionInfoStatus = 'running' | 'no-machine' | 'not-enough-resources';

export interface RunningContainerConnection {
name: string;
status: 'running';
}

export interface NotEnoughResourcesContainerConnection {
name: string;
cpus: number;
memoryIdle: number;
cpusExpected: number;
memoryExpected: number;
status: 'not-enough-resources';
canEdit: boolean;
}

export interface NoContainerConnection {
status: 'no-machine';
}

0 comments on commit 58d9d1c

Please sign in to comment.