Files

195 lines
7.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { queueSteps, architectures, benchmarks } from "./model.js";
const chunkSlider = document.getElementById("chunk-slider");
const computeSlider = document.getElementById("compute-slider");
const queueChart = document.getElementById("queue-chart");
function renderQueue() {
const chunkMs = Number(chunkSlider.value);
const computeMs = Number(computeSlider.value);
const steps = queueSteps(chunkMs, computeMs);
const totalMs = Math.max(steps.at(-1).end, chunkMs * 9);
const percentage = (value) => `${(value / totalMs) * 100}%`;
document.getElementById("chunk-value").textContent = `${chunkMs} ms`;
document.getElementById("compute-value").textContent = `${computeMs} ms`;
const ratio = document.getElementById("rtf-value");
ratio.textContent = `${(computeMs / chunkMs).toFixed(2)}×`;
ratio.classList.toggle("overloaded", computeMs >= chunkMs);
document.getElementById("backlog-value").textContent =
`${steps.at(-1).wait} ms`;
document.getElementById("growth-value").textContent =
`${Math.max(0, computeMs - chunkMs)} ms`;
queueChart.replaceChildren();
const ticks = document.createElement("div");
ticks.className = "queue-ticks";
for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
const tick = document.createElement("span");
tick.textContent = `${Math.round(totalMs * fraction)}${fraction === 1 ? " ms" : ""}`;
ticks.append(tick);
}
queueChart.append(ticks);
for (const step of steps) {
const row = document.createElement("div");
row.className = "queue-row";
row.setAttribute(
"aria-label",
`第${step.index}块在${step.arrival}毫秒就绪,等待${step.wait}毫秒,在${step.end}毫秒计算完成`,
);
const label = document.createElement("span");
label.className = "queue-label";
label.textContent = `块 ${step.index}`;
const arrival = document.createElement("span");
arrival.className = "queue-arrival";
arrival.style.left = percentage(step.arrival);
const wait = document.createElement("span");
wait.className = "queue-wait";
wait.style.left = percentage(step.arrival);
wait.style.width = percentage(step.wait);
const compute = document.createElement("span");
compute.className = "queue-compute";
compute.style.left = percentage(step.start);
compute.style.width = percentage(computeMs);
row.append(label, wait, compute, arrival);
queueChart.append(row);
}
const explanation = document.getElementById("queue-explanation");
if (computeMs < chunkMs) {
explanation.textContent = `每块留有 ${chunkMs - computeMs} ms 计算余量。此理想条件下等待为零,单块 ${computeMs} ms 的计算延迟仍然存在。`;
} else if (computeMs === chunkMs) {
explanation.textContent =
"理想条件下刚好赶上输入节奏。出现额外计算或调度抖动时,队列就会开始积压。";
} else {
explanation.textContent = `每块增加 ${computeMs - chunkMs} ms 等待,第 8 块已等待 ${steps.at(-1).wait} ms。需要减少计算、增加处理能力或调整处理策略。`;
}
}
function selectArchitecture(key) {
const selected = architectures[key];
const flow = document.getElementById("architecture-flow");
flow.replaceChildren();
selected.nodes.forEach(([title, subtitle, type], index) => {
if (index) {
const arrow = document.createElement("span");
arrow.className = "flow-arrow";
arrow.textContent = "→";
arrow.setAttribute("aria-hidden", "true");
flow.append(arrow);
}
const node = document.createElement("div");
node.className = `flow-node ${type || ""}`;
const strong = document.createElement("strong");
strong.textContent = title;
const small = document.createElement("small");
small.textContent = subtitle;
node.append(strong, small);
flow.append(node);
});
document.getElementById("architecture-detail").innerHTML = selected.detail;
document.querySelectorAll("[data-architecture]").forEach((button) => {
button.setAttribute(
"aria-pressed",
String(button.dataset.architecture === key),
);
});
}
function renderBenchmarks() {
const chart = document.getElementById("benchmark-chart");
for (const model of benchmarks) {
const row = document.createElement("div");
row.className = "benchmark-row";
const label = document.createElement("span");
label.className = "benchmark-label";
label.textContent = model.name;
const tracks = document.createElement("div");
tracks.className = "benchmark-tracks";
for (const [key, className] of [
["first", "first"],
["answer", "answer"],
]) {
const bar = document.createElement("div");
bar.className = `benchmark-bar ${className}`;
bar.style.width = `${(model[key] / 12) * 100}%`;
const value = document.createElement("span");
value.className = "benchmark-value";
value.textContent = `${model[key].toFixed(2)} s`;
bar.append(value);
tracks.append(bar);
}
row.append(label, tracks);
chart.append(row);
}
const axis = document.createElement("div");
axis.className = "benchmark-axis";
for (const value of ["0", "3", "6", "9", "12 s"]) {
const tick = document.createElement("span");
tick.textContent = value;
axis.append(tick);
}
chart.append(axis);
}
chunkSlider.addEventListener("input", renderQueue);
computeSlider.addEventListener("input", renderQueue);
document.querySelectorAll("[data-preset]").forEach((button) => {
button.addEventListener("click", () => {
const [chunk, compute] = button.dataset.preset.split(",");
chunkSlider.value = chunk;
computeSlider.value = compute;
renderQueue();
});
});
document.querySelectorAll("[data-architecture]").forEach((button) => {
button.addEventListener("click", () =>
selectArchitecture(button.dataset.architecture),
);
});
const themeToggle = document.getElementById("theme-toggle");
try {
const preference = localStorage.getItem("voice-duplex-theme");
if (preference === "dark" || preference === "light")
document.documentElement.dataset.theme = preference;
} catch {
/* The page remains usable when storage is unavailable. */
}
themeToggle.addEventListener("click", () => {
const theme =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
document.documentElement.dataset.theme = theme;
themeToggle.setAttribute(
"aria-label",
theme === "dark" ? "切换浅色主题" : "切换深色主题",
);
try {
localStorage.setItem("voice-duplex-theme", theme);
} catch {
/* Theme changes still apply for this visit. */
}
});
const navigationLinks = [...document.querySelectorAll(".contents nav a")];
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
if (!visible.length) return;
const currentId = visible[0].target.id;
navigationLinks.forEach((link) => {
const active = link.hash === `#${currentId}`;
link.classList.toggle("active", active);
if (active) link.setAttribute("aria-current", "location");
else link.removeAttribute("aria-current");
});
},
{ rootMargin: "-110px 0px -55% 0px", threshold: 0 },
);
document
.querySelectorAll(".article-section")
.forEach((section) => observer.observe(section));
selectArchitecture("cascade");
renderQueue();
renderBenchmarks();