feat: add GetManPage function and integrate parent process name in ProcessDetail
This commit is contained in:
5
app.go
5
app.go
@@ -56,3 +56,8 @@ func (a *App) GetProcessDetail(pid int32) (*backend.ProcessDetail, error) {
|
|||||||
func (a *App) KillProcess(pid int32, force bool) error {
|
func (a *App) KillProcess(pid int32, force bool) error {
|
||||||
return backend.KillProcess(pid, force)
|
return backend.KillProcess(pid, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetManPage returns the plain-text manual page for the given command.
|
||||||
|
func (a *App) GetManPage(name string) string {
|
||||||
|
return backend.GetManPage(name)
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ type ProcessDetail struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
CreatedAt int64 `json:"created_at"` // unix ms
|
CreatedAt int64 `json:"created_at"` // unix ms
|
||||||
ParentPID int32 `json:"parent_pid"`
|
ParentPID int32 `json:"parent_pid"`
|
||||||
|
ParentName string `json:"parent_name"`
|
||||||
Nice int32 `json:"nice"`
|
Nice int32 `json:"nice"`
|
||||||
NumThreads int32 `json:"num_threads"`
|
NumThreads int32 `json:"num_threads"`
|
||||||
NumFDs int32 `json:"num_fds"`
|
NumFDs int32 `json:"num_fds"`
|
||||||
@@ -258,6 +259,13 @@ func FetchProcessDetail(pid int32) (*ProcessDetail, error) {
|
|||||||
}
|
}
|
||||||
if v, e := p.Ppid(); e == nil {
|
if v, e := p.Ppid(); e == nil {
|
||||||
d.ParentPID = v
|
d.ParentPID = v
|
||||||
|
if d.ParentPID > 0 {
|
||||||
|
if pp, err2 := ps.NewProcess(d.ParentPID); err2 == nil {
|
||||||
|
if pname, err2 := pp.Name(); err2 == nil {
|
||||||
|
d.ParentName = pname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v, e := p.Nice(); e == nil {
|
if v, e := p.Nice(); e == nil {
|
||||||
d.Nice = v
|
d.Nice = v
|
||||||
@@ -305,3 +313,29 @@ func KillProcess(pid int32, force bool) error {
|
|||||||
}
|
}
|
||||||
return p.Terminate()
|
return p.Terminate()
|
||||||
}
|
}
|
||||||
|
// GetManPage returns the plain-text man page for the given command name.
|
||||||
|
// Returns an empty string if man is not installed or the page does not exist.
|
||||||
|
func GetManPage(name string) string {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "man", "-P", "cat", name)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Strip nroff backspace-based bold/underline formatting (char + backspace + char)
|
||||||
|
raw := []byte(out)
|
||||||
|
cleaned := make([]byte, 0, len(raw))
|
||||||
|
for i := 0; i < len(raw); i++ {
|
||||||
|
if i+1 < len(raw) && raw[i+1] == '\b' {
|
||||||
|
i++ // skip formatting char and backspace; the real char follows
|
||||||
|
} else {
|
||||||
|
cleaned = append(cleaned, raw[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text := strings.TrimSpace(string(cleaned))
|
||||||
|
if len(text) > 12000 {
|
||||||
|
text = text[:12000] + "\n…[truncated]"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ type ProcessDetail = {
|
|||||||
username: string
|
username: string
|
||||||
created_at: number
|
created_at: number
|
||||||
parent_pid: number
|
parent_pid: number
|
||||||
|
parent_name: string
|
||||||
nice: number
|
nice: number
|
||||||
num_threads: number
|
num_threads: number
|
||||||
num_fds: number
|
num_fds: number
|
||||||
@@ -64,6 +65,7 @@ type ProcessDetail = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SortField = 'cpu' | 'mem'
|
type SortField = 'cpu' | 'mem'
|
||||||
|
type NavItem = { pid: number; name: string }
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -119,10 +121,10 @@ function MetricValue({ value, pct }: { value: string; pct: number }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailRow({ label, value, mono = false }: { label: string; value: React.ReactNode; mono?: boolean }) {
|
function DetailRow({ label, value, mono = false, tooltip }: { label: string; value: React.ReactNode; mono?: boolean; tooltip?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5" title={tooltip}>
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-widest text-zinc-600">{label}</span>
|
<span className="text-[10px] font-semibold uppercase tracking-widest text-zinc-600 cursor-help">{label}</span>
|
||||||
<span className={cn('text-xs text-zinc-300 break-all', mono && 'font-mono')}>{value || '—'}</span>
|
<span className={cn('text-xs text-zinc-300 break-all', mono && 'font-mono')}>{value || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -136,9 +138,9 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatBlock({ label, value, sub }: { label: string; value: React.ReactNode; sub?: string }) {
|
function StatBlock({ label, value, sub, tooltip }: { label: string; value: React.ReactNode; sub?: string; tooltip?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-zinc-800/40 rounded-lg px-3 py-2.5">
|
<div className="bg-zinc-800/40 rounded-lg px-3 py-2.5 cursor-help" title={tooltip}>
|
||||||
<p className="text-[10px] text-zinc-600 uppercase tracking-widest mb-1">{label}</p>
|
<p className="text-[10px] text-zinc-600 uppercase tracking-widest mb-1">{label}</p>
|
||||||
<p className="text-sm font-semibold text-zinc-100 tabular-nums">{value}</p>
|
<p className="text-sm font-semibold text-zinc-100 tabular-nums">{value}</p>
|
||||||
{sub && <p className="text-[10px] text-zinc-600 mt-0.5">{sub}</p>}
|
{sub && <p className="text-[10px] text-zinc-600 mt-0.5">{sub}</p>}
|
||||||
@@ -153,16 +155,28 @@ function StatBlock({ label, value, sub }: { label: string; value: React.ReactNod
|
|||||||
function ProcessDetailPanel({
|
function ProcessDetailPanel({
|
||||||
pid,
|
pid,
|
||||||
onClose,
|
onClose,
|
||||||
|
onNavigate,
|
||||||
|
navStack,
|
||||||
|
onNavBack,
|
||||||
|
gpuType,
|
||||||
}: {
|
}: {
|
||||||
pid: number
|
pid: number
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onNavigate: (pid: number, fromName: string) => void
|
||||||
|
navStack: NavItem[]
|
||||||
|
onNavBack: (index: number) => void
|
||||||
|
gpuType?: string
|
||||||
}) {
|
}) {
|
||||||
const [detail, setDetail] = useState<ProcessDetail | null>(null)
|
const [detail, setDetail] = useState<ProcessDetail | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [killing, setKilling] = useState(false)
|
const [killing, setKilling] = useState(false)
|
||||||
const [killConfirm, setKillConfirm] = useState(false)
|
const [killConfirm, setKillConfirm] = useState(false)
|
||||||
const [killError, setKillError] = useState<string | null>(null)
|
const [killError, setKillError] = useState<string | null>(null)
|
||||||
|
const [manPage, setManPage] = useState<string | null>(null)
|
||||||
|
const [manPageLoading, setManPageLoading] = useState(false)
|
||||||
|
const [manOpen, setManOpen] = useState(false)
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
const lastManNameRef = useRef<string>('')
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -204,6 +218,17 @@ function ProcessDetailPanel({
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [onClose])
|
}, [onClose])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!detail?.name || detail.name === lastManNameRef.current) return
|
||||||
|
lastManNameRef.current = detail.name
|
||||||
|
setManPageLoading(true)
|
||||||
|
setManPage(null)
|
||||||
|
;(window as any)['go']['main']['App']['GetManPage'](detail.name)
|
||||||
|
.then((text: string) => { setManPage(text || null) })
|
||||||
|
.catch(() => setManPage(null))
|
||||||
|
.finally(() => setManPageLoading(false))
|
||||||
|
}, [detail?.name])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
@@ -217,10 +242,40 @@ function ProcessDetailPanel({
|
|||||||
{/* Panel header */}
|
{/* Panel header */}
|
||||||
<div className="flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0">
|
<div className="flex items-start justify-between px-5 py-4 border-b border-zinc-800/80 shrink-0">
|
||||||
<div className="min-w-0 flex-1 pr-4">
|
<div className="min-w-0 flex-1 pr-4">
|
||||||
|
{navStack.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1 flex-wrap mb-1.5 min-w-0">
|
||||||
|
{navStack.map((item, i) => (
|
||||||
|
<React.Fragment key={i}>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavBack(i)}
|
||||||
|
className="text-[10px] text-zinc-500 hover:text-zinc-300 transition-colors hover:underline truncate max-w-[100px]"
|
||||||
|
title={`Back to ${item.name || `PID ${item.pid}`} (PID ${item.pid})`}
|
||||||
|
>
|
||||||
|
{item.name || `PID ${item.pid}`}
|
||||||
|
</button>
|
||||||
|
<ChevronRight className="w-2.5 h-2.5 text-zinc-700 shrink-0" />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
<span className="text-[10px] text-zinc-400 truncate max-w-[100px]">{detail?.name ?? `PID ${pid}`}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="text-sm font-semibold text-zinc-100 truncate">
|
<span className="text-sm font-semibold text-zinc-100 truncate">
|
||||||
{detail?.name ?? `PID ${pid}`}
|
{detail?.name ?? `PID ${pid}`}
|
||||||
</span>
|
</span>
|
||||||
|
{gpuType && (
|
||||||
|
<span
|
||||||
|
title={gpuType === 'C' ? 'GPU Compute — CUDA/OpenCL/Vulkan Compute' : gpuType === 'G' ? 'GPU Graphics — rendering & display' : 'GPU Compute + Graphics'}
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 text-[10px] font-mono px-1.5 py-0.5 rounded cursor-help',
|
||||||
|
gpuType === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
||||||
|
gpuType === 'G' ? 'bg-blue-900/50 text-blue-300' :
|
||||||
|
'bg-indigo-900/50 text-indigo-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
GPU·{gpuType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{detail?.status && (
|
{detail?.status && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -315,25 +370,39 @@ function ProcessDetailPanel({
|
|||||||
{/* Identity */}
|
{/* Identity */}
|
||||||
<SectionTitle>Identity</SectionTitle>
|
<SectionTitle>Identity</SectionTitle>
|
||||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||||
<StatBlock label="Parent PID" value={detail.parent_pid > 0 ? detail.parent_pid : '—'} />
|
{detail.parent_pid > 0 ? (
|
||||||
<StatBlock label="Nice / Priority" value={detail.nice} />
|
<div
|
||||||
|
className="bg-zinc-800/40 rounded-lg px-3 py-2.5 cursor-pointer hover:bg-zinc-700/50 transition-colors group"
|
||||||
|
title={`Navigate to parent process${detail.parent_name ? ` (${detail.parent_name})` : ''}`}
|
||||||
|
onClick={() => onNavigate(detail.parent_pid, detail.name)}
|
||||||
|
>
|
||||||
|
<p className="text-[10px] text-zinc-600 uppercase tracking-widest mb-1 group-hover:text-zinc-400">Parent PID</p>
|
||||||
|
<p className="text-sm font-semibold tabular-nums text-emerald-400/80 group-hover:text-emerald-400 transition-colors">{detail.parent_pid}</p>
|
||||||
|
{detail.parent_name && <p className="text-[10px] text-zinc-500 mt-0.5 truncate">{detail.parent_name}</p>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<StatBlock label="Parent PID" value="—" tooltip="No parent process (root process)" />
|
||||||
|
)}
|
||||||
|
<StatBlock label="Nice / Priority" value={detail.nice} tooltip="Scheduling priority: -20 = highest, +19 = lowest. Default is 0. Lower values get more CPU time" />
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="Created"
|
label="Created"
|
||||||
value={detail.created_at ? new Date(detail.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—'}
|
value={detail.created_at ? new Date(detail.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '—'}
|
||||||
sub={detail.created_at ? fmtRelative(detail.created_at) : undefined}
|
sub={detail.created_at ? fmtRelative(detail.created_at) : undefined}
|
||||||
|
tooltip="Time when this process was started"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Paths */}
|
{/* Paths */}
|
||||||
<SectionTitle>Executable</SectionTitle>
|
<SectionTitle>Executable</SectionTitle>
|
||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
<DetailRow label="Path" value={detail.exe} mono />
|
<DetailRow label="Path" value={detail.exe} mono tooltip="Full filesystem path to the executable binary" />
|
||||||
<DetailRow
|
<DetailRow
|
||||||
label="Command"
|
label="Command"
|
||||||
value={detail.cmdline || detail.exe}
|
value={detail.cmdline || detail.exe}
|
||||||
mono
|
mono
|
||||||
|
tooltip="Full command line including arguments used to launch this process"
|
||||||
/>
|
/>
|
||||||
{detail.cwd && <DetailRow label="Working directory" value={detail.cwd} mono />}
|
{detail.cwd && <DetailRow label="Working directory" value={detail.cwd} mono tooltip="Current working directory of the process at the time of sampling" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CPU & Memory */}
|
{/* CPU & Memory */}
|
||||||
@@ -342,26 +411,31 @@ function ProcessDetailPanel({
|
|||||||
<StatBlock
|
<StatBlock
|
||||||
label="CPU"
|
label="CPU"
|
||||||
value={`${detail.cpu_percent.toFixed(1)}%`}
|
value={`${detail.cpu_percent.toFixed(1)}%`}
|
||||||
|
tooltip="CPU usage as a percentage of one core (can exceed 100% on multi-threaded processes)"
|
||||||
/>
|
/>
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="Threads"
|
label="Threads"
|
||||||
value={detail.num_threads}
|
value={detail.num_threads}
|
||||||
sub={`${detail.num_fds} open FDs`}
|
sub={`${detail.num_fds} open FDs`}
|
||||||
|
tooltip="Number of OS threads running in this process. Open FDs = open file descriptors (files, sockets, pipes, devices)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="RSS (physical)"
|
label="RSS (physical)"
|
||||||
value={fmtBytes(detail.rss)}
|
value={fmtBytes(detail.rss)}
|
||||||
|
tooltip="Resident Set Size — physical RAM pages currently mapped and in use by this process"
|
||||||
/>
|
/>
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="VMS (virtual)"
|
label="VMS (virtual)"
|
||||||
value={fmtBytes(detail.vms)}
|
value={fmtBytes(detail.vms)}
|
||||||
|
tooltip="Virtual Memory Size — total virtual address space reserved, including shared libraries and mapped files"
|
||||||
/>
|
/>
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="Swap"
|
label="Swap"
|
||||||
value={fmtBytes(detail.swap)}
|
value={fmtBytes(detail.swap)}
|
||||||
sub={`${detail.mem_percent.toFixed(1)}% of RAM`}
|
sub={`${detail.mem_percent.toFixed(1)}% of RAM`}
|
||||||
|
tooltip="Memory paged out to disk swap. % = share of total system RAM used by this process"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -374,11 +448,13 @@ function ProcessDetailPanel({
|
|||||||
label="Read"
|
label="Read"
|
||||||
value={fmtBytes(detail.read_bytes)}
|
value={fmtBytes(detail.read_bytes)}
|
||||||
sub={`${detail.read_ops.toLocaleString()} ops`}
|
sub={`${detail.read_ops.toLocaleString()} ops`}
|
||||||
|
tooltip="Total bytes read from disk since process start. ops = number of read syscalls issued"
|
||||||
/>
|
/>
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="Write"
|
label="Write"
|
||||||
value={fmtBytes(detail.write_bytes)}
|
value={fmtBytes(detail.write_bytes)}
|
||||||
sub={`${detail.write_ops.toLocaleString()} ops`}
|
sub={`${detail.write_ops.toLocaleString()} ops`}
|
||||||
|
tooltip="Total bytes written to disk since process start. ops = number of write syscalls issued"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -390,12 +466,39 @@ function ProcessDetailPanel({
|
|||||||
<StatBlock
|
<StatBlock
|
||||||
label="Open files"
|
label="Open files"
|
||||||
value={detail.open_files_count}
|
value={detail.open_files_count}
|
||||||
|
tooltip="Number of open file descriptors: regular files, sockets, pipes, and device handles"
|
||||||
/>
|
/>
|
||||||
<StatBlock
|
<StatBlock
|
||||||
label="Connections"
|
label="Connections"
|
||||||
value={detail.conn_count}
|
value={detail.conn_count}
|
||||||
|
tooltip="Number of active network connections (TCP/UDP sockets) opened by this process"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Man page */}
|
||||||
|
{(manPage !== null || manPageLoading) && (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Manual page</SectionTitle>
|
||||||
|
{manPageLoading ? (
|
||||||
|
<p className="text-xs text-zinc-700 mb-4">Loading…</p>
|
||||||
|
) : (
|
||||||
|
<div className="mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setManOpen(o => !o)}
|
||||||
|
className="flex items-center gap-1.5 text-xs text-zinc-600 hover:text-zinc-400 transition-colors mb-2"
|
||||||
|
>
|
||||||
|
{manOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
|
{manOpen ? 'Collapse' : 'Show'} man {detail.name}
|
||||||
|
</button>
|
||||||
|
{manOpen && (
|
||||||
|
<pre className="text-[10px] font-mono text-zinc-500 whitespace-pre-wrap break-words bg-zinc-900/60 rounded-lg p-3 max-h-80 overflow-y-auto leading-relaxed border border-zinc-800/60">
|
||||||
|
{manPage}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -421,9 +524,15 @@ export default function App() {
|
|||||||
const [procOpen, setProcOpen] = useState(true)
|
const [procOpen, setProcOpen] = useState(true)
|
||||||
const [gpuOpen, setGpuOpen] = useState(true)
|
const [gpuOpen, setGpuOpen] = useState(true)
|
||||||
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
|
const [gpuSortBy, setGpuSortBy] = useState<'vram' | 'ram'>('vram')
|
||||||
|
const [navStack, setNavStack] = useState<NavItem[]>([])
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const openDetail = useCallback((pid: number) => {
|
||||||
const GPU_PAGE_SIZE = 4
|
setSelectedPid(pid)
|
||||||
|
setNavStack([])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [pageSize, setPageSize] = useState(20)
|
||||||
|
const [gpuPageSize, setGpuPageSize] = useState(4)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (m: Metrics) => setMetrics(m)
|
const handler = (m: Metrics) => setMetrics(m)
|
||||||
@@ -448,16 +557,16 @@ export default function App() {
|
|||||||
const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) =>
|
const gpuProcesses = [...(metrics?.gpu_processes ?? [])].sort((a, b) =>
|
||||||
gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram
|
gpuSortBy === 'vram' ? b.vram_mb - a.vram_mb : b.ram - a.ram
|
||||||
)
|
)
|
||||||
const gpuTotalPages = Math.ceil(gpuProcesses.length / GPU_PAGE_SIZE)
|
const gpuTotalPages = Math.ceil(gpuProcesses.length / gpuPageSize)
|
||||||
const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1))
|
const safeGpuPage = Math.min(gpuPage, Math.max(0, gpuTotalPages - 1))
|
||||||
const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * GPU_PAGE_SIZE, (safeGpuPage + 1) * GPU_PAGE_SIZE)
|
const pagedGpuProcesses = gpuProcesses.slice(safeGpuPage * gpuPageSize, (safeGpuPage + 1) * gpuPageSize)
|
||||||
|
|
||||||
const sortedProcesses = metrics
|
const sortedProcesses = metrics
|
||||||
? [...metrics.processes].sort((a, b) => b[sortBy] - a[sortBy])
|
? [...metrics.processes].sort((a, b) => b[sortBy] - a[sortBy])
|
||||||
: []
|
: []
|
||||||
const totalPages = Math.ceil(sortedProcesses.length / PAGE_SIZE)
|
const totalPages = Math.ceil(sortedProcesses.length / pageSize)
|
||||||
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
const safePage = Math.min(page, Math.max(0, totalPages - 1))
|
||||||
const pagedProcesses = sortedProcesses.slice(safePage * PAGE_SIZE, (safePage + 1) * PAGE_SIZE)
|
const pagedProcesses = sortedProcesses.slice(safePage * pageSize, (safePage + 1) * pageSize)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-zinc-950 text-zinc-100 antialiased select-none">
|
<div className="min-h-screen bg-zinc-950 text-zinc-100 antialiased select-none">
|
||||||
@@ -554,17 +663,17 @@ export default function App() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-zinc-800/80">
|
<tr className="border-b border-zinc-800/80">
|
||||||
<th className="py-2 px-5 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider w-16">PID</th>
|
<th className="py-2 px-5 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider w-16 cursor-help" title="Process ID — unique identifier assigned by the OS">PID</th>
|
||||||
<th className="py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider">Name</th>
|
<th className="py-2 text-left text-xs font-medium text-zinc-600 uppercase tracking-wider cursor-help" title="Executable name of the process">Name</th>
|
||||||
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28">CPU</th>
|
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="CPU usage as a % of a single core (can exceed 100% on multi-threaded processes)">CPU</th>
|
||||||
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28">Memory</th>
|
<th className="py-2 px-5 text-right text-xs font-medium text-zinc-600 uppercase tracking-wider w-28 cursor-help" title="Physical RAM (RSS) currently used by this process">Memory</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pagedProcesses.map((p) => (
|
{pagedProcesses.map((p) => (
|
||||||
<tr
|
<tr
|
||||||
key={p.pid}
|
key={p.pid}
|
||||||
onClick={() => setSelectedPid(p.pid)}
|
onClick={() => openDetail(p.pid)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b border-zinc-900 transition-colors cursor-pointer',
|
'border-b border-zinc-900 transition-colors cursor-pointer',
|
||||||
selectedPid === p.pid
|
selectedPid === p.pid
|
||||||
@@ -602,11 +711,21 @@ export default function App() {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 0 && (
|
||||||
<div className="flex items-center justify-between px-5 py-3 border-t border-zinc-800/60">
|
<div className="flex items-center justify-between px-5 py-3 border-t border-zinc-800/60">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-zinc-600">
|
<span className="text-xs text-zinc-600">
|
||||||
{safePage * PAGE_SIZE + 1}–{Math.min((safePage + 1) * PAGE_SIZE, sortedProcesses.length)} of {sortedProcesses.length}
|
{safePage * pageSize + 1}–{Math.min((safePage + 1) * pageSize, sortedProcesses.length)} of {sortedProcesses.length}
|
||||||
</span>
|
</span>
|
||||||
|
<select
|
||||||
|
value={pageSize}
|
||||||
|
onChange={e => { setPageSize(Number(e.target.value)); setPage(0) }}
|
||||||
|
className="text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
|
||||||
|
title="Items per page"
|
||||||
|
>
|
||||||
|
{[10, 20, 50, 100].map(n => <option key={n} value={n}>{n} / page</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(0)}
|
onClick={() => setPage(0)}
|
||||||
@@ -746,10 +865,10 @@ export default function App() {
|
|||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider">Name</th>
|
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider cursor-help" title="Process name using this GPU">Name</th>
|
||||||
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-16">Type</th>
|
<th className="pb-1.5 text-left text-xs font-medium text-zinc-700 uppercase tracking-wider w-16 cursor-help" title="C = Compute (CUDA/OpenCL), G = Graphics (rendering), C+G = both">Type</th>
|
||||||
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-24">VRAM</th>
|
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-24 cursor-help" title="GPU VRAM used by this process (video memory on the graphics card)">VRAM</th>
|
||||||
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-28">RAM</th>
|
<th className="pb-1.5 text-right text-xs font-medium text-zinc-700 uppercase tracking-wider w-28 cursor-help" title="System RAM (CPU-side memory) used by this process">RAM</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -757,13 +876,18 @@ export default function App() {
|
|||||||
<tr
|
<tr
|
||||||
key={p.pid}
|
key={p.pid}
|
||||||
className="border-t border-zinc-900 hover:bg-zinc-800/30 transition-colors cursor-pointer"
|
className="border-t border-zinc-900 hover:bg-zinc-800/30 transition-colors cursor-pointer"
|
||||||
onClick={() => setSelectedPid(p.pid)}
|
onClick={() => openDetail(p.pid)}
|
||||||
>
|
>
|
||||||
<td className="py-1.5 text-zinc-300 text-xs truncate max-w-[180px]">{p.name}</td>
|
<td className="py-1.5 text-zinc-300 text-xs truncate max-w-[180px]">{p.name}</td>
|
||||||
<td className="py-1.5">
|
<td className="py-1.5">
|
||||||
<span
|
<span
|
||||||
|
title={
|
||||||
|
p.type === 'C' ? 'C — Compute: uses the GPU for compute workloads (CUDA, OpenCL, Vulkan Compute…)' :
|
||||||
|
p.type === 'G' ? 'G — Graphics: uses the GPU for rendering and display output' :
|
||||||
|
'C+G — Compute + Graphics: uses the GPU for both compute and rendering workloads'
|
||||||
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-block px-1.5 py-0.5 rounded text-xs font-mono font-semibold',
|
'inline-block px-1.5 py-0.5 rounded text-xs font-mono font-semibold cursor-help',
|
||||||
p.type === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
p.type === 'C' ? 'bg-violet-900/50 text-violet-300' :
|
||||||
p.type === 'G' ? 'bg-blue-900/50 text-blue-300' :
|
p.type === 'G' ? 'bg-blue-900/50 text-blue-300' :
|
||||||
'bg-indigo-900/50 text-indigo-300'
|
'bg-indigo-900/50 text-indigo-300'
|
||||||
@@ -780,11 +904,21 @@ export default function App() {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{/* GPU pagination */}
|
{/* GPU pagination */}
|
||||||
{gpuTotalPages > 1 && (
|
{gpuTotalPages > 0 && (
|
||||||
<div className="flex items-center justify-between pt-2 mt-1 border-t border-zinc-900">
|
<div className="flex items-center justify-between pt-2 mt-1 border-t border-zinc-900">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-zinc-600">
|
<span className="text-xs text-zinc-600">
|
||||||
{safeGpuPage * GPU_PAGE_SIZE + 1}–{Math.min((safeGpuPage + 1) * GPU_PAGE_SIZE, gpuProcesses.length)} of {gpuProcesses.length}
|
{safeGpuPage * gpuPageSize + 1}–{Math.min((safeGpuPage + 1) * gpuPageSize, gpuProcesses.length)} of {gpuProcesses.length}
|
||||||
</span>
|
</span>
|
||||||
|
<select
|
||||||
|
value={gpuPageSize}
|
||||||
|
onChange={e => { setGpuPageSize(Number(e.target.value)); setGpuPage(0) }}
|
||||||
|
className="text-xs bg-zinc-900 border border-zinc-800 text-zinc-400 rounded px-1.5 py-0.5 cursor-pointer hover:border-zinc-600 transition-colors"
|
||||||
|
title="Items per page"
|
||||||
|
>
|
||||||
|
{[4, 10, 20, 50].map(n => <option key={n} value={n}>{n} / page</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0}
|
<button onClick={() => setGpuPage(0)} disabled={safeGpuPage === 0}
|
||||||
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">«</button>
|
className="px-2 py-1 rounded text-xs text-zinc-600 hover:text-zinc-300 hover:bg-zinc-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors">«</button>
|
||||||
@@ -827,7 +961,18 @@ export default function App() {
|
|||||||
{selectedPid !== null && (
|
{selectedPid !== null && (
|
||||||
<ProcessDetailPanel
|
<ProcessDetailPanel
|
||||||
pid={selectedPid}
|
pid={selectedPid}
|
||||||
onClose={() => setSelectedPid(null)}
|
onClose={() => { setSelectedPid(null); setNavStack([]) }}
|
||||||
|
onNavigate={(targetPid, fromName) => {
|
||||||
|
setNavStack(s => [...s, { pid: selectedPid, name: fromName }])
|
||||||
|
setSelectedPid(targetPid)
|
||||||
|
}}
|
||||||
|
navStack={navStack}
|
||||||
|
onNavBack={(index) => {
|
||||||
|
const target = navStack[index]
|
||||||
|
setNavStack(s => s.slice(0, index))
|
||||||
|
setSelectedPid(target.pid)
|
||||||
|
}}
|
||||||
|
gpuType={metrics?.gpu_processes?.find(gp => gp.pid === selectedPid)?.type}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
frontend/wailsjs/go/main/App.d.ts
vendored
2
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -3,6 +3,8 @@
|
|||||||
import {backend} from '../models';
|
import {backend} from '../models';
|
||||||
import {context} from '../models';
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function GetManPage(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function GetMetrics():Promise<backend.Metrics>;
|
export function GetMetrics():Promise<backend.Metrics>;
|
||||||
|
|
||||||
export function GetProcessDetail(arg1:number):Promise<backend.ProcessDetail>;
|
export function GetProcessDetail(arg1:number):Promise<backend.ProcessDetail>;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function GetManPage(arg1) {
|
||||||
|
return window['go']['main']['App']['GetManPage'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetMetrics() {
|
export function GetMetrics() {
|
||||||
return window['go']['main']['App']['GetMetrics']();
|
return window['go']['main']['App']['GetMetrics']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export namespace backend {
|
|||||||
username: string;
|
username: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
parent_pid: number;
|
parent_pid: number;
|
||||||
|
parent_name: string;
|
||||||
nice: number;
|
nice: number;
|
||||||
num_threads: number;
|
num_threads: number;
|
||||||
num_fds: number;
|
num_fds: number;
|
||||||
@@ -126,6 +127,7 @@ export namespace backend {
|
|||||||
this.username = source["username"];
|
this.username = source["username"];
|
||||||
this.created_at = source["created_at"];
|
this.created_at = source["created_at"];
|
||||||
this.parent_pid = source["parent_pid"];
|
this.parent_pid = source["parent_pid"];
|
||||||
|
this.parent_name = source["parent_name"];
|
||||||
this.nice = source["nice"];
|
this.nice = source["nice"];
|
||||||
this.num_threads = source["num_threads"];
|
this.num_threads = source["num_threads"];
|
||||||
this.num_fds = source["num_fds"];
|
this.num_fds = source["num_fds"];
|
||||||
|
|||||||
Reference in New Issue
Block a user