UNPKG

tempus

Version:
330 lines 10.6 kB
// packages/core/src/profiler.ts import Tempus from "tempus"; var isClient = typeof window !== "undefined"; var PROFILER_LABEL = "tempus:profiler"; var STYLE_ID = "tempus-profiler-style"; var CORNERS = { "top-left": "top:12px;left:12px;", "top-right": "top:12px;right:12px;", "bottom-left": "bottom:12px;left:12px;", "bottom-right": "bottom:12px;right:12px;" }; function hueFor(label) { let h = 0; for (let i = 0; i < label.length; i++) { h = h * 31 + label.charCodeAt(i) | 0; } return Math.abs(h) % 360; } function colorFor(label) { return `hsl(${hueFor(label)} 70% 55%)`; } var CSS = ` .tempus-profiler { position: fixed; z-index: 2147483647; width: 320px; padding: 10px 12px 12px; border-radius: 10px; background: rgba(12, 12, 14, 0.92); color: #e8e8ea; font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45); user-select: none; } .tempus-profiler-head { display: flex; gap: 10px; align-items: baseline; margin-bottom: 8px; cursor: grab; touch-action: none; } .tempus-profiler.dragging { cursor: grabbing; } .tempus-profiler.dragging .tempus-profiler-head { cursor: grabbing; } .tempus-profiler-toggle { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0; border: 0; border-radius: 4px; background: rgba(255, 255, 255, 0.1); color: #e8e8ea; font-size: 9px; line-height: 1; cursor: pointer; } .tempus-profiler-toggle:hover { background: rgba(255, 255, 255, 0.18); } .tempus-profiler-toggle.paused { background: rgba(125, 220, 125, 0.22); color: #7ddc7d; } .tempus-profiler-title { font-weight: 600; letter-spacing: 0.02em; } .tempus-profiler-head .spacer { flex: 1; } .tempus-profiler-head b { color: #fff; font-weight: 600; } .tempus-profiler-track { position: relative; height: 18px; border-radius: 4px; background: rgba(255, 255, 255, 0.06); overflow: hidden; } .tempus-profiler-seg { position: absolute; top: 0; bottom: 0; min-width: 1px; box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.35); } .tempus-profiler-seg.throttled { background-image: repeating-linear-gradient( -45deg, rgba(255, 255, 255, 0.18) 0 4px, transparent 4px 8px ); } .tempus-profiler-danger { position: absolute; top: 0; bottom: 0; background: rgba(255, 64, 64, 0.22); pointer-events: none; } .tempus-profiler-budget { position: absolute; top: -2px; bottom: -2px; width: 2px; background: #fff; box-shadow: 0 0 4px rgba(255, 255, 255, 0.6); pointer-events: none; } .tempus-profiler-scale { display: flex; justify-content: space-between; margin: 3px 1px 8px; color: rgba(232, 232, 234, 0.5); } .tempus-profiler-list { display: flex; flex-direction: column; gap: 3px; } .tempus-profiler-row { display: flex; align-items: center; gap: 7px; } .tempus-profiler-order { flex: none; width: 22px; text-align: right; color: rgba(232, 232, 234, 0.45); font-size: 10px; } .tempus-profiler-dot { width: 8px; height: 8px; border-radius: 2px; flex: none; } .tempus-profiler-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tempus-profiler-badge { flex: none; padding: 0 4px; border-radius: 3px; background: rgba(255, 255, 255, 0.1); color: rgba(232, 232, 234, 0.7); font-size: 9px; } .tempus-profiler-ms { flex: none; color: #fff; } .tempus-profiler-pct { flex: none; width: 40px; text-align: right; } .tempus-profiler.collapsed .tempus-profiler-track, .tempus-profiler.collapsed .tempus-profiler-scale, .tempus-profiler.collapsed .tempus-profiler-list { display: none; } `; function injectStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = CSS; document.head.appendChild(style); } function average(samples) { if (!samples.length) return 0; let sum = 0; for (const s of samples) sum += s; return sum / samples.length; } function pctColor(pct) { if (pct > 66) return "#ff5b5b"; if (pct > 33) return "#ffb000"; return "#7ddc7d"; } function formatOrder(order) { if (order === Number.POSITIVE_INFINITY) return "\u221E"; if (order === Number.NEGATIVE_INFINITY) return "-\u221E"; return String(order); } function profiler(options = {}) { if (!isClient) { return { element: null, destroy() { } }; } const { fps = 5, corner = "top-left", container = document.body } = options; injectStyle(); const root = document.createElement("div"); root.className = "tempus-profiler"; root.style.cssText = CORNERS[corner]; root.innerHTML = ` <div class="tempus-profiler-head"> <button class="tempus-profiler-toggle" data-toggle type="button"></button> <span class="tempus-profiler-title">tempus</span> <span class="spacer"></span> <span class="tempus-profiler-stat" data-fps></span> <span class="tempus-profiler-stat" data-usage></span> </div> <div class="tempus-profiler-track" data-track></div> <div class="tempus-profiler-scale"> <span>0</span> <span data-budget></span> </div> <div class="tempus-profiler-list" data-list></div> `; container.appendChild(root); const head = root.querySelector(".tempus-profiler-head"); const toggleEl = root.querySelector("[data-toggle]"); const fpsEl = root.querySelector("[data-fps]"); const usageEl = root.querySelector("[data-usage]"); const trackEl = root.querySelector("[data-track]"); const budgetEl = root.querySelector("[data-budget]"); const listEl = root.querySelector("[data-list]"); const DRAG_THRESHOLD = 4; let dragging = false; let moved = false; let startX = 0; let startY = 0; let originLeft = 0; let originTop = 0; const onPointerDown = (e) => { if (e.button !== 0) return; const rect = root.getBoundingClientRect(); originLeft = rect.left; originTop = rect.top; startX = e.clientX; startY = e.clientY; dragging = true; moved = false; head.setPointerCapture(e.pointerId); }; const onPointerMove = (e) => { if (!dragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; if (!moved && Math.hypot(dx, dy) < DRAG_THRESHOLD) return; if (!moved) { moved = true; root.classList.add("dragging"); root.style.right = "auto"; root.style.bottom = "auto"; } const maxLeft = window.innerWidth - root.offsetWidth; const maxTop = window.innerHeight - root.offsetHeight; const left = Math.min(Math.max(originLeft + dx, 0), Math.max(0, maxLeft)); const top = Math.min(Math.max(originTop + dy, 0), Math.max(0, maxTop)); root.style.left = `${left}px`; root.style.top = `${top}px`; }; const onPointerUp = (e) => { if (!dragging) return; dragging = false; root.classList.remove("dragging"); if (head.hasPointerCapture(e.pointerId)) head.releasePointerCapture(e.pointerId); if (!moved) root.classList.toggle("collapsed"); }; head.addEventListener("pointerdown", onPointerDown); head.addEventListener("pointermove", onPointerMove); head.addEventListener("pointerup", onPointerUp); head.addEventListener("pointercancel", onPointerUp); const syncToggle = () => { const playing = Tempus.isPlaying; toggleEl.textContent = playing ? "\u23F8" : "\u25B6"; toggleEl.classList.toggle("paused", !playing); toggleEl.setAttribute("aria-label", playing ? "Stop the loop" : "Start the loop"); }; toggleEl.addEventListener("click", (e) => { e.stopPropagation(); if (Tempus.isPlaying) Tempus.pause(); else Tempus.play(); syncToggle(); }); toggleEl.addEventListener("pointerdown", (e) => e.stopPropagation()); syncToggle(); const render = () => { syncToggle(); const budget = Tempus.frameBudget; const entries = Tempus.inspect().filter((entry) => entry.label !== PROFILER_LABEL).map((entry) => { const duration = average(entry.samples); return { label: entry.source === "patch" ? `${entry.label} \u27F3` : entry.label, duration, order: entry.order, throttled: entry.fps !== Number.POSITIVE_INFINITY, fps: entry.fps, color: colorFor(entry.label) }; }); const total = entries.reduce((acc, e) => acc + e.duration, 0); const span = Math.max(budget, total) || budget; const budgetLeft = budget / span * 100; fpsEl.innerHTML = `<b>${Math.round(Tempus.fps ?? 0)}</b> fps`; const usagePct = Math.round(total / budget * 100); usageEl.innerHTML = `<b style="color:${pctColor(usagePct)}">${usagePct}%</b> budget`; budgetEl.textContent = `${budget.toFixed(1)}ms`; let offset = 0; let segHTML = ""; for (const e of entries) { const left = offset / span * 100; const width = e.duration / span * 100; offset += e.duration; segHTML += `<div class="tempus-profiler-seg${e.throttled ? " throttled" : ""}" style="left:${left}%;width:${width}%;background:${e.color}" title="${e.label}: ${e.duration.toFixed(2)}ms"></div>`; } if (total > budget) { segHTML += `<div class="tempus-profiler-danger" style="left:${budgetLeft}%;width:${100 - budgetLeft}%"></div>`; } segHTML += `<div class="tempus-profiler-budget" style="left:${budgetLeft}%"></div>`; trackEl.innerHTML = segHTML; listEl.innerHTML = [...entries].sort((a, b) => a.order - b.order).map((e) => { const pct = e.duration / budget * 100; return `<div class="tempus-profiler-row"> <span class="tempus-profiler-order" title="order ${formatOrder( e.order )}">${formatOrder(e.order)}</span> <span class="tempus-profiler-dot" style="background:${e.color}"></span> <span class="tempus-profiler-label">${e.label}</span> ${e.throttled ? `<span class="tempus-profiler-badge">${e.fps}${typeof e.fps === "number" ? "fps" : ""}</span>` : ""} <span class="tempus-profiler-ms">${e.duration.toFixed(2)}ms</span> <span class="tempus-profiler-pct" style="color:${pctColor(pct)}">${pct.toFixed( 0 )}%</span> </div>`; }).join(""); }; const unsubscribe = Tempus.add(render, { fps, order: Number.POSITIVE_INFINITY, label: PROFILER_LABEL }); return { element: root, destroy() { unsubscribe?.(); root.remove(); } }; } export { profiler }; //# sourceMappingURL=tempus-profiler.mjs.map