isomorphic-git / isomorphic-git/lightning-fs

Potential for significant perf improvements in large repos

Open
#53 9 comments 4 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
610
Forks
62
Avg merge
14h 47m
Merged PRs (30d)
1

Description

First, thanks for your work on this project 🙂

The current implementation is fairly slow with large repos, for instance vscode, which has around 5000 files or typescript, which has around 50k. It takes about a minute to clone vscode with `--singleBranch` and `--depth 1`, and doesn't manage to clone typescript in the ~15 minutes I waited.

By adding batching to the indexdb writes (Put all writes into a single transaction rather than one transaction per file) and changing the autoinc in the cachefs to increment a counter rather than search for the highest inode (the search means writing N files is N^2 time), I am able to see vscode clone in ~20 seconds and typescript clone in about 2 minutes. this is approx 3x slower than native for vscode and 6x slower than native for typescript.

Batching:
```diff
diff --git a/idb-keyval.ts b/idb-keyval.ts
index 45a0d97..94920ef 100644
--- a/idb-keyval.ts
+++ b/idb-keyval.ts
@@ -2,10 +2,12 @@ export class Store {
private _dbp: Promise | undefined;
readonly _dbName: string;
readonly _storeName: string;
+ readonly id: string

constructor(dbName = 'keyval-store', readonly storeName = 'keyval') {
this._dbName = dbName;
this._storeName = storeName;
+ this.id = `dbName:${dbName};;storeName:${storeName}`
this._init();
}

@@ -44,6 +46,31 @@ export class Store {
}
}

+class Batcher {
+ private ongoing: Promise | undefined
+ private items: { item: T, onProcessed: () => void }[] = []
+
+ constructor(private executor: (items: T[]) => Promise) { }
+
+ private async process() {
+ const toProcess = this.items;
+ this.items = [];
+ await this.executor(toProcess.map(({ item }) => item))
+ toProcess.map(({ onProcessed }) => onProcessed())
+ if (this.items.length) {
+ this.ongoing = this.process()
+ } else {
+ this.ongoing = undefined
+ }
+ }
+
+ async queue(item: T): Promise {
+ const result = new Promise((resolve) => this.items.push({ item, onProcessed: resolve }))
+ if (!this.ongoing) this.ongoing = this.process()
+ return result
+ }
+}
+
let store: Store;

function getDefaultStore() {
@@ -58,10 +85,17 @@ export function get(key: IDBValidKey, store = getDefaultStore()): Promise<
}).then(() => req.result);
}

+const setBatchers: Record> = {}
export function set(key: IDBValidKey, value: any, store = getDefaultStore()): Promise {
- return store._withIDBStore('readwrite', store => {
- store.put(value, key);
- });
+ if (!setBatchers[store.id]) {
+ setBatchers[store.id] = new Batcher((items) =>
+ store._withIDBStore('readwrite', store => {
+ for (const item of items) {
+ store.put(item.value, item.key)
+ }
+ }))
+ }
+ return setBatchers[store.id].queue({ key, value })
}

export function update(key: IDBValidKey, updater: (val: any) => any, store = getDefaultStore()): Promise {

```

Counter:
```diff
diff --git a/src/CacheFS.js b/src/CacheFS.js
index ed26c57..0dc6950 100755
--- a/src/CacheFS.js
+++ b/src/CacheFS.js
@@ -5,6 +5,7 @@ const STAT = 0;

module.exports = class CacheFS {
constructor() {
+ this._maxInode = 0
}
_makeRoot(root = new Map()) {
root.set(STAT, { mode: 0o777, type: "dir", size: 0, ino: 0, mtimeMs: Date.now() });
@@ -38,16 +39,7 @@ module.exports = class CacheFS {
return count;
}
autoinc () {
- let val = this._maxInode(this._root.get("/")) + 1;
- return val;
- }
- _maxInode(map) {
- let max = map.get(STAT).ino;
- for (let [key, val] of map) {
- if (key === STAT) continue;
- max = Math.max(max, this._maxInode(val));
- }
- return max;
+ return ++this._maxInode;
}
print(root = this._root.get("/")) {
let str = "";
```

Please let me know if you'd consider incorporating these changes... the batching should be safe, I'm not super sure about the autoinc, but I don't see a reason why it would cause issues (the main difference is deleting a file would free up its inode value in the original implementation but doesn't here, but that shouldn't be a problem AFAIK)

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.