This commit is contained in:
whilb 2025-09-01 20:07:20 -07:00
parent ad664c32ea
commit 5ec3a6006f
3 changed files with 1419 additions and 0 deletions

390
public/calculators/zfs.js Normal file
View file

@ -0,0 +1,390 @@
import {revive, persist, labelInput, labelSelect} from '/js/util.js';
export default {
id:'zfs', name:'ZFS Calculator', about:'Calculate ZFS pool configurations, performance tuning, and capacity planning.',
render(root){
const key='calc_zfs_v1';
const s = revive(key,{
poolType: 'raidz2',
diskCount: 6,
diskSize: 4000,
diskSizeUnit: 'GB',
blockSize: '128K',
compression: 'lz4',
dedup: false,
ashift: 12,
arcMax: 8192,
arcMaxUnit: 'MB'
});
const ui = document.createElement('div');
// Pool configuration section
const poolSection = document.createElement('div');
poolSection.innerHTML = `
<h3 style="color: var(--accent); margin-bottom: 15px;">Pool Configuration</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Pool Type
</label>
<select name="poolType" data-ui="lite" style="width: 100%;">
<option value="stripe">Stripe (No Redundancy)</option>
<option value="mirror">Mirror (2-way)</option>
<option value="raidz1">RAID-Z1 (Single Parity)</option>
<option value="raidz2">RAID-Z2 (Double Parity)</option>
<option value="raidz3">RAID-Z3 (Triple Parity)</option>
<option value="mirror2x2">Mirror 2x2 (2 mirrors of 2 disks)</option>
<option value="mirror3x2">Mirror 3x2 (3 mirrors of 2 disks)</option>
<option value="raidz2x2">RAID-Z2 2x2 (2 RAID-Z2 vdevs)</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Number of Disks
</label>
<input type="number" name="diskCount" value="${s.diskCount}" min="1" max="20"
style="width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Disk Size
</label>
<div style="display: flex; gap: 10px;">
<input type="number" name="diskSize" value="${s.diskSize}" min="1"
style="flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
<select name="diskSizeUnit" data-ui="lite" style="width: 80px;">
<option value="GB">GB</option>
<option value="TB">TB</option>
</select>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Block Size
</label>
<select name="blockSize" data-ui="lite" style="width: 100%;">
<option value="4K">4K</option>
<option value="8K">8K</option>
<option value="16K">16K</option>
<option value="32K">32K</option>
<option value="64K">64K</option>
<option value="128K">128K</option>
<option value="256K">256K</option>
<option value="512K">512K</option>
<option value="1M">1M</option>
</select>
</div>
</div>
`;
// Performance tuning section
const perfSection = document.createElement('div');
perfSection.innerHTML = `
<h3 style="color: var(--accent); margin-bottom: 15px;">Performance Tuning</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Compression
</label>
<select name="compression" data-ui="lite" style="width: 100%;">
<option value="off">Off</option>
<option value="lz4">LZ4 (Fast)</option>
<option value="gzip">Gzip (Balanced)</option>
<option value="gzip-1">Gzip-1 (Fast)</option>
<option value="gzip-9">Gzip-9 (Best)</option>
<option value="zstd">Zstd (Modern)</option>
<option value="zstd-1">Zstd-1 (Fast)</option>
<option value="zstd-19">Zstd-19 (Best)</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Deduplication
</label>
<select name="dedup" data-ui="lite" style="width: 100%;">
<option value="false">Off (Recommended)</option>
<option value="true">On (Use with Caution)</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
Ashift Value
</label>
<select name="ashift" data-ui="lite" style="width: 100%;">
<option value="9">9 (512B sectors)</option>
<option value="12">12 (4KB sectors)</option>
<option value="13">13 (8KB sectors)</option>
<option value="14">14 (16KB sectors)</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);">
ARC Max Size
</label>
<div style="display: flex; gap: 10px;">
<input type="number" name="arcMax" value="${s.arcMax}" min="64"
style="flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;">
<select name="arcMaxUnit" data-ui="lite" style="width: 80px;">
<option value="MB">MB</option>
<option value="GB">GB</option>
</select>
</div>
</div>
</div>
`;
ui.append(poolSection, perfSection);
// Results section
const out = document.createElement('div');
out.className = 'result';
out.style.cssText = `
margin: 20px 0;
padding: 15px;
background: var(--k-bg);
border-radius: 8px;
border-left: 4px solid var(--accent);
`;
ui.append(out);
// Calculation function
function calc(){
const poolType = ui.querySelector('[name=poolType]').value;
const diskCount = +ui.querySelector('[name=diskCount]').value;
const diskSize = +ui.querySelector('[name=diskSize]').value;
const diskSizeUnit = ui.querySelector('[name=diskSizeUnit]').value;
const blockSize = ui.querySelector('[name=blockSize]').value;
const compression = ui.querySelector('[name=compression]').value;
const dedup = ui.querySelector('[name=dedup]').value === 'true';
const ashift = +ui.querySelector('[name=ashift]').value;
const arcMax = +ui.querySelector('[name=arcMax]').value;
const arcMaxUnit = ui.querySelector('[name=arcMaxUnit]').value;
// Convert disk size to GB for calculations
let diskSizeGB = diskSize;
if (diskSizeUnit === 'TB') {
diskSizeGB = diskSize * 1024;
}
// Calculate usable capacity based on pool type
let usableCapacity, redundancy, minDisks, recommendedDisks, vdevCount;
switch(poolType) {
case 'stripe':
usableCapacity = diskCount * diskSizeGB;
redundancy = 'None';
minDisks = 1;
recommendedDisks = 1;
vdevCount = 1;
break;
case 'mirror':
usableCapacity = Math.floor(diskCount / 2) * diskSizeGB;
redundancy = '50%';
minDisks = 2;
recommendedDisks = 2;
vdevCount = Math.floor(diskCount / 2);
break;
case 'raidz1':
usableCapacity = (diskCount - 1) * diskSizeGB;
redundancy = '1 disk';
minDisks = 3;
recommendedDisks = 3;
vdevCount = 1;
break;
case 'raidz2':
usableCapacity = (diskCount - 2) * diskSizeGB;
redundancy = '2 disks';
minDisks = 4;
recommendedDisks = 6;
vdevCount = 1;
break;
case 'raidz3':
usableCapacity = (diskCount - 3) * diskSizeGB;
redundancy = '3 disks';
minDisks = 5;
recommendedDisks = 9;
vdevCount = 1;
break;
case 'mirror2x2':
usableCapacity = 2 * diskSizeGB; // 2 mirrors, each with 1 usable disk
redundancy = '50%';
minDisks = 4;
recommendedDisks = 4;
vdevCount = 2;
break;
case 'mirror3x2':
usableCapacity = 3 * diskSizeGB; // 3 mirrors, each with 1 usable disk
redundancy = '50%';
minDisks = 6;
recommendedDisks = 6;
vdevCount = 3;
break;
case 'raidz2x2':
usableCapacity = 2 * (diskCount / 2 - 2) * diskSizeGB; // 2 RAID-Z2 vdevs
redundancy = '2 disks per vdev';
minDisks = 8;
recommendedDisks = 12;
vdevCount = 2;
break;
}
// Validate disk count
if (diskCount < minDisks) {
out.innerHTML = `<div style="color: var(--error);">
<strong>Error:</strong> ${poolType.toUpperCase()} requires at least ${minDisks} disks
</div>`;
return;
}
// Calculate compression ratios
const compressionRatios = {
'off': 1.0,
'lz4': 2.1,
'gzip': 2.5,
'gzip-1': 2.0,
'gzip-9': 3.0,
'zstd': 2.8,
'zstd-1': 2.2,
'zstd-19': 3.5
};
const compressionRatio = compressionRatios[compression] || 1.0;
const effectiveCapacity = usableCapacity * compressionRatio;
// Calculate performance metrics
const blockSizeKB = parseInt(blockSize.replace(/[^0-9]/g, ''));
const ashiftBytes = Math.pow(2, ashift);
// Convert ARC max to MB
let arcMaxMB = arcMax;
if (arcMaxUnit === 'GB') {
arcMaxMB = arcMax * 1024;
}
// Helper function to format capacity
function formatCapacity(gb) {
if (gb >= 1024) {
return `${gb.toFixed(1)} GB (${(gb / 1024).toFixed(2)} TB)`;
}
return `${gb.toFixed(1)} GB`;
}
// Calculate I/O performance estimates
const estimatedIOPS = Math.floor(diskCount * 100); // Rough estimate: 100 IOPS per disk
const estimatedThroughput = Math.floor(diskCount * 150); // Rough estimate: 150 MB/s per disk
// Calculate memory requirements (rule of thumb: 1GB RAM per 1TB storage)
const recommendedRAM = Math.ceil((diskCount * diskSizeGB) / 1024);
// Generate results
out.innerHTML = `
<div style="font-size: 24px; font-weight: 700; color: var(--accent); margin-bottom: 15px;">
ZFS Pool Configuration
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div>
<h4 style="color: var(--text); margin-bottom: 10px;">Capacity & Redundancy</h4>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Raw Capacity:</strong> ${formatCapacity(diskCount * diskSizeGB)}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Usable Capacity:</strong> ${formatCapacity(usableCapacity)}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Effective Capacity (with compression):</strong> ${formatCapacity(effectiveCapacity)}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Redundancy:</strong> ${redundancy}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Efficiency:</strong> ${((usableCapacity / (diskCount * diskSizeGB)) * 100).toFixed(1)}%
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Compression Savings:</strong> ${((effectiveCapacity - usableCapacity) / usableCapacity * 100).toFixed(1)}%
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>VDev Count:</strong> ${vdevCount} ${vdevCount > 1 ? 'vdevs' : 'vdev'}
</div>
</div>
<div>
<h4 style="color: var(--text); margin-bottom: 10px;">Performance Settings</h4>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Block Size:</strong> ${blockSize}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Ashift:</strong> ${ashift} (${ashiftBytes} bytes)
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Compression:</strong> ${compression} (${compressionRatio.toFixed(1)}x ratio)
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Deduplication:</strong> ${dedup ? 'On' : 'Off'}
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>ARC Max:</strong> ${arcMaxMB} MB
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
<div>
<h4 style="color: var(--text); margin-bottom: 10px;">Performance Estimates</h4>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Estimated IOPS:</strong> ${estimatedIOPS.toLocaleString()} (random 4K reads)
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Estimated Throughput:</strong> ${estimatedThroughput} MB/s (sequential)
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Recommended RAM:</strong> ${recommendedRAM} GB minimum
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Current ARC:</strong> ${(arcMaxMB / 1024).toFixed(1)} GB
</div>
</div>
<div>
<h4 style="color: var(--text); margin-bottom: 10px;">System Requirements</h4>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Minimum RAM:</strong> ${Math.max(8, recommendedRAM)} GB
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Recommended RAM:</strong> ${Math.max(16, recommendedRAM * 2)} GB
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>CPU Cores:</strong> ${Math.max(4, Math.ceil(diskCount / 2))} cores recommended
</div>
<div style="color: var(--text); margin-bottom: 5px;">
<strong>Network:</strong> 10 Gbps recommended for ${estimatedThroughput} MB/s throughput
</div>
</div>
</div>
`;
persist(key, {poolType, diskCount, diskSize, diskSizeUnit, blockSize, compression, dedup, ashift, arcMax, arcMaxUnit});
}
// Event listeners
ui.querySelector('[name=poolType]').addEventListener('change', calc);
ui.querySelector('[name=diskCount]').addEventListener('input', calc);
ui.querySelector('[name=diskSize]').addEventListener('input', calc);
ui.querySelector('[name=diskSizeUnit]').addEventListener('change', calc);
ui.querySelector('[name=blockSize]').addEventListener('change', calc);
ui.querySelector('[name=compression]').addEventListener('change', calc);
ui.querySelector('[name=dedup]').addEventListener('change', calc);
ui.querySelector('[name=ashift]').addEventListener('change', calc);
ui.querySelector('[name=arcMax]').addEventListener('input', calc);
ui.querySelector('[name=arcMaxUnit]').addEventListener('change', calc);
// Initial calculation
calc();
root.append(ui);
}
}

View file

@ -6,6 +6,7 @@ const CALCS = [
{ id:'bandwidth', name:'Bandwidth', about:'Bits↔bytes unit conv.', path:'../calculators/bandwidth.js' },
{ id:'nmea', name:'NMEA', about:'0183 XOR checksum', path:'../calculators/nmea.js' },
{ id:'currency', name:'Currency Converter', about:'Convert between currencies', path:'../calculators/currency.js' },
{ id:'zfs', name:'ZFS', about:'Pool configuration & performance', path:'../calculators/zfs.js' },
{ id:'subnet', name:'IP Subnet', about:'IPv4/IPv6 subnet calculations', path:'../calculators/subnet.js' }
];