406 lines
10 KiB
TypeScript
406 lines
10 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
FormControlLabel,
|
|
InputLabel,
|
|
Link,
|
|
MenuItem,
|
|
Select,
|
|
TextField,
|
|
Tooltip,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import React, { PropsWithChildren, useEffect, useState } from "react";
|
|
import { HelpInfo } from "../components/Tooltip";
|
|
import { ClassNameProps } from "./props";
|
|
|
|
let cachedProfilingEnabled: boolean | null = null;
|
|
let fetchPromise: Promise<void> | null = null;
|
|
|
|
const fetchProfilingEnabled = (): Promise<void> => {
|
|
if (cachedProfilingEnabled !== null) {
|
|
return Promise.resolve();
|
|
}
|
|
if (!fetchPromise) {
|
|
fetchPromise = fetch("/api/profiling_enabled")
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
cachedProfilingEnabled = data.data.profilingEnabled;
|
|
})
|
|
.catch(() => {
|
|
cachedProfilingEnabled = false;
|
|
});
|
|
}
|
|
return fetchPromise;
|
|
};
|
|
|
|
const useProfilingEnabled = () => {
|
|
const [enabled, setEnabled] = useState(cachedProfilingEnabled ?? false);
|
|
|
|
useEffect(() => {
|
|
fetchProfilingEnabled().then(() =>
|
|
setEnabled(cachedProfilingEnabled ?? false),
|
|
);
|
|
}, []);
|
|
|
|
return enabled;
|
|
};
|
|
|
|
const PROFILING_DISABLED_TOOLTIP =
|
|
"Profiling is disabled by default for security. " +
|
|
"Set RAY_DASHBOARD_ENABLE_PROFILING=1 environment variable on the Ray head node to enable. " +
|
|
"See https://docs.ray.io/en/latest/ray-observability/user-guides/profiling.html#enabling-dashboard-profiling";
|
|
|
|
const DisabledProfilingLabel = ({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) => (
|
|
<Tooltip title={PROFILING_DISABLED_TOOLTIP}>
|
|
<Typography component="span" color="text.disabled">
|
|
{children}
|
|
</Typography>
|
|
</Tooltip>
|
|
);
|
|
|
|
type CpuProfilingLinkProps = PropsWithChildren<
|
|
{
|
|
pid: string | number | null | undefined;
|
|
nodeId: string | null | undefined;
|
|
type: string | null;
|
|
} & ClassNameProps
|
|
>;
|
|
|
|
type TaskProfilingStackTraceProps = {
|
|
taskId: string | null | undefined;
|
|
attemptNumber: number;
|
|
nodeId: string;
|
|
};
|
|
|
|
type MemoryProfilingProps = PropsWithChildren<
|
|
{
|
|
pid: string | number | null | undefined;
|
|
nodeId: string | null | undefined;
|
|
type?: string | null;
|
|
} & ClassNameProps
|
|
>;
|
|
|
|
type TaskMemoryProfilingProps = {
|
|
taskId: string | null | undefined;
|
|
attemptNumber: number;
|
|
nodeId: string;
|
|
};
|
|
|
|
type MemoryProfilingButtonProps = {
|
|
profilerUrl: string;
|
|
type?: string | null;
|
|
};
|
|
|
|
export const TaskCpuProfilingLink = ({
|
|
taskId,
|
|
attemptNumber,
|
|
nodeId,
|
|
}: TaskProfilingStackTraceProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (!taskId) {
|
|
return null;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return (
|
|
<DisabledProfilingLabel>CPU Flame Graph</DisabledProfilingLabel>
|
|
);
|
|
}
|
|
return (
|
|
<Link
|
|
href={`task/cpu_profile?task_id=${taskId}&attempt_number=${attemptNumber}&node_id=${nodeId}`}
|
|
target="_blank"
|
|
title="Profile the Python worker for 5 seconds (default) and display a CPU flame graph."
|
|
rel="noreferrer"
|
|
>
|
|
CPU Flame Graph
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
export const TaskCpuStackTraceLink = ({
|
|
taskId,
|
|
attemptNumber,
|
|
nodeId,
|
|
}: TaskProfilingStackTraceProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (!taskId) {
|
|
return null;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return <DisabledProfilingLabel>Stack Trace</DisabledProfilingLabel>;
|
|
}
|
|
return (
|
|
<Link
|
|
href={`task/traceback?task_id=${taskId}&attempt_number=${attemptNumber}&node_id=${nodeId}`}
|
|
target="_blank"
|
|
title="Sample the current Python stack trace for this worker."
|
|
rel="noreferrer"
|
|
>
|
|
Stack Trace
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
export const CpuStackTraceLink = ({
|
|
pid,
|
|
nodeId,
|
|
type = "",
|
|
}: CpuProfilingLinkProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (
|
|
!pid ||
|
|
!nodeId ||
|
|
typeof pid === "undefined" ||
|
|
typeof nodeId === "undefined"
|
|
) {
|
|
return <div></div>;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return (
|
|
<DisabledProfilingLabel>
|
|
Stack Trace{type ? ` (${type})` : ""}
|
|
</DisabledProfilingLabel>
|
|
);
|
|
}
|
|
return (
|
|
<Link
|
|
href={`worker/traceback?pid=${pid}&node_id=${nodeId}&native=0`}
|
|
target="_blank"
|
|
title="Sample the current Python stack trace for this worker."
|
|
rel="noreferrer"
|
|
>
|
|
Stack Trace{type ? ` (${type})` : ""}
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
export const CpuProfilingLink = ({
|
|
pid,
|
|
nodeId,
|
|
type = "",
|
|
}: CpuProfilingLinkProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (!pid || !nodeId) {
|
|
return <div></div>;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return (
|
|
<DisabledProfilingLabel>
|
|
CPU Flame Graph{type ? ` (${type})` : ""}
|
|
</DisabledProfilingLabel>
|
|
);
|
|
}
|
|
return (
|
|
<Link
|
|
href={`worker/cpu_profile?pid=${pid}&node_id=${nodeId}&duration=5&native=0`}
|
|
target="_blank"
|
|
title="Profile the Python worker for 5 seconds (default) and display a CPU flame graph."
|
|
rel="noreferrer"
|
|
>
|
|
CPU Flame Graph{type ? ` (${type})` : ""}
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
export const ProfilerButton = ({
|
|
profilerUrl,
|
|
type,
|
|
}: MemoryProfilingButtonProps) => {
|
|
const [duration, setDuration] = useState(5);
|
|
const [leaks, setLeaks] = useState(true);
|
|
const [native, setNative] = useState(false);
|
|
const [allocator, setAllocator] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
const [format, setFormat] = useState("flamegraph");
|
|
|
|
const handleOpen = () => {
|
|
setOpen(true);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Link
|
|
onClick={handleOpen}
|
|
aria-label="Memory Profiling"
|
|
sx={{ cursor: "pointer" }}
|
|
>
|
|
Memory Profiling{type ? ` (${type})` : ""}
|
|
</Link>
|
|
|
|
<Dialog open={open} onClose={handleClose}>
|
|
<DialogTitle>Memory Profiling Config</DialogTitle>
|
|
<DialogContent>
|
|
<InputLabel id="format-label">Format</InputLabel>
|
|
<Select
|
|
labelId="format-label"
|
|
id="format"
|
|
value={format}
|
|
aria-label={format}
|
|
onChange={(e) => setFormat(e.target.value as string)}
|
|
fullWidth
|
|
style={{ marginBottom: "12px" }}
|
|
>
|
|
<MenuItem value="flamegraph">Flamegraph</MenuItem>
|
|
<MenuItem value="table">Table</MenuItem>
|
|
</Select>
|
|
<TextField
|
|
label="Duration (seconds)"
|
|
type="number"
|
|
value={duration !== null ? duration : ""}
|
|
onChange={(e) => setDuration(parseInt(e.target.value, 10))}
|
|
required
|
|
/>
|
|
<br />
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={leaks}
|
|
onChange={(e) => setLeaks(e.target.checked)}
|
|
/>
|
|
}
|
|
label={
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<span style={{ marginRight: "4px" }}>Leaks</span>
|
|
<HelpInfo>
|
|
<Typography>
|
|
Enable memory leaks, instead of peak memory usage. Refer to
|
|
Memray documentation for more details.
|
|
</Typography>
|
|
</HelpInfo>
|
|
</div>
|
|
}
|
|
/>
|
|
<br />
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={native}
|
|
onChange={(e) => setNative(e.target.checked)}
|
|
/>
|
|
}
|
|
label={
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<span style={{ marginRight: "4px" }}>Native</span>
|
|
<HelpInfo>
|
|
<Typography>
|
|
Track native (C/C++) stack frames. Refer to Memray
|
|
documentation for more details.
|
|
</Typography>
|
|
</HelpInfo>
|
|
</div>
|
|
}
|
|
/>
|
|
<br />
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={allocator}
|
|
onChange={(e) => setAllocator(e.target.checked)}
|
|
/>
|
|
}
|
|
label={
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<span style={{ marginRight: "4px" }}>
|
|
Python Allocator Tracing
|
|
</span>
|
|
<HelpInfo>
|
|
<Typography>
|
|
Record allocations made by the pymalloc allocator. Refer to
|
|
Memray documentation for more details.
|
|
</Typography>
|
|
</HelpInfo>
|
|
</div>
|
|
}
|
|
/>
|
|
</DialogContent>
|
|
<Box
|
|
sx={{ padding: "12px", display: "flex", justifyContent: "flex-end" }}
|
|
>
|
|
<Button
|
|
onClick={handleClose}
|
|
variant="text"
|
|
sx={(theme) => ({
|
|
textTransform: "capitalize",
|
|
color: theme.palette.text.secondary,
|
|
})}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
color="primary"
|
|
variant="text"
|
|
onClick={handleClose}
|
|
style={{ textTransform: "capitalize" }}
|
|
>
|
|
<Link
|
|
href={
|
|
`${profilerUrl}&format=${format}&duration=${duration}` +
|
|
`&leaks=${leaks ? "1" : "0"}` +
|
|
`&native=${native ? "1" : "0"}` +
|
|
`&trace_python_allocators=${allocator ? "1" : "0"}`
|
|
}
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
>
|
|
Generate report
|
|
</Link>
|
|
</Button>
|
|
</Box>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const MemoryProfilingButton = ({
|
|
pid,
|
|
nodeId,
|
|
type = "",
|
|
}: MemoryProfilingProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (!pid || !nodeId) {
|
|
return <div></div>;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return (
|
|
<DisabledProfilingLabel>
|
|
Memory Profiling{type ? ` (${type})` : ""}
|
|
</DisabledProfilingLabel>
|
|
);
|
|
}
|
|
const profilerUrl = `memory_profile?pid=${pid}&node_id=${nodeId}`;
|
|
|
|
return <ProfilerButton profilerUrl={profilerUrl} type={type} />;
|
|
};
|
|
|
|
export const TaskMemoryProfilingButton = ({
|
|
taskId,
|
|
attemptNumber,
|
|
nodeId,
|
|
}: TaskMemoryProfilingProps) => {
|
|
const profilingEnabled = useProfilingEnabled();
|
|
if (!taskId) {
|
|
return null;
|
|
}
|
|
if (!profilingEnabled) {
|
|
return (
|
|
<DisabledProfilingLabel>Memory Profiling</DisabledProfilingLabel>
|
|
);
|
|
}
|
|
const profilerUrl = `memory_profile?task_id=${taskId}&attempt_number=${attemptNumber}&node_id=${nodeId}`;
|
|
|
|
return <ProfilerButton profilerUrl={profilerUrl} />;
|
|
};
|