// =================================== // RAID Calculator - Core Logic // =================================== // Storage conversion constants const TB_TO_BYTES_DECIMAL = 1000000000000; // 1 TB = 1,000,000,000,000 bytes (decimal) const TB_TO_BYTES_BINARY = 1099511627776; // 1 TiB = 1,099,511,627,776 bytes (binary) // RAID Configuration Metadata const RAID_CONFIGS = { raid0: { name: 'RAID 0', description: 'Striping - Maximum performance, no redundancy', minDrives: 2, faultTolerance: 0, readPerformance: 'Excellent', writePerformance: 'Excellent', efficiency: 1.0 }, raid1: { name: 'RAID 1', description: 'Mirroring - 100% redundancy', minDrives: 2, faultTolerance: 1, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: 0.5 }, raid5: { name: 'RAID 5', description: 'Single parity - Good balance of performance and redundancy', minDrives: 3, faultTolerance: 1, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-1)/n }, raid6: { name: 'RAID 6', description: 'Double parity - Enhanced fault tolerance', minDrives: 4, faultTolerance: 2, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-2)/n }, raid10: { name: 'RAID 10', description: 'Mirrored stripes - Best performance with redundancy', minDrives: 4, faultTolerance: 1, readPerformance: 'Excellent', writePerformance: 'Good', efficiency: 0.5 }, shr: { name: 'Synology Hybrid RAID', description: 'Flexible RAID with single disk fault tolerance', minDrives: 2, faultTolerance: 1, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated based on drive configuration }, shr2: { name: 'Synology Hybrid RAID 2', description: 'Flexible RAID with dual disk fault tolerance', minDrives: 4, faultTolerance: 2, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated based on drive configuration }, 'zfs-stripe': { name: 'ZFS Stripe', description: 'No redundancy - Maximum capacity', minDrives: 1, faultTolerance: 0, readPerformance: 'Excellent', writePerformance: 'Excellent', efficiency: 1.0 }, 'zfs-mirror': { name: 'ZFS Mirror', description: 'Mirrored vdevs - High redundancy', minDrives: 2, faultTolerance: 1, readPerformance: 'Excellent', writePerformance: 'Good', efficiency: 0.5 }, raidz1: { name: 'RAIDZ1', description: 'Single parity - ZFS equivalent to RAID 5', minDrives: 3, faultTolerance: 1, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-1)/n }, raidz2: { name: 'RAIDZ2', description: 'Double parity - ZFS equivalent to RAID 6', minDrives: 4, faultTolerance: 2, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-2)/n }, raidz3: { name: 'RAIDZ3', description: 'Triple parity - Maximum fault tolerance', minDrives: 5, faultTolerance: 3, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-3)/n }, 'unraid-1': { name: 'Unraid (1 Parity)', description: 'Flexible array with single parity drive - Individual drive access', minDrives: 2, faultTolerance: 1, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-1)/n }, 'unraid-2': { name: 'Unraid (2 Parity)', description: 'Flexible array with dual parity drives - Individual drive access', minDrives: 3, faultTolerance: 2, readPerformance: 'Good', writePerformance: 'Moderate', efficiency: null // Calculated: (n-2)/n } }; // =================================== // Storage Calculation Functions // =================================== /** * Convert TB (decimal) to actual usable TiB (binary) * Example: 12 TB = 10.91 TiB usable */ function convertTBtoTiB(tb) { const bytes = tb * TB_TO_BYTES_DECIMAL; const tib = bytes / TB_TO_BYTES_BINARY; return tib; } /** * Calculate usable storage for RAID configuration */ function calculateRAIDStorage(raidType, drives) { const numDrives = drives.length; const config = RAID_CONFIGS[raidType]; // Validate minimum drives if (numDrives < config.minDrives) { throw new Error(`${config.name} requires at least ${config.minDrives} drives`); } // Convert all drives to TiB (binary) const drivesInTiB = drives.map(tb => convertTBtoTiB(tb)); let usableCapacity = 0; let rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0); switch (raidType) { case 'raid0': case 'zfs-stripe': // Sum of all drives usableCapacity = rawCapacity; break; case 'raid1': case 'zfs-mirror': // Smallest drive capacity (all drives mirror the smallest) usableCapacity = Math.min(...drivesInTiB); break; case 'raid5': case 'raidz1': // (n-1) * smallest drive usableCapacity = (numDrives - 1) * Math.min(...drivesInTiB); break; case 'raid6': case 'raidz2': // (n-2) * smallest drive usableCapacity = (numDrives - 2) * Math.min(...drivesInTiB); break; case 'raidz3': // (n-3) * smallest drive usableCapacity = (numDrives - 3) * Math.min(...drivesInTiB); break; case 'raid10': // n/2 * smallest drive (assumes even number of drives) if (numDrives % 2 !== 0) { throw new Error('RAID 10 requires an even number of drives'); } usableCapacity = (numDrives / 2) * Math.min(...drivesInTiB); break; case 'shr': // Synology Hybrid RAID calculation usableCapacity = calculateSHR(drivesInTiB, 1); break; case 'shr2': // Synology Hybrid RAID 2 calculation usableCapacity = calculateSHR(drivesInTiB, 2); break; case 'unraid-1': // Unraid with 1 parity drive - sum of all drives minus largest (parity) usableCapacity = calculateUnraid(drivesInTiB, 1); break; case 'unraid-2': // Unraid with 2 parity drives - sum of all drives minus 2 largest (parity) usableCapacity = calculateUnraid(drivesInTiB, 2); break; } return { usableCapacity, rawCapacity, efficiency: usableCapacity / rawCapacity, wastedSpace: rawCapacity - usableCapacity }; } /** * Calculate Synology Hybrid RAID capacity * SHR optimizes storage by using different RAID levels based on drive sizes */ function calculateSHR(drivesInTiB, parityDisks) { const sorted = [...drivesInTiB].sort((a, b) => a - b); const numDrives = sorted.length; if (numDrives < parityDisks + 1) { throw new Error(`SHR-${parityDisks} requires at least ${parityDisks + 1} drives`); } let usableCapacity = 0; // SHR algorithm: Build up capacity by size tiers for (let i = 0; i < numDrives; i++) { const currentSize = sorted[i]; const availableDrives = numDrives - i; if (availableDrives > parityDisks) { // Calculate contribution of this drive if (i === 0) { // First (smallest) drives contribute their full capacity minus parity usableCapacity += currentSize * (availableDrives - parityDisks) / availableDrives; } else { // Larger drives contribute the difference from the previous tier const difference = currentSize - sorted[i - 1]; usableCapacity += difference * (availableDrives - parityDisks) / availableDrives; } } } return usableCapacity; } /** * Calculate Unraid capacity * Unraid uses individual drives with parity - largest drive(s) become parity * Usable capacity = sum of all drives minus the largest N drives (where N = parity count) */ function calculateUnraid(drivesInTiB, parityCount) { const sorted = [...drivesInTiB].sort((a, b) => b - a); // Sort descending const numDrives = sorted.length; if (numDrives < parityCount + 1) { throw new Error(`Unraid with ${parityCount} parity requires at least ${parityCount + 1} drives`); } // Remove the largest N drives (parity drives) const dataDrives = sorted.slice(parityCount); // Sum remaining drives for usable capacity return dataDrives.reduce((sum, size) => sum + size, 0); } /** * Calculate ZFS storage with multiple vdevs * Each vdev uses the specified RAID type, and total capacity is the sum of all vdevs */ function calculateZFSVdevStorage(raidType, allDrives, numVdevs, drivesPerVdev) { const totalDrives = allDrives.length; if (totalDrives !== numVdevs * drivesPerVdev) { throw new Error(`Total drives (${totalDrives}) must equal vdevs (${numVdevs}) × drives per vdev (${drivesPerVdev})`); } // Convert all drives to TiB const drivesInTiB = allDrives.map(tb => convertTBtoTiB(tb)); let totalUsableCapacity = 0; const rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0); // Calculate capacity for each vdev for (let vdevIndex = 0; vdevIndex < numVdevs; vdevIndex++) { const startIdx = vdevIndex * drivesPerVdev; const endIdx = startIdx + drivesPerVdev; const vdevDrives = drivesInTiB.slice(startIdx, endIdx); const smallestInVdev = Math.min(...vdevDrives); let vdevCapacity = 0; switch (raidType) { case 'zfs-mirror': // Mirror: capacity of smallest drive in vdev vdevCapacity = smallestInVdev; break; case 'raidz1': // RAIDZ1: (n-1) * smallest drive vdevCapacity = (drivesPerVdev - 1) * smallestInVdev; break; case 'raidz2': // RAIDZ2: (n-2) * smallest drive vdevCapacity = (drivesPerVdev - 2) * smallestInVdev; break; case 'raidz3': // RAIDZ3: (n-3) * smallest drive vdevCapacity = (drivesPerVdev - 3) * smallestInVdev; break; } totalUsableCapacity += vdevCapacity; } return { usableCapacity: totalUsableCapacity, rawCapacity, efficiency: totalUsableCapacity / rawCapacity, wastedSpace: rawCapacity - totalUsableCapacity }; } /** * Format capacity for display */ function formatCapacity(tib, showBoth = true) { const tb = (tib * TB_TO_BYTES_BINARY) / TB_TO_BYTES_DECIMAL; if (showBoth) { return `${tib.toFixed(2)} TiB (${tb.toFixed(2)} TB)`; } return `${tib.toFixed(2)} TiB`; } /** * Format percentage */ function formatPercentage(value) { return `${(value * 100).toFixed(1)}%`; } // =================================== // UI Interaction Functions // =================================== class RAIDCalculator { constructor() { this.initializeElements(); this.attachEventListeners(); this.updateDriveDisplay(); this.handleRaidTypeChange(); } initializeElements() { this.raidTypeSelect = document.getElementById('raid-type'); this.numDrivesSlider = document.getElementById('num-drives'); this.numDrivesDisplay = document.getElementById('num-drives-display'); this.driveSizeSelect = document.getElementById('drive-size'); this.mixedDrivesToggle = document.getElementById('mixed-drives'); this.customDrivesContainer = document.getElementById('custom-drives-container'); this.driveInputsContainer = document.getElementById('drive-inputs'); this.calculateBtn = document.getElementById('calculate-btn'); this.resultsContent = document.getElementById('results-content'); // ZFS vdev elements this.zfsVdevConfig = document.getElementById('zfs-vdev-config'); this.numVdevsSlider = document.getElementById('num-vdevs'); this.numVdevsDisplay = document.getElementById('num-vdevs-display'); this.drivesPerVdevConfig = document.getElementById('drives-per-vdev-config'); this.drivesPerVdevSlider = document.getElementById('drives-per-vdev'); this.drivesPerVdevDisplay = document.getElementById('drives-per-vdev-display'); this.totalDrivesText = document.getElementById('total-drives-text'); } attachEventListeners() { this.numDrivesSlider.addEventListener('input', () => this.updateDriveDisplay()); this.mixedDrivesToggle.addEventListener('change', () => this.toggleMixedDrives()); this.calculateBtn.addEventListener('click', () => this.calculate()); this.raidTypeSelect.addEventListener('change', () => this.handleRaidTypeChange()); // ZFS vdev listeners this.numVdevsSlider.addEventListener('input', () => this.updateVdevDisplay()); this.drivesPerVdevSlider.addEventListener('input', () => this.updateVdevDisplay()); } handleRaidTypeChange() { const raidType = this.raidTypeSelect.value; const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType); // Show/hide ZFS vdev configuration this.zfsVdevConfig.style.display = isZFSVdev ? 'block' : 'none'; this.drivesPerVdevConfig.style.display = isZFSVdev ? 'block' : 'none'; // Show/hide regular drive count slider const numDrivesGroup = this.numDrivesSlider.closest('.form-group'); numDrivesGroup.style.display = isZFSVdev ? 'none' : 'block'; if (isZFSVdev) { this.updateVdevDisplay(); } else { this.validateConfiguration(); } } updateVdevDisplay() { const numVdevs = parseInt(this.numVdevsSlider.value); const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value); const totalDrives = numVdevs * drivesPerVdev; this.numVdevsDisplay.textContent = numVdevs; this.drivesPerVdevDisplay.textContent = drivesPerVdev; this.totalDrivesText.textContent = `Total drives: ${totalDrives}`; // Update the main drive slider to match (for mixed drives feature) this.numDrivesSlider.value = totalDrives; this.numDrivesDisplay.textContent = totalDrives; if (this.mixedDrivesToggle.checked) { this.generateDriveInputs(totalDrives); } this.validateConfiguration(); } updateDriveDisplay() { const numDrives = parseInt(this.numDrivesSlider.value); this.numDrivesDisplay.textContent = numDrives; if (this.mixedDrivesToggle.checked) { this.generateDriveInputs(numDrives); } this.validateConfiguration(); } toggleMixedDrives() { const isEnabled = this.mixedDrivesToggle.checked; this.customDrivesContainer.style.display = isEnabled ? 'block' : 'none'; this.driveSizeSelect.disabled = isEnabled; if (isEnabled) { const raidType = this.raidTypeSelect.value; const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType); const numDrives = isZFSVdev ? parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) : parseInt(this.numDrivesSlider.value); this.generateDriveInputs(numDrives); } } generateDriveInputs(numDrives) { this.driveInputsContainer.innerHTML = ''; const driveSizes = [1, 2, 3, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24]; for (let i = 0; i < numDrives; i++) { const row = document.createElement('div'); row.className = 'drive-input-row'; const label = document.createElement('span'); label.className = 'drive-label'; label.textContent = `Drive ${i + 1}:`; const select = document.createElement('select'); select.className = 'drive-select'; select.id = `drive-${i}`; driveSizes.forEach(size => { const option = document.createElement('option'); option.value = size; option.textContent = `${size} TB`; if (size === 12) option.selected = true; select.appendChild(option); }); row.appendChild(label); row.appendChild(select); this.driveInputsContainer.appendChild(row); } } validateConfiguration() { const raidType = this.raidTypeSelect.value; const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType); const numDrives = isZFSVdev ? parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) : parseInt(this.numDrivesSlider.value); const config = RAID_CONFIGS[raidType]; if (numDrives < config.minDrives) { this.calculateBtn.disabled = true; this.calculateBtn.textContent = `Requires ${config.minDrives}+ drives`; } else if (raidType === 'raid10' && numDrives % 2 !== 0) { this.calculateBtn.disabled = true; this.calculateBtn.textContent = 'RAID 10 requires even number of drives'; } else { this.calculateBtn.disabled = false; this.calculateBtn.innerHTML = ` Calculate Storage `; } } getDriveSizes() { const raidType = this.raidTypeSelect.value; const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType); const numDrives = isZFSVdev ? parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) : parseInt(this.numDrivesSlider.value); if (this.mixedDrivesToggle.checked) { const drives = []; for (let i = 0; i < numDrives; i++) { const select = document.getElementById(`drive-${i}`); drives.push(parseFloat(select.value)); } return drives; } else { const driveSize = parseFloat(this.driveSizeSelect.value); return Array(numDrives).fill(driveSize); } } calculate() { try { const raidType = this.raidTypeSelect.value; const drives = this.getDriveSizes(); const config = RAID_CONFIGS[raidType]; // Check if this is a ZFS vdev configuration const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType); let result; if (isZFSVdev) { const numVdevs = parseInt(this.numVdevsSlider.value); const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value); result = calculateZFSVdevStorage(raidType, drives, numVdevs, drivesPerVdev); result.numVdevs = numVdevs; result.drivesPerVdev = drivesPerVdev; } else { result = calculateRAIDStorage(raidType, drives); } this.displayResults(raidType, config, drives, result); } catch (error) { this.displayError(error.message); } } displayResults(raidType, config, drives, result) { const { usableCapacity, rawCapacity, efficiency, wastedSpace } = result; const html = `
${message}