From 62132324da52ec845ea3ec7cc61660a1530678f3 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Fri, 9 Oct 2026 14:45:53 +0800 Subject: [PATCH] feat(frontend): add submission performance distribution previews --- .../src/components/SubmissionPerformance.vue | 387 ++++++++++++++++++ frontend/src/data/performancePreview.ts | 104 +++++ frontend/src/data/preview.ts | 5 +- frontend/src/lib/performance.ts | 47 +++ frontend/src/views/SubmissionDetailView.vue | 2 + frontend/tests/performance.test.ts | 64 +++ 6 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/SubmissionPerformance.vue create mode 100644 frontend/src/data/performancePreview.ts create mode 100644 frontend/src/lib/performance.ts create mode 100644 frontend/tests/performance.test.ts diff --git a/frontend/src/components/SubmissionPerformance.vue b/frontend/src/components/SubmissionPerformance.vue new file mode 100644 index 0000000..599f6f7 --- /dev/null +++ b/frontend/src/components/SubmissionPerformance.vue @@ -0,0 +1,387 @@ + + + + + diff --git a/frontend/src/data/performancePreview.ts b/frontend/src/data/performancePreview.ts new file mode 100644 index 0000000..9e629e4 --- /dev/null +++ b/frontend/src/data/performancePreview.ts @@ -0,0 +1,104 @@ +import type { DistributionPoint } from '../lib/performance' +import type { Submission } from './preview' + +interface PerformancePreview { + problemId: string + language: string + time: DistributionPoint[] + memory: DistributionPoint[] +} + +// Fixed display fixtures, independent of real judge results. Memory values use KiB. +const previews: PerformancePreview[] = [ + { + problemId: '1001', + language: 'Rust', + time: [ + { value: 0, count: 8 }, + { value: 4, count: 12 }, + { value: 8, count: 25 }, + { value: 12, count: 16 }, + { value: 16, count: 34 }, + { value: 20, count: 46 }, + { value: 24, count: 29 }, + { value: 28, count: 18 }, + { value: 32, count: 13 }, + { value: 40, count: 9 }, + { value: 48, count: 6 }, + { value: 64, count: 4 }, + ], + memory: [ + { value: 1024, count: 7 }, + { value: 1536, count: 19 }, + { value: 2048, count: 58 }, + { value: 2560, count: 41 }, + { value: 3072, count: 34 }, + { value: 3584, count: 22 }, + { value: 4096, count: 15 }, + { value: 5120, count: 13 }, + { value: 6144, count: 7 }, + { value: 8192, count: 4 }, + ], + }, + { + problemId: '1002', + language: 'Python', + time: [ + { value: 24, count: 3 }, + { value: 32, count: 8 }, + { value: 40, count: 16 }, + { value: 48, count: 28 }, + { value: 56, count: 39 }, + { value: 64, count: 31 }, + { value: 72, count: 24 }, + { value: 80, count: 18 }, + { value: 96, count: 11 }, + { value: 112, count: 6 }, + { value: 128, count: 4 }, + ], + memory: [ + { value: 5120, count: 6 }, + { value: 6144, count: 18 }, + { value: 7168, count: 46 }, + { value: 8192, count: 53 }, + { value: 9216, count: 31 }, + { value: 10240, count: 19 }, + { value: 12288, count: 9 }, + { value: 14336, count: 6 }, + ], + }, + { + problemId: '1003', + language: 'C++', + time: [ + { value: 16, count: 8 }, + { value: 24, count: 16 }, + { value: 34, count: 21 }, + { value: 40, count: 32 }, + { value: 48, count: 41 }, + { value: 56, count: 28 }, + { value: 64, count: 19 }, + { value: 80, count: 12 }, + { value: 96, count: 7 }, + { value: 120, count: 4 }, + ], + memory: [ + { value: 2048, count: 9 }, + { value: 3072, count: 24 }, + { value: 4096, count: 51 }, + { value: 5120, count: 47 }, + { value: 6144, count: 29 }, + { value: 8192, count: 17 }, + { value: 10240, count: 8 }, + { value: 12288, count: 3 }, + ], + }, +] + +export function findPerformancePreview(submission: Submission) { + if (submission.verdict !== 'AC') return undefined + return previews.find( + (preview) => + preview.problemId === submission.problemId && preview.language === submission.language, + ) +} diff --git a/frontend/src/data/preview.ts b/frontend/src/data/preview.ts index d0b0496..33ecda5 100644 --- a/frontend/src/data/preview.ts +++ b/frontend/src/data/preview.ts @@ -264,7 +264,7 @@ export const verdictLabels: Record = { CE: 'Compile Error', Pending: '等待评测', } -export const submissions: { +export interface Submission { id: string problemId: string username: string @@ -273,7 +273,8 @@ export const submissions: { time: number | null memory: number | null created: string -}[] = [ +} +export const submissions: Submission[] = [ { id: '10248', problemId: '1001', diff --git a/frontend/src/lib/performance.ts b/frontend/src/lib/performance.ts new file mode 100644 index 0000000..ffbef05 --- /dev/null +++ b/frontend/src/lib/performance.ts @@ -0,0 +1,47 @@ +export interface DistributionPoint { + value: number + count: number +} + +export function summarizeDistribution( + points: readonly DistributionPoint[], + current: number | null, +) { + const valid = points.filter( + ({ value, count }) => + Number.isFinite(value) && value >= 0 && Number.isSafeInteger(count) && count > 0, + ) + const total = valid.reduce((sum, point) => sum + point.count, 0) + const measured = current !== null && Number.isFinite(current) && current >= 0 + const slower = measured + ? valid.reduce((sum, point) => sum + (point.value > current ? point.count : 0), 0) + : 0 + const tied = measured + ? valid.reduce((sum, point) => sum + (point.value === current ? point.count : 0), 0) + : 0 + const maximum = valid.reduce((max, point) => Math.max(max, point.value), 0) + const extent = Math.max(maximum, measured ? current : 0, 1) + const rawStep = extent / 24 + const magnitude = 10 ** Math.floor(Math.log10(rawStep)) + const step = [1, 2, 5, 10].find((size) => size * magnitude >= rawStep)! * magnitude + const binCount = Math.max(1, Math.ceil(extent / step)) + const bins = Array.from({ length: binCount }, (_, index) => ({ + start: index * step, + end: (index + 1) * step, + count: 0, + percent: 0, + })) + for (const point of valid) { + // The last interval includes its upper endpoint; other intervals exclude it. + bins[Math.min(binCount - 1, Math.floor(point.value / step))]!.count += point.count + } + for (const bin of bins) bin.percent = total ? (bin.count / total) * 100 : 0 + return { + total, + bins, + end: binCount * step, + betterThan: total && measured ? (slower / total) * 100 : null, + tied, + current: measured ? current : null, + } +} diff --git a/frontend/src/views/SubmissionDetailView.vue b/frontend/src/views/SubmissionDetailView.vue index a2e9f1e..fcc546f 100644 --- a/frontend/src/views/SubmissionDetailView.vue +++ b/frontend/src/views/SubmissionDetailView.vue @@ -6,6 +6,7 @@ import PageHeading from '../components/PageHeading.vue' import PanelCard from '../components/PanelCard.vue' import VerdictTag from '../components/VerdictTag.vue' import EmptyState from '../components/EmptyState.vue' +import SubmissionPerformance from '../components/SubmissionPerformance.vue' const route = useRoute() const submission = computed(() => submissions.find((s) => s.id === route.params.id)) @@ -48,6 +49,7 @@ const submission = computed(() => submissions.find((s) => s.id === route.params. >{{ submission.created }} +

