diff --git a/packages/ui-utils/src/formatters.ts b/packages/ui-utils/src/formatters.ts
index 76869c9..f55309a 100644
--- a/packages/ui-utils/src/formatters.ts
+++ b/packages/ui-utils/src/formatters.ts
@@ -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) + '%';
}
diff --git a/packages/web/src/components/Analysis.tsx b/packages/web/src/components/Analysis.tsx
index 2fc65ca..14c29ac 100644
--- a/packages/web/src/components/Analysis.tsx
+++ b/packages/web/src/components/Analysis.tsx
@@ -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 => {
@@ -143,15 +148,15 @@ const Analysis: Component<{ stats: StatsData }> = props => {
@@ -160,17 +165,17 @@ const Analysis: Component<{ stats: StatsData }> = props => {
@@ -180,22 +185,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
@@ -205,22 +210,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
@@ -235,7 +240,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
{i() + 1}
{item.name}
- {formatNumber(item.count)}
+ {formatNumber(item.count, prec())}
)}
@@ -251,7 +256,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
{i() + 1}
{item.name || ''}
-
{formatNumber(item.count)}
+
{formatNumber(item.count, prec())}
{item.file}:{item.line}
@@ -267,22 +272,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
@@ -299,7 +304,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
{item.file}:{item.line}
-
{formatNumber(item.count)}
+
{formatNumber(item.count, prec())}
)}
diff --git a/packages/web/src/components/ComparisonView.tsx b/packages/web/src/components/ComparisonView.tsx
index c989099..1c7941e 100644
--- a/packages/web/src/components/ComparisonView.tsx
+++ b/packages/web/src/components/ComparisonView.tsx
@@ -10,9 +10,11 @@ interface ComparisonViewProps {
entries: ComparisonEntry[];
onSelect: (entry: ComparisonEntry) => void;
onDelete: (id: number) => void;
+ precision?: number;
}
const ComparisonView: Component = props => {
+ const prec = () => props.precision ?? 2;
const [leftEntry, setLeftEntry] = createSignal(null);
const [rightEntry, setRightEntry] = createSignal(null);
@@ -203,7 +205,7 @@ const ComparisonView: Component = props => {
- {Math.abs(row.change).toFixed(2)}%
+ {Math.abs(row.change).toFixed(prec())}%
}
diff --git a/packages/web/src/components/OperationsChart.tsx b/packages/web/src/components/OperationsChart.tsx
index 13c28e2..d49fa35 100644
--- a/packages/web/src/components/OperationsChart.tsx
+++ b/packages/web/src/components/OperationsChart.tsx
@@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
interface OperationsChartProps {
stats: StatsData;
+ precision?: number;
}
const OperationsChart: Component = 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 = props => {
}}
/>
- {formatNumber(item.value)}
+ {formatNumber(item.value, prec())}
)}
diff --git a/packages/web/src/components/ThunkChart.tsx b/packages/web/src/components/ThunkChart.tsx
index e403ad8..7a78938 100644
--- a/packages/web/src/components/ThunkChart.tsx
+++ b/packages/web/src/components/ThunkChart.tsx
@@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
interface ThunkChartProps {
stats: StatsData;
+ precision?: number;
}
const ThunkChart: Component = 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 = props => {
- Avoidance rate: {(avoidedRatio() * 100).toFixed(1)}%
+ Avoidance rate: {(avoidedRatio() * 100).toFixed(prec())}%
);
diff --git a/packages/web/src/components/TimeChart.tsx b/packages/web/src/components/TimeChart.tsx
index 50b07bc..2f5737b 100644
--- a/packages/web/src/components/TimeChart.tsx
+++ b/packages/web/src/components/TimeChart.tsx
@@ -4,9 +4,11 @@ import { formatBytes, formatTime, formatPercent } from '@ns/ui-utils';
interface TimeChartProps {
stats: StatsData;
+ precision?: number;
}
const TimeChart: Component = 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 = props => {
}}
/>
- {formatTime(item.value)}
+ {formatTime(item.value, prec())}
)}
@@ -59,11 +61,13 @@ const TimeChart: Component = props => {
Heap Size
- {formatBytes(props.stats.gc?.heapSize || 0)}
+ {formatBytes(props.stats.gc?.heapSize || 0, prec())}
GC Fraction
- {formatPercent(props.stats.time.gcFraction || 0)}
+
+ {formatPercent(props.stats.time.gcFraction || 0, prec())}
+
diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx
index a173b19..578bb8a 100644
--- a/packages/web/src/index.tsx
+++ b/packages/web/src/index.tsx
@@ -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 | 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() {
+
@@ -213,7 +230,7 @@ function App() {
/>
-
+
setShowSaveDialog(false)}>
e.stopPropagation()}>
@@ -246,6 +263,7 @@ function App() {
entries={snapshots()}
onSelect={entry => setCurrentStats(entry.data)}
onDelete={deleteSnapshot}
+ precision={precision()}
/>
diff --git a/packages/web/src/styles.css b/packages/web/src/styles.css
index 3a43b85..95e9806 100644
--- a/packages/web/src/styles.css
+++ b/packages/web/src/styles.css
@@ -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);
+}