fix: add sentinel run memory detail curves
This commit is contained in:
@@ -351,6 +351,23 @@ select {
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.trend-red,
|
||||
.trend-warning,
|
||||
.memory-line {
|
||||
fill: none;
|
||||
stroke-width: 2.4;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.trend-red {
|
||||
stroke: var(--red);
|
||||
}
|
||||
|
||||
.trend-warning {
|
||||
stroke: var(--amber);
|
||||
}
|
||||
|
||||
.trend-grid-line {
|
||||
stroke: #e5ecea;
|
||||
stroke-width: 1;
|
||||
@@ -362,6 +379,18 @@ select {
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.trend-dot-red {
|
||||
fill: var(--red);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.trend-dot-warning {
|
||||
fill: var(--amber);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
.trend-dot-hit {
|
||||
cursor: default;
|
||||
outline: none;
|
||||
@@ -442,6 +471,42 @@ select {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-1,
|
||||
.memory-line-1 {
|
||||
stroke: #2a6fbb;
|
||||
background: #2a6fbb;
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-2,
|
||||
.memory-line-2 {
|
||||
stroke: #1f8a5b;
|
||||
background: #1f8a5b;
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-3,
|
||||
.memory-line-3 {
|
||||
stroke: #b7791f;
|
||||
background: #b7791f;
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-4,
|
||||
.memory-line-4 {
|
||||
stroke: #7a5dc7;
|
||||
background: #7a5dc7;
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-5,
|
||||
.memory-line-5 {
|
||||
stroke: #c04f7a;
|
||||
background: #c04f7a;
|
||||
}
|
||||
|
||||
.legend-swatch.memory.memory-line-6,
|
||||
.memory-line-6 {
|
||||
stroke: #327b89;
|
||||
background: #327b89;
|
||||
}
|
||||
|
||||
.timeline-panel {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -888,6 +953,46 @@ select {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-card-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-card-heading h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.memory-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.memory-chart-wrap {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.memory-chart {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 154px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.memory-axis,
|
||||
.memory-legend {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -49,9 +49,13 @@ createApp({
|
||||
const trendRows = computed(() => runs.value.slice().sort((a, b) => Date.parse(a.updatedAt || a.createdAt || "") - Date.parse(b.updatedAt || b.createdAt || "")).slice(-48));
|
||||
const latestTrendRun = computed(() => trendRows.value.length > 0 ? trendRows.value[trendRows.value.length - 1] : latestRun.value);
|
||||
const trendDurationMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendDurationMinutes(run))));
|
||||
const trendMax = computed(() => Math.max(1, trendDurationMax.value));
|
||||
const trendErrorMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendErrorCount(run))));
|
||||
const trendWarningMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendWarningCount(run))));
|
||||
const trendMax = computed(() => Math.max(1, trendDurationMax.value, trendErrorMax.value, trendWarningMax.value));
|
||||
const trendPolylines = computed(() => ({
|
||||
duration: trendPolyline((run) => trendDurationMinutes(run)),
|
||||
red: trendPolyline((run) => trendErrorCount(run)),
|
||||
warning: trendPolyline((run) => trendWarningCount(run)),
|
||||
}));
|
||||
const trendDots = computed(() => trendRows.value.map((run, index) => {
|
||||
const red = trendErrorCount(run);
|
||||
@@ -60,17 +64,22 @@ createApp({
|
||||
const duration = trendDurationMinutes(run);
|
||||
const x = trendX(index, trendRows.value.length);
|
||||
const durationY = trendY(duration);
|
||||
const redY = trendY(red);
|
||||
const warningY = trendY(warning);
|
||||
const rawTime = run.updatedAt || run.createdAt || "";
|
||||
const durationText = runDurationText(run);
|
||||
const configTimingText = runTimingConfigText(run);
|
||||
const tooltipY = Math.min(durationY, redY, warningY);
|
||||
return {
|
||||
id: run.id || String(index),
|
||||
runId: run.id || "",
|
||||
x,
|
||||
durationY,
|
||||
redY,
|
||||
warningY,
|
||||
duration,
|
||||
tooltipLeft: `${clamp((x / 720) * 100, 16, 84)}%`,
|
||||
tooltipTop: `${clamp(((durationY + 18) / 142) * 100, 24, 76)}%`,
|
||||
tooltipTop: `${clamp(((tooltipY + 18) / 142) * 100, 24, 76)}%`,
|
||||
red,
|
||||
warning,
|
||||
total,
|
||||
@@ -94,6 +103,16 @@ createApp({
|
||||
const rows = selectedDetail.value?.findings;
|
||||
return Array.isArray(rows) ? rows : [];
|
||||
});
|
||||
const detailMemory = computed(() => selectedDetail.value?.memory && typeof selectedDetail.value.memory === "object" ? selectedDetail.value.memory : {});
|
||||
const detailMemorySeries = computed(() => {
|
||||
const rows = Array.isArray(detailMemory.value.pageSeries) ? detailMemory.value.pageSeries : [];
|
||||
return rows.map((series) => ({
|
||||
...series,
|
||||
points: Array.isArray(series.points) ? series.points.filter((point) => Number.isFinite(Number(point?.memoryMb))) : [],
|
||||
})).filter((series) => series.points.length > 0);
|
||||
});
|
||||
const detailMemoryMax = computed(() => Math.max(1, ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => number(point.memoryMb)))));
|
||||
const detailMemoryDurationMax = computed(() => Math.max(1, ...detailMemorySeries.value.flatMap((series) => series.points.map((point) => number(point.elapsedMinutes)))));
|
||||
const scopedCheckFindings = computed(() => {
|
||||
if (checkScope.value === "history") return historicalCheckFindings.value;
|
||||
return runDetailCheckFindings.value;
|
||||
@@ -291,6 +310,32 @@ createApp({
|
||||
return Math.round(126 - (Number(value || 0) / trendMax.value) * 102);
|
||||
}
|
||||
|
||||
function memoryPolyline(series) {
|
||||
const points = Array.isArray(series?.points) ? series.points : [];
|
||||
if (points.length < 2) return "";
|
||||
return points.map((point) => `${memoryX(point)},${memoryY(point)}`).join(" ");
|
||||
}
|
||||
|
||||
function memoryX(point) {
|
||||
const elapsed = number(point?.elapsedMinutes);
|
||||
return Math.round(24 + (elapsed / detailMemoryDurationMax.value) * 672);
|
||||
}
|
||||
|
||||
function memoryY(point) {
|
||||
const memory = number(point?.memoryMb);
|
||||
return Math.round(126 - (memory / detailMemoryMax.value) * 102);
|
||||
}
|
||||
|
||||
function memoryLineClass(index) {
|
||||
return `memory-line-${(index % 6) + 1}`;
|
||||
}
|
||||
|
||||
function memorySeriesLabel(series) {
|
||||
const sampleCount = Number.isFinite(Number(series?.sampleCount)) ? Number(series.sampleCount) : Array.isArray(series?.points) ? series.points.length : 0;
|
||||
const latest = Array.isArray(series?.points) && series.points.length > 0 ? series.points[series.points.length - 1] : null;
|
||||
return `${series?.label || series?.pageRole || series?.pageId || "page"} · 最新 ${formatMb(latest?.memoryMb)} · 样本 ${sampleCount}`;
|
||||
}
|
||||
|
||||
function showTrendTooltip(dot) {
|
||||
hoveredTrendDot.value = dot;
|
||||
}
|
||||
@@ -346,11 +391,17 @@ createApp({
|
||||
trendRows,
|
||||
latestTrendRun,
|
||||
trendDurationMax,
|
||||
trendErrorMax,
|
||||
trendWarningMax,
|
||||
trendPolylines,
|
||||
trendDots,
|
||||
timelineRuns,
|
||||
historicalCheckFindings,
|
||||
runDetailCheckFindings,
|
||||
detailMemory,
|
||||
detailMemorySeries,
|
||||
detailMemoryMax,
|
||||
detailMemoryDurationMax,
|
||||
scopedCheckFindings,
|
||||
scopedCheckSummary,
|
||||
visibleCheckFindings,
|
||||
@@ -380,6 +431,9 @@ createApp({
|
||||
trendErrorCount,
|
||||
trendWarningCount,
|
||||
trendDurationMinutes,
|
||||
memoryPolyline,
|
||||
memoryLineClass,
|
||||
memorySeriesLabel,
|
||||
runDurationText,
|
||||
runTimingConfigText,
|
||||
severityClass,
|
||||
@@ -387,6 +441,7 @@ createApp({
|
||||
formatAbsoluteDate,
|
||||
formatDuration,
|
||||
formatMinutes,
|
||||
formatMb,
|
||||
shortId,
|
||||
rootCauseText,
|
||||
findingTitle,
|
||||
@@ -455,17 +510,19 @@ createApp({
|
||||
<section class="trend-panel" aria-labelledby="trend-heading">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 id="trend-heading">运行耗时曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次巡检耗时,单位为分钟</p>
|
||||
<h2 id="trend-heading">运行趋势曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次巡检的耗时、独立错误类型和独立告警类型</p>
|
||||
</div>
|
||||
<span class="pill" :class="cadence.stale ? 'warning' : 'healthy'">{{ cadence.stale ? "非阻塞报警" : "新鲜" }}</span>
|
||||
</div>
|
||||
<div class="trend-chart-wrap">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="运行耗时分钟趋势" data-monitor-trend-curve="true">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="运行耗时、独立错误和独立告警趋势" data-monitor-trend-curve="true">
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
|
||||
<polyline v-if="trendPolylines.duration" class="trend-duration" :points="trendPolylines.duration"></polyline>
|
||||
<polyline v-if="trendPolylines.red" class="trend-red" :points="trendPolylines.red" data-monitor-trend-error-curve="true"></polyline>
|
||||
<polyline v-if="trendPolylines.warning" class="trend-warning" :points="trendPolylines.warning" data-monitor-trend-warning-curve="true"></polyline>
|
||||
<g
|
||||
v-for="dot in trendDots"
|
||||
:key="dot.id"
|
||||
@@ -479,6 +536,8 @@ createApp({
|
||||
@focusout="hideTrendTooltip"
|
||||
>
|
||||
<circle class="trend-hit-area" :cx="dot.x" :cy="dot.durationY" r="12"></circle>
|
||||
<circle class="trend-dot-red" :cx="dot.x" :cy="dot.redY" r="3"></circle>
|
||||
<circle class="trend-dot-warning" :cx="dot.x" :cy="dot.warningY" r="3"></circle>
|
||||
<circle class="trend-dot-duration" :cx="dot.x" :cy="dot.durationY" r="4">
|
||||
<title>{{ dot.title }}</title>
|
||||
</circle>
|
||||
@@ -502,8 +561,9 @@ createApp({
|
||||
</div>
|
||||
<div class="trend-legend">
|
||||
<span class="legend-item"><span class="legend-swatch duration"></span>最新运行耗时 {{ runDurationText(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch red"></span>独立错误 {{ trendErrorCount(latestTrendRun) }} / 最高 {{ trendErrorMax }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>独立告警 {{ trendWarningCount(latestTrendRun) }} / 最高 {{ trendWarningMax }}</span>
|
||||
<span class="legend-item">最近最高耗时 {{ formatMinutes(trendDurationMax) }}</span>
|
||||
<span class="legend-item">最新错误 {{ trendErrorCount(latestTrendRun) }} / 告警 {{ trendWarningCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item">{{ cadence.alert }}</span>
|
||||
</div>
|
||||
@@ -651,6 +711,48 @@ createApp({
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="detail-card detail-card-wide memory-card"
|
||||
data-run-memory-chart="true"
|
||||
:data-memory-run-id="selectedRunId"
|
||||
:data-memory-page-count="detailMemorySeries.length"
|
||||
:data-memory-sample-count="detailMemory.sampleCount || 0"
|
||||
:data-memory-source="detailMemory.source || ''"
|
||||
>
|
||||
<div class="detail-card-heading">
|
||||
<h3>页面内存曲线</h3>
|
||||
<span class="tag">每个页面一条线 · 峰值 {{ formatMb(detailMemory.maxMemoryMb || detailMemoryMax) }}</span>
|
||||
</div>
|
||||
<div v-if="detailMemorySeries.length > 0" class="memory-chart-wrap">
|
||||
<svg class="memory-chart" viewBox="0 0 720 142" role="img" aria-label="运行详情页面内存曲线">
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
|
||||
<polyline
|
||||
v-for="(series, index) in detailMemorySeries"
|
||||
:key="series.key || series.label || index"
|
||||
class="memory-line"
|
||||
:class="memoryLineClass(index)"
|
||||
:points="memoryPolyline(series)"
|
||||
></polyline>
|
||||
</svg>
|
||||
<div class="memory-axis">
|
||||
<span>0m</span>
|
||||
<span>{{ formatMinutes(detailMemoryDurationMax) }}</span>
|
||||
<span>峰值 {{ formatMb(detailMemory.maxMemoryMb || detailMemoryMax) }}</span>
|
||||
</div>
|
||||
<div class="memory-legend">
|
||||
<span
|
||||
v-for="(series, index) in detailMemorySeries"
|
||||
:key="(series.key || series.label || index) + '-legend'"
|
||||
class="legend-item"
|
||||
>
|
||||
<span class="legend-swatch memory" :class="memoryLineClass(index)"></span>{{ memorySeriesLabel(series) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty">暂无页面级内存样本</div>
|
||||
</section>
|
||||
<section class="detail-card">
|
||||
<h3>复现命令</h3>
|
||||
<pre>{{ commandSummary(selectedDetail) }}</pre>
|
||||
@@ -963,6 +1065,14 @@ function formatMinutes(value) {
|
||||
return `${Number.isInteger(rounded) ? String(rounded) : String(rounded).replace(/0+$/u, "").replace(/\.$/u, "")} 分钟`;
|
||||
}
|
||||
|
||||
function formatMb(value) {
|
||||
const numberValue = optionalNumber(value);
|
||||
if (numberValue === null) return "-";
|
||||
const rounded = Math.round(numberValue * 100) / 100;
|
||||
const text = rounded >= 100 ? String(Math.round(rounded)) : rounded >= 10 ? rounded.toFixed(1).replace(/\.0$/u, "") : rounded.toFixed(2).replace(/0+$/u, "").replace(/\.$/u, "");
|
||||
return `${text} MB`;
|
||||
}
|
||||
|
||||
function runDurationText(run) {
|
||||
const timing = run?.timing || {};
|
||||
return formatMinutes(optionalNumber(run?.runDurationMinutes, run?.durationMinutes, timing.runDurationMinutes, timing.durationMinutes));
|
||||
|
||||
Reference in New Issue
Block a user