Skip to content

Commit

Permalink
Reduce excess file system writes by implementing query queue
Browse files Browse the repository at this point in the history
There are multiple ways that running queries can be triggered. Previous
to this PR, Gatsby had done some de-duping of queries but in practice,
queries for pages/layouts can often be running multiple times in short
succession. This is normally fine-ish but @Gaeron was running into
troubles while editing markdown on reactjs.org where webpack would error
when it detected a change in a file and before it could read it, Gatsby
would start writing to the file again before webpack could finish
reading it resulting in a JSON.parse error due to the incomplete JSON.

On top of this, Gatsby would write out the result of every query if even
the query returned the same result as previously.

To address these this PR adds a query queue that deduplicates
added pages/layouts. This means we're writing out results far less
often.

Second we hash each query result and only write to file if the result
has changed which means far less work for webpack and far faster hot
reloading of query updates to the browser.
  • Loading branch information
KyleAMathews committed Dec 16, 2017
1 parent 8bd5b16 commit 719cd41
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 75 deletions.
6 changes: 3 additions & 3 deletions docs/docs/gatsby-starters.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,11 @@ Community:
* Colorful homepage, and also includes a Landing Page and Generic Page components.
* Many elements are available, including buttons, forms, tables, and pagination.
* Styling with SCSS

* [gatsby-firebase-authentication](https://github.com/rwieruch/gatsby-firebase-authentication) [(demo)](https://react-firebase-authentication.wieruch.com/)

Features:

* Sign In, Sign Up, Sign Out
* Password Forget
* Password Change
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"babel-runtime": "^6.26.0",
"babel-traverse": "^6.24.1",
"babylon": "^6.17.3",
"better-queue": "^3.8.6",
"bluebird": "^3.5.0",
"chalk": "^1.1.3",
"chokidar": "^1.7.0",
Expand Down Expand Up @@ -66,6 +67,7 @@
"lodash": "^4.17.4",
"lodash-id": "^0.14.0",
"lowdb": "^0.16.2",
"md5": "^2.2.1",
"md5-file": "^3.1.1",
"mime": "^1.3.6",
"mitt": "^1.1.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/gatsby/src/bootstrap/page-hot-reloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const runCreatePages = async () => {
deleteComponentsDependencies([page.path])
deletePage(page)
})

emitter.emit(`CREATE_PAGE_END`)
}

