feat(query): cross-database timing details, drop OceanBase audit query
This commit is contained in:
1 parent
4b9a1c4d5c
commit
d0497e0ca6
69 files changed
+973
-180
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import QueryTimingDetails from "./QueryTimingDetails.vue";
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import { applyDdlSearchMarks } from "@/lib/sql/ddlSearchMarks";
|
||||
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
|
||||
@@ -6713,7 +6714,7 @@ let dataGridIsActive = true;
|
||||
let canvasRuntime: DataGridCanvasRuntime;
|
||||
const { elapsedMs: resultViewUpdateMs, canvasDrawCompleted: completeResultCanvasDraw } = useResultViewUpdateTiming(
|
||||
() => props.result,
|
||||
() => resolvedDatabaseType.value === "oceanbase-oracle" && isResultsContext.value,
|
||||
() => isResultsContext.value,
|
||||
() => (useCanvasGridRows.value ? "canvas" : "dom"),
|
||||
);
|
||||
|
||||
@@ -13987,12 +13988,7 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
|
||||
</span>
|
||||
<span v-if="showTruncationWarning" class="shrink-0 text-amber-500 text-xs">(truncated)</span>
|
||||
<span v-if="!hasData" class="shrink-0">{{ t("grid.rowsAffected", { count: result.affected_rows }) }}</span>
|
||||
<template v-if="resolvedDatabaseType === 'oceanbase-oracle' && isResultsContext">
|
||||
<span class="shrink-0" :title="t('grid.serverExecuteTimeHint')">{{ result.server_execute_time_us !== undefined ? t("grid.serverExecuteTime", { us: result.server_execute_time_us }) : t("grid.serverExecuteTimeUnavailable") }}</span>
|
||||
<span class="shrink-0" :title="t('grid.agentExecuteTimeHint')">{{ t("grid.agentExecuteTime", { ms: result.execution_time_ms }) }}</span>
|
||||
<span v-if="result.client_request_wait_ms !== undefined" class="shrink-0" :title="t('grid.clientRequestWaitHint')">{{ t("grid.clientRequestWait", { ms: result.client_request_wait_ms }) }}</span>
|
||||
<span v-if="resultViewUpdateMs !== undefined" class="shrink-0" :title="t('grid.resultViewUpdateHint')">{{ t("grid.resultViewUpdate", { ms: resultViewUpdateMs }) }}</span>
|
||||
</template>
|
||||
<QueryTimingDetails v-if="isResultsContext" :result="result" :render-ms="resultViewUpdateMs" />
|
||||
<span v-else class="shrink-0">{{ result.execution_time_ms }}ms</span>
|
||||
|
||||
<template v-if="editable && hasDataGridSaveTarget">
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
const props = defineProps<{ result: QueryResult; renderMs?: number }>();
|
||||
const { t } = useI18n();
|
||||
const measured = (value: number | undefined): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
||||
const format = (value: number | undefined) => (measured(value) ? `${Number(value.toFixed(3))}ms` : t("grid.timingUnavailable"));
|
||||
const wait = computed(() => (measured(props.result.client_request_wait_ms) ? `${Math.round(props.result.client_request_wait_ms)}ms` : "—"));
|
||||
const hasAgentTiming = computed(() => measured(props.result.query_timings_ms?.agent_total));
|
||||
const phases = computed(() => {
|
||||
const m = props.result.query_timings_ms ?? {};
|
||||
const waitMs = props.result.client_request_wait_ms;
|
||||
// This is a cross-boundary remainder, not a measured network duration.
|
||||
const remainder = measured(waitMs) && measured(m.agent_total) && measured(m.core_lock) ? Math.max(0, waitMs - m.agent_total - m.core_lock) : undefined;
|
||||
const agentOther = measured(m.agent_total)
|
||||
? Math.max(
|
||||
0,
|
||||
m.agent_total -
|
||||
Object.entries(m)
|
||||
.filter(([key]) => !["agent_total", "core_lock"].includes(key))
|
||||
.reduce((sum, [, value]) => sum + (measured(value) ? value : 0), 0),
|
||||
)
|
||||
: undefined;
|
||||
const stages = [
|
||||
{ key: "prepare", value: props.result.client_prepare_ms },
|
||||
{ key: "request", value: waitMs },
|
||||
{ key: "core_lock", value: m.core_lock },
|
||||
{ key: "pool_acquire", value: m.pool_acquire },
|
||||
{ key: "session_prepare", value: m.session_prepare },
|
||||
{ key: "schema", value: m.schema },
|
||||
{ key: "statement_prepare", value: m.statement_prepare },
|
||||
{ key: "jdbc_execute", value: m.jdbc_execute },
|
||||
{ key: "metadata", value: m.metadata },
|
||||
{ key: "fetch", value: m.fetch },
|
||||
{ key: "pool_release", value: m.pool_release },
|
||||
{ key: "agent_other", value: agentOther },
|
||||
{ key: "remainder", value: remainder },
|
||||
{ key: "result", value: props.result.client_result_ms },
|
||||
{ key: "render", value: props.renderMs },
|
||||
];
|
||||
return stages.filter((stage) => {
|
||||
if (["prepare", "result", "render"].includes(stage.key)) return true;
|
||||
if (stage.key === "request") return !hasAgentTiming.value;
|
||||
return hasAgentTiming.value && measured(stage.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider>
|
||||
<Tooltip :delay-duration="200">
|
||||
<TooltipTrigger as-child>
|
||||
<button type="button" class="shrink-0 cursor-help rounded-sm tabular-nums focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" :aria-label="t('grid.timingDetails') + ': ' + wait" data-testid="query-timing-trigger">{{ wait }}</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" class="block max-w-sm max-h-[var(--reka-tooltip-content-available-height)] overflow-y-auto">
|
||||
<div class="space-y-2" data-testid="query-timing-details">
|
||||
<p class="font-medium">{{ t("grid.clientRequestWait", { ms: measured(result.client_request_wait_ms) ? Number(result.client_request_wait_ms.toFixed(3)) : t("grid.timingUnavailable") }) }}</p>
|
||||
<p v-if="(result.timing_page_count ?? 1) > 1">{{ t("grid.timingPages", { count: result.timing_page_count }) }}</p>
|
||||
<table class="w-full border-collapse text-left" :aria-label="t('grid.timingDetails')">
|
||||
<thead class="border-b border-current/20">
|
||||
<tr>
|
||||
<th scope="col" class="pb-1 pr-3 font-medium">{{ t("grid.timingNumber") }}</th>
|
||||
<th scope="col" class="pb-1 pr-4 font-medium">{{ t("grid.timingStage") }}</th>
|
||||
<th scope="col" class="pb-1 text-right font-medium">{{ t("grid.timingDuration") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(phase, index) in phases" :key="phase.key">
|
||||
<td class="py-0.5 pr-3 tabular-nums">{{ index + 1 }}.</td>
|
||||
<th scope="row" class="py-0.5 pr-4 font-normal">{{ t(`grid.timingPhase_${phase.key}`) }}</th>
|
||||
<td class="whitespace-nowrap py-0.5 text-right tabular-nums">{{ format(phase.value) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dl v-if="hasAgentTiming" class="border-t border-current/20 pt-2">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt>{{ t("grid.timingBackend") }}</dt>
|
||||
<dd class="tabular-nums">{{ format(result.execution_time_ms) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { createApp, nextTick } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import QueryTimingDetails from "../QueryTimingDetails.vue";
|
||||
import zh from "@/i18n/locales/zh-CN";
|
||||
|
||||
describe("query timing disclosure", () => {
|
||||
it("shows only wait, then ordered details on keyboard focus", async () => {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp(QueryTimingDetails, {
|
||||
result: {
|
||||
columns: [],
|
||||
rows: [],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 55,
|
||||
client_request_wait_ms: 7030,
|
||||
server_execute_time_us: 1019,
|
||||
query_timings_ms: { pool_acquire: 350, pool_release: 5, session_prepare: 2, schema: 3, statement_prepare: 1, jdbc_execute: 40, metadata: 2, fetch: 7, trace: 1, audit: 6000, agent_total: 6411 },
|
||||
},
|
||||
renderMs: 4,
|
||||
});
|
||||
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
expect(host.textContent).toBe("7030ms");
|
||||
expect(host.textContent).not.toContain("数据库执行");
|
||||
host.querySelector<HTMLButtonElement>("button")!.focus();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await nextTick();
|
||||
const text = document.body.textContent!;
|
||||
expect(text).toContain("1.");
|
||||
expect(text).not.toContain("读取审计记录");
|
||||
expect(text).not.toContain("读取追踪 ID");
|
||||
expect(text).not.toContain("数据库计划");
|
||||
expect(text).toContain("后端报告耗时");
|
||||
expect(text).not.toContain("驱动报告耗时");
|
||||
expect(text).toContain("获取连接");
|
||||
expect(text).toContain("归还连接");
|
||||
const details = document.querySelector('[data-testid="query-timing-details"]')!;
|
||||
const stages = [...details.querySelectorAll("tbody tr")].map((row) => row.textContent!);
|
||||
// No Core lock sample means both the lock and dependent remainder are absent.
|
||||
expect(stages).toHaveLength(12);
|
||||
expect([...details.querySelectorAll("thead th")].map((cell) => cell.textContent)).toEqual(["序号", "阶段", "耗时"]);
|
||||
expect(stages.findIndex((s) => s.includes("获取连接"))).toBeLessThan(stages.findIndex((s) => s.includes("会话")));
|
||||
expect(stages.findIndex((s) => s.includes("归还连接"))).toBeGreaterThan(stages.findIndex((s) => s.includes("读取结果")));
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
it("shows common request stages without inventing JDBC steps for a native or HTTP result", async () => {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp(QueryTimingDetails, { result: { columns: [], rows: [], affected_rows: 0, execution_time_ms: 4, client_request_wait_ms: 12, client_prepare_ms: 2, client_result_ms: 1 }, renderMs: 3 });
|
||||
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
host.querySelector<HTMLButtonElement>("button")!.focus();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
await nextTick();
|
||||
const details = document.querySelector('[data-testid="query-timing-details"]')!;
|
||||
expect(details.querySelectorAll("tbody tr")).toHaveLength(4);
|
||||
expect(details.textContent).toContain("请求等待");
|
||||
expect(details.textContent).not.toContain("JDBC");
|
||||
expect(details.textContent).not.toContain("获取连接");
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
it("does not substitute execution time when wait or phases are missing", async () => {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp(QueryTimingDetails, { result: { columns: [], rows: [], affected_rows: 0, execution_time_ms: 55 } });
|
||||
app.use(createI18n({ legacy: false, locale: "zh-CN", messages: { "zh-CN": zh } }));
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
expect(host.textContent).toBe("—");
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user