Skip to content

Commit 473e885

Browse files
Nathan Sobomaxbrunsfeld
authored andcommitted
Merge pull request atom#10017 from atom/as-sha1-v8-cache
Cache v8 code by source file's SHA1
1 parent 92e16ee commit 473e885

File tree

7 files changed

+148
-55
lines changed

7 files changed

+148
-55
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"atom-keymap": "^6.1.1",
1919
"babel-core": "^5.8.21",
2020
"bootstrap": "^3.3.4",
21-
"cached-run-in-this-context": "0.4.0",
21+
"cached-run-in-this-context": "0.4.1",
2222
"clear-cut": "^2.0.1",
2323
"coffee-script": "1.8.0",
2424
"color": "^0.7.3",

spec/file-system-blob-store-spec.coffee

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,61 +9,71 @@ describe "FileSystemBlobStore", ->
99
blobStore = FileSystemBlobStore.load(storageDirectory)
1010

1111
it "is empty when the file doesn't exist", ->
12-
expect(blobStore.get("foo")).toBeUndefined()
13-
expect(blobStore.get("bar")).toBeUndefined()
12+
expect(blobStore.get("foo", "invalidation-key-1")).toBeUndefined()
13+
expect(blobStore.get("bar", "invalidation-key-2")).toBeUndefined()
1414

1515
it "allows to read and write buffers from/to memory without persisting them", ->
16-
blobStore.set("foo", new Buffer("foo"))
17-
blobStore.set("bar", new Buffer("bar"))
16+
blobStore.set("foo", "invalidation-key-1", new Buffer("foo"))
17+
blobStore.set("bar", "invalidation-key-2", new Buffer("bar"))
1818

19-
expect(blobStore.get("foo")).toEqual(new Buffer("foo"))
20-
expect(blobStore.get("bar")).toEqual(new Buffer("bar"))
19+
expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo"))
20+
expect(blobStore.get("bar", "invalidation-key-2")).toEqual(new Buffer("bar"))
21+
22+
expect(blobStore.get("foo", "unexisting-key")).toBeUndefined()
23+
expect(blobStore.get("bar", "unexisting-key")).toBeUndefined()
2124

2225
it "persists buffers when saved and retrieves them on load, giving priority to in-memory ones", ->
23-
blobStore.set("foo", new Buffer("foo"))
24-
blobStore.set("bar", new Buffer("bar"))
26+
blobStore.set("foo", "invalidation-key-1", new Buffer("foo"))
27+
blobStore.set("bar", "invalidation-key-2", new Buffer("bar"))
2528
blobStore.save()
2629

2730
blobStore = FileSystemBlobStore.load(storageDirectory)
2831

29-
expect(blobStore.get("foo")).toEqual(new Buffer("foo"))
30-
expect(blobStore.get("bar")).toEqual(new Buffer("bar"))
32+
expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo"))
33+
expect(blobStore.get("bar", "invalidation-key-2")).toEqual(new Buffer("bar"))
34+
expect(blobStore.get("foo", "unexisting-key")).toBeUndefined()
35+
expect(blobStore.get("bar", "unexisting-key")).toBeUndefined()
3136

32-
blobStore.set("foo", new Buffer("changed"))
37+
blobStore.set("foo", "new-key", new Buffer("changed"))
3338

34-
expect(blobStore.get("foo")).toEqual(new Buffer("changed"))
39+
expect(blobStore.get("foo", "new-key")).toEqual(new Buffer("changed"))
40+
expect(blobStore.get("foo", "invalidation-key-1")).toBeUndefined()
3541

3642
it "persists both in-memory and previously stored buffers when saved", ->
37-
blobStore.set("foo", new Buffer("foo"))
38-
blobStore.set("bar", new Buffer("bar"))
43+
blobStore.set("foo", "invalidation-key-1", new Buffer("foo"))
44+
blobStore.set("bar", "invalidation-key-2", new Buffer("bar"))
3945
blobStore.save()
4046

4147
blobStore = FileSystemBlobStore.load(storageDirectory)
42-
blobStore.set("bar", new Buffer("changed"))
43-
blobStore.set("qux", new Buffer("qux"))
48+
blobStore.set("bar", "invalidation-key-3", new Buffer("changed"))
49+
blobStore.set("qux", "invalidation-key-4", new Buffer("qux"))
4450
blobStore.save()
4551

4652
blobStore = FileSystemBlobStore.load(storageDirectory)
4753