const debouncedCreatePages = _.debounce(runCreatePages, 100)
Expand Down
1 change: 0 additions & 1 deletion packages/gatsby/src/commands/data-explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,3 @@ module.exports = async (program: any) => {
console.log(`Gatsby data explorer running at`, `http://${host}:${port}`)
app.listen(port, host)
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
*/

const _ = require(`lodash`)
const async = require(`async`)

const queue = require(`./query-queue`)
const { store, emitter } = require(`../../redux`)
const queryRunner = require(`./query-runner`)

let queuedDirtyActions = []
let active = false
Expand Down Expand Up @@ -95,34 +94,27 @@ const findIdsWithoutDataDependencies = () => {
}

const runQueriesForIds = ids => {
ids = _.uniq(ids)
if (ids.length < 1) {
const state = store.getState()
const pagesAndLayouts = [...state.pages, ...state.layouts]
let didNotQueueItems = true
ids.forEach(id => {
const plObj = pagesAndLayouts.find(
pl => pl.path === id || `LAYOUT___${pl.id}` === id
)
if (plObj) {
didNotQueueItems = false
queue.push({ ...plObj, _id: plObj.id, id: plObj.jsonName })
}
})

if (didNotQueueItems || !ids || ids.length === 0) {
return Promise.resolve()
}
const state = store.getState()

return new Promise((resolve, reject) => {
async.mapLimit(
ids,
4,
(id, callback) => {
const pagesAndLayouts = [...state.pages, ...state.layouts]
const plObj = pagesAndLayouts.find(
pl => pl.path === id || `LAYOUT___${pl.id}` === id
)
if (plObj) {
return queryRunner(plObj, state.components[plObj.component]).then(
result => callback(null, result),
error => callback(error)
)
} else {
return callback(null, null)
}
},
(error, result) => {
error ? reject(error) : resolve(result)
}
)
return new Promise(resolve => {
queue.on(`drain`, () => {
resolve()
})
})
}

Expand Down
44 changes: 23 additions & 21 deletions packages/gatsby/src/internal-plugins/query-runner/pages-writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ const writePages = async () => {
pageLayouts = _.uniq(pageLayouts)
components = _.uniqBy(components, c => c.componentChunkName)

await fs.writeFile(
joinPath(program.directory, `.cache/pages.json`),
JSON.stringify(pagesData, null, 4)
)

// Create file with sync requires of layouts/components/json files.
let syncRequires = `// prefer default export if available
const preferDefault = m => m && m.default || m
Expand Down Expand Up @@ -93,10 +88,6 @@ const preferDefault = m => m && m.default || m
.join(`,\n`)}
}`

await fs.writeFile(
`${program.directory}/.cache/sync-requires.js`,
syncRequires
)
// Create file with async requires of layouts/components/json files.
let asyncRequires = `// prefer default export if available
const preferDefault = m => m && m.default || m
Expand Down Expand Up @@ -131,10 +122,17 @@ const preferDefault = m => m && m.default || m
.join(`,\n`)}
}`

await fs.writeFile(
joinPath(program.directory, `.cache/async-requires.js`),
asyncRequires
)
await Promise.all([
fs.writeFile(
joinPath(program.directory, `.cache/pages.json`),
JSON.stringify(pagesData, null, 4)
),
fs.writeFile(`${program.directory}/.cache/sync-requires.js`, syncRequires),
fs.writeFile(
joinPath(program.directory, `.cache/async-requires.js`),
asyncRequires
),
])

return
}
Expand All @@ -143,15 +141,19 @@ exports.writePages = writePages

let bootstrapFinished = false
let oldPages
const debouncedWritePages = _.debounce(() => {
// Don't write pages again until bootstrap has finished.
if (bootstrapFinished && !_.isEqual(oldPages, store.getState().pages)) {
writePages()
oldPages = store.getState().pages
}
}, 250)
const debouncedWritePages = _.debounce(
() => {
// Don't write pages again until bootstrap has finished.
if (bootstrapFinished && !_.isEqual(oldPages, store.getState().pages)) {
writePages()
oldPages = store.getState().pages
}
},
500,
{ leading: true }
)

emitter.on(`CREATE_PAGE`, () => {
emitter.on(`CREATE_PAGE_END`, () => {
debouncedWritePages()
})
emitter.on(`DELETE_PAGE`, () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/gatsby/src/internal-plugins/query-runner/query-queue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const Queue = require(`better-queue`)

const queryRunner = require(`./query-runner`)
const { store } = require(`../../redux`)

const queue = new Queue(
(plObj, callback) => {
const state = store.getState()
return queryRunner(plObj, state.components[plObj.component]).then(
result => callback(null, result),
error => callback(error)
)
},
{ concurrent: 4 }
)

module.exports = queue
21 changes: 16 additions & 5 deletions packages/gatsby/src/internal-plugins/query-runner/query-runner.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { graphql as graphqlFunction } from "graphql"
const fs = require(`fs-extra`)
const report = require(`gatsby-cli/lib/reporter`)
const md5 = require(`md5`)

const { joinPath } = require(`../../utils/path`)
const { store } = require(`../../redux`)

const resultHashes = {}

// Run query for a page
module.exports = async (pageOrLayout, component) => {
pageOrLayout.id = pageOrLayout._id
const { schema, program } = store.getState()

const graphql = (query, context) =>
Expand Down Expand Up @@ -51,10 +55,17 @@ module.exports = async (pageOrLayout, component) => {
contextKey = `layoutContext`
}
result[contextKey] = pageOrLayout.context
const resultJSON = JSON.stringify(result, null, 4)

await fs.writeFile(
joinPath(program.directory, `.cache`, `json`, pageOrLayout.jsonName),
resultJSON
const resultJSON = JSON.stringify(result)
const resultHash = md5(resultJSON)
const resultPath = joinPath(
program.directory,
`.cache`,
`json`,
pageOrLayout.jsonName
)

if (resultHashes[resultPath] !== resultHash) {
resultHashes[resultPath] = resultHash
await fs.writeFile(resultPath, resultJSON)
}
}
24 changes: 7 additions & 17 deletions packages/gatsby/src/internal-plugins/query-runner/query-watcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@

const _ = require(`lodash`)
const chokidar = require(`chokidar`)
const async = require(`async`)

const { store } = require(`../../redux/`)
const { boundActionCreators } = require(`../../redux/actions`)
const queryCompiler = require(`./query-compiler`).default
const queryRunner = require(`./query-runner`)
const queue = require(`./query-queue`)
const invariant = require(`invariant`)
const normalize = require(`normalize-path`)

Expand Down Expand Up @@ -57,21 +56,12 @@ const runQueriesForComponent = componentPath => {
boundActionCreators.deleteComponentsDependencies(
pages.map(p => p.path || p.id)
)
const component = store.getState().components[componentPath]
return new Promise((resolve, reject) => {
async.mapLimit(
pages,
4,
(page, callback) => {
queryRunner(page, component).then(
result => callback(null, result),
error => callback(error)
)
},
(error, result) => {
error ? reject(error) : resolve(result)
}
)
pages.forEach(page =>
queue.push({ ...page, _id: page.id, id: page.jsonName })
)

return new Promise(resolve => {
queue.on(`drain`, () => resolve())
})
}

Expand Down
18 changes: 17 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,18 @@ better-assert@~1.0.0:
dependencies:
callsite "1.0.0"

better-queue-memory@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-queue-memory/-/better-queue-memory-1.0.2.tgz#aa6d169aa1d0cc77409185cb9cb5c7dc251bcd41"

better-queue@^3.8.6:
version "3.8.6"
resolved "https://registry.yarnpkg.com/better-queue/-/better-queue-3.8.6.tgz#73220bdfab403924cffa7497220dd387abb73a63"
dependencies:
better-queue-memory "^1.0.1"
node-eta "^0.9.0"
uuid "^3.0.0"

big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
Expand Down Expand Up @@ -7705,7 +7717,7 @@ md5.js@^1.3.4:
hash-base "^3.0.0"
inherits "^2.0.1"

md5@^2.0.0:
md5@^2.0.0, md5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9"
dependencies:
Expand Down Expand Up @@ -8161,6 +8173,10 @@ node-emoji@^1.0.4:
dependencies:
lodash.toarray "^4.4.0"

node-eta@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.9.0.tgz#9fb0b099bcd2a021940e603c64254dc003d9a7a8"

node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
Expand Down

0 comments on commit 719cd41

Please sign in to comment.