-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⚡ feat(bundler): Add cache to bundler (#498)
## Description Add persistent cache to bundler. When passed in an object the solc compile results are persisted to the cache object ## Testing Explain the quality checks that have been done on the code changes ## Additional Information - [ ] I read the [contributing docs](../docs/contributing.md) (if this is your first contribution) Your ENS/address: --------- Co-authored-by: Will Cory <[email protected]>
- Loading branch information
1 parent
2f6d176
commit b626090
Showing
16 changed files
with
767 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
"@evmts/bundler": minor | ||
--- | ||
|
||
Added persistent cache support. When passed in an optional cache object to the bundler solc results are persisted across compilations. This is important to add before EVMts vm is added as it will require bytecode. Compiling bytecode can be as much as 80 times slower. This will improve performance in following situations: | ||
|
||
1. Importing the same contract in multiple places. With the cache the contract will only be compiled once | ||
2. Compiling in dev mode. Now if contracts don't change previous compilations will be cached | ||
|
||
In future the cache will offer ability to persist in a file. This will allow the cache to persist across different processes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import type { Logger } from '.' | ||
import { type Cache, createCache } from './createCache' | ||
import { beforeEach, describe, expect, it, vi } from 'vitest' | ||
|
||
describe(createCache.name, () => { | ||
let mockLogger: Logger = { | ||
log: vi.fn(), | ||
warn: vi.fn(), | ||
error: vi.fn(), | ||
info: vi.fn(), | ||
} | ||
let cache: Cache = createCache(mockLogger) | ||
|
||
beforeEach(() => { | ||
mockLogger = { | ||
log: vi.fn(), | ||
warn: vi.fn(), | ||
error: vi.fn(), | ||
info: vi.fn(), | ||
} | ||
cache = createCache(mockLogger) | ||
}) | ||
|
||
describe(cache.read.name, () => { | ||
it('should throw error on cache miss', () => { | ||
expect(() => cache.read('someModuleId')).toThrow(Error) | ||
}) | ||
|
||
it('should return cached value', () => { | ||
const expectedOutput = { | ||
sources: { 'some/path': { content: 'content' } }, | ||
} | ||
cache.write('someModuleId', expectedOutput as any) | ||
const result = cache.read('someModuleId') | ||
expect(result).toEqual(expectedOutput) | ||
}) | ||
}) | ||
|
||
describe(cache.write.name, () => { | ||
it('should write value to cache', () => { | ||
const output = { | ||
sources: { 'some/path': { content: 'content' } }, | ||
} | ||
cache.write('someModuleId', output as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
}) | ||
expect(result).toBe(true) | ||
}) | ||
}) | ||
|
||
describe(cache.isCached.name, () => { | ||
it('should return false if there is no previous cached item', () => { | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
}) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should return false if the number of sources differ', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': { content: 'content' }, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
'another/path': { content: 'another content' }, | ||
}) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should return false if a source is missing in cached data', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': { content: 'content' }, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
'new/path': { content: 'new content' }, | ||
}) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should return false and log an error if content is missing in cached or new source', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': {}, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
}) | ||
expect(result).toBe(false) | ||
expect(mockLogger.error).toHaveBeenCalledWith( | ||
'Unexpected error: Unable to use cache because content is undefined. Continuing without cache.', | ||
) | ||
}) | ||
|
||
it('should return false if old sources had different file', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': { content: 'content' }, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'different/path': { content: 'content' }, | ||
}) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should return false if content in sources differs', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': { content: 'old content' }, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'new content' }, | ||
}) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
it('should return true if sources match with the cache', () => { | ||
cache.write('someModuleId', { | ||
sources: { | ||
'some/path': { content: 'content' }, | ||
}, | ||
} as any) | ||
const result = cache.isCached('someModuleId', { | ||
'some/path': { content: 'content' }, | ||
}) | ||
expect(result).toBe(true) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import type { SolcInputDescription, SolcOutput } from './solc/solc' | ||
import type { Logger } from './types' | ||
|
||
type CacheObject = { | ||
[filePath: string]: SolcOutput | ||
} | ||
|
||
export type Cache = { | ||
read: (entryModuleId: string) => SolcOutput | ||
write: (entryModuleId: string, compiledContracts: SolcOutput) => void | ||
isCached: ( | ||
entryModuleId: string, | ||
sources: SolcInputDescription['sources'], | ||
) => boolean | ||
} | ||
|
||
export const createCache = (logger: Logger): Cache => { | ||
const cache: CacheObject = {} | ||
|
||
const write = (entryModuleId: string, compiledContracts: SolcOutput) => { | ||
cache[entryModuleId] = compiledContracts | ||
} | ||
|
||
const read = (entryModuleId: string): SolcOutput => { | ||
const out = cache[entryModuleId] | ||
if (!out) { | ||
throw new Error( | ||
`Cache miss for ${entryModuleId}. Try calling isCached first`, | ||
) | ||
} | ||
return out | ||
} | ||
|
||
const isCached = ( | ||
entryModuleId: string, | ||
sources: SolcInputDescription['sources'], | ||
) => { | ||
const previousCachedItem = cache[entryModuleId] | ||
if (!previousCachedItem) { | ||
return false | ||
} | ||
const { sources: previousSources } = previousCachedItem | ||
if (Object.keys(sources).length !== Object.keys(previousSources).length) { | ||
return false | ||
} | ||
for (const [key, newSource] of Object.entries(sources)) { | ||
const oldSource = previousSources[key] | ||
if (!oldSource) { | ||
return false | ||
} | ||
if (!('content' in oldSource) || !('content' in newSource)) { | ||
logger.error( | ||
'Unexpected error: Unable to use cache because content is undefined. Continuing without cache.', | ||
) | ||
return false | ||
} | ||
if (oldSource.content !== newSource.content) { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
return { | ||
read, | ||
write, | ||
isCached, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
b626090
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
evmts-docs – ./
evmts-docs-evmts.vercel.app
evmts.dev
evmts-docs-git-main-evmts.vercel.app