Skip to content

Commit

Permalink
refactor: add caching layer and improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
scolladon committed Nov 11, 2024
1 parent e523e24 commit 2b9dfef
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 7 deletions.
72 changes: 72 additions & 0 deletions __tests__/unit/lib/adapter/GitAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { describe, expect, it, jest } from '@jest/globals'
import { readFile } from 'fs-extra'

import { EOL } from 'os'
import GitAdapter from '../../../../src/adapter/GitAdapter'
import { IGNORE_WHITESPACE_PARAMS } from '../../../../src/constant/gitConstants'
import type { Config } from '../../../../src/types/config'
Expand Down Expand Up @@ -142,6 +143,23 @@ describe('GitAdapter', () => {
}
)
})
describe('when called multiple times with the same parameters', () => {
it('returns cached value', async () => {
// Arrange
const gitAdapter = GitAdapter.getInstance(config)
mockedCatFile.mockResolvedValue('blob' as never)

// Act
const result = await gitAdapter.pathExists('path')
const cachedResult = await gitAdapter.pathExists('path')

// Assert
expect(result).toBe(true)
expect(cachedResult).toStrictEqual(result)
expect(mockedCatFile).toBeCalledTimes(1)
expect(mockedCatFile).toBeCalledWith(['-t', `${config.to}:path`])
})
})
describe('when catFile throws', () => {
it('returns false', async () => {
// Arrange
Expand Down Expand Up @@ -256,6 +274,60 @@ describe('GitAdapter', () => {
config.source,
])
})

it('memoize call', async () => {
// Arrange
const gitAdapter = GitAdapter.getInstance(config)
const rawOutput = [
'path/from/file',
'path/to/file',
'path/to/another/file',
]
mockedRaw.mockResolvedValue(rawOutput.join(EOL) as never)

// Act
const result = await gitAdapter.getFilesPath(config.source)
const cachedResult = await gitAdapter.getFilesPath(config.source)

// Assert
expect(result).toEqual(rawOutput)
expect(cachedResult).toStrictEqual(result)
expect(mockedRaw).toBeCalledTimes(1)
expect(mockedRaw).toBeCalledWith([
'ls-tree',
'--name-only',
'-r',
config.to,
config.source,
])
})

it('memoize sub call', async () => {
// Arrange
const gitAdapter = GitAdapter.getInstance(config)
const rawOutput = [
'path/from/file',
'path/to/file',
'path/to/another/file',
]
mockedRaw.mockResolvedValue(rawOutput.join(EOL) as never)

// Act
const result = await gitAdapter.getFilesPath('path')
const subCachedResult = await gitAdapter.getFilesPath('path/to')

// Assert
expect(result).toEqual(rawOutput)
expect(subCachedResult).toEqual(rawOutput.slice(1))
expect(mockedRaw).toBeCalledTimes(1)
expect(mockedRaw).toBeCalledWith([
'ls-tree',
'--name-only',
'-r',
config.to,
'path',
])
})
})

describe('getFilesFrom', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/unit/lib/utils/fsHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ describe('pathExists', () => {
mockPathExists.mockImplementation(() => Promise.resolve(false))

// Act
const result = await pathExists('path', work.config)
const result = await pathExists('not/existing/path', work.config)

// Assert
expect(result).toBe(false)
Expand Down
48 changes: 42 additions & 6 deletions src/adapter/GitAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from 'path/posix'
import { readFile } from 'fs-extra'
import { SimpleGit, simpleGit } from 'simple-git'

import { UTF8_ENCODING } from '../constant/fsConstants'
import { PATH_SEP, UTF8_ENCODING } from '../constant/fsConstants'
import {
ADDITION,
BLOB_TYPE,
Expand All @@ -22,7 +22,6 @@ import { getLFSObjectContentPath, isLFS } from '../utils/gitLfsHelper'
const EOL = new RegExp(/\r?\n/)

const revPath = (pathDef: FileGitRef) => `${pathDef.oid}:${pathDef.path}`

export default class GitAdapter {
private static instances: Map<Config, GitAdapter> = new Map()

Expand All @@ -36,9 +35,13 @@ export default class GitAdapter {
}

protected readonly simpleGit: SimpleGit
protected readonly getFilesPathCache: Map<string, Set<string>>
protected readonly pathExistsCache: Map<string, boolean>

private constructor(protected readonly config: Config) {
this.simpleGit = simpleGit({ baseDir: config.repo, trimmed: true })
this.getFilesPathCache = new Map<string, Set<string>>()
this.pathExistsCache = new Map<string, boolean>()
}

public async configureRepository() {
Expand All @@ -50,16 +53,27 @@ export default class GitAdapter {
return await this.simpleGit.revparse([ref])
}

public async pathExists(path: string) {
protected async pathExistsImpl(path: string) {
let doesPathExists = false
try {
const type = await this.simpleGit.catFile([
'-t',
revPath({ path, oid: this.config.to }),
])
return [TREE_TYPE, BLOB_TYPE].includes(type.trimEnd())
doesPathExists = [TREE_TYPE, BLOB_TYPE].includes(type.trimEnd())
} catch {
return false
doesPathExists = false
}
return doesPathExists
}

public async pathExists(path: string) {
if (this.pathExistsCache.has(path)) {
return this.pathExistsCache.get(path)
}
const doesPathExists = await this.pathExistsImpl(path)
this.pathExistsCache.set(path, doesPathExists)
return doesPathExists
}

public async getFirstCommitRef() {
Expand All @@ -81,7 +95,7 @@ export default class GitAdapter {
return content.toString(UTF8_ENCODING)
}

public async getFilesPath(path: string): Promise<string[]> {
protected async getFilesPathImpl(path: string): Promise<string[]> {
return (
await this.simpleGit.raw([
'ls-tree',
Expand All @@ -96,6 +110,28 @@ export default class GitAdapter {
.map(line => treatPathSep(line))
}

public async getFilesPath(path: string): Promise<string[]> {
if (this.getFilesPathCache.has(path)) {
return Array.from(this.getFilesPathCache.get(path)!)
}
const filesPath = await this.getFilesPathImpl(path)
// Memoize every containing sub path for each filesPath
for (const filePath of filesPath) {
const currentPath = []
for (const segment of filePath.split(PATH_SEP)) {
currentPath.push(segment)
const subPath = currentPath.join(PATH_SEP)

if (!this.getFilesPathCache.has(subPath)) {
this.getFilesPathCache.set(subPath, new Set())
}
this.getFilesPathCache.get(subPath)!.add(filePath)
}
}
this.getFilesPathCache.set(path, new Set(filesPath))
return filesPath
}

public async *getFilesFrom(path: string) {
const filesPath = await this.getFilesPath(path)
for (const filePath of filesPath) {
Expand Down

0 comments on commit 2b9dfef

Please sign in to comment.