treewide: adapt for monorepo layout; initial TUI work

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id40b5f5ccb55a8a1ea2793192a38f0256a6a6964
This commit is contained in:
raf 2026-02-07 12:43:59 +03:00
commit 33ec901788
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
35 changed files with 1699 additions and 413 deletions

27
packages/core/README.md Normal file
View file

@ -0,0 +1,27 @@
# @ns/core
Core types and parsing logic for Nix evaluator statistics. This package is
framework-agnostic and can be used in any JavaScript/TypeScript environment.
## Usage
```typescript
import { calculateChange, parseStats, StatsData } from "@ns/core";
// Parse raw stats from Nix
const raw = JSON.parse(statsJson);
const stats: StatsData = parseStats(raw);
console.log(`CPU Time: ${stats.cpuTime}s`);
console.log(`Expressions: ${stats.nrExprs}`);
// Compare two values
const change = calculateChange(stats.nrThunks, previousStats.nrThunks);
console.log(`Thunks changed by ${change.percent.toFixed(2)}%`);
```
## Version Compatibility
The parser handles different Nix implementations (Nix, Lix, Snix, etc.) by
checking for field existence in the raw JSON, since not all implementations
expose the same statistics.

View file

@ -0,0 +1,20 @@
{
"name": "@ns/core",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"check": "tsc --noEmit"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,2 @@
export * from './types';
export * from './parser';

105
packages/core/src/parser.ts Normal file
View file

@ -0,0 +1,105 @@
import { StatsData } from './types';
const num = (val: unknown): number => (typeof val === 'number' ? val : 0);
export function parseStats(json: Record<string, unknown>): StatsData {
const timeObj = json.time as Record<string, unknown> | undefined;
const envsObj = json.envs as Record<string, unknown> | undefined;
const listObj = json.list as Record<string, unknown> | undefined;
const valuesObj = json.values as Record<string, unknown> | undefined;
const symbolsObj = json.symbols as Record<string, unknown> | undefined;
const setsObj = json.sets as Record<string, unknown> | undefined;
const sizesObj = json.sizes as Record<string, unknown> | undefined;
const stats: StatsData = {
cpuTime: num(json.cpuTime),
time: {
cpu: num(timeObj?.cpu) || num(json.cpuTime),
gc: num(timeObj?.gc),
gcNonIncremental: num(timeObj?.gcNonIncremental),
gcFraction: num(timeObj?.gcFraction),
gcNonIncrementalFraction: num(timeObj?.gcNonIncrementalFraction),
},
envs: {
number: num(envsObj?.number),
elements: num(envsObj?.elements),
bytes: num(envsObj?.bytes),
},
list: {
elements: num(listObj?.elements),
bytes: num(listObj?.bytes),
concats: num(listObj?.concats),
},
values: { number: num(valuesObj?.number), bytes: num(valuesObj?.bytes) },
symbols: { number: num(symbolsObj?.number), bytes: num(symbolsObj?.bytes) },
sets: {
number: num(setsObj?.number),
elements: num(setsObj?.elements),
bytes: num(setsObj?.bytes),
},
sizes: {
Env: num(sizesObj?.Env),
Value: num(sizesObj?.Value),
Bindings: num(sizesObj?.Bindings),
Attr: num(sizesObj?.Attr),
},
nrExprs: num(json.nrExprs),
nrThunks: num(json.nrThunks),
nrAvoided: num(json.nrAvoided),
nrLookups: num(json.nrLookups),
nrOpUpdates: num(json.nrOpUpdates),
nrOpUpdateValuesCopied: num(json.nrOpUpdateValuesCopied),
nrPrimOpCalls: num(json.nrPrimOpCalls),
nrFunctionCalls: num(json.nrFunctionCalls),
};
if (json.gc && typeof json.gc === 'object') {
const gc = json.gc as Record<string, unknown>;
stats.gc = {
heapSize: num(gc.heapSize),
totalBytes: num(gc.totalBytes),
cycles: num(gc.cycles),
};
}
if (json.primops && typeof json.primops === 'object')
stats.primops = json.primops as Record<string, number>;
if (json.functions && Array.isArray(json.functions))
stats.functions = json.functions as StatsData['functions'];
if (json.attributes && Array.isArray(json.attributes))
stats.attributes = json.attributes as StatsData['attributes'];
const storeFields = [
'narInfoRead',
'narInfoReadAverted',
'narInfoMissing',
'narInfoWrite',
'narRead',
'narReadBytes',
'narReadCompressedBytes',
'narWrite',
'narWriteAverted',
'narWriteBytes',
'narWriteCompressedBytes',
] as const;
for (const field of storeFields) {
if (typeof json[field] === 'number')
(stats as unknown as Record<string, number>)[field] = json[field] as number;
}
return stats;
}
export function calculateChange(
current: number,
previous: number,
): { value: number; percent: number; isReduction: boolean } {
if (previous === 0) {
const percent = current === 0 ? 0 : 100;
return { value: current, percent, isReduction: false };
}
const value = current - previous;
const percent = (value / previous) * 100;
return { value, percent, isReduction: value < 0 };
}

View file

@ -0,0 +1,85 @@
export interface StatsData {
cpuTime: number;
time: {
cpu: number;
gc?: number;
gcNonIncremental?: number;
gcFraction?: number;
gcNonIncrementalFraction?: number;
};
envs: {
number: number;
elements: number;
bytes: number;
};
list: {
elements: number;
bytes: number;
concats: number;
};
values: {
number: number;
bytes: number;
};
symbols: {
number: number;
bytes: number;
};
sets: {
number: number;
elements: number;
bytes: number;
};
sizes: {
Env: number;
Value: number;
Bindings: number;
Attr: number;
};
nrExprs: number;
nrThunks: number;
nrAvoided: number;
nrLookups: number;
nrOpUpdates: number;
nrOpUpdateValuesCopied: number;
nrPrimOpCalls: number;
nrFunctionCalls: number;
gc?: {
heapSize: number;
totalBytes: number;
cycles: number;
};
primops?: Record<string, number>;
functions?: Array<{
name: string | null;
file: string;
line: number;
column: number;
count: number;
}>;
attributes?: Array<{
file: string;
line: number;
column: number;
count: number;
}>;
narInfoRead?: number;
narInfoReadAverted?: number;
narInfoMissing?: number;
narInfoWrite?: number;
narRead?: number;
narReadBytes?: number;
narReadCompressedBytes?: number;
narWrite?: number;
narWriteAverted?: number;
narWriteBytes?: number;
narWriteCompressedBytes?: number;
}
export interface ComparisonEntry {
id: number;
name: string;
data: StatsData;
raw: Record<string, unknown>;
timestamp: Date;
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}