Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use content-hash to invalidate cache #1006

Merged
merged 4 commits into from
Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions packages/hardhat-core/src/builtin-tasks/compile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import chalk from "chalk";
import { exec } from "child_process";
import debug from "debug";
import fsExtra from "fs-extra";
import path from "path";
import semver from "semver";

Expand Down Expand Up @@ -69,6 +70,7 @@ import {
TASK_COMPILE_SOLIDITY_LOG_RUN_COMPILER_END,
TASK_COMPILE_SOLIDITY_LOG_RUN_COMPILER_START,
TASK_COMPILE_SOLIDITY_MERGE_COMPILATION_JOBS,
TASK_COMPILE_SOLIDITY_READ_FILE,
TASK_COMPILE_SOLIDITY_RUN_SOLC,
TASK_COMPILE_SOLIDITY_RUN_SOLCJS,
} from "./task-names";
Expand Down Expand Up @@ -137,6 +139,18 @@ subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_NAMES)
}
);

subtask(TASK_COMPILE_SOLIDITY_READ_FILE)
.addParam("absolutePath", undefined, undefined, types.string)
.setAction(
async ({ absolutePath }: { absolutePath: string }): Promise<string> => {
const content = await fsExtra.readFile(absolutePath, {
encoding: "utf8",
});

return content;
}
);

