diff --git a/example/.gitignore b/example/.gitignore index de99955..29a2b90 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -73,3 +73,8 @@ yarn-error.log !.yarn/releases !.yarn/sdks !.yarn/versions + +# Expo +.expo +dist/ +web-build/ \ No newline at end of file diff --git a/example/App.tsx b/example/App.tsx index 60430bc..e6a9f74 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,6 +1,7 @@ -import React from 'react'; -import { StatusBar, StyleSheet } from 'react-native'; +import React, { useState } from 'react'; +import { StatusBar, StyleSheet, View, Text, TouchableOpacity } from 'react-native'; import { ActionPanel } from './src/components/action-panel'; +import { BenchmarkPage } from './src/components/benchmark-page'; import { DirectoryNavigation } from './src/components/directory-navigation'; import { FileEditor } from './src/components/file-editor'; import { FileList } from './src/components/file-list'; @@ -10,7 +11,10 @@ import { ProgressIndicator } from './src/components/progress-indicator'; import { useFileSystem } from './src/hooks/use-file-system'; import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; +type Tab = 'explorer' | 'benchmark'; + const AppContent = () => { + const [activeTab, setActiveTab] = useState('explorer'); const { currentPath, files, @@ -44,50 +48,75 @@ const AppContent = () => {
- + + setActiveTab('explorer')} + > + + Explorer + + + setActiveTab('benchmark')} + > + + Benchmark + + + + + {activeTab === 'explorer' ? ( + <> + - + - + - + - + - + + + ) : ( + + )} ); }; @@ -105,6 +134,36 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#f8f9fa', }, + tabBar: { + flexDirection: 'row', + marginHorizontal: 12, + marginVertical: 8, + backgroundColor: '#e9ecef', + borderRadius: 10, + padding: 3, + }, + tab: { + flex: 1, + paddingVertical: 8, + alignItems: 'center', + borderRadius: 8, + }, + tabActive: { + backgroundColor: '#fff', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + }, + tabText: { + fontSize: 14, + fontWeight: '600', + color: '#6c757d', + }, + tabTextActive: { + color: '#007AFF', + }, }); export default App; diff --git a/example/babel.config.js b/example/babel.config.js index eaddc88..762c0f0 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -4,7 +4,7 @@ const pak = require('../package.json'); module.exports = api => { api.cache(true); return { - presets: ['module:@react-native/babel-preset'], + presets: ['babel-preset-expo'], plugins: [ [ 'module-resolver', diff --git a/example/metro.config.js b/example/metro.config.js index d268384..783a519 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -1,4 +1,5 @@ -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const { getDefaultConfig } = require('expo/metro-config'); +const { mergeConfig } = require('@react-native/metro-config'); const path = require('path'); const root = path.resolve(__dirname, '..'); diff --git a/example/package.json b/example/package.json index 7bd3525..d0dd07e 100644 --- a/example/package.json +++ b/example/package.json @@ -11,6 +11,9 @@ "pod": "bundle install && bundle exec pod install --project-directory=ios" }, "dependencies": { + "@dr.pogodin/react-native-fs": "^2.37.0", + "expo": "~54.0.0", + "expo-file-system": "~19.0.21", "react": "19.1.0", "react-native": "0.81.5", "react-native-nitro-document-picker": "^1.2.0", @@ -32,6 +35,7 @@ "@types/react": "^19.1.0", "@types/react-test-renderer": "^19.1.0", "babel-plugin-module-resolver": "^5.0.2", + "babel-preset-expo": "~54.0.10", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", diff --git a/example/src/components/benchmark-page.tsx b/example/src/components/benchmark-page.tsx new file mode 100644 index 0000000..55e39c5 --- /dev/null +++ b/example/src/components/benchmark-page.tsx @@ -0,0 +1,350 @@ +import React, { useState, useCallback } from 'react'; +import { + View, + Text, + ScrollView, + TouchableOpacity, + StyleSheet, + ActivityIndicator, +} from 'react-native'; +import { measure, type BenchmarkResult, type MeasureResult } from '../utils/benchmark-runner'; +import { benchmarkTests } from '../utils/benchmark-tests'; + +type ResultsMap = Record; + +export const BenchmarkPage = () => { + const [results, setResults] = useState({}); + const [runningId, setRunningId] = useState(null); + const [runningAll, setRunningAll] = useState(false); + + const runSingle = useCallback(async (testId: string) => { + const test = benchmarkTests.find(t => t.id === testId); + if (!test) return; + + setRunningId(testId); + + const result: BenchmarkResult = { nitro: null, expo: null, rnfs: null }; + + try { + if (test.setup) await test.setup(); + result.nitro = await measure(test.nitro, test.iterations); + if (test.teardown) await test.teardown(); + + if (test.setup) await test.setup(); + result.expo = await measure(test.expo, test.iterations); + if (test.teardown) await test.teardown(); + + if (test.setup) await test.setup(); + result.rnfs = await measure(test.rnfs, test.iterations); + if (test.teardown) await test.teardown(); + } catch (error) { + console.error(`Benchmark "${test.name}" failed:`, error); + } + + setResults(prev => ({ ...prev, [testId]: result })); + setRunningId(null); + }, []); + + const runAll = useCallback(async () => { + setRunningAll(true); + setResults({}); + + for (const test of benchmarkTests) { + setRunningId(test.id); + const result: BenchmarkResult = { nitro: null, expo: null, rnfs: null }; + + try { + if (test.setup) await test.setup(); + result.nitro = await measure(test.nitro, test.iterations); + if (test.teardown) await test.teardown(); + + if (test.setup) await test.setup(); + result.expo = await measure(test.expo, test.iterations); + if (test.teardown) await test.teardown(); + + if (test.setup) await test.setup(); + result.rnfs = await measure(test.rnfs, test.iterations); + if (test.teardown) await test.teardown(); + } catch (error) { + console.error(`Benchmark "${test.name}" failed:`, error); + } + + setResults(prev => ({ ...prev, [test.id]: result })); + } + + setRunningId(null); + setRunningAll(false); + }, []); + + const formatMs = (ms: number | undefined) => { + if (ms === undefined || ms === null) return '-'; + if (ms < 1) return `${(ms * 1000).toFixed(0)}us`; + if (ms < 100) return `${ms.toFixed(2)}ms`; + return `${ms.toFixed(0)}ms`; + }; + + const getSpeedup = (r: BenchmarkResult, lib: 'nitro' | 'expo' | 'rnfs'): string | null => { + const avgs: number[] = []; + if (r.nitro) avgs.push(r.nitro.avg); + if (r.expo) avgs.push(r.expo.avg); + if (r.rnfs) avgs.push(r.rnfs.avg); + if (avgs.length < 2) return null; + + const thisAvg = r[lib]?.avg; + if (!thisAvg) return null; + + const slowest = Math.max(...avgs); + if (thisAvg >= slowest) return null; + + const ratio = slowest / thisAvg; + if (ratio < 1.05) return null; + return `${ratio.toFixed(1)}x`; + }; + + const getFastestLib = (r: BenchmarkResult): 'nitro' | 'expo' | 'rnfs' | null => { + const vals: { lib: 'nitro' | 'expo' | 'rnfs'; avg: number }[] = []; + if (r.nitro) vals.push({ lib: 'nitro', avg: r.nitro.avg }); + if (r.expo) vals.push({ lib: 'expo', avg: r.expo.avg }); + if (r.rnfs) vals.push({ lib: 'rnfs', avg: r.rnfs.avg }); + if (vals.length === 0) return null; + vals.sort((a, b) => a.avg - b.avg); + return vals[0].lib; + }; + + const renderCell = ( + result: MeasureResult | null, + fastest: 'nitro' | 'expo' | 'rnfs' | null, + lib: 'nitro' | 'expo' | 'rnfs', + benchmarkResult: BenchmarkResult | undefined, + ) => { + const isFastest = fastest === lib; + const speedup = benchmarkResult ? getSpeedup(benchmarkResult, lib) : null; + return ( + + + {result ? formatMs(result.avg) : '-'} + + {speedup && ( + + {speedup} + + )} + + ); + }; + + return ( + + + FS Benchmarks + {benchmarkTests[0]?.iterations} iterations avg + + + + {runningAll ? ( + + ) : ( + Run All Benchmarks + )} + + + {/* Table header */} + + + Test + + + NitroFS + + + ExpoFS + + + RNFS + + + + + + + + {benchmarkTests.map(test => { + const r = results[test.id]; + const fastest = r ? getFastestLib(r) : null; + const isRunning = runningId === test.id; + + return ( + + + + {test.name} + + + + {isRunning ? ( + + + Running... + + ) : ( + <> + {renderCell(r?.nitro ?? null, fastest, 'nitro', r)} + {renderCell(r?.expo ?? null, fastest, 'expo', r)} + {renderCell(r?.rnfs ?? null, fastest, 'rnfs', r)} + + )} + + runSingle(test.id)} + disabled={runningAll || runningId !== null} + > + Run + + + ); + })} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + }, + title: { + fontSize: 18, + fontWeight: '700', + color: '#1a1a1a', + }, + subtitle: { + fontSize: 13, + color: '#666', + }, + runAllButton: { + backgroundColor: '#007AFF', + marginHorizontal: 16, + paddingVertical: 12, + borderRadius: 10, + alignItems: 'center', + marginBottom: 12, + }, + runAllButtonDisabled: { + opacity: 0.6, + }, + runAllText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + tableHeader: { + flexDirection: 'row', + paddingHorizontal: 8, + paddingVertical: 8, + backgroundColor: '#e9ecef', + marginHorizontal: 8, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + }, + headerText: { + fontSize: 11, + fontWeight: '700', + color: '#495057', + textAlign: 'center', + }, + scrollView: { + flex: 1, + marginHorizontal: 8, + }, + scrollContent: { + paddingBottom: 20, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#dee2e6', + backgroundColor: '#fff', + }, + testNameHeader: { + flex: 1.2, + }, + testNameCell: { + flex: 1.2, + }, + testName: { + fontSize: 12, + color: '#1a1a1a', + fontWeight: '500', + }, + cell: { + flex: 1, + alignItems: 'center', + }, + cellText: { + fontSize: 12, + color: '#495057', + fontFamily: 'Menlo', + }, + speedupText: { + fontSize: 9, + color: '#6c757d', + fontWeight: '600', + marginTop: 1, + }, + speedupTextFastest: { + color: '#155724', + }, + fastestCell: { + backgroundColor: '#d4edda', + borderRadius: 6, + paddingVertical: 2, + paddingHorizontal: 4, + }, + fastestText: { + color: '#155724', + fontWeight: '700', + }, + actionCell: { + width: 44, + }, + runButton: { + width: 44, + paddingVertical: 4, + paddingHorizontal: 6, + backgroundColor: '#e9ecef', + borderRadius: 6, + alignItems: 'center', + }, + runButtonText: { + fontSize: 11, + fontWeight: '600', + color: '#495057', + }, + runningRow: { + flex: 3, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + runningText: { + fontSize: 12, + color: '#007AFF', + }, +}); diff --git a/example/src/utils/benchmark-runner.ts b/example/src/utils/benchmark-runner.ts new file mode 100644 index 0000000..66e41b5 --- /dev/null +++ b/example/src/utils/benchmark-runner.ts @@ -0,0 +1,40 @@ +export interface MeasureResult { + avg: number; + min: number; + max: number; + times: number[]; +} + +export async function measure( + fn: () => Promise, + iterations: number, +): Promise { + const times: number[] = []; + for (let i = 0; i < iterations; i++) { + const start = performance.now(); + await fn(); + const elapsed = performance.now() - start; + times.push(elapsed); + } + const avg = times.reduce((a, b) => a + b, 0) / times.length; + const min = Math.min(...times); + const max = Math.max(...times); + return { avg, min, max, times }; +} + +export interface BenchmarkResult { + nitro: MeasureResult | null; + expo: MeasureResult | null; + rnfs: MeasureResult | null; +} + +export interface BenchmarkTest { + id: string; + name: string; + iterations: number; + setup?: () => Promise; + teardown?: () => Promise; + nitro: () => Promise; + expo: () => Promise; + rnfs: () => Promise; +} diff --git a/example/src/utils/benchmark-tests.ts b/example/src/utils/benchmark-tests.ts new file mode 100644 index 0000000..2f402ef --- /dev/null +++ b/example/src/utils/benchmark-tests.ts @@ -0,0 +1,514 @@ +import NitroFS from 'react-native-nitro-fs'; +import { File, Directory, Paths } from 'expo-file-system'; +import { + CachesDirectoryPath, + writeFile as rnfsWriteFile, + readFile as rnfsReadFile, + exists as rnfsExists, + mkdir as rnfsMkdir, + unlink as rnfsUnlink, + stat as rnfsStat, + copyFile as rnfsCopyFile, + moveFile as rnfsMoveFile, + readDir as rnfsReadDir, +} from '@dr.pogodin/react-native-fs'; +import type { BenchmarkTest } from './benchmark-runner'; + +const ITERATIONS = 50; + +function generateData(sizeBytes: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < sizeBytes; i++) { + result += chars.charAt(i % chars.length); + } + return result; +} + +const SMALL_DATA = generateData(1024); // 1KB +const MEDIUM_DATA = generateData(100 * 1024); // 100KB +const LARGE_DATA = generateData(1024 * 1024); // 1MB + +const BASE64_DATA = 'SGVsbG8gV29ybGQhIFRoaXMgaXMgYSBiYXNlNjQgYmVuY2htYXJrIHRlc3Qu'; + +// Workspace paths +const nitroWorkspace = () => `${NitroFS.CACHE_DIR}/benchmark_workspace`; +const expoWorkspace = () => new Directory(Paths.cache, 'benchmark_workspace_expo'); +const rnfsWorkspaceDir = () => `${CachesDirectoryPath}/benchmark_workspace_rnfs`; + +async function ensureWorkspaces() { + const nitroDir = nitroWorkspace(); + if (!(await NitroFS.exists(nitroDir))) { + await NitroFS.mkdir(nitroDir); + } + const expoDir = expoWorkspace(); + if (!expoDir.exists) { + expoDir.create({ intermediates: true }); + } + const rnfsDir = rnfsWorkspaceDir(); + if (!(await rnfsExists(rnfsDir))) { + await rnfsMkdir(rnfsDir); + } +} + +async function cleanWorkspaces() { + try { await NitroFS.unlink(nitroWorkspace()); } catch {} + try { const d = expoWorkspace(); if (d.exists) { d.delete(); } } catch {} + try { await rnfsUnlink(rnfsWorkspaceDir()); } catch {} +} + +let counter = 0; +function uniqueName(prefix: string, ext: string = 'txt') { + return `${prefix}_${Date.now()}_${counter++}.${ext}`; +} + +function createWriteTest(label: string, data: string, iterations: number): BenchmarkTest { + return { + id: `write_${label}`, + name: `Write ${label}`, + iterations, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const path = `${nitroWorkspace()}/${uniqueName('write')}`; + await NitroFS.writeFile(path, data, 'utf8'); + }, + expo: async () => { + const file = new File(expoWorkspace(), uniqueName('write')); + file.create(); + file.write(data); + }, + rnfs: async () => { + const path = `${rnfsWorkspaceDir()}/${uniqueName('write')}`; + await rnfsWriteFile(path, data, 'utf8'); + }, + }; +} + +function createReadTest(label: string, data: string, iterations: number): BenchmarkTest { + const nitroPath = () => `${nitroWorkspace()}/read_fixture_${label}.txt`; + const expoFile = () => new File(expoWorkspace(), `read_fixture_${label}.txt`); + const rnfsPath = () => `${rnfsWorkspaceDir()}/read_fixture_${label}.txt`; + + return { + id: `read_${label}`, + name: `Read ${label}`, + iterations, + setup: async () => { + await ensureWorkspaces(); + await NitroFS.writeFile(nitroPath(), data, 'utf8'); + const ef = expoFile(); + ef.create({ overwrite: true }); + ef.write(data); + await rnfsWriteFile(rnfsPath(), data, 'utf8'); + }, + teardown: cleanWorkspaces, + nitro: async () => { + await NitroFS.readFile(nitroPath(), 'utf8'); + }, + expo: async () => { + await expoFile().text(); + }, + rnfs: async () => { + await rnfsReadFile(rnfsPath(), 'utf8'); + }, + }; +} + +export const benchmarkTests: BenchmarkTest[] = [ + // Write tests + createWriteTest('1KB', SMALL_DATA, ITERATIONS), + createWriteTest('100KB', MEDIUM_DATA, ITERATIONS), + createWriteTest('1MB', LARGE_DATA, ITERATIONS), + + // Read tests + createReadTest('1KB', SMALL_DATA, ITERATIONS), + createReadTest('100KB', MEDIUM_DATA, ITERATIONS), + createReadTest('1MB', LARGE_DATA, ITERATIONS), + + // File exists + { + id: 'exists', + name: 'File Exists', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + await NitroFS.writeFile(`${nitroWorkspace()}/exists_test.txt`, 'test', 'utf8'); + const ef = new File(expoWorkspace(), 'exists_test.txt'); + ef.create({ overwrite: true }); + ef.write('test'); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/exists_test.txt`, 'test', 'utf8'); + }, + teardown: cleanWorkspaces, + nitro: async () => { + await NitroFS.exists(`${nitroWorkspace()}/exists_test.txt`); + }, + expo: async () => { + new File(expoWorkspace(), 'exists_test.txt').exists; + }, + rnfs: async () => { + await rnfsExists(`${rnfsWorkspaceDir()}/exists_test.txt`); + }, + }, + + // File stat + { + id: 'stat', + name: 'File Stat', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + await NitroFS.writeFile(`${nitroWorkspace()}/stat_test.txt`, MEDIUM_DATA, 'utf8'); + const ef = new File(expoWorkspace(), 'stat_test.txt'); + ef.create({ overwrite: true }); + ef.write(MEDIUM_DATA); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/stat_test.txt`, MEDIUM_DATA, 'utf8'); + }, + teardown: cleanWorkspaces, + nitro: async () => { + await NitroFS.stat(`${nitroWorkspace()}/stat_test.txt`); + }, + expo: async () => { + new File(expoWorkspace(), 'stat_test.txt').info(); + }, + rnfs: async () => { + await rnfsStat(`${rnfsWorkspaceDir()}/stat_test.txt`); + }, + }, + + // Create + delete file + { + id: 'create_delete_file', + name: 'Create + Delete File', + iterations: ITERATIONS, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const path = `${nitroWorkspace()}/${uniqueName('cd')}`; + await NitroFS.writeFile(path, 'temp', 'utf8'); + await NitroFS.unlink(path); + }, + expo: async () => { + const file = new File(expoWorkspace(), uniqueName('cd')); + file.create(); + file.delete(); + }, + rnfs: async () => { + const path = `${rnfsWorkspaceDir()}/${uniqueName('cd')}`; + await rnfsWriteFile(path, 'temp', 'utf8'); + await rnfsUnlink(path); + }, + }, + + // Create + delete directory + { + id: 'create_delete_dir', + name: 'Create + Delete Dir', + iterations: ITERATIONS, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const path = `${nitroWorkspace()}/${uniqueName('dir')}`; + await NitroFS.mkdir(path); + await NitroFS.unlink(path); + }, + expo: async () => { + const dir = new Directory(expoWorkspace(), uniqueName('dir')); + dir.create(); + dir.delete(); + }, + rnfs: async () => { + const path = `${rnfsWorkspaceDir()}/${uniqueName('dir')}`; + await rnfsMkdir(path); + await rnfsUnlink(path); + }, + }, + + // Copy file (100KB) + { + id: 'copy_file', + name: 'Copy File (100KB)', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + await NitroFS.writeFile(`${nitroWorkspace()}/copy_src.txt`, MEDIUM_DATA, 'utf8'); + const ef = new File(expoWorkspace(), 'copy_src.txt'); + ef.create({ overwrite: true }); + ef.write(MEDIUM_DATA); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/copy_src.txt`, MEDIUM_DATA, 'utf8'); + }, + teardown: cleanWorkspaces, + nitro: async () => { + const dest = `${nitroWorkspace()}/${uniqueName('copy_dest')}`; + await NitroFS.copyFile(`${nitroWorkspace()}/copy_src.txt`, dest); + }, + expo: async () => { + const src = new File(expoWorkspace(), 'copy_src.txt'); + const dest = new File(expoWorkspace(), uniqueName('copy_dest')); + src.copy(dest); + }, + rnfs: async () => { + const dest = `${rnfsWorkspaceDir()}/${uniqueName('copy_dest')}`; + await rnfsCopyFile(`${rnfsWorkspaceDir()}/copy_src.txt`, dest); + }, + }, + + // Rename / move file + { + id: 'rename_file', + name: 'Rename File', + iterations: ITERATIONS, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const name = uniqueName('rename_src'); + const srcPath = `${nitroWorkspace()}/${name}`; + const destPath = `${nitroWorkspace()}/${uniqueName('rename_dest')}`; + await NitroFS.writeFile(srcPath, 'temp', 'utf8'); + await NitroFS.rename(srcPath, destPath); + }, + expo: async () => { + const srcName = uniqueName('rename_src'); + const file = new File(expoWorkspace(), srcName); + file.create(); + file.write('temp'); + file.move(new File(expoWorkspace(), uniqueName('rename_dest'))); + }, + rnfs: async () => { + const name = uniqueName('rename_src'); + const srcPath = `${rnfsWorkspaceDir()}/${name}`; + const destPath = `${rnfsWorkspaceDir()}/${uniqueName('rename_dest')}`; + await rnfsWriteFile(srcPath, 'temp', 'utf8'); + await rnfsMoveFile(srcPath, destPath); + }, + }, + + // Read directory + { + id: 'readdir', + name: 'Read Directory', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + for (let i = 0; i < 20; i++) { + await NitroFS.writeFile(`${nitroWorkspace()}/dir_file_${i}.txt`, 'content', 'utf8'); + const ef = new File(expoWorkspace(), `dir_file_${i}.txt`); + ef.create({ overwrite: true }); + ef.write('content'); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/dir_file_${i}.txt`, 'content', 'utf8'); + } + }, + teardown: cleanWorkspaces, + nitro: async () => { + await NitroFS.readdir(nitroWorkspace()); + }, + expo: async () => { + expoWorkspace().list(); + }, + rnfs: async () => { + await rnfsReadDir(rnfsWorkspaceDir()); + }, + }, + + // Base64 write + read + { + id: 'base64', + name: 'Base64 Write+Read', + iterations: ITERATIONS, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const path = `${nitroWorkspace()}/${uniqueName('b64')}.bin`; + await NitroFS.writeFile(path, BASE64_DATA, 'base64'); + await NitroFS.readFile(path, 'base64'); + }, + expo: async () => { + const file = new File(expoWorkspace(), `${uniqueName('b64')}.bin`); + file.create(); + file.write(BASE64_DATA); + await file.base64(); + }, + rnfs: async () => { + const path = `${rnfsWorkspaceDir()}/${uniqueName('b64')}.bin`; + await rnfsWriteFile(path, BASE64_DATA, 'base64'); + await rnfsReadFile(path, 'base64'); + }, + }, + + // === Parallel / Concurrent tests (Nitro's strength: thread pool vs RNFS serial queue) === + + // Parallel writes — 10 concurrent file writes + { + id: 'parallel_write', + name: 'Parallel Write x10', + iterations: ITERATIONS, + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + const promises = Array.from({ length: 10 }, (_, i) => { + const path = `${nitroWorkspace()}/${uniqueName(`pw_${i}`)}`; + return NitroFS.writeFile(path, SMALL_DATA, 'utf8'); + }); + await Promise.all(promises); + }, + expo: async () => { + for (let i = 0; i < 10; i++) { + const file = new File(expoWorkspace(), uniqueName(`pw_${i}`)); + file.create(); + file.write(SMALL_DATA); + } + }, + rnfs: async () => { + const promises = Array.from({ length: 10 }, (_, i) => { + const path = `${rnfsWorkspaceDir()}/${uniqueName(`pw_${i}`)}`; + return rnfsWriteFile(path, SMALL_DATA, 'utf8'); + }); + await Promise.all(promises); + }, + }, + + // Parallel reads — 10 concurrent file reads + { + id: 'parallel_read', + name: 'Parallel Read x10', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + for (let i = 0; i < 10; i++) { + await NitroFS.writeFile(`${nitroWorkspace()}/pr_${i}.txt`, MEDIUM_DATA, 'utf8'); + const ef = new File(expoWorkspace(), `pr_${i}.txt`); + ef.create({ overwrite: true }); + ef.write(MEDIUM_DATA); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/pr_${i}.txt`, MEDIUM_DATA, 'utf8'); + } + }, + teardown: cleanWorkspaces, + nitro: async () => { + const promises = Array.from({ length: 10 }, (_, i) => + NitroFS.readFile(`${nitroWorkspace()}/pr_${i}.txt`, 'utf8') + ); + await Promise.all(promises); + }, + expo: async () => { + const promises = Array.from({ length: 10 }, (_, i) => + new File(expoWorkspace(), `pr_${i}.txt`).text() + ); + await Promise.all(promises); + }, + rnfs: async () => { + const promises = Array.from({ length: 10 }, (_, i) => + rnfsReadFile(`${rnfsWorkspaceDir()}/pr_${i}.txt`, 'utf8') + ); + await Promise.all(promises); + }, + }, + + // Parallel mixed ops — write + stat + read + exists concurrently + { + id: 'parallel_mixed', + name: 'Parallel Mixed Ops', + iterations: ITERATIONS, + setup: async () => { + await ensureWorkspaces(); + await NitroFS.writeFile(`${nitroWorkspace()}/mixed_fixture.txt`, MEDIUM_DATA, 'utf8'); + const ef = new File(expoWorkspace(), 'mixed_fixture.txt'); + ef.create({ overwrite: true }); + ef.write(MEDIUM_DATA); + await rnfsWriteFile(`${rnfsWorkspaceDir()}/mixed_fixture.txt`, MEDIUM_DATA, 'utf8'); + }, + teardown: cleanWorkspaces, + nitro: async () => { + const ws = nitroWorkspace(); + await Promise.all([ + NitroFS.writeFile(`${ws}/${uniqueName('mix_w')}`, SMALL_DATA, 'utf8'), + NitroFS.stat(`${ws}/mixed_fixture.txt`), + NitroFS.readFile(`${ws}/mixed_fixture.txt`, 'utf8'), + NitroFS.exists(`${ws}/mixed_fixture.txt`), + NitroFS.writeFile(`${ws}/${uniqueName('mix_w2')}`, SMALL_DATA, 'utf8'), + ]); + }, + expo: async () => { + const ws = expoWorkspace(); + const fixture = new File(ws, 'mixed_fixture.txt'); + const f1 = new File(ws, uniqueName('mix_w')); + f1.create(); + f1.write(SMALL_DATA); + fixture.info(); + await fixture.text(); + fixture.exists; + const f2 = new File(ws, uniqueName('mix_w2')); + f2.create(); + f2.write(SMALL_DATA); + }, + rnfs: async () => { + const ws = rnfsWorkspaceDir(); + await Promise.all([ + rnfsWriteFile(`${ws}/${uniqueName('mix_w')}`, SMALL_DATA, 'utf8'), + rnfsStat(`${ws}/mixed_fixture.txt`), + rnfsReadFile(`${ws}/mixed_fixture.txt`, 'utf8'), + rnfsExists(`${ws}/mixed_fixture.txt`), + rnfsWriteFile(`${ws}/${uniqueName('mix_w2')}`, SMALL_DATA, 'utf8'), + ]); + }, + }, + + // Rapid sequential small writes — 50 tiny files in sequence + { + id: 'rapid_writes', + name: 'Rapid 50 Writes', + iterations: Math.floor(ITERATIONS / 5), + setup: ensureWorkspaces, + teardown: cleanWorkspaces, + nitro: async () => { + for (let i = 0; i < 50; i++) { + await NitroFS.writeFile(`${nitroWorkspace()}/${uniqueName('rw')}`, 'x', 'utf8'); + } + }, + expo: async () => { + for (let i = 0; i < 50; i++) { + const f = new File(expoWorkspace(), uniqueName('rw')); + f.create(); + f.write('x'); + } + }, + rnfs: async () => { + for (let i = 0; i < 50; i++) { + await rnfsWriteFile(`${rnfsWorkspaceDir()}/${uniqueName('rw')}`, 'x', 'utf8'); + } + }, + }, + + // Sync path ops (Nitro-exclusive: synchronous JSI calls, no async overhead) + { + id: 'sync_path_ops', + name: 'Sync Path Ops x100', + iterations: ITERATIONS, + setup: async () => {}, + teardown: async () => {}, + nitro: async () => { + const testPath = '/var/mobile/Containers/Data/Application/ABC123/Documents/photos/vacation/IMG_1234.jpg'; + for (let i = 0; i < 100; i++) { + NitroFS.dirname(testPath); + NitroFS.basename(testPath); + NitroFS.extname(testPath); + } + }, + expo: async () => { + const testPath = '/var/mobile/Containers/Data/Application/ABC123/Documents/photos/vacation/IMG_1234.jpg'; + for (let i = 0; i < 100; i++) { + Paths.dirname(testPath); + Paths.basename(testPath); + Paths.extname(testPath); + } + }, + rnfs: async () => { + // RNFS has no path utilities — use JS string operations as baseline + const testPath = '/var/mobile/Containers/Data/Application/ABC123/Documents/photos/vacation/IMG_1234.jpg'; + for (let i = 0; i < 100; i++) { + testPath.substring(0, testPath.lastIndexOf('/')); + testPath.substring(testPath.lastIndexOf('/') + 1); + testPath.substring(testPath.lastIndexOf('.')); + } + }, + }, +];