Skip to content

Commit ca7859f

Browse files
authored
feature: pause render queues (#396)
1 parent 2b9646a commit ca7859f

8 files changed

Lines changed: 666 additions & 90 deletions

File tree

components/clips/RenderQueuePanel.vue

Lines changed: 171 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import {
2323
RotateCw,
2424
ChevronRight,
2525
Play,
26+
Pause,
2627
} from "lucide-vue-next";
2728
import { useNuxtApp } from "#app";
29+
import gql from "graphql-tag";
2830
import getGraphqlClient from "~/graphql/getGraphqlClient";
2931
import { generateMutation, generateSubscription } from "~/graphql/graphqlGen";
3032
import { clipRenderJobFields } from "~/graphql/clipRenderJob";
@@ -37,6 +39,7 @@ import {
3739
TooltipTrigger,
3840
} from "~/components/ui/tooltip";
3941
import { useAuthStore } from "~/stores/AuthStore";
42+
import { useGpuPoolStatusStore } from "~/stores/GpuPoolStatusStore";
4043
import { useClipModal, type ClipQueueItem } from "~/composables/useClipModal";
4144
4245
const authStore = useAuthStore();
@@ -70,6 +73,7 @@ type Job = {
7073
user_steam_id: string;
7174
match_map_id: string;
7275
status: string;
76+
paused?: boolean | null;
7377
progress: number | string | null;
7478
error_message: string | null;
7579
clip_id: string | null;
@@ -304,15 +308,14 @@ type BatchGroup = {
304308
// 0..1 across the batch — sum of per-job render fractions / total.
305309
overallProgress: number;
306310
isFinished: boolean;
307-
// Pod boot state — null once any job has left "queued" or no
308-
// recent booting tick exists. Same tick fans out to every job.
309311
bootInfo: {
310312
stage: string;
311313
stageSub: string | null;
312314
progress: number | null;
313315
at: string;
314316
firedStages: Set<string>;
315317
} | null;
318+
isPaused: boolean;
316319
};
317320
318321
function buildBatchGroup(matchMapId: string, list: Job[]): BatchGroup {
@@ -362,9 +365,11 @@ function buildBatchGroup(matchMapId: string, list: Job[]): BatchGroup {
362365
const inFlightCount = sorted.length - terminalCount;
363366
const overallProgress = sorted.length === 0 ? 0 : progressSum / sorted.length;
364367
368+
const isPaused = sorted.some((j) => j.paused === true);
369+
365370
let bootInfo: BatchGroup["bootInfo"] = null;
366371
const noneStarted = sorted.every((j) => j.status === "queued");
367-
if (noneStarted) {
372+
if (noneStarted && !isPaused) {
368373
const firedStages = new Set<string>();
369374
let latest: { entry: StatusHistoryEntry; at: number } | null = null;
370375
for (const j of sorted) {
@@ -412,6 +417,7 @@ function buildBatchGroup(matchMapId: string, list: Job[]): BatchGroup {
412417
overallProgress,
413418
isFinished: inFlightCount === 0,
414419
bootInfo,
420+
isPaused,
415421
};
416422
}
417423
@@ -734,6 +740,29 @@ async function cancelBatch(matchMapId: string) {
734740
}
735741
}
736742
743+
const resumingBatch = ref<Record<string, boolean>>({});
744+
const RESUME_CLIP_RENDER_BATCH = gql`
745+
mutation ResumeClipRenderBatch($match_map_id: uuid!) {
746+
resumeClipRenderBatch(match_map_id: $match_map_id) {
747+
success
748+
}
749+
}
750+
`;
751+
async function resumeBatch(matchMapId: string) {
752+
if (resumingBatch.value[matchMapId]) return;
753+
resumingBatch.value = { ...resumingBatch.value, [matchMapId]: true };
754+
try {
755+
await nuxtApp.$apollo.defaultClient.mutate({
756+
mutation: RESUME_CLIP_RENDER_BATCH,
757+
variables: { match_map_id: matchMapId },
758+
});
759+
} catch (e) {
760+
console.error("[render-queue] batch resume failed:", e);
761+
} finally {
762+
resumingBatch.value = { ...resumingBatch.value, [matchMapId]: false };
763+
}
764+
}
765+
737766
function formatTimeAgo(iso: string | null): string {
738767
if (!iso) return "";
739768
const t = new Date(iso).getTime();
@@ -749,15 +778,56 @@ function formatTimeAgo(iso: string | null): string {
749778
}
750779
751780
const totalInFlight = computed(() => inFlight.value.length);
752-
const totalActive = computed(
781+
const totalRendering = computed(
753782
() =>
754783
inFlight.value.filter(
755784
(j) => j.status === "rendering" || j.status === "uploading",
756785
).length,
757786
);
758-
const totalQueued = computed(
759-
() => inFlight.value.filter((j) => j.status === "queued").length,
787+
const totalPaused = computed(
788+
() => inFlight.value.filter((j) => j.paused === true).length,
760789
);
790+
const allPaused = computed(
791+
() =>
792+
inFlight.value.length > 0 && totalPaused.value === inFlight.value.length,
793+
);
794+
795+
const gpuPool = useGpuPoolStatusStore();
796+
gpuPool.subscribeToPool();
797+
798+
const resumeBlockedReason = computed<string | null>(() => {
799+
const s = gpuPool.status;
800+
if (!gpuPool.hasLoaded || !s) return null;
801+
if (s.live_in_progress) {
802+
return t("clips.render_queue.resume_blocked_live");
803+
}
804+
return null;
805+
});
806+
807+
const queueStatus = computed<{
808+
key: string;
809+
tone: "amber" | "muted" | "danger";
810+
}>(() => {
811+
if (totalRendering.value > 0) {
812+
return { key: "rendering", tone: "amber" };
813+
}
814+
if (allPaused.value) {
815+
return { key: "paused", tone: "amber" };
816+
}
817+
const s = gpuPool.status;
818+
if (gpuPool.hasLoaded && s) {
819+
if (s.total_gpu_nodes === 0) {
820+
return { key: "no_gpu_registered", tone: "danger" };
821+
}
822+
if (s.free_gpu_nodes === 0) {
823+
if (s.live_in_progress) return { key: "blocked_live", tone: "amber" };
824+
if (s.demo_in_progress) return { key: "blocked_demo", tone: "amber" };
825+
return { key: "no_gpu_free", tone: "amber" };
826+
}
827+
}
828+
if (inFlight.value.length === 0) return { key: "idle", tone: "muted" };
829+
return { key: "waiting", tone: "muted" };
830+
});
761831
</script>
762832

763833
<template>
@@ -775,28 +845,46 @@ const totalQueued = computed(
775845
>
776846
<div class="flex items-center gap-2">
777847
<ListVideo class="h-4 w-4 text-[hsl(var(--tac-amber))]" />
848+
<span class="font-mono text-sm font-semibold tabular-nums">
849+
{{ totalInFlight }}
850+
</span>
778851
<span
779852
class="font-mono text-[0.65rem] uppercase tracking-[0.18em] text-muted-foreground"
780853
>
781-
{{ $t("clips.render_queue.in_flight") }}
782-
</span>
783-
<span class="font-mono text-sm font-semibold tabular-nums">
784-
{{ totalInFlight }}
854+
{{ $t("clips.render_queue.queued") }}
785855
</span>
786856
</div>
787857
<div
788-
class="ml-auto flex items-center gap-3 font-mono text-[0.65rem] uppercase tracking-[0.16em] text-muted-foreground"
858+
class="ml-auto flex items-center gap-2 font-mono text-[0.65rem] uppercase tracking-[0.16em]"
859+
:class="
860+
queueStatus.tone === 'danger'
861+
? 'text-destructive'
862+
: queueStatus.tone === 'amber'
863+
? 'text-[hsl(var(--tac-amber))]'
864+
: 'text-muted-foreground'
865+
"
789866
>
790-
<span class="inline-flex items-center gap-1.5">
791-
<Loader2
792-
class="h-3 w-3 animate-spin text-[hsl(var(--tac-amber))]"
793-
/>
794-
{{ totalActive }} active
795-
</span>
796-
<span class="text-border">·</span>
797-
<span class="inline-flex items-center gap-1.5">
798-
<Clock class="h-3 w-3" />
799-
{{ totalQueued }} queued
867+
<Loader2
868+
v-if="queueStatus.key === 'rendering'"
869+
class="h-3 w-3 animate-spin"
870+
/>
871+
<Pause v-else-if="queueStatus.key === 'paused'" class="h-3 w-3" />
872+
<AlertCircle
873+
v-else-if="queueStatus.tone === 'danger'"
874+
class="h-3 w-3"
875+
/>
876+
<Clock v-else class="h-3 w-3" />
877+
<span>
878+
<template v-if="queueStatus.key === 'rendering'">
879+
{{
880+
$t("clips.render_queue.status_rendering", {
881+
count: totalRendering,
882+
})
883+
}}
884+
</template>
885+
<template v-else>
886+
{{ $t(`clips.render_queue.status_${queueStatus.key}`) }}
887+
</template>
800888
</span>
801889
</div>
802890
</div>
@@ -889,18 +977,33 @@ const totalQueued = computed(
889977
<div
890978
class="flex items-center justify-between font-mono text-[0.6rem] uppercase tracking-[0.16em] text-muted-foreground"
891979
>
892-
<span>{{
893-
g.bootInfo
894-
? $t("ui_extras.pod_boot")
895-
: $t("ui_extras.batch_progress")
896-
}}</span>
980+
<span
981+
:class="g.isPaused ? 'text-[hsl(var(--tac-amber))]' : ''"
982+
>
983+
<template v-if="g.isPaused">
984+
<Pause class="inline h-3 w-3 mr-1 -mt-0.5" />
985+
{{ $t("ui_extras.batch_paused") }}
986+
</template>
987+
<template v-else-if="g.bootInfo">
988+
{{ $t("ui_extras.pod_boot") }}
989+
</template>
990+
<template v-else>
991+
{{ $t("ui_extras.batch_progress") }}
992+
</template>
993+
</span>
897994
<span class="tabular-nums">
898995
<template
899-
v-if="g.bootInfo && g.bootInfo.progress !== null"
996+
v-if="
997+
!g.isPaused &&
998+
g.bootInfo &&
999+
g.bootInfo.progress !== null
1000+
"
9001001
>
9011002
{{ Math.round(g.bootInfo.progress * 100) }}%
9021003
</template>
903-
<template v-else-if="g.bootInfo">…</template>
1004+
<template v-else-if="!g.isPaused && g.bootInfo"
1005+
>…</template
1006+
>
9041007
<template v-else>
9051008
{{ Math.round(g.overallProgress * 100) }}%
9061009
</template>
@@ -910,7 +1013,7 @@ const totalQueued = computed(
9101013
class="relative h-1.5 overflow-hidden rounded-full bg-muted/40"
9111014
>
9121015
<div
913-
v-if="g.bootInfo"
1016+
v-if="!g.isPaused && g.bootInfo"
9141017
class="h-full rounded-full bg-primary transition-[width] duration-300"
9151018
:style="{
9161019
width:
@@ -919,7 +1022,12 @@ const totalQueued = computed(
9191022
></div>
9201023
<div
9211024
v-else
922-
class="h-full rounded-full bg-[hsl(var(--tac-amber))] transition-[width] duration-300"
1025+
class="h-full rounded-full transition-[width] duration-300"
1026+
:class="
1027+
g.isPaused
1028+
? 'bg-[hsl(var(--tac-amber)/0.5)]'
1029+
: 'bg-[hsl(var(--tac-amber))]'
1030+
"
9231031
:style="{
9241032
width: Math.round(g.overallProgress * 100) + '%',
9251033
}"
@@ -928,6 +1036,39 @@ const totalQueued = computed(
9281036
</div>
9291037
</div>
9301038
<div class="flex shrink-0 items-center gap-1.5">
1039+
<Tooltip v-if="g.isPaused && resumeBlockedReason">
1040+
<TooltipTrigger as-child>
1041+
<span tabindex="0" class="inline-flex">
1042+
<Button
1043+
size="sm"
1044+
variant="outline"
1045+
class="h-7 px-2 text-[0.7rem] opacity-60 cursor-not-allowed"
1046+
:disabled="true"
1047+
>
1048+
<Play class="h-3 w-3 mr-1" />
1049+
{{ $t("ui_extras.resume") }}
1050+
</Button>
1051+
</span>
1052+
</TooltipTrigger>
1053+
<TooltipContent side="left">
1054+
{{ resumeBlockedReason }}
1055+
</TooltipContent>
1056+
</Tooltip>
1057+
<Button
1058+
v-else-if="g.isPaused"
1059+
size="sm"
1060+
variant="outline"
1061+
class="h-7 px-2 text-[0.7rem] hover:border-[hsl(var(--tac-amber))] hover:text-[hsl(var(--tac-amber))]"
1062+
:disabled="resumingBatch[g.matchMapId]"
1063+
@click="resumeBatch(g.matchMapId)"
1064+
>
1065+
<Loader2
1066+
v-if="resumingBatch[g.matchMapId]"
1067+
class="h-3 w-3 mr-1 animate-spin"
1068+
/>
1069+
<Play v-else class="h-3 w-3 mr-1" />
1070+
{{ $t("ui_extras.resume") }}
1071+
</Button>
9311072
<Button
9321073
v-if="isAdmin && (g.errorCount > 0 || g.cancelledCount > 0)"
9331074
size="sm"

0 commit comments

Comments
 (0)