48-
expect(blobStore.get("foo")).toEqual(new Buffer("foo"))
49-
expect(blobStore.get("bar")).toEqual(new Buffer("changed"))
50-
expect(blobStore.get("qux")).toEqual(new Buffer("qux"))
54+
expect(blobStore.get("foo", "invalidation-key-1")).toEqual(new Buffer("foo"))
55+
expect(blobStore.get("bar", "invalidation-key-3")).toEqual(new Buffer("changed"))
56+
expect(blobStore.get("qux", "invalidation-key-4")).toEqual(new Buffer("qux"))
57+
expect(blobStore.get("foo", "unexisting-key")).toBeUndefined()
58+
expect(blobStore.get("bar", "invalidation-key-2")).toBeUndefined()
59+
expect(blobStore.get("qux", "unexisting-key")).toBeUndefined()
5160

5261
it "allows to delete keys from both memory and stored buffers", ->
53-
blobStore.set("a", new Buffer("a"))
54-
blobStore.set("b", new Buffer("b"))
62+
blobStore.set("a", "invalidation-key-1", new Buffer("a"))
63+
blobStore.set("b", "invalidation-key-2", new Buffer("b"))
5564
blobStore.save()
5665

5766
blobStore = FileSystemBlobStore.load(storageDirectory)
5867

59-
blobStore.set("b", new Buffer("b"))
60-
blobStore.set("c", new Buffer("c"))
68+
blobStore.set("b", "invalidation-key-3", new Buffer("b"))
69+
blobStore.set("c", "invalidation-key-4", new Buffer("c"))
6170
blobStore.delete("b")
6271
blobStore.delete("c")
6372
blobStore.save()
6473

6574
blobStore = FileSystemBlobStore.load(storageDirectory)
6675

67-
expect(blobStore.get("a")).toEqual(new Buffer("a"))
68-
expect(blobStore.get("b")).toBeUndefined()
69-
expect(blobStore.get("c")).toBeUndefined()
76+
expect(blobStore.get("a", "invalidation-key-1")).toEqual(new Buffer("a"))
77+
expect(blobStore.get("b", "invalidation-key-2")).toBeUndefined()
78+
expect(blobStore.get("b", "invalidation-key-3")).toBeUndefined()
79+
expect(blobStore.get("c", "invalidation-key-4")).toBeUndefined()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = function () { return "file-4" }

spec/native-compile-cache-spec.coffee

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,100 @@
1+
fs = require 'fs'
2+
path = require 'path'
3+
Module = require 'module'
4+
15
describe "NativeCompileCache", ->
26
nativeCompileCache = require '../src/native-compile-cache'
37
[fakeCacheStore, cachedFiles] = []
48

59
beforeEach ->
610
cachedFiles = []
711
fakeCacheStore = jasmine.createSpyObj("cache store", ["set", "get", "has", "delete"])
12+
fakeCacheStore.has.andCallFake (cacheKey, invalidationKey) ->
13+
fakeCacheStore.get(cacheKey, invalidationKey)?
14+
fakeCacheStore.get.andCallFake (cacheKey, invalidationKey) ->
15+
for entry in cachedFiles by -1
16+
continue if entry.cacheKey isnt cacheKey
17+
continue if entry.invalidationKey isnt invalidationKey
18+
return entry.cacheBuffer
19+
return
20+
fakeCacheStore.set.andCallFake (cacheKey, invalidationKey, cacheBuffer) ->
21+
cachedFiles.push({cacheKey, invalidationKey, cacheBuffer})
22+
823
nativeCompileCache.setCacheStore(fakeCacheStore)
24+
nativeCompileCache.setV8Version("a-v8-version")
925
nativeCompileCache.install()
1026

1127
it "writes and reads from the cache storage when requiring files", ->
12-
fakeCacheStore.has.andReturn(false)
13-
fakeCacheStore.set.andCallFake (filename, cacheBuffer) ->
14-
cachedFiles.push({filename, cacheBuffer})
15-
1628
fn1 = require('./fixtures/native-cache/file-1')
1729
fn2 = require('./fixtures/native-cache/file-2')
1830

1931
expect(cachedFiles.length).toBe(2)
2032

21-
expect(cachedFiles[0].filename).toBe(require.resolve('./fixtures/native-cache/file-1'))
33+
expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-1'))
2234
expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array)
2335
expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0)
2436
expect(fn1()).toBe(1)
2537

26-
expect(cachedFiles[1].filename).toBe(require.resolve('./fixtures/native-cache/file-2'))
38+
expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-2'))
2739
expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array)
2840
expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0)
2941
expect(fn2()).toBe(2)
3042

31-
fakeCacheStore.has.andReturn(true)
32-
fakeCacheStore.get.andReturn(cachedFiles[0].cacheBuffer)
33-
fakeCacheStore.set.reset()
34-
43+
delete Module._cache[require.resolve('./fixtures/native-cache/file-1')]
3544
fn1 = require('./fixtures/native-cache/file-1')
36-
37-
expect(fakeCacheStore.set).not.toHaveBeenCalled()
45+
expect(cachedFiles.length).toBe(2)
3846
expect(fn1()).toBe(1)
3947