当前展示评测界面的示例记录,实际用例结果、日志和源码尚未接入。

{ + const summary = summarizeDistribution( + [ + { value: 0, count: 3 }, + { value: 12, count: 2 }, + { value: 24, count: 5 }, + ], + 12, + ) + assert.equal(summary.total, 10) + assert.equal(summary.betterThan, 50) + assert.equal(summary.tied, 2) + assert.equal(summarizeDistribution([{ value: 0, count: 3 }], 0).betterThan, 0) +}) + +test('bin boundaries and the final upper endpoint preserve every sample', () => { + const summary = summarizeDistribution( + [ + { value: 0, count: 2 }, + { value: 5, count: 3 }, + { value: 100, count: 7 }, + ], + 100, + ) + assert.equal(summary.bins[0].count, 2) + assert.equal(summary.bins[1].count, 3) + assert.equal(summary.bins.at(-1)!.count, 7) + assert.equal( + summary.bins.reduce((sum, bin) => sum + bin.count, 0), + summary.total, + ) + assert.ok(Math.abs(summary.bins.reduce((sum, bin) => sum + bin.percent, 0) - 100) < 1e-9) +}) + +test('missing measurements and invalid samples produce no percentile', () => { + const summary = summarizeDistribution( + [ + { value: -1, count: 3 }, + { value: NaN, count: 2 }, + { value: Infinity, count: 1 }, + { value: 10, count: -1 }, + { value: 20, count: 1.5 }, + { value: 24, count: 0 }, + ], + null, + ) + assert.equal(summary.total, 0) + assert.equal(summary.betterThan, null) + assert.equal(summary.current, null) + assert.ok(summary.bins.every((bin) => bin.percent === 0)) + assert.equal(summarizeDistribution([{ value: 12, count: 2 }], null).betterThan, null) + assert.equal(summarizeDistribution([{ value: 12, count: 2 }], NaN).current, null) +}) + +test('current measurements outside the cohort remain within the plot', () => { + const summary = summarizeDistribution([{ value: 12, count: 2 }], 140) + assert.ok(summary.end >= 140) + assert.equal(summary.betterThan, 0) + assert.equal(summary.total, 2) +})