/**
* Receives a list of source names and returns a dependency graph. This task
* is responsible for both resolving dependencies (like getting files from
Expand All @@ -151,10 +165,15 @@ subtask(TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH)
sourceNames,
solidityFilesCache,
}: { sourceNames: string[]; solidityFilesCache?: SolidityFilesCache },
{ config }
{ config, run }
): Promise<taskTypes.DependencyGraph> => {
const parser = new Parser(solidityFilesCache);
const resolver = new Resolver(config.paths.root, parser);
const resolver = new Resolver(
config.paths.root,
parser,
(absolutePath: string) =>
run(TASK_COMPILE_SOLIDITY_READ_FILE, { absolutePath })
);

const resolvedFiles = await Promise.all(
sourceNames.map((sn) => resolver.resolveSourceName(sn))
Expand Down Expand Up @@ -1185,6 +1204,7 @@ subtask(TASK_COMPILE_SOLIDITY)
for (const { file, artifactsEmitted } of artifactsEmittedPerFile) {
solidityFilesCache.addFile(file.absolutePath, {
lastModificationDate: file.lastModificationDate.valueOf(),
contentHash: file.contentHash,
sourceName: file.sourceName,
solcConfig: compilationJob.getSolcConfig(),
imports: file.content.imports,
Expand Down Expand Up @@ -1287,7 +1307,7 @@ function needsCompilation(
for (const file of job.getResolvedFiles()) {
const hasChanged = cache.hasFileChanged(
file.absolutePath,
file.lastModificationDate,
file.contentHash,
// we only check if the solcConfig is different for files that
// emit artifacts
job.emitsArtifacts(file) ? job.getSolcConfig() : undefined
Expand Down
1 change: 1 addition & 0 deletions packages/hardhat-core/src/builtin-tasks/task-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS =
"compile:solidity:get-source-paths";
export const TASK_COMPILE_SOLIDITY_GET_SOURCE_NAMES =
"compile:solidity:get-source-names";
export const TASK_COMPILE_SOLIDITY_READ_FILE = "compile:solidity:read-file";
export const TASK_COMPILE_SOLIDITY_GET_DEPENDENCY_GRAPH =
"compile:solidity:get-dependency-graph";
export const TASK_COMPILE_SOLIDITY_GET_COMPILATION_JOBS =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import debug from "debug";
import fsExtra from "fs-extra";
import * as t from "io-ts";
import type { LoDashStatic } from "lodash";
Expand All @@ -6,10 +7,13 @@ import * as path from "path";
import { SOLIDITY_FILES_CACHE_FILENAME } from "../../internal/constants";
import type { ProjectPathsConfig, SolcConfig } from "../../types";

const FORMAT_VERSION = "hh-sol-cache-1";
const log = debug("hardhat:core:tasks:compile:cache");

const FORMAT_VERSION = "hh-sol-cache-2";

const CacheEntryCodec = t.type({
lastModificationDate: t.number,
contentHash: t.string,
sourceName: t.string,
solcConfig: t.any,
imports: t.array(t.string),
Expand All @@ -24,6 +28,7 @@ const CacheCodec = t.type({

export interface CacheEntry {
lastModificationDate: number;
contentHash: string;
sourceName: string;
solcConfig: SolcConfig;
imports: string[];
Expand Down Expand Up @@ -52,30 +57,26 @@ export class SolidityFilesCache {

if (result.isRight()) {
const solidityFilesCache = new SolidityFilesCache(result.value);
await solidityFilesCache.removeModifiedFiles();
await solidityFilesCache.removeNonExistingFiles();
return solidityFilesCache;
}

// tslint:disable-next-line only-hardhat-error
throw new Error("Couldn't read cache file, try running the clean task"); // TODO use HardhatError
log("There was a problem reading the cache");

return new SolidityFilesCache({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will clean the cache as soon as the user upgrades, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, because of the change in the cache file format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I forgot to add a comment about this.

I think in an ideal world we would handle changes in the files format in a graceful way. For example: detecting that the file has the format v1 but the code expects format v2, and acting accordingly. But in this case, just ignoring everything and starting over is both easier and (I think) correct. We might need to change this approach in the future, but for now I think this is enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

_format: FORMAT_VERSION,
files: {},
});
}

constructor(private _cache: Cache) {}

public async removeModifiedFiles() {
for (const [absolutePath, cachedData] of Object.entries(
this._cache.files
)) {
public async removeNonExistingFiles() {
for (const absolutePath of Object.keys(this._cache.files)) {
if (!fsExtra.existsSync(absolutePath)) {
this.removeEntry(absolutePath);
continue;
}
const stats = await fsExtra.stat(absolutePath);
const lastModificationDate = new Date(stats.ctime);

if (lastModificationDate.valueOf() !== cachedData.lastModificationDate) {
this.removeEntry(absolutePath);
}
}
}

Expand Down Expand Up @@ -103,7 +104,7 @@ export class SolidityFilesCache {

public hasFileChanged(
absolutePath: string,
lastModificationDate: Date,
contentHash: string,
solcConfig?: SolcConfig
): boolean {
const { isEqual }: LoDashStatic = require("lodash");
Expand All @@ -115,7 +116,7 @@ export class SolidityFilesCache {
return true;
}

if (cacheEntry.lastModificationDate < lastModificationDate.valueOf()) {
if (cacheEntry.contentHash !== contentHash) {
return true;
}

Expand Down
32 changes: 26 additions & 6 deletions packages/hardhat-core/src/internal/solidity/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ export class Parser {

constructor(private _solidityFilesCache?: SolidityFilesCache) {}

public parse(fileContent: string, absolutePath: string): ParsedData {
const cacheResult = this._getFromCache(absolutePath);
public parse(
fileContent: string,
absolutePath: string,
contentHash: string
): ParsedData {
const cacheResult = this._getFromCache(absolutePath, contentHash);

if (cacheResult !== null) {
return cacheResult;
Expand Down Expand Up @@ -55,12 +59,28 @@ export class Parser {
return result;
}

private _getFromCache(absolutePath: string): ParsedData | null {
const cacheEntry = this._solidityFilesCache?.getEntry(absolutePath);
/**
* Get parsed data from the internal cache, or from the solidity files cache.
*
* Returns null if cannot find it in either one.
*/
private _getFromCache(
absolutePath: string,
contentHash: string
): ParsedData | null {
if (this._solidityFilesCache === undefined) {
return this._cache.get(absolutePath) ?? null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we use the content hash instead of the absolute path here? I think it will be more robust. I can imagine using the absolutePath bringing issues for things like the hardhat-watcher plugin.

}

const cacheEntry = this._solidityFilesCache.getEntry(absolutePath);

if (cacheEntry === undefined) {
return this._cache.get(absolutePath) ?? null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

}

if (cacheEntry !== undefined) {
const { imports, versionPragmas } = cacheEntry;
const { imports, versionPragmas } = cacheEntry;

if (cacheEntry.contentHash === contentHash) {
return { imports, versionPragmas };
}

Expand Down
20 changes: 15 additions & 5 deletions packages/hardhat-core/src/internal/solidity/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "../../utils/source-names";
import { HardhatError } from "../core/errors";
import { ERRORS } from "../core/errors-list";
import { createNonCryptographicHashBasedIdentifier } from "../util/hash";

import { Parser } from "./parse";

Expand All @@ -33,6 +34,7 @@ export class ResolvedFile implements IResolvedFile {
public readonly sourceName: string,
public readonly absolutePath: string,
public readonly content: FileContent,
public readonly contentHash: string,
public readonly lastModificationDate: Date,
libraryName?: string,
libraryVersion?: string
Expand All @@ -56,7 +58,8 @@ export class ResolvedFile implements IResolvedFile {
export class Resolver {
constructor(
private readonly _projectRoot: string,
private readonly _parser: Parser
private readonly _parser: Parser,
private readonly _readFile: (absolutePath: string) => Promise<string>
) {}

/**
Expand Down Expand Up @@ -294,13 +297,19 @@ export class Resolver {
libraryName?: string,
libraryVersion?: string
): Promise<ResolvedFile> {
const rawContent = await fsExtra.readFile(absolutePath, {
encoding: "utf8",
});
const rawContent = await this._readFile(absolutePath);
const stats = await fsExtra.stat(absolutePath);
const lastModificationDate = new Date(stats.ctime);

const parsedContent = this._parser.parse(rawContent, absolutePath);
const contentHash = createNonCryptographicHashBasedIdentifier(
Buffer.from(rawContent)
).toString("hex");

const parsedContent = this._parser.parse(
rawContent,
absolutePath,
contentHash
);

const content = {
rawContent,
Expand All @@ -311,6 +320,7 @@ export class Resolver {
sourceName,
absolutePath,
content,
contentHash,
lastModificationDate,
libraryName,
libraryVersion
Expand Down
1 change: 1 addition & 0 deletions packages/hardhat-core/src/types/builtin-tasks/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ResolvedFile {
// IMPORTANT: Mapped to ctime, NOT mtime. mtime isn't updated when the file
// properties (e.g. its name) are changed, only when it's content changes.
lastModificationDate: Date;
contentHash: string;
getVersionedName(): string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import {
SolidityFilesCache,
} from "../../../src/builtin-tasks/utils/solidity-files-cache";

const UNMODIFIED_CONTENT_HASH = "<unmodified-content-hash>";
const MODIFIED_CONTENT_HASH = "<modified-content-hash>";

function mockCachedFile(
sourceName: string,
other: Partial<CacheEntry> = {}
): CacheEntry {
return {
sourceName,
lastModificationDate: new Date().valueOf(),
contentHash: UNMODIFIED_CONTENT_HASH,
solcConfig: { version: "0.6.6", settings: {} },
imports: [],
versionPragmas: [],
Expand Down Expand Up @@ -66,7 +70,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
oneHourAgo,
UNMODIFIED_CONTENT_HASH,
solcConfig
);

Expand All @@ -88,7 +92,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
now,
MODIFIED_CONTENT_HASH,
solcConfig
);

Expand All @@ -110,7 +114,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/anotherFile.sol",
oneHourAgo,
UNMODIFIED_CONTENT_HASH,
solcConfig
);

Expand All @@ -132,7 +136,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
oneHourAgo,
UNMODIFIED_CONTENT_HASH,
{ version: "0.6.7", settings: {} }
);

Expand All @@ -154,7 +158,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
oneHourAgo,
UNMODIFIED_CONTENT_HASH,
{ version: "0.6.6", settings: { optimizer: true, runs: 200 } }
);

Expand All @@ -176,7 +180,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
oneHourAgo,
UNMODIFIED_CONTENT_HASH,
{ version: "0.6.6", settings: {} }
);

Expand All @@ -198,7 +202,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
oneHourAgo
UNMODIFIED_CONTENT_HASH
);

assert.isFalse(hasChanged);
Expand All @@ -219,7 +223,7 @@ describe("SolidityFilesCache", function () {

const hasChanged = solidityFilesCache.hasFileChanged(
"/path/to/contracts/file.sol",
now
MODIFIED_CONTENT_HASH
);

assert.isTrue(hasChanged);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ describe("compiler-input module", function () {
sourceName1,
path1,
{ rawContent: content1, imports: [], versionPragmas: [] },
"<content-hash-1>",
new Date()
),
new ResolvedFile(
sourceName2,
path2,
{ rawContent: content2, imports: [], versionPragmas: [] },
"<content-hash-2>",
new Date()
),
];
Expand Down
Loading