import * as fs from "fs"; import * as proxyquire from "proxyquire"; import * as obsidianMocks from "./mock-obsidian"; import * as os from "os"; import * as path from "path"; import * as crypto from "crypto"; const proxyquireNonStrict = proxyquire.noCallThru(); const LoggerModule = proxyquireNonStrict("./src/logger", { obsidian: obsidianMocks, }); const GithubClientModule = proxyquireNonStrict("./src/github/client", { obsidian: obsidianMocks, }); const MetadataStoreModule = proxyquireNonStrict("./src/metadata-store", { obsidian: obsidianMocks, }); const EventsListenerModule = proxyquireNonStrict("./src/events-listener", { obsidian: obsidianMocks, "./metadata-store": MetadataStoreModule, }); const UtilsModule = proxyquireNonStrict("./src/utils", { obsidian: obsidianMocks, }); const SyncManagerModule = proxyquireNonStrict("./src/sync-manager", { obsidian: obsidianMocks, "./metadata-store": MetadataStoreModule, "./events-listener": EventsListenerModule, "./github/client": GithubClientModule, "./utils": UtilsModule, }); async function runBenchmark(vaultRootDir: string) { const vault = new obsidianMocks.Vault(vaultRootDir); console.log("Vault config dir:", vault.configDir); // Create a real logger with our mock vault const logger = new LoggerModule.default(vault, false); // Settings for the sync manager const settings = { githubToken: process.env.GITHUB_TOKEN, githubOwner: process.env.REPO_OWNER, githubRepo: process.env.REPO_NAME, githubBranch: process.env.REPO_BRANCH, syncConfigDir: false, }; // We're not going to get any conflicts, this is useless const onConflicts = async () => { return []; }; // Create the sync manager const SyncManager = SyncManagerModule.default; const syncManager = new SyncManager(vault, settings, onConflicts, logger); await syncManager.loadMetadata(); const startTime = performance.now(); await syncManager.firstSync(); return performance.now() - startTime; } const generateRandomFiles = ( rootPath: string, numFiles: number, maxDepth: number, maxTotalSize: number, ) => { // Create root directory if it doesn't exist if (!fs.existsSync(rootPath)) { fs.mkdirSync(rootPath, { recursive: true }); } // Generate folder structure first const allFolderPaths = [rootPath]; for (let depth = 1; depth <= maxDepth; depth++) { const numFoldersAtThisDepth = Math.floor(Math.random() * 3) + 1; // 1-3 folders per level for (let i = 0; i < numFoldersAtThisDepth; i++) { const parentPath = allFolderPaths[Math.floor(Math.random() * allFolderPaths.length)]; const currentDepthOfParent = parentPath.split(path.sep).length - rootPath.split(path.sep).length; // Only create subfolders if we haven't reached max depth for this path if (currentDepthOfParent < maxDepth) { const folderName = crypto.randomBytes(5).toString("hex"); const newFolderPath = path.join(parentPath, folderName); fs.mkdirSync(newFolderPath, { recursive: true }); allFolderPaths.push(newFolderPath); } } } // Now generate files let totalSize = 0; // Calculate approximate size per file to ensure we create all files const targetSizePerFile = Math.floor(maxTotalSize / numFiles); for (let i = 0; i < numFiles; i++) { // Pick a random folder to place the file in const targetFolder = allFolderPaths[Math.floor(Math.random() * allFolderPaths.length)]; // Generate random file name const fileName = crypto.randomBytes(8).toString("hex") + ".md"; const filePath = path.join(targetFolder, fileName); // Calculate remaining size and files const remainingSize = maxTotalSize - totalSize; const remainingFiles = numFiles - i; if (remainingSize <= 0) { // If we've hit the max size but still need files, create empty files fs.writeFileSync(filePath, ""); } else { // Calculate appropriate file size to ensure we can create all files const maxPossibleSize = Math.floor(remainingSize / remainingFiles); const contentSize = Math.floor( Math.random() * Math.min(maxPossibleSize, targetSizePerFile), ) + 1; // Generate random content const content = crypto.randomBytes(contentSize).toString("hex"); // Write file fs.writeFileSync(filePath, content); totalSize += contentSize; } } return totalSize; }; const cleanupRemote = async () => { const owner = process.env.REPO_OWNER; const repo = process.env.REPO_NAME; const branch = process.env.REPO_BRANCH; const headers = { Accept: "application/vnd.github+json", Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, "X-GitHub-Api-Version": "2022-11-28", }; try { // Get the manifest SHA const contentResponse = await obsidianMocks.requestUrl({ url: `https://api.github.com/repos/${owner}/${repo}/contents/.obsidian/github-sync-metadata.json?ref=${branch}`, headers: headers, }); // Delete manifest await obsidianMocks.requestUrl({ url: `https://api.github.com/repos/${owner}/${repo}/contents/.obsidian/github-sync-metadata.json`, method: "DELETE", headers: headers, body: JSON.stringify({ message: "Cleanup", sha: contentResponse.json.sha, branch: branch, }), }); console.log("Remote repository cleaned up successfully"); } catch (error) { console.error("Failed to clean up remote repository:", error); } }; const BENCHMARK_DATA = [ { files: 1, maxDepth: 1, maxSize: 7000, }, ]; (async () => { const tmp = os.tmpdir(); const benchmarkRootDir = path.join(tmp, "github-gitless-sync-benchmark"); try { const results = []; for (const data of BENCHMARK_DATA) { const vaultRootDir = path.join( benchmarkRootDir, `${data.files}-${data.maxDepth}-${data.maxSize}`, ); // Generates random files generateRandomFiles( vaultRootDir, data.files, data.maxDepth, data.maxSize, ); // Run first sync by uploading all local files const uploadTime = await runBenchmark(vaultRootDir); // Cleanup vault dir completely fs.rmSync(vaultRootDir, { recursive: true, force: true }); // Run first sync again, this time we download the files we just uploaded const downloadTime = await runBenchmark(vaultRootDir); // Cleanup the remote repo so it's ready for another benchmark await cleanupRemote(); results.push({ data, uploadTime, downloadTime, }); } fs.writeFileSync("benchmark_result.json", JSON.stringify(results), { flag: "w", }); } catch (error) { console.error("Benchmark failed:", error); } fs.rm(benchmarkRootDir, { recursive: true, force: true }, (err) => { if (err) { throw err; } }); })();