feat: use hardware information api

This commit is contained in:
Louis 2025-06-30 13:11:20 +07:00
parent d264220245
commit 9b730058b4
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
5 changed files with 180 additions and 181 deletions

View File

@ -24,7 +24,14 @@ impl CpuStaticInfo {
let name = system
.cpus()
.first()
.map(|cpu| cpu.brand())
.map(|cpu| {
let brand = cpu.brand();
if brand.is_empty() {
cpu.name()
} else {
brand
}
})
.unwrap_or("unknown")
.to_string();

View File

@ -6,9 +6,9 @@ import { setActiveGpus } from '@/services/hardware'
// Hardware data types
export interface CPU {
arch: string
cores: number
instructions: string[]
model: string
core_count: number
extensions: string[]
name: string
usage: number
}
@ -18,14 +18,21 @@ export interface GPUAdditionalInfo {
}
export interface GPU {
activated: boolean
additional_information: GPUAdditionalInfo
free_vram: number
id: string
name: string
total_vram: number
total_memory: number
vendor: string
uuid: string
version: string
driver_version: string
nvidia_info: {
index: number
compute_capability: string
}
vulkan_info: {
index: number
device_id: number
device_type: string
api_version: string
}
}
export interface OS {
@ -41,33 +48,48 @@ export interface RAM {
export interface HardwareData {
cpu: CPU
gpus: GPU[]
os: OS
ram: RAM
os_type: string
os_name: string
total_memory: number
}
export interface SystemUsage {
cpu: number
used_memory: number
total_memory: number
gpus: {
uuid: string
used_memory: number
total_memory: number
}[]
}
// Default values
const defaultHardwareData: HardwareData = {
cpu: {
arch: '',
cores: 0,
instructions: [],
model: '',
core_count: 0,
extensions: [],
name: '',
usage: 0,
},
gpus: [],
os: {
name: '',
version: '',
},
ram: {
available: 0,
total: 0,
},
os_type: '',
os_name: '',
total_memory: 0,
}
const defaultSystemUsage: SystemUsage = {
cpu: 0,
used_memory: 0,
total_memory: 0,
gpus: [],
}
interface HardwareStore {
// Hardware data
hardwareData: HardwareData
systemUsage: SystemUsage
// Update functions
setCPU: (cpu: CPU) => void
@ -81,11 +103,8 @@ interface HardwareStore {
// Update individual GPU
updateGPU: (index: number, gpu: GPU) => void
// Update CPU usage
updateCPUUsage: (usage: number) => void
// Update RAM available
updateRAMAvailable: (available: number) => void
updateSystemUsage: (usage: SystemUsage) => void
// Toggle GPU activation (async, with loading)
toggleGPUActivation: (index: number) => Promise<void>
@ -107,11 +126,15 @@ export const useHardware = create<HardwareStore>()(
persist(
(set, get) => ({
hardwareData: defaultHardwareData,
systemUsage: defaultSystemUsage,
gpuLoading: {},
pollingPaused: false,
setGpuLoading: (index, loading) =>
set((state) => ({
gpuLoading: { ...state.gpuLoading, [state.hardwareData.gpus[index].uuid]: loading },
gpuLoading: {
...state.gpuLoading,
[state.hardwareData.gpus[index].uuid]: loading,
},
})),
pausePolling: () => set({ pollingPaused: true }),
resumePolling: () => set({ pollingPaused: false }),
@ -167,56 +190,41 @@ export const useHardware = create<HardwareStore>()(
}
}),
updateCPUUsage: (usage) =>
set((state) => ({
hardwareData: {
...state.hardwareData,
cpu: {
...state.hardwareData.cpu,
usage,
},
},
})),
updateRAMAvailable: (available) =>
set((state) => ({
hardwareData: {
...state.hardwareData,
ram: {
...state.hardwareData.ram,
available,
},
},
updateSystemUsage: (systemUsage) =>
set(() => ({
systemUsage,
})),
toggleGPUActivation: async (index) => {
const { pausePolling, setGpuLoading, resumePolling } = get();
pausePolling();
setGpuLoading(index, true);
try {
await new Promise((resolve) => setTimeout(resolve, 200)); // Simulate async, replace with real API if needed
set((state) => {
const newGPUs = [...state.hardwareData.gpus];
if (index >= 0 && index < newGPUs.length) {
newGPUs[index] = {
...newGPUs[index],
activated: !newGPUs[index].activated,
};
}
setActiveGpus({
gpus: newGPUs.filter((e) => e.activated).map((e) => parseInt(e.id)),
});
return {
hardwareData: {
...state.hardwareData,
gpus: newGPUs,
},
};
});
} finally {
setGpuLoading(index, false);
setTimeout(resumePolling, 1000); // Resume polling after 1s
}
const { pausePolling, setGpuLoading, resumePolling } = get()
pausePolling()
setGpuLoading(index, true)
// try {
// await new Promise((resolve) => setTimeout(resolve, 200)) // Simulate async, replace with real API if needed
// set((state) => {
// const newGPUs = [...state.hardwareData.gpus]
// if (index >= 0 && index < newGPUs.length) {
// newGPUs[index] = {
// ...newGPUs[index],
// activated: !newGPUs[index].activated,
// }
// }
// setActiveGpus({
// gpus: newGPUs
// .filter((e) => e.activated)
// .map((e) => parseInt(e.id)),
// })
// return {
// hardwareData: {
// ...state.hardwareData,
// gpus: newGPUs,
// },
// }
// })
// } finally {
// setGpuLoading(index, false)
// setTimeout(resumePolling, 1000) // Resume polling after 1s
// }
},
reorderGPUs: (oldIndex, newIndex) =>

View File

@ -29,10 +29,11 @@ import {
IconGripVertical,
IconDeviceDesktopAnalytics,
} from '@tabler/icons-react'
import { getHardwareInfo } from '@/services/hardware'
import { getHardwareInfo, getSystemUsage } from '@/services/hardware'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { formatMegaBytes } from '@/lib/utils'
import { windowKey } from '@/constants/windows'
import { toNumber } from '@/utils/number'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.hardware as any)({
@ -47,10 +48,11 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
transform,
transition,
isDragging,
} = useSortable({ id: gpu.id || index })
} = useSortable({ id: index })
const { t } = useTranslation()
const { toggleGPUActivation, gpuLoading } = useHardware()
const { systemUsage, toggleGPUActivation, gpuLoading } = useHardware()
const usage = systemUsage.gpus[index]
const style = {
transform: CSS.Transform.toString(transform),
@ -78,7 +80,7 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
actions={
<div className="flex items-center gap-4">
<Switch
checked={gpu.activated}
checked={true}
disabled={!!gpuLoading[index]}
onCheckedChange={() => toggleGPUActivation(index)}
/>
@ -90,8 +92,9 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
title={t('settings:hardware.vram')}
actions={
<span className="text-main-view-fg/80">
{formatMegaBytes(gpu.free_vram)} {t('settings:hardware.freeOf')}{' '}
{formatMegaBytes(gpu.total_vram)}
{formatMegaBytes(usage?.used_memory)}{' '}
{t('settings:hardware.freeOf')}{' '}
{formatMegaBytes(gpu.total_memory)}
</span>
}
/>
@ -99,7 +102,7 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
title={t('settings:hardware.driverVersion')}
actions={
<span className="text-main-view-fg/80">
{gpu.additional_information?.driver_version || '-'}
{gpu.driver_version?.slice(0, 50) || '-'}
</span>
}
/>
@ -107,7 +110,8 @@ function SortableGPUItem({ gpu, index }: { gpu: GPU; index: number }) {
title={t('settings:hardware.computeCapability')}
actions={
<span className="text-main-view-fg/80">
{gpu.additional_information?.compute_cap || '-'}
{gpu.nvidia_info?.compute_capability ??
gpu.vulkan_info?.api_version}
</span>
}
/>
@ -120,9 +124,9 @@ function Hardware() {
const { t } = useTranslation()
const {
hardwareData,
systemUsage,
setHardwareData,
updateCPUUsage,
updateRAMAvailable,
updateSystemUsage,
reorderGPUs,
pollingPaused,
} = useHardware()
@ -147,9 +151,11 @@ function Hardware() {
if (over && active.id !== over.id) {
// Find the indices of the dragged item and the drop target
const oldIndex = hardwareData.gpus.findIndex(
(gpu) => gpu.id === active.id
(gpu, index) => index === active.id
)
const newIndex = hardwareData.gpus.findIndex(
(gpu, index) => index === over.id
)
const newIndex = hardwareData.gpus.findIndex((gpu) => gpu.id === over.id)
if (oldIndex !== -1 && newIndex !== -1) {
reorderGPUs(oldIndex, newIndex)
@ -160,14 +166,13 @@ function Hardware() {
useEffect(() => {
if (pollingPaused) return
const intervalId = setInterval(() => {
getHardwareInfo().then((data) => {
updateCPUUsage(data.cpu.usage)
updateRAMAvailable(data.ram.available)
getSystemUsage().then((data) => {
updateSystemUsage(data)
})
}, 5000)
return () => clearInterval(intervalId)
}, [setHardwareData, updateCPUUsage, updateRAMAvailable, pollingPaused])
}, [setHardwareData, updateSystemUsage, pollingPaused])
const handleClickSystemMonitor = async () => {
try {
@ -229,8 +234,8 @@ function Hardware() {
<CardItem
title={t('settings:hardware.name')}
actions={
<span className="text-main-view-fg/80">
{hardwareData.os?.name}
<span className="text-main-view-fg/80 capitalize">
{hardwareData.os_type}
</span>
}
/>
@ -238,7 +243,7 @@ function Hardware() {
title={t('settings:hardware.version')}
actions={
<span className="text-main-view-fg/80">
{hardwareData.os?.version}
{hardwareData.os_name}
</span>
}
/>
@ -250,7 +255,7 @@ function Hardware() {
title={t('settings:hardware.model')}
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu?.model}
{hardwareData.cpu?.name}
</span>
}
/>
@ -266,17 +271,17 @@ function Hardware() {
title={t('settings:hardware.cores')}
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu?.cores}
{hardwareData.cpu?.core_count}
</span>
}
/>
{hardwareData.cpu?.instructions.join(', ').length > 0 && (
{hardwareData.cpu?.extensions?.join(', ').length > 0 && (
<CardItem
title={t('settings:hardware.instructions')}
column={hardwareData.cpu?.instructions.length > 6}
column={hardwareData.cpu?.extensions.length > 6}
actions={
<span className="text-main-view-fg/80 break-words">
{hardwareData.cpu?.instructions?.join(', ')}
{hardwareData.cpu?.extensions?.join(', ')}
</span>
}
/>
@ -285,14 +290,14 @@ function Hardware() {
title={t('settings:hardware.usage')}
actions={
<div className="flex items-center gap-2">
{hardwareData.cpu?.usage > 0 && (
{systemUsage.cpu > 0 && (
<>
<Progress
value={hardwareData.cpu?.usage}
value={systemUsage.cpu}
className="h-2 w-10"
/>
<span className="text-main-view-fg/80">
{hardwareData.cpu?.usage?.toFixed(2)}%
{systemUsage.cpu?.toFixed(2)}%
</span>
</>
)}
@ -307,7 +312,7 @@ function Hardware() {
title={t('settings:hardware.totalRam')}
actions={
<span className="text-main-view-fg/80">
{formatMegaBytes(hardwareData.ram.total)}
{formatMegaBytes(hardwareData.total_memory)}
</span>
}
/>
@ -315,7 +320,9 @@ function Hardware() {
title={t('settings:hardware.availableRam')}
actions={
<span className="text-main-view-fg/80">
{formatMegaBytes(hardwareData.ram?.available)}
{formatMegaBytes(
hardwareData.total_memory - systemUsage.used_memory
)}
</span>
}
/>
@ -323,23 +330,21 @@ function Hardware() {
title={t('settings:hardware.usage')}
actions={
<div className="flex items-center gap-2">
{hardwareData.ram?.total > 0 && (
{hardwareData.total_memory > 0 && (
<>
<Progress
value={
((hardwareData.ram?.total -
hardwareData.ram?.available) /
hardwareData.ram?.total) *
100
toNumber(
systemUsage.used_memory / systemUsage.total_memory
) * 100
}
className="h-2 w-10"
/>
<span className="text-main-view-fg/80">
{(
((hardwareData.ram?.total -
hardwareData.ram?.available) /
hardwareData.ram?.total) *
100
toNumber(
systemUsage.used_memory / systemUsage.total_memory
) * 100
).toFixed(2)}
%
</span>
@ -383,15 +388,11 @@ function Hardware() {
onDragEnd={handleDragEnd}
>
<SortableContext
items={hardwareData.gpus.map((gpu) => gpu.id)}
items={hardwareData.gpus.map((gpu, index) => index)}
strategy={verticalListSortingStrategy}
>
{hardwareData.gpus.map((gpu, index) => (
<SortableGPUItem
key={gpu.id || index}
gpu={gpu}
index={index}
/>
<SortableGPUItem key={index} gpu={gpu} index={index} />
))}
</SortableContext>
</DndContext>

View File

@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { useHardware } from '@/hooks/useHardware'
import { getHardwareInfo } from '@/services/hardware'
import { getHardwareInfo, getSystemUsage } from '@/services/hardware'
import { Progress } from '@/components/ui/progress'
import type { HardwareData } from '@/hooks/useHardware'
import { route } from '@/constants/routes'
@ -10,6 +10,7 @@ import { IconDeviceDesktopAnalytics } from '@tabler/icons-react'
import { getActiveModels, stopModel } from '@/services/models'
import { Button } from '@/components/ui/button'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { toNumber } from '@/utils/number'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.systemMonitor as any)({
@ -18,7 +19,7 @@ export const Route = createFileRoute(route.systemMonitor as any)({
function SystemMonitor() {
const { t } = useTranslation()
const { hardwareData, setHardwareData, updateCPUUsage, updateRAMAvailable } =
const { hardwareData, systemUsage, setHardwareData, updateSystemUsage } =
useHardware()
const [activeModels, setActiveModels] = useState<string[]>([])
@ -31,16 +32,15 @@ function SystemMonitor() {
// Set up interval for real-time updates
const intervalId = setInterval(() => {
getHardwareInfo().then((data) => {
setHardwareData(data as unknown as HardwareData)
updateCPUUsage(data.cpu?.usage)
updateRAMAvailable(data.ram?.available)
getSystemUsage().then((data) => {
// setHardwareData(data as unknown as HardwareData)
updateSystemUsage(data)
})
getActiveModels().then(setActiveModels)
}, 5000)
return () => clearInterval(intervalId)
}, [setHardwareData, setActiveModels, updateCPUUsage, updateRAMAvailable])
}, [setHardwareData, setActiveModels, updateSystemUsage])
const stopRunningModel = (modelId: string) => {
stopModel(modelId)
@ -56,9 +56,10 @@ function SystemMonitor() {
// Calculate RAM usage percentage
const ramUsagePercentage =
((hardwareData.ram.total - hardwareData.ram.available) /
hardwareData.ram.total) *
100
toNumber(
(hardwareData.total_memory - systemUsage.used_memory) /
hardwareData.total_memory
) * 100
return (
<div className="flex flex-col h-full bg-main-view overflow-y-auto p-6">
@ -80,16 +81,14 @@ function SystemMonitor() {
<span className="text-main-view-fg/70">
{t('system-monitor:model')}
</span>
<span className="text-main-view-fg">
{hardwareData.cpu.model}
</span>
<span className="text-main-view-fg">{hardwareData.cpu.name}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-main-view-fg/70">
{t('system-monitor:cores')}
</span>
<span className="text-main-view-fg">
{hardwareData.cpu.cores}
{hardwareData.cpu.core_count}
</span>
</div>
<div className="flex justify-between items-center">
@ -104,10 +103,10 @@ function SystemMonitor() {
{t('system-monitor:currentUsage')}
</span>
<span className="text-main-view-fg font-bold">
{hardwareData.cpu.usage.toFixed(2)}%
{systemUsage.cpu.toFixed(2)}%
</span>
</div>
<Progress value={hardwareData.cpu.usage} className="h-3 w-full" />
<Progress value={systemUsage.cpu} className="h-3 w-full" />
</div>
</div>
</div>
@ -123,7 +122,7 @@ function SystemMonitor() {
{t('system-monitor:totalRam')}
</span>
<span className="text-main-view-fg">
{formatMegaBytes(hardwareData.ram.total)}
{formatMegaBytes(hardwareData.total_memory)}
</span>
</div>
<div className="flex justify-between items-center">
@ -131,7 +130,9 @@ function SystemMonitor() {
{t('system-monitor:availableRam')}
</span>
<span className="text-main-view-fg">
{formatMegaBytes(hardwareData.ram.available)}
{formatMegaBytes(
hardwareData.total_memory - systemUsage.used_memory
)}
</span>
</div>
<div className="flex justify-between items-center">
@ -140,7 +141,7 @@ function SystemMonitor() {
</span>
<span className="text-main-view-fg">
{formatMegaBytes(
hardwareData.ram.total - hardwareData.ram.available
hardwareData.total_memory - systemUsage.used_memory
)}
</span>
</div>
@ -222,10 +223,10 @@ function SystemMonitor() {
{hardwareData.gpus.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{hardwareData.gpus
.filter((gpu) => gpu.activated)
// .filter((gpu) => gpu.activated)
.map((gpu, index) => (
<div
key={gpu.id || index}
key={gpu.uuid || index}
className="bg-main-view-fg/3 rounded-lg p-4"
>
<div className="flex justify-between items-center mb-2">
@ -242,8 +243,11 @@ function SystemMonitor() {
{t('system-monitor:vramUsage')}
</span>
<span className="text-main-view-fg">
{formatMegaBytes(gpu.total_vram - gpu.free_vram)} /{' '}
{formatMegaBytes(gpu.total_vram)}
{formatMegaBytes(
gpu.total_memory -
systemUsage.gpus[index]?.used_memory
)}{' '}
/ {formatMegaBytes(gpu.total_memory)}
</span>
</div>
<div className="flex justify-between items-center">
@ -251,7 +255,7 @@ function SystemMonitor() {
{t('system-monitor:driverVersion')}
</span>
<span className="text-main-view-fg">
{gpu.additional_information?.driver_version || '-'}
{gpu.driver_version || '-'}
</span>
</div>
<div className="flex justify-between items-center">
@ -259,13 +263,16 @@ function SystemMonitor() {
{t('system-monitor:computeCapability')}
</span>
<span className="text-main-view-fg">
{gpu.additional_information?.compute_cap || '-'}
{gpu.nvidia_info?.compute_capability ||
gpu.vulkan_info.api_version}
</span>
</div>
<div className="mt-2">
<Progress
value={
((gpu.total_vram - gpu.free_vram) / gpu.total_vram) *
((gpu.total_memory -
systemUsage.gpus[index]?.used_memory) /
gpu.total_memory) *
100
}
className="h-2 w-full"
@ -280,12 +287,6 @@ function SystemMonitor() {
{t('system-monitor:noGpus')}
</div>
)}
{hardwareData.gpus.length > 0 &&
!hardwareData.gpus.some((gpu) => gpu.activated) && (
<div className="text-center text-main-view-fg/50 py-4">
{t('system-monitor:noActiveGpus')}
</div>
)}
</div>
</div>
)

View File

@ -1,24 +1,20 @@
import { ExtensionManager } from '@/lib/extension'
import { ExtensionTypeEnum, HardwareManagementExtension } from '@janhq/core'
import { HardwareData, SystemUsage } from '@/hooks/useHardware'
import { invoke } from '@tauri-apps/api/core'
/**
* Get hardware information from the HardwareManagementExtension.
* @returns {Promise<HardwareInfo>} A promise that resolves to the hardware information.
*/
export const getHardwareInfo = async () => {
const extension =
ExtensionManager.getInstance().get<HardwareManagementExtension>(
ExtensionTypeEnum.Hardware
)
if (!extension) throw new Error('Hardware extension not found')
try {
return await extension?.getHardware()
} catch (error) {
console.error('Failed to download model:', error)
throw error
return invoke('get_system_info') as Promise<HardwareData>
}
/**
* Get hardware information from the HardwareManagementExtension.
* @returns {Promise<HardwareInfo>} A promise that resolves to the hardware information.
*/
export const getSystemUsage = async () => {
return invoke('get_system_usage') as Promise<SystemUsage>
}
/**
@ -26,20 +22,6 @@ export const getHardwareInfo = async () => {
* @returns A Promise that resolves set gpus activate.
*/
export const setActiveGpus = async (data: { gpus: number[] }) => {
const extension =
ExtensionManager.getInstance().get<HardwareManagementExtension>(
ExtensionTypeEnum.Hardware
)
if (!extension) {
throw new Error('Extension is not available')
}
try {
const response = await extension.setActiveGpu(data)
return response
} catch (error) {
console.error('Failed to install engine variant:', error)
throw error
}
// TODO: llama.cpp extension should handle this
console.log(data)
}