40-
it "deletes previously cached code when the cache is not valid", ->
48+
describe "when v8 version changes", ->
49+
it "updates the cache of previously required files", ->
50+
nativeCompileCache.setV8Version("version-1")
51+
fn4 = require('./fixtures/native-cache/file-4')
52+
53+
expect(cachedFiles.length).toBe(1)
54+
expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-4'))
55+
expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array)
56+
expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0)
57+
expect(fn4()).toBe("file-4")
58+
59+
nativeCompileCache.setV8Version("version-2")
60+
delete Module._cache[require.resolve('./fixtures/native-cache/file-4')]
61+
fn4 = require('./fixtures/native-cache/file-4')
62+
63+
expect(cachedFiles.length).toBe(2)
64+
expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-4'))
65+
expect(cachedFiles[1].invalidationKey).not.toBe(cachedFiles[0].invalidationKey)
66+
expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array)
67+
expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0)
68+
69+
describe "when a previously required and cached file changes", ->
70+
beforeEach ->
71+
fs.writeFileSync path.resolve('./spec/fixtures/native-cache/file-5'), """
72+
module.exports = function () { return "file-5" }
73+
"""
74+
75+
afterEach ->
76+
fs.unlinkSync path.resolve('./spec/fixtures/native-cache/file-5')
77+
78+
it "removes it from the store and re-inserts it with the new cache", ->
79+
fn5 = require('./fixtures/native-cache/file-5')
80+
81+
expect(cachedFiles.length).toBe(1)
82+
expect(cachedFiles[0].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-5'))
83+
expect(cachedFiles[0].cacheBuffer).toBeInstanceOf(Uint8Array)
84+
expect(cachedFiles[0].cacheBuffer.length).toBeGreaterThan(0)
85+
expect(fn5()).toBe("file-5")
86+
87+
delete Module._cache[require.resolve('./fixtures/native-cache/file-5')]
88+
fs.appendFileSync(require.resolve('./fixtures/native-cache/file-5'), "\n\n")
89+
fn5 = require('./fixtures/native-cache/file-5')
90+
91+
expect(cachedFiles.length).toBe(2)
92+
expect(cachedFiles[1].cacheKey).toBe(require.resolve('./fixtures/native-cache/file-5'))
93+
expect(cachedFiles[1].invalidationKey).not.toBe(cachedFiles[0].invalidationKey)
94+
expect(cachedFiles[1].cacheBuffer).toBeInstanceOf(Uint8Array)
95+
expect(cachedFiles[1].cacheBuffer.length).toBeGreaterThan(0)
96+
97+
it "deletes previously cached code when the cache is an invalid file", ->
4198
fakeCacheStore.has.andReturn(true)
4299
fakeCacheStore.get.andCallFake -> new Buffer("an invalid cache")
43100

src/file-system-blob-store.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ class FileSystemBlobStore {
1313

1414
constructor (directory) {
1515
this.inMemoryBlobs = new Map()
16+
this.invalidationKeys = {}
1617
this.blobFilename = path.join(directory, 'BLOB')
1718
this.blobMapFilename = path.join(directory, 'MAP')
19+
this.invalidationKeysFilename = path.join(directory, 'INVKEYS')
1820
this.lockFilename = path.join(directory, 'LOCK')
1921
this.storedBlob = new Buffer(0)
2022
this.storedBlobMap = {}
@@ -27,14 +29,19 @@ class FileSystemBlobStore {
2729
if (!fs.existsSync(this.blobFilename)) {
2830
return
2931
}
32+
if (!fs.existsSync(this.invalidationKeysFilename)) {
33+
return
34+
}
3035
this.storedBlob = fs.readFileSync(this.blobFilename)
3136
this.storedBlobMap = JSON.parse(fs.readFileSync(this.blobMapFilename))
37+
this.invalidationKeys = JSON.parse(fs.readFileSync(this.invalidationKeysFilename))
3238
}
3339

3440
save () {
3541
let dump = this.getDump()
3642
let blobToStore = Buffer.concat(dump[0])
3743
let mapToStore = JSON.stringify(dump[1])
44+
let invalidationKeysToStore = JSON.stringify(this.invalidationKeys)
3845

3946
let acquiredLock = false
4047
try {
@@ -43,6 +50,7 @@ class FileSystemBlobStore {
4350

4451
fs.writeFileSync(this.blobFilename, blobToStore)
4552
fs.writeFileSync(this.blobMapFilename, mapToStore)
53+
fs.writeFileSync(this.invalidationKeysFilename, invalidationKeysToStore)
4654
} catch (error) {
4755
// Swallow the exception silently only if we fail to acquire the lock.
4856
if (error.code !== 'EEXIST') {
@@ -55,15 +63,20 @@ class FileSystemBlobStore {
5563
}
5664
}
5765

58-
has (key) {
59-
return this.inMemoryBlobs.hasOwnProperty(key) || this.storedBlobMap.hasOwnProperty(key)
66+
has (key, invalidationKey) {
67+
let containsKey = this.inMemoryBlobs.has(key) || this.storedBlobMap.hasOwnProperty(key)
68+
let isValid = this.invalidationKeys[key] === invalidationKey
69+
return containsKey && isValid
6070
}
6171

62-
get (key) {
63-
return this.getFromMemory(key) || this.getFromStorage(key)
72+
get (key, invalidationKey) {
73+
if (this.has(key, invalidationKey)) {
74+
return this.getFromMemory(key) || this.getFromStorage(key)
75+
}
6476
}
6577

66-
set (key, buffer) {
78+
set (key, invalidationKey, buffer) {
79+
this.invalidationKeys[key] = invalidationKey
6780
return this.inMemoryBlobs.set(key, buffer)
6881
}
6982

src/native-compile-cache.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
const Module = require('module')
44
const path = require('path')
55
const cachedVm = require('cached-run-in-this-context')
6+
const crypto = require('crypto')
7+
8+
function computeHash (contents) {
9+
return crypto.createHash('sha1').update(contents, 'utf8').digest('hex')
10+
}
611

712
class NativeCompileCache {
813
constructor () {
@@ -14,6 +19,10 @@ class NativeCompileCache {
1419
this.cacheStore = store
1520
}
1621

22+
setV8Version (v8Version) {
23+
this.v8Version = v8Version.toString()
24+
}
25+
1726
install () {
1827
this.savePreviousModuleCompile()
1928
this.overrideModuleCompile()
@@ -28,20 +37,20 @@ class NativeCompileCache {
2837
}
2938

3039
overrideModuleCompile () {
31-
let cacheStore = this.cacheStore
40+
let self = this
3241
let resolvedArgv = null
3342
// Here we override Node's module.js
3443
// (https://github.com/atom/node/blob/atom/lib/module.js#L378), changing
3544
// only the bits that affect compilation in order to use the cached one.
3645
Module.prototype._compile = function (content, filename) {
37-
let self = this
46+
let moduleSelf = this
3847
// remove shebang
3948
content = content.replace(/^\#\!.*/, '')
4049
function require (path) {
41-
return self.require(path)
50+
return moduleSelf.require(path)
4251
}
4352
require.resolve = function (request) {
44-
return Module._resolveFilename(request, self)
53+
return Module._resolveFilename(request, moduleSelf)
4554
}
4655
require.main = process.mainModule
4756

@@ -54,18 +63,20 @@ class NativeCompileCache {
5463
// create wrapper function
5564
let wrapper = Module.wrap(content)
5665

66+
let cacheKey = filename
67+
let invalidationKey = computeHash(wrapper + self.v8Version)
5768
let compiledWrapper = null
58-
if (cacheStore.has(filename)) {
59-
let buffer = cacheStore.get(filename)
69+
if (self.cacheStore.has(cacheKey, invalidationKey)) {
70+
let buffer = self.cacheStore.get(cacheKey, invalidationKey)
6071
let compilationResult = cachedVm.runInThisContextCached(wrapper, filename, buffer)
6172
compiledWrapper = compilationResult.result
6273
if (compilationResult.wasRejected) {
63-
cacheStore.delete(filename)
74+
self.cacheStore.delete(cacheKey)
6475
}
6576
} else {
6677
let compilationResult = cachedVm.runInThisContext(wrapper, filename)
6778
if (compilationResult.cacheBuffer) {
68-
cacheStore.set(filename, compilationResult.cacheBuffer)
79+
self.cacheStore.set(cacheKey, invalidationKey, compilationResult.cacheBuffer)
6980
}
7081
compiledWrapper = compilationResult.result
7182
}
@@ -88,8 +99,8 @@ class NativeCompileCache {
8899
global.v8debug.Debug.setBreakPoint(compiledWrapper, 0, 0)
89100
}
90101
}
91-
let args = [self.exports, require, self, filename, dirname, process, global]
92-
return compiledWrapper.apply(self.exports, args)
102+
let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global]
103+
return compiledWrapper.apply(moduleSelf.exports, args)
93104
}
94105
}
95106

static/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
path.join(process.env.ATOM_HOME, 'blob-store/')
2020
)
2121
NativeCompileCache.setCacheStore(blobStore)
22+
NativeCompileCache.setV8Version(process.versions.v8)
2223
NativeCompileCache.install()
2324

2425
// Normalize to make sure drive letter case is consistent on Windows

0 commit comments

Comments
 (0)