mirror of
https://github.com/NotAShelf/nix-evaluator-stats.git
synced 2026-04-27 12:25:20 +00:00
packages/web: allow selecting integer precision in visuals & comparison view
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Idd4ed0b32ba827254de9c501685d5edb6a6a6964
This commit is contained in:
parent
6ec645f3de
commit
5af2457177
8 changed files with 143 additions and 48 deletions
|
|
@ -1,23 +1,23 @@
|
|||
export function formatBytes(bytes: number): string {
|
||||
export function formatBytes(bytes: number, precision = 2): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log2(bytes) / 10);
|
||||
return `${(bytes / (1 << (i * 10))).toFixed(2)} ${units[i]}`;
|
||||
return `${(bytes / (1 << (i * 10))).toFixed(precision)} ${units[i]}`;
|
||||
}
|
||||
|
||||
export function formatNumber(num: number): string {
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
||||
export function formatNumber(num: number, precision = 2): string {
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(precision) + 'B';
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(precision) + 'M';
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(precision) + 'K';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
export function formatTime(seconds: number): string {
|
||||
if (seconds < 0.001) return (seconds * 1e6).toFixed(2) + 'μs';
|
||||
if (seconds < 1) return (seconds * 1000).toFixed(2) + 'ms';
|
||||
export function formatTime(seconds: number, precision = 2): string {
|
||||
if (seconds < 0.001) return (seconds * 1e6).toFixed(precision) + 'μs';
|
||||
if (seconds < 1) return (seconds * 1000).toFixed(precision) + 'ms';
|
||||
return seconds.toFixed(3) + 's';
|
||||
}
|
||||
|
||||
export function formatPercent(value: number): string {
|
||||
return (value * 100).toFixed(2) + '%';
|
||||
export function formatPercent(value: number, precision = 2): string {
|
||||
return (value * 100).toFixed(precision) + '%';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ const TOOLTIPS = {
|
|||
attrSelect: 'Number of attribute selections performed (accessing .attr from an attribute set)',
|
||||
};
|
||||
|
||||
const Analysis: Component<{ stats: StatsData }> = props => {
|
||||
const Analysis: Component<{ stats: StatsData; precision?: number }> = props => {
|
||||
const prec = () => props.precision ?? 2;
|
||||
const totalMemory = createMemo(
|
||||
() =>
|
||||
props.stats.envs.bytes +
|
||||
|
|
@ -105,18 +106,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="header-stats">
|
||||
<MetricCard
|
||||
label="CPU Time"
|
||||
value={formatTime(props.stats.cpuTime)}
|
||||
value={formatTime(props.stats.cpuTime, prec())}
|
||||
tooltip={TOOLTIPS.cpuTime}
|
||||
/>
|
||||
<MetricCard label="Memory" value={formatBytes(totalMemory())} tooltip={TOOLTIPS.memory} />
|
||||
<MetricCard
|
||||
label="Memory"
|
||||
value={formatBytes(totalMemory(), prec())}
|
||||
tooltip={TOOLTIPS.memory}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Expressions"
|
||||
value={formatNumber(props.stats.nrExprs)}
|
||||
value={formatNumber(props.stats.nrExprs, prec())}
|
||||
tooltip={TOOLTIPS.nrExprs}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Thunks"
|
||||
value={`${formatNumber(props.stats.nrAvoided)} / ${formatNumber(props.stats.nrThunks)}`}
|
||||
value={`${formatNumber(props.stats.nrAvoided, prec())} / ${formatNumber(props.stats.nrThunks, prec())}`}
|
||||
tooltip="Avoided / Created thunks. Avoided means the value was already computed and reused."
|
||||
highlight={props.stats.nrAvoided >= props.stats.nrThunks}
|
||||
/>
|
||||
|
|
@ -131,11 +136,11 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="gc-header">GC Statistics</div>
|
||||
<div class="gc-values">
|
||||
<span class="gc-label">Heap:</span>
|
||||
<span class="gc-value">{formatBytes(props.stats.gc?.heapSize || 0)}</span>
|
||||
<span class="gc-value">{formatBytes(props.stats.gc?.heapSize || 0, prec())}</span>
|
||||
<span class="gc-label">Allocated:</span>
|
||||
<span class="gc-value">{formatBytes(props.stats.gc?.totalBytes || 0)}</span>
|
||||
<span class="gc-value">{formatBytes(props.stats.gc?.totalBytes || 0, prec())}</span>
|
||||
<span class="gc-label">Cycles:</span>
|
||||
<span class="gc-value">{formatNumber(props.stats.gc?.cycles || 0)}</span>
|
||||
<span class="gc-value">{formatNumber(props.stats.gc?.cycles || 0, prec())}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -143,15 +148,15 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
|
||||
<Section title="Time & Thunks" collapsible>
|
||||
<div class="time-thunks-combined">
|
||||
<TimeChart stats={props.stats} />
|
||||
<TimeChart stats={props.stats} precision={prec()} />
|
||||
<div class="thunks-section">
|
||||
<ThunkChart stats={props.stats} />
|
||||
<ThunkChart stats={props.stats} precision={prec()} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Operations" collapsible>
|
||||
<OperationsChart stats={props.stats} />
|
||||
<OperationsChart stats={props.stats} precision={prec()} />
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
|
|
@ -160,17 +165,17 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="metrics-grid small">
|
||||
<MetricCard
|
||||
label="Count"
|
||||
value={formatNumber(props.stats.envs.number)}
|
||||
value={formatNumber(props.stats.envs.number, prec())}
|
||||
tooltip={TOOLTIPS.envsNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Elements"
|
||||
value={formatNumber(props.stats.envs.elements)}
|
||||
value={formatNumber(props.stats.envs.elements, prec())}
|
||||
tooltip={TOOLTIPS.envsElements}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Memory"
|
||||
value={formatBytes(props.stats.envs.bytes)}
|
||||
value={formatBytes(props.stats.envs.bytes, prec())}
|
||||
tooltip={TOOLTIPS.envsBytes}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -180,22 +185,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="metrics-grid small">
|
||||
<MetricCard
|
||||
label="Values"
|
||||
value={formatNumber(props.stats.values.number)}
|
||||
value={formatNumber(props.stats.values.number, prec())}
|
||||
tooltip={TOOLTIPS.valuesNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Value Bytes"
|
||||
value={formatBytes(props.stats.values.bytes)}
|
||||
value={formatBytes(props.stats.values.bytes, prec())}
|
||||
tooltip={TOOLTIPS.valuesBytes}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Symbols"
|
||||
value={formatNumber(props.stats.symbols.number)}
|
||||
value={formatNumber(props.stats.symbols.number, prec())}
|
||||
tooltip={TOOLTIPS.symbolsNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Symbol Bytes"
|
||||
value={formatBytes(props.stats.symbols.bytes)}
|
||||
value={formatBytes(props.stats.symbols.bytes, prec())}
|
||||
tooltip={TOOLTIPS.symbolsBytes}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -205,22 +210,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="metrics-grid small">
|
||||
<MetricCard
|
||||
label="List Elements"
|
||||
value={formatNumber(props.stats.list.elements)}
|
||||
value={formatNumber(props.stats.list.elements, prec())}
|
||||
tooltip={TOOLTIPS.listElements}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Concat Ops"
|
||||
value={formatNumber(props.stats.list.concats)}
|
||||
value={formatNumber(props.stats.list.concats, prec())}
|
||||
tooltip={TOOLTIPS.listConcats}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Set Count"
|
||||
value={formatNumber(props.stats.sets.number)}
|
||||
value={formatNumber(props.stats.sets.number, prec())}
|
||||
tooltip={TOOLTIPS.setsNumber}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Attributes"
|
||||
value={formatNumber(props.stats.sets.elements)}
|
||||
value={formatNumber(props.stats.sets.elements, prec())}
|
||||
tooltip={TOOLTIPS.setsElements}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -235,7 +240,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="top-item">
|
||||
<span class="rank">{i() + 1}</span>
|
||||
<span class="name">{item.name}</span>
|
||||
<span class="count">{formatNumber(item.count)}</span>
|
||||
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -251,7 +256,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="top-item">
|
||||
<span class="rank">{i() + 1}</span>
|
||||
<span class="name">{item.name || '<lambda>'}</span>
|
||||
<span class="count">{formatNumber(item.count)}</span>
|
||||
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||
<span class="location">
|
||||
{item.file}:{item.line}
|
||||
</span>
|
||||
|
|
@ -267,22 +272,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<div class="metrics-grid small">
|
||||
<MetricCard
|
||||
label="NAR Reads"
|
||||
value={formatNumber(props.stats.narRead || 0)}
|
||||
value={formatNumber(props.stats.narRead || 0, prec())}
|
||||
tooltip={TOOLTIPS.narRead}
|
||||
/>
|
||||
<MetricCard
|
||||
label="NAR Writes"
|
||||
value={formatNumber(props.stats.narWrite || 0)}
|
||||
value={formatNumber(props.stats.narWrite || 0, prec())}
|
||||
tooltip={TOOLTIPS.narWrite}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Read Bytes"
|
||||
value={formatBytes(props.stats.narReadBytes || 0)}
|
||||
value={formatBytes(props.stats.narReadBytes || 0, prec())}
|
||||
tooltip={TOOLTIPS.narReadBytes}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Write Bytes"
|
||||
value={formatBytes(props.stats.narWriteBytes || 0)}
|
||||
value={formatBytes(props.stats.narWriteBytes || 0, prec())}
|
||||
tooltip={TOOLTIPS.narWriteBytes}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -299,7 +304,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
|||
<span class="location">
|
||||
{item.file}:{item.line}
|
||||
</span>
|
||||
<span class="count">{formatNumber(item.count)}</span>
|
||||
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ interface ComparisonViewProps {
|
|||
entries: ComparisonEntry[];
|
||||
onSelect: (entry: ComparisonEntry) => void;
|
||||
onDelete: (id: number) => void;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||
const prec = () => props.precision ?? 2;
|
||||
const [leftEntry, setLeftEntry] = createSignal<ComparisonEntry | null>(null);
|
||||
const [rightEntry, setRightEntry] = createSignal<ComparisonEntry | null>(null);
|
||||
|
||||
|
|
@ -203,7 +205,7 @@ const ComparisonView: Component<ComparisonViewProps> = props => {
|
|||
<Show when={!row.isReduction}>
|
||||
<ArrowUpIcon size={14} />
|
||||
</Show>
|
||||
{Math.abs(row.change).toFixed(2)}%
|
||||
{Math.abs(row.change).toFixed(prec())}%
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
|
|||
|
||||
interface OperationsChartProps {
|
||||
stats: StatsData;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
const OperationsChart: Component<OperationsChartProps> = props => {
|
||||
const prec = () => props.precision ?? 2;
|
||||
const operations = createMemo(() =>
|
||||
[
|
||||
{ label: 'Lookups', value: props.stats.nrLookups, colorClass: 'chart-1' },
|
||||
|
|
@ -34,7 +36,7 @@ const OperationsChart: Component<OperationsChartProps> = props => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="op-value">{formatNumber(item.value)}</div>
|
||||
<div class="op-value">{formatNumber(item.value, prec())}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
|
|||
|
||||
interface ThunkChartProps {
|
||||
stats: StatsData;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
const ThunkChart: Component<ThunkChartProps> = props => {
|
||||
const prec = () => props.precision ?? 2;
|
||||
const maxValue = createMemo(() => Math.max(props.stats.nrThunks, props.stats.nrAvoided));
|
||||
|
||||
const avoidedRatio = createMemo(() => {
|
||||
|
|
@ -47,7 +49,7 @@ const ThunkChart: Component<ThunkChartProps> = props => {
|
|||
<div class="ratio-bar">
|
||||
<div class="ratio-fill" style={{ width: `${avoidedRatio() * 100}%` }} />
|
||||
</div>
|
||||
<span class="ratio-label">Avoidance rate: {(avoidedRatio() * 100).toFixed(1)}%</span>
|
||||
<span class="ratio-label">Avoidance rate: {(avoidedRatio() * 100).toFixed(prec())}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import { formatBytes, formatTime, formatPercent } from '@ns/ui-utils';
|
|||
|
||||
interface TimeChartProps {
|
||||
stats: StatsData;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
const TimeChart: Component<TimeChartProps> = props => {
|
||||
const prec = () => props.precision ?? 2;
|
||||
const timeData = createMemo(() => {
|
||||
const cpu = props.stats.time.cpu;
|
||||
const gc = props.stats.time.gc || 0;
|
||||
|
|
@ -45,7 +47,7 @@ const TimeChart: Component<TimeChartProps> = props => {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="time-bar-value">{formatTime(item.value)}</div>
|
||||
<div class="time-bar-value">{formatTime(item.value, prec())}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
@ -59,11 +61,13 @@ const TimeChart: Component<TimeChartProps> = props => {
|
|||
</div>
|
||||
<div class="gc-stat">
|
||||
<span class="gc-stat-label">Heap Size</span>
|
||||
<span class="gc-stat-value">{formatBytes(props.stats.gc?.heapSize || 0)}</span>
|
||||
<span class="gc-stat-value">{formatBytes(props.stats.gc?.heapSize || 0, prec())}</span>
|
||||
</div>
|
||||
<div class="gc-stat">
|
||||
<span class="gc-stat-label">GC Fraction</span>
|
||||
<span class="gc-stat-value">{formatPercent(props.stats.time.gcFraction || 0)}</span>
|
||||
<span class="gc-stat-value">
|
||||
{formatPercent(props.stats.time.gcFraction || 0, prec())}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ function App() {
|
|||
const [showSaveDialog, setShowSaveDialog] = createSignal(false);
|
||||
const [showHelp, setShowHelp] = createSignal(false);
|
||||
const [showManageSnapshots, setShowManageSnapshots] = createSignal(false);
|
||||
const [precision, setPrecision] = createSignal(2);
|
||||
const [isLoading, setIsLoading] = createSignal(true);
|
||||
|
||||
const STORAGE_KEY = 'ns-data';
|
||||
|
|
@ -58,6 +59,9 @@ function App() {
|
|||
if (parsed.view) {
|
||||
setView(parsed.view);
|
||||
}
|
||||
if (typeof parsed.precision === 'number' && parsed.precision >= 0) {
|
||||
setPrecision(parsed.precision);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load saved data:', e);
|
||||
|
|
@ -84,6 +88,7 @@ function App() {
|
|||
raw: Record<string, unknown> | null,
|
||||
snaps: ComparisonEntry[],
|
||||
v: 'analysis' | 'compare',
|
||||
prec: number,
|
||||
) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
|
|
@ -93,6 +98,7 @@ function App() {
|
|||
currentStats: stats,
|
||||
currentRaw: raw,
|
||||
view: v,
|
||||
precision: prec,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
@ -108,10 +114,11 @@ function App() {
|
|||
const raw = currentRaw();
|
||||
const snaps = snapshots();
|
||||
const v = view();
|
||||
const prec = precision();
|
||||
|
||||
if (isLoading()) return;
|
||||
|
||||
saveToStorage(stats, raw, snaps, v);
|
||||
saveToStorage(stats, raw, snaps, v, prec);
|
||||
});
|
||||
|
||||
const saveSnapshot = () => {
|
||||
|
|
@ -190,6 +197,16 @@ function App() {
|
|||
<Trash2Icon size={16} />
|
||||
</button>
|
||||
</Show>
|
||||
<select
|
||||
class="precision-select"
|
||||
value={precision()}
|
||||
onChange={e => setPrecision(parseInt(e.currentTarget.value, 10))}
|
||||
title="Decimal Precision"
|
||||
>
|
||||
<For each={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}>
|
||||
{p => <option value={p}>{p} dp</option>}
|
||||
</For>
|
||||
</select>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
|
@ -213,7 +230,7 @@ function App() {
|
|||
/>
|
||||
</Show>
|
||||
<Show when={currentStats()}>
|
||||
<Analysis stats={currentStats()!} />
|
||||
<Analysis stats={currentStats()!} precision={precision()} />
|
||||
<Show when={showSaveDialog()}>
|
||||
<div class="modal-overlay" onClick={() => setShowSaveDialog(false)}>
|
||||
<div class="modal" onClick={e => e.stopPropagation()}>
|
||||
|
|
@ -246,6 +263,7 @@ function App() {
|
|||
entries={snapshots()}
|
||||
onSelect={entry => setCurrentStats(entry.data)}
|
||||
onDelete={deleteSnapshot}
|
||||
precision={precision()}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -1628,3 +1628,65 @@ body {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.precision-select {
|
||||
padding: 0.5rem 1.5rem 0.5rem 0.75rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
}
|
||||
|
||||
.precision-select:hover {
|
||||
border-color: var(--text-muted);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.precision-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.precision-input {
|
||||
width: 120px;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9375rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.precision-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Hide number input spinners */
|
||||
.precision-input::-webkit-outer-spin-button,
|
||||
.precision-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.precision-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.settings-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue