tempus
Version:
one rAF to rule them all
330 lines • 10.6 kB
JavaScript
// 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