Skip to content

Commit 5e254e2

Browse files
piehDSchau
authored andcommitted
fix(gatsby-plugin-sharp): don't write to same temporary time multiple times at a same time (#12927)
Memoization key for tracedSVG was not entirely correct and we were trying to write to temporary file multiple times. This was causing those temporary files to become corrupted in dependencies of `potrace` were crashing while trying to read those corrupted input files (there was a lot of concurrency, so that's why those crashes were very random - sometimes timing was bad and we would end up . This _should_ fix memoization key and avoid writing to same file more than once - there is also extra memoization layer just for that, so if general memoization key for `traceSVG` function fails, there is another memoization that checks just temporary file location. This was happening most likely when there were copies of same image in multiple places (as memoization key was using `absolutePath` before). I've moved some code around (mostly because I needed to export some trace SVG related functions for added tests). I'll try to mark places in removed code that did change. related issue #8301
1 parent e3fc602 commit 5e254e2

File tree

6 files changed

+459
-178
lines changed

6 files changed

+459
-178
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
jest.mock(`sharp`, () => {
2+
const sharp = path => {
3+
const pipeline = {
4+
rotate: () => pipeline,
5+
resize: () => pipeline,
6+
png: () => pipeline,
7+
jpeg: () => pipeline,
8+
toFile: (_, cb) => cb(),
9+
}
10+
return pipeline
11+
}
12+
13+
sharp.simd = () => {}
14+
15+
return sharp
16+
})
17+
18+
jest.mock(`potrace`, () => {
19+
return {
20+
trace: (_, _2, cb) => cb(null, ``),
21+
Potrace: {
22+
TURNPOLICY_MAJORITY: `wat`,
23+
},
24+
}
25+
})
26+
27+
const path = require(`path`)
28+
29+
const traceSVGHelpers = require(`../trace-svg`)
30+
31+
const notMemoizedtraceSVG = jest.spyOn(traceSVGHelpers, `notMemoizedtraceSVG`)
32+
const notMemoizedPrepareTraceSVGInputFile = jest.spyOn(
33+
traceSVGHelpers,
34+
`notMemoizedPrepareTraceSVGInputFile`
35+
)
36+
// note that we started spying on not memoized functions first
37+
// now we recreate memoized functions that will use function we just started
38+
// spying on
39+
traceSVGHelpers.createMemoizedFunctions()
40+
const memoizedTraceSVG = jest.spyOn(traceSVGHelpers, `memoizedTraceSVG`)
41+
const memoizedPrepareTraceSVGInputFile = jest.spyOn(
42+
traceSVGHelpers,
43+
`memoizedPrepareTraceSVGInputFile`
44+
)
45+
46+
const { traceSVG } = require(`../`)
47+
48+
function getFileObject(absolutePath, name = path.parse(absolutePath).name) {
49+
return {
50+
id: `${absolutePath} absPath of file`,
51+
name: name,
52+
absolutePath,
53+
extension: `png`,
54+
internal: {
55+
contentDigest: `1234`,
56+
},
57+
}
58+
}
59+
60+
describe(`traceSVG memoization`, () => {
61+
const file = getFileObject(path.join(__dirname, `images/test.png`))
62+
const copyOfFile = getFileObject(path.join(__dirname, `images/test-copy.png`))
63+
const differentFile = getFileObject(
64+
path.join(__dirname, `images/different.png`)
65+
)
66+
differentFile.internal.contentDigest = `4321`
67+
68+
beforeEach(() => {
69+
traceSVGHelpers.clearMemoizeCaches()
70+
memoizedTraceSVG.mockClear()
71+
notMemoizedtraceSVG.mockClear()
72+
memoizedPrepareTraceSVGInputFile.mockClear()
73+
notMemoizedPrepareTraceSVGInputFile.mockClear()
74+
})
75+
76+
it(`Baseline`, async () => {
77+
await traceSVG({
78+
file,
79+
})
80+
81+
expect(memoizedTraceSVG).toBeCalledTimes(1)
82+
expect(notMemoizedtraceSVG).toBeCalledTimes(1)
83+
expect(memoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
84+
expect(notMemoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
85+
})
86+
87+
it(`Is memoizing results for same args`, async () => {
88+
await traceSVG({
89+
file,
90+
})
91+
92+
await traceSVG({
93+
file,
94+
})
95+
96+
expect(memoizedTraceSVG).toBeCalledTimes(2)
97+
expect(notMemoizedtraceSVG).toBeCalledTimes(1)
98+
expect(memoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
99+
expect(notMemoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
100+
})
101+
102+
it(`Is calling functions with same input file when params change`, async () => {
103+
await traceSVG({
104+
file,
105+
args: {
106+
color: `red`,
107+
},
108+
fileArgs: {
109+
width: 400,
110+
},
111+
})
112+
await traceSVG({
113+
file,
114+
args: {
115+
color: `blue`,
116+
},
117+
fileArgs: {
118+
width: 400,
119+
},
120+
})
121+
await traceSVG({
122+
file,
123+
args: {
124+
color: `red`,
125+
},
126+
fileArgs: {
127+
width: 200,
128+
},
129+
})
130+
await traceSVG({
131+
file,
132+
args: {
133+
color: `blue`,
134+
},
135+
fileArgs: {
136+
width: 200,
137+
},
138+
})
139+
await traceSVG({
140+
file: differentFile,
141+
args: {
142+
color: `red`,
143+
},
144+
fileArgs: {
145+
width: 400,
146+
},
147+
})
148+
149+
expect(memoizedTraceSVG).toBeCalledTimes(5)
150+
expect(notMemoizedtraceSVG).toBeCalledTimes(5)
151+
expect(memoizedPrepareTraceSVGInputFile).toBeCalledTimes(5)
152+
// trace svg should be actually created just 3 times
153+
// because it's affected just by `fileArgs`, and not `args`
154+
// this makes sure we don't try to write to same input file multiple times
155+
expect(notMemoizedPrepareTraceSVGInputFile).toBeCalledTimes(3)
156+
expect(notMemoizedPrepareTraceSVGInputFile).toHaveBeenNthCalledWith(
157+
1,
158+
expect.objectContaining({
159+
file,
160+
options: expect.objectContaining({
161+
width: 400,
162+
}),
163+
})
164+
)
165+
expect(notMemoizedPrepareTraceSVGInputFile).toHaveBeenNthCalledWith(
166+
2,
167+
expect.objectContaining({
168+
file,
169+
options: expect.objectContaining({
170+
width: 200,
171+
}),
172+
})
173+
)
174+
expect(notMemoizedPrepareTraceSVGInputFile).toHaveBeenNthCalledWith(
175+
3,
176+
expect.objectContaining({
177+
file: differentFile,
178+
options: expect.objectContaining({
179+
width: 400,
180+
}),
181+
})
182+
)
183+
184+
const usedTmpFilePaths = notMemoizedPrepareTraceSVGInputFile.mock.calls.map(
185+
args => args[0].tmpFilePath
186+
)
187+
188+
// tmpFilePath was always unique
189+
expect(usedTmpFilePaths.length).toBe(new Set(usedTmpFilePaths).size)
190+
})
191+
192+
it(`Use memoized results for file copies`, async () => {
193+
await traceSVG({
194+
file,
195+
args: {
196+
color: `red`,
197+
},
198+
fileArgs: {
199+
width: 400,
200+
},
201+
})
202+
await traceSVG({
203+
file: copyOfFile,
204+
args: {
205+
color: `red`,
206+
},
207+
fileArgs: {
208+
width: 400,
209+
},
210+
})
211+
212+
expect(memoizedTraceSVG).toBeCalledTimes(2)
213+
expect(notMemoizedtraceSVG).toBeCalledTimes(1)
214+
expect(memoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
215+
expect(notMemoizedPrepareTraceSVGInputFile).toBeCalledTimes(1)
216+
})
217+
})

packages/gatsby-plugin-sharp/src/gatsby-node.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const {
22
setBoundActionCreators,
3-
setPluginOptions,
43
// queue: jobQueue,
54
// reportError,
65
} = require(`./index`)
6+
7+
const { setPluginOptions } = require(`./plugin-options`)
8+
79
// const { scheduleJob } = require(`./scheduler`)
810
// let normalizedOptions = {}
911

0 commit comments

Comments
 (0)