Skip to content

Commit

Permalink
Replace file-entry-cache with custom impl + built-in serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Jun 19, 2024
1 parent be3abbc commit 7aa2f6d
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 79 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 0 additions & 2 deletions packages/knip/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"@snyk/github-codeowners": "1.1.0",
"easy-table": "1.2.0",
"fast-glob": "^3.3.2",
"file-entry-cache": "8.0.0",
"jiti": "^1.21.0",
"js-yaml": "^4.1.0",
"minimist": "^1.2.8",
Expand All @@ -84,7 +83,6 @@
"@jest/types": "^29.6.3",
"@release-it/bumper": "^6.0.1",
"@types/bun": "^1.1.4",
"@types/file-entry-cache": "5.0.4",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/picomatch": "2.3.3",
Expand Down
22 changes: 6 additions & 16 deletions packages/knip/src/CacheConsultant.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fileEntryCache, { type FileEntryCache, type FileDescriptor } from 'file-entry-cache';
import { timerify } from './util/Performance.js';
import parsedArgValues from './util/cli-arguments.js';
import { type FileDescriptor, FileEntryCache } from './util/file-entry-cache.js';
import { cwd, join } from './util/path.js';
import { version } from './version.js';

Expand All @@ -10,35 +10,25 @@ const { cache: isCache = false, watch: isWatch = false } = parsedArgValues;

const cacheLocation = parsedArgValues['cache-location'] ?? defaultCacheLocation;

interface FD<T> extends FileDescriptor {
readonly meta?: {
readonly size?: number;
readonly mtime?: number;
readonly hash?: string;
data?: T;
};
}

const create = timerify(fileEntryCache.create, 'createCache');

const dummyFileDescriptor = { key: '', changed: true, notFound: true, meta: undefined };
// biome-ignore lint/suspicious/noExplicitAny: deal with it
const dummyFileDescriptor: FileDescriptor<any> = { key: '', changed: true, notFound: true };

const isEnabled = isCache || isWatch;

export class CacheConsultant<T> {
private cache: undefined | FileEntryCache;
private cache: undefined | FileEntryCache<T>;
constructor(name: string) {
if (isCache) {
const cacheName = `${name.replace(/[^a-z0-9]/g, '-').replace(/-*$/, '')}-${version}`;
this.cache = create(cacheName, cacheLocation);
this.cache = new FileEntryCache(cacheName, cacheLocation);
this.reconcile = timerify(this.cache.reconcile).bind(this.cache);
this.getFileDescriptor = timerify(this.cache.getFileDescriptor).bind(this.cache);
}
}
static getCacheLocation() {
return cacheLocation;
}
public getFileDescriptor(file: string): FD<T> {
public getFileDescriptor(file: string): FileDescriptor<T> {
if (isEnabled && this.cache) return this.cache?.getFileDescriptor(file);
return dummyFileDescriptor;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/knip/src/ProjectPrincipal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { timerify } from './util/Performance.js';
import { compact } from './util/array.js';
import { getPackageNameFromModuleSpecifier, isStartsLikePackageName, sanitizeSpecifier } from './util/modules.js';
import { dirname, extname, isInNodeModules, join } from './util/path.js';
import { _deserialize, _serialize } from './util/serialize.js';
import type { ToSourceFilePath } from './util/to-source-path.js';

// These compiler options override local options
Expand Down Expand Up @@ -226,7 +225,7 @@ export class ProjectPrincipal {
getPrincipalByFilePath: (filePath: string) => undefined | ProjectPrincipal
) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd.changed && fd.meta?.data) return _deserialize(fd.meta.data);
if (!fd.changed && fd.meta?.data) return fd.meta.data;

const typeChecker = this.backend.typeChecker;

Expand Down Expand Up @@ -339,7 +338,8 @@ export class ProjectPrincipal {
for (const [filePath, file] of graph.entries()) {
const fd = this.cache.getFileDescriptor(filePath);
if (!fd?.meta) continue;
fd.meta.data = _serialize(file);
const { imported, internalImportCache, ...clone } = file;
fd.meta.data = clone;
}
this.cache.reconcile();
}
Expand Down
39 changes: 24 additions & 15 deletions packages/knip/src/WorkspaceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type WorkspaceManagerOptions = {

export type ReferencedDependencies = Set<[string, string]>;

type CacheItem = { resolveEntryPaths?: string[]; resolveConfig?: string[] };

const nullConfig: EnsuredPluginConfiguration = { config: null, entry: null, project: null };

const initEnabledPluginsMap = () =>
Expand Down Expand Up @@ -65,7 +67,7 @@ export class WorkspaceWorker {
enabledPlugins: PluginName[] = [];
enabledPluginsInAncestors: string[];

cache: CacheConsultant<unknown>;
cache: CacheConsultant<CacheItem>;

constructor({
name,
Expand Down Expand Up @@ -286,21 +288,28 @@ export class WorkspaceWorker {
if (hasResolveEntryPaths || shouldRunConfigResolver) {
const isManifest = basename(configFilePath) === 'package.json';
const fd = isManifest ? undefined : this.cache.getFileDescriptor(configFilePath);
const config =
fd?.meta?.data && !fd.changed
? fd.meta.data
: await loadConfigForPlugin(configFilePath, plugin, opts, pluginName);
if (config) {
if (hasResolveEntryPaths) {
const dependencies = (await plugin.resolveEntryPaths?.(config, opts)) ?? [];
for (const id of dependencies) configEntryPaths.push(id);
}
if (shouldRunConfigResolver) {
const dependencies = (await plugin.resolveConfig?.(config, opts)) ?? [];
for (const id of dependencies) addDependency(id, configFilePath);
}

if (!isManifest && fd?.changed && fd.meta) fd.meta.data = config;
if (fd?.meta?.data && !fd.changed) {
if (fd.meta.data.resolveEntryPaths)
for (const id of fd.meta.data.resolveEntryPaths) configEntryPaths.push(id);
if (fd.meta.data.resolveConfig)
for (const id of fd.meta.data.resolveConfig) addDependency(id, configFilePath);
} else {
const config = await loadConfigForPlugin(configFilePath, plugin, opts, pluginName);
const data: CacheItem = {};
if (config) {
if (hasResolveEntryPaths) {
const dependencies = (await plugin.resolveEntryPaths?.(config, opts)) ?? [];
for (const id of dependencies) configEntryPaths.push(id);
data.resolveEntryPaths = dependencies;
}
if (shouldRunConfigResolver) {
const dependencies = (await plugin.resolveConfig?.(config, opts)) ?? [];
for (const id of dependencies) addDependency(id, configFilePath);
data.resolveConfig = dependencies;
}
if (!isManifest && fd?.changed && fd.meta) fd.meta.data = data;
}
}
}
}
Expand Down
130 changes: 130 additions & 0 deletions packages/knip/src/util/file-entry-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import fs from 'node:fs';
import { timerify } from './Performance.js';
import { debugLog } from './debug.js';
import { isDirectory, isFile } from './fs.js';
import { dirname, isAbsolute, resolve } from './path.js';
import { deserialize, serialize } from './serialize.js';

type MetaData<T> = { size: number; mtime: number; data?: T };

export type FileDescriptor<T> = {
key: string;
changed?: boolean;
notFound?: boolean;
err?: unknown;
meta?: MetaData<T>;
};

const cwd = process.cwd();

const createCache = (filePath: string) => {
try {
return deserialize(fs.readFileSync(filePath));
} catch (_err) {
debugLog('*', `Error reading cache from ${filePath}`);
}
};

const create = timerify(createCache);

export class FileEntryCache<T> {
filePath: string;
cache = new Map<string, MetaData<T>>();
normalizedEntries = new Map<string, FileDescriptor<T>>();

constructor(cacheId: string, _path: string) {
this.filePath = isAbsolute(_path) ? resolve(_path, cacheId) : resolve(cwd, _path, cacheId);
if (isFile(this.filePath)) this.cache = create(this.filePath);
this.removeNotFoundFiles();
}

removeNotFoundFiles() {
for (const filePath of this.normalizedEntries.keys()) {
try {
fs.statSync(filePath);
} catch (error) {
// @ts-expect-error
if (error.code === 'ENOENT') this.cache.delete(filePath);
}
}
}

getFileDescriptor(filePath: string): FileDescriptor<T> {
let fstat: fs.Stats;

try {
if (!isAbsolute(filePath)) filePath = resolve(filePath);
fstat = fs.statSync(filePath);
} catch (error: unknown) {
this.removeEntry(filePath);
return { key: filePath, notFound: true, err: error };
}

return this._getFileDescriptorUsingMtimeAndSize(filePath, fstat);
}

_getFileDescriptorUsingMtimeAndSize(filePath: string, fstat: fs.Stats) {
let meta = this.cache.get(filePath);
const cacheExists = Boolean(meta);

const cSize = fstat.size;
const cTime = fstat.mtime.getTime();

let isDifferentDate: undefined | boolean;
let isDifferentSize: undefined | boolean;

if (meta) {
isDifferentDate = cTime !== meta.mtime;
isDifferentSize = cSize !== meta.size;
} else {
meta = { size: cSize, mtime: cTime };
}

const fd: FileDescriptor<T> = {
key: filePath,
changed: !cacheExists || isDifferentDate || isDifferentSize,
meta,
};

this.normalizedEntries.set(filePath, fd);

return fd;
}

removeEntry(entryName: string) {
if (!isAbsolute(entryName)) entryName = resolve(cwd, entryName);
this.normalizedEntries.delete(entryName);
this.cache.delete(entryName);
}

_getMetaForFileUsingMtimeAndSize(cacheEntry: FileDescriptor<T>) {
const stat = fs.statSync(cacheEntry.key);
const meta = Object.assign(cacheEntry.meta ?? {}, {
size: stat.size,
mtime: stat.mtime.getTime(),
});
return meta;
}

reconcile() {
this.removeNotFoundFiles();

for (const [entryName, cacheEntry] of this.normalizedEntries.entries()) {
try {
const meta = this._getMetaForFileUsingMtimeAndSize(cacheEntry);
this.cache.set(entryName, meta);
} catch (error) {
// @ts-expect-error
if (error.code !== 'ENOENT') throw error;
}
}

try {
const dir = dirname(this.filePath);
if (!isDirectory(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(this.filePath, serialize(this.cache));
} catch (_err) {
debugLog('*', `Error writing cache to ${this.filePath}`);
}
}
}
55 changes: 14 additions & 41 deletions packages/knip/src/util/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,14 @@
import type { FileNode } from '../types/dependency-graph.js';
import { timerify } from './Performance.js';

// biome-ignore lint/suspicious/noExplicitAny: deal with it
const serializeObj = (obj: any): any => {
if (!obj) return obj;
if (obj instanceof Set) return Array.from(obj);
if (obj instanceof Map) {
const o: { [key: string]: unknown } = { _m: 1 };
for (const [key, value] of obj) o[key] = serializeObj(value);
return o;
}
if (typeof obj === 'object') for (const key in obj) obj[key] = serializeObj(obj[key]);
return obj;
};

// biome-ignore lint/suspicious/noExplicitAny: deal with it
const deserializeObj = (obj: any): any => {
if (!obj) return obj;
if (Array.isArray(obj)) return new Set(obj);
if (obj._m) {
const map = new Map();
for (const key in obj) key !== '_m' && map.set(key, deserializeObj(obj[key]));
return map;
}
if (typeof obj === 'object') for (const key in obj) obj[key] = deserializeObj(obj[key]);
return obj;
};

const serialize = (data: FileNode): FileNode => {
const clone = structuredClone(data);
clone.imported = undefined;
clone.internalImportCache = undefined;
return serializeObj(clone);
};

const deserialize = (data: FileNode): FileNode => deserializeObj(data);

export const _serialize = timerify(serialize);

export const _deserialize = timerify(deserialize);
// biome-ignore lint: well
let s: (data: any) => Buffer, d: (buffer: Buffer) => any;

if (typeof Bun !== 'undefined') {
const { serialize, deserialize } = await import('bun:jsc');
s = serialize;
d = deserialize;
} else {
const { serialize, deserialize } = await import('node:v8');
s = serialize;
d = deserialize;
}

export { s as serialize, d as deserialize };
4 changes: 2 additions & 2 deletions packages/knip/test/util/serialize.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { _deserialize, _serialize } from '../../src/util/serialize.js';
import { deserialize, serialize } from '../../src/util/serialize.js';

test('Should serialize and deserialize file back to original', () => {
const file = {
Expand Down Expand Up @@ -63,5 +63,5 @@ test('Should serialize and deserialize file back to original', () => {
traceRefs: new Set(['ref']),
};

assert.deepEqual(_deserialize(_serialize(file)), file);
assert.deepEqual(deserialize(serialize(file)), file);
});

0 comments on commit 7aa2f6d

Please sign in to comment.