From 03d58e53fa35ec26b03ce5117cf55bad6ead662c Mon Sep 17 00:00:00 2001 From: Benson Wong <83972+mostlygeek@users.noreply.github.com> Date: Sat, 30 May 2026 17:04:30 -0700 Subject: [PATCH] Add load testing tool to the UI (#805) Wouldn't it be nice to test the performance, swapping and concurrency from the UI? Now we can! This is a port of `cmd/test-concurrency` into the UI Here's a demo of it working with a swap matrix: https://github.com/user-attachments/assets/b6bb12ec-0381-46f1-a6b8-27d1c3c0ddb3 --- .../playground/ConcurrencyInterface.svelte | 632 ++++++++++++++++++ ui-svelte/src/routes/Playground.svelte | 18 +- 2 files changed, 646 insertions(+), 4 deletions(-) create mode 100644 ui-svelte/src/components/playground/ConcurrencyInterface.svelte diff --git a/ui-svelte/src/components/playground/ConcurrencyInterface.svelte b/ui-svelte/src/components/playground/ConcurrencyInterface.svelte new file mode 100644 index 0000000..54eb76b --- /dev/null +++ b/ui-svelte/src/components/playground/ConcurrencyInterface.svelte @@ -0,0 +1,632 @@ + + +
+ +
+ +
+ {#if isRunning} + + {:else} + + {/if} + +
+ + +
+
+ Models — click to queue (add the same model more than once to test parallel requests) +
+
+ {#if !hasModels} +
No models configured.
+ {:else} +
    + {#each availableModels as m (m.id)} +
  • + +
  • + {/each} +
+ {/if} +
+
+ + +
+
+ + +
+ + + +
+
+ + +
+ {#if $testListStore.length === 0} +
+
+

Load Test

+

+ Fire several streaming chat completions at llama-swap at the same time to see how it handles parallel + loading and concurrent inference. Each request streams into its own panel with a live timer and status. +

+
    +
  1. Click models on the left to queue them — repeat a model to hit it with parallel requests.
  2. +
  3. Tweak the prompt and max_tokens if you want.
  4. +
  5. Press Go to launch them concurrently.
  6. +
+

Tip: drag a result card's header to reorder, or hit × to drop it.

+
+
+ {:else} + +
+ + {#if !$timelineCollapsedStore} +
+ + + +
+ {#each $testListStore as entry, i (entry.id)} + {@const run = runs[entry.id]} + {@const waitingPct = run ? (run.waitingMs / timelineMaxMs) * 100 : 0} + {@const loadingPct = run ? (run.loadingMs / timelineMaxMs) * 100 : 0} + {@const reasoningPct = run ? (run.reasoningMs / timelineMaxMs) * 100 : 0} + {@const contentPct = run ? (run.contentMs / timelineMaxMs) * 100 : 0} +
+
+ {i + 1}. + {entry.model} +
+
+ {#each timelineTicks as t (t)} + + {/each} + {#if run && run.waitingMs > 0} +
+ {/if} + {#if run && run.loadingMs > 0} +
+ {/if} + {#if run && run.reasoningMs > 0} +
+ {/if} + {#if run && run.contentMs > 0} +
+ {/if} +
+
+ {run ? formatElapsed(run.elapsedMs) : "—"} +
+
+ {/each} +
+
+ {/if} +
+
+ {#each $testListStore as entry, i (entry.id)} + {@const run = runs[entry.id]} + {@const status = run?.status ?? "waiting"} +
onDragOver(i, e)} + ondrop={(e) => onDrop(i, e)} + > +
onDragStart(i, e)} + ondragend={onDragEnd} + class:cursor-grab={!isRunning} + title={isRunning ? "" : "Drag to reorder"} + > + + {i + 1}. + {entry.model} + + {run ? formatElapsed(run.elapsedMs) : "—"} + + {status} + +
+
+ {#if run?.loadingText} +
{run.loadingText.trim()}
+ {/if} + {#if run?.reasoningContent} +
{run.reasoningContent}
+ {/if} + {#if run?.content} +
{run.content}
+ {/if} + {#if run?.status === "error" && run?.error} +
[error] {run.error}
+ {/if} +
+
+ {/each} +
+ {/if} +
+
diff --git a/ui-svelte/src/routes/Playground.svelte b/ui-svelte/src/routes/Playground.svelte index 561cff7..c337531 100644 --- a/ui-svelte/src/routes/Playground.svelte +++ b/ui-svelte/src/routes/Playground.svelte @@ -5,8 +5,9 @@ import AudioInterface from "../components/playground/AudioInterface.svelte"; import SpeechInterface from "../components/playground/SpeechInterface.svelte"; import RerankInterface from "../components/playground/RerankInterface.svelte"; + import ConcurrencyInterface from "../components/playground/ConcurrencyInterface.svelte"; - type Tab = "chat" | "images" | "speech" | "audio" | "rerank"; + type Tab = "chat" | "images" | "speech" | "audio" | "rerank" | "concurrency"; const selectedTabStore = persistentStore("playground-selected-tab", "chat"); let mobileMenuOpen = $state(false); @@ -17,6 +18,7 @@ { id: "speech", label: "Speech" }, { id: "audio", label: "Transcription" }, { id: "rerank", label: "Rerank" }, + { id: "concurrency", label: "Load Test" }, ]; function selectTab(tab: Tab) { @@ -25,7 +27,7 @@ } function getTabLabel(tabId: Tab): string { - return tabs.find(t => t.id === tabId)?.label || ""; + return tabs.find((t) => t.id === tabId)?.label || ""; } @@ -49,10 +51,15 @@ {#if mobileMenuOpen} -
+
{#each tabs as tab (tab.id)}