mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
perf: Cache package.json location between getNearestPackageJson invocations (#11580)
* perf: Cache package.json location between invocation Cache package.json location to improve performance of migrations with a lot of files Closes: #4136 * refactor: Use map. Move tests to appropriate files Move tests and use Map instead of object as per review comments * test: Check number of invocations in test Change test to assert number of stat and readFile calls * test: Change assert for CI Added assertion to make both local and CI work * Create package.json in test * Create file only if not existed before * test: Fix test assertion based on platform * test: Change package.json type * ci: Trigger tests --------- Co-authored-by: Bartlomiej Rutkowski <brutkowski@tilt.app>
This commit is contained in:
parent
4d204adf56
commit
b6ffd462dd
@ -40,11 +40,39 @@ export async function importOrRequireFile(
|
||||
return tryToRequire()
|
||||
}
|
||||
|
||||
const packageJsonCache = new Map<string, object | null>()
|
||||
const MAX_CACHE_SIZE = 1000
|
||||
|
||||
function setPackageJsonCache(paths: string[], packageJson: object | null) {
|
||||
for (const path of paths) {
|
||||
// Simple LRU-like behavior: if we're at capacity, remove oldest entry
|
||||
if (
|
||||
packageJsonCache.size >= MAX_CACHE_SIZE &&
|
||||
!packageJsonCache.has(path)
|
||||
) {
|
||||
const firstKey = packageJsonCache.keys().next().value
|
||||
if (firstKey) packageJsonCache.delete(firstKey)
|
||||
}
|
||||
packageJsonCache.set(path, packageJson)
|
||||
}
|
||||
}
|
||||
|
||||
async function getNearestPackageJson(filePath: string): Promise<object | null> {
|
||||
let currentPath = filePath
|
||||
const paths: string[] = []
|
||||
|
||||
while (currentPath !== path.dirname(currentPath)) {
|
||||
currentPath = path.dirname(currentPath)
|
||||
|
||||
// Check if we have already cached the package.json for this path
|
||||
if (packageJsonCache.has(currentPath)) {
|
||||
setPackageJsonCache(paths, packageJsonCache.get(currentPath)!)
|
||||
return packageJsonCache.get(currentPath)!
|
||||
}
|
||||
|
||||
// Add the current path to the list of paths to cache
|
||||
paths.push(currentPath)
|
||||
|
||||
const potentialPackageJson = path.join(currentPath, "package.json")
|
||||
|
||||
try {
|
||||
@ -54,10 +82,15 @@ async function getNearestPackageJson(filePath: string): Promise<object | null> {
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(
|
||||
const parsedPackage = JSON.parse(
|
||||
await fs.readFile(potentialPackageJson, "utf8"),
|
||||
)
|
||||
// Cache the parsed package.json object and return it
|
||||
setPackageJsonCache(paths, parsedPackage)
|
||||
return parsedPackage
|
||||
} catch {
|
||||
// If parsing fails, we still cache null to avoid repeated attempts
|
||||
setPackageJsonCache(paths, null)
|
||||
return null
|
||||
}
|
||||
} catch {
|
||||
@ -66,5 +99,6 @@ async function getNearestPackageJson(filePath: string): Promise<object | null> {
|
||||
}
|
||||
|
||||
// the top of the file tree is reached
|
||||
setPackageJsonCache(paths, null)
|
||||
return null
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { expect } from "chai"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { strict as assert } from "assert"
|
||||
import sinon from "sinon"
|
||||
import fsAsync from "fs"
|
||||
|
||||
import { importOrRequireFile } from "../../../src/util/ImportUtils"
|
||||
|
||||
@ -177,4 +180,85 @@ describe("ImportUtils.importOrRequireFile", () => {
|
||||
|
||||
await fs.rmdir(testDir, { recursive: true })
|
||||
})
|
||||
|
||||
it("Should use cache to find package.json", async () => {
|
||||
// Create package.json if not exists
|
||||
const packageJsonPath = path.join(__dirname, "package.json")
|
||||
const packageJsonAlreadyExisted = fsAsync.existsSync(packageJsonPath)
|
||||
if (!packageJsonAlreadyExisted) {
|
||||
await fs.writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify({ name: "test-package" }),
|
||||
"utf8",
|
||||
)
|
||||
}
|
||||
|
||||
const statSpy = sinon.spy(fsAsync.promises, "stat")
|
||||
const readFileSpy = sinon.spy(fsAsync.promises, "readFile")
|
||||
|
||||
assert.equal(
|
||||
statSpy.callCount,
|
||||
0,
|
||||
"stat should not be called before importOrRequireFile",
|
||||
)
|
||||
assert.equal(
|
||||
readFileSpy.callCount,
|
||||
0,
|
||||
"readFile should not be called before importOrRequireFile",
|
||||
)
|
||||
|
||||
const filePath1 = path.join(__dirname, "file1.js")
|
||||
const filePath2 = path.join(__dirname, "file2.js")
|
||||
const filePath3 = path.join(__dirname, "file3.js")
|
||||
|
||||
await fs.writeFile(filePath1, "", "utf8")
|
||||
await fs.writeFile(filePath2, "", "utf8")
|
||||
await fs.writeFile(filePath3, "", "utf8")
|
||||
|
||||
// Trigger the first import to create the cache
|
||||
await importOrRequireFile(filePath1)
|
||||
|
||||
// Get the number of calls to stat and readFile after the first import
|
||||
const numberOfStatCalls = statSpy.callCount
|
||||
const numberOfReadFileCalls = readFileSpy.callCount
|
||||
|
||||
assert.equal(
|
||||
numberOfStatCalls,
|
||||
1,
|
||||
"stat should be called for the first import",
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
numberOfReadFileCalls,
|
||||
1,
|
||||
"readFile should be called for the first import",
|
||||
)
|
||||
|
||||
// Trigger next imports to check if cache is used
|
||||
await importOrRequireFile(filePath2)
|
||||
await importOrRequireFile(filePath3)
|
||||
|
||||
assert.equal(
|
||||
statSpy.callCount,
|
||||
numberOfStatCalls,
|
||||
"stat should be called only during the first import",
|
||||
)
|
||||
assert.equal(
|
||||
readFileSpy.callCount,
|
||||
numberOfReadFileCalls,
|
||||
"readFile should be called only during the first import",
|
||||
)
|
||||
|
||||
// Clean up test files
|
||||
await fs.unlink(filePath1)
|
||||
await fs.unlink(filePath2)
|
||||
await fs.unlink(filePath3)
|
||||
|
||||
// If package.json was created by this test, remove it
|
||||
if (!packageJsonAlreadyExisted) {
|
||||
await fs.unlink(packageJsonPath)
|
||||
}
|
||||
|
||||
sinon.restore()
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user