Skip to content

Commit

Permalink
⚡ feat(bundler): Add cache to bundler (#498)
Browse files Browse the repository at this point in the history
## 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
roninjin10 and Will Cory authored Oct 1, 2023
1 parent 2f6d176 commit b626090
Show file tree
Hide file tree
Showing 16 changed files with 767 additions and 44 deletions.
10 changes: 10 additions & 0 deletions .changeset/gold-baboons-sniff.md
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
10 changes: 9 additions & 1 deletion bundlers/bundler/src/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Bundler } from './types'
// @ts-ignore
import solc from 'solc'

export const bundler: Bundler = (config, logger, fao) => {
export const bundler: Bundler = (config, logger, fao, cache) => {
return {
name: bundler.name,
config,
Expand All @@ -21,6 +21,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
if (artifacts) {
const evmtsImports = `import { EvmtsContract } from '@evmts/core'`
Expand Down Expand Up @@ -50,6 +51,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
if (artifacts) {
const evmtsImports = `import { EvmtsContract } from '@evmts/core'`
Expand Down Expand Up @@ -79,6 +81,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = generateRuntimeSync(artifacts, 'ts', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand All @@ -98,6 +101,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = await generateRuntime(artifacts, 'ts', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand All @@ -117,6 +121,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = generateRuntimeSync(artifacts, 'cjs', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand All @@ -136,6 +141,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = await generateRuntime(artifacts, 'cjs', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand All @@ -155,6 +161,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = generateRuntimeSync(artifacts, 'mjs', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand All @@ -173,6 +180,7 @@ export const bundler: Bundler = (config, logger, fao) => {
config,
includeAst,
fao,
cache,
)
const code = await generateRuntime(artifacts, 'mjs', logger)
return { code, modules, solcInput, solcOutput, asts }
Expand Down
137 changes: 137 additions & 0 deletions bundlers/bundler/src/createCache.spec.ts
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)
})
})
})
69 changes: 69 additions & 0 deletions bundlers/bundler/src/createCache.ts
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,
}
}
1 change: 1 addition & 0 deletions bundlers/bundler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './unplugin'
export * from './types'
export * from './bundler'
export * from './runtime'
export * from './createCache'
Loading

1 comment on commit b626090

@vercel
Copy link

@vercel vercel bot commented on b626090 Oct 1, 2023

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

Please sign in to comment.