Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(gatsby-plugin-sharp): hot fix for "Generating image thumbnails" progress bar reporting duplicates that don't do actual processing #20839

Merged
merged 4 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions packages/gatsby-plugin-sharp/src/gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const {
setBoundActionCreators,
getProgressBar,
// queue: jobQueue,
// reportError,
} = require(`./index`)
const { getProgressBar, createOrGetProgressBar } = require(`./utils`)

const { setPluginOptions } = require(`./plugin-options`)

Expand Down Expand Up @@ -63,9 +63,52 @@ const finishProgressBar = () => {
exports.onPostBuild = () => finishProgressBar()
exports.onCreateDevServer = () => finishProgressBar()

exports.onPreBootstrap = ({ actions }, pluginOptions) => {
// TO-DO - figure this out before merging (it can't unconditionally return false),
// so we don't end up with duplicate progress bars (one from this plugin and one from core)
const GatsbyCoreVersionManagesProgressBar = () => false
pieh marked this conversation as resolved.
Show resolved Hide resolved

exports.onPreBootstrap = ({ actions, emitter, reporter }, pluginOptions) => {
setBoundActionCreators(actions)
setPluginOptions(pluginOptions)

// below is a hack / hot fix for confusing progress bar behaviour
// that doesn't recognize duplicate jobs, as it's now
// in gatsby core internals (if `createJobV2` is available)
// we should remove this or make this code path not hit
// (we should never use emitter in plugins)
// as soon as possible (possibly by moving progress bar handling
// inside jobs-manager in core)

// sanity check - in case we remove `emitter` at some point
if (!GatsbyCoreVersionManagesProgressBar() && emitter) {
// track how many image transformation each job has
// END_JOB_V2 doesn't contain that information
// so we store it in <JobContentHash, TransformsCount> map
// when job is created. Then retrieve that when job finishes
// and remove that entry from the map.
const imageCountInJobsMap = new Map()

emitter.on(`CREATE_JOB_V2`, action => {
if (action.plugin.name === `gatsby-plugin-sharp`) {
const job = action.payload.job
const imageCount = job.args.operations.length
imageCountInJobsMap.set(job.contentDigest, imageCount)
const progress = createOrGetProgressBar(reporter)
progress.addImageToProcess(imageCount)
}
})

emitter.on(`END_JOB_V2`, action => {
if (action.plugin.name === `gatsby-plugin-sharp`) {
const jobContentDigest = action.payload.jobContentDigest
const imageCount = imageCountInJobsMap.get(jobContentDigest)
const progress = createOrGetProgressBar(reporter)
progress.tick(imageCount)
imageCountInJobsMap.delete(jobContentDigest)
}
})
}

// normalizedOptions = setPluginOptions(pluginOptions)
}

Expand Down
63 changes: 4 additions & 59 deletions packages/gatsby-plugin-sharp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const {
const { memoizedTraceSVG, notMemoizedtraceSVG } = require(`./trace-svg`)
const duotone = require(`./duotone`)
const { IMAGE_PROCESSING_JOB_NAME } = require(`./gatsby-worker`)
const { createProgress } = require(`./utils`)

const imageSizeCache = new Map()
const getImageSize = file => {
Expand All @@ -36,46 +35,6 @@ const getImageSize = file => {
}
}

let progressBar
let pendingImagesCounter = 0
let firstPass = true
const createOrGetProgressBar = reporter => {
if (!progressBar) {
progressBar = createProgress(`Generating image thumbnails`, reporter)

const originalDoneFn = progressBar.done

// TODO this logic should be moved to the reporter.
// when done is called we remove the progressbar instance and reset all the things
// this will be called onPostBuild or when devserver is created
progressBar.done = () => {
originalDoneFn.call(progressBar)
progressBar = null
pendingImagesCounter = 0
}

// when we create a progressBar for the second time so when .done() has been called before
// we create a modified tick function that automatically stops the progressbar when total is reached
// this is used for development as we're watching for changes
if (!firstPass) {
let progressBarCurrentValue = 0
const originalTickFn = progressBar.tick
progressBar.tick = (ticks = 1) => {
originalTickFn.call(progressBar, ticks)
progressBarCurrentValue += ticks

if (progressBarCurrentValue === pendingImagesCounter) {
progressBar.done()
}
}
}
firstPass = false
}

return progressBar
}
exports.getProgressBar = () => progressBar

// Bound action creators should be set when passed to onPreInit in gatsby-node.
// ** It is NOT safe to just directly require the gatsby module **.
// There is no guarantee that the module resolved is the module executing!
Expand Down Expand Up @@ -148,16 +107,6 @@ function prepareQueue({ file, args }) {
}

function createJob(job, { reporter }) {
const progressBar = createOrGetProgressBar(reporter)

if (pendingImagesCounter === 0) {
progressBar.start()
}

const transformsCount = job.args.operations.length
pendingImagesCounter += transformsCount
progressBar.total = pendingImagesCounter

// Jobs can be duplicates and usually are long running tasks.
// Because of that we shouldn't use async/await and instead opt to use
// .then() /.catch() handlers, because this allows V8 to release
Expand All @@ -169,16 +118,12 @@ function createJob(job, { reporter }) {
if (boundActionCreators.createJobV2) {
promise = boundActionCreators.createJobV2(job)
} else {
promise = scheduleJob(job, boundActionCreators)
promise = scheduleJob(job, boundActionCreators, reporter)
}

promise
.catch(err => {
reporter.panic(err)
})
.then(() => {
progressBar.tick(transformsCount)
})
promise.catch(err => {
reporter.panic(err)
})

return promise
}
Expand Down
15 changes: 10 additions & 5 deletions packages/gatsby-plugin-sharp/src/scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fs = require(`fs-extra`)
const got = require(`got`)
const { createContentDigest } = require(`gatsby-core-utils`)
const worker = require(`./gatsby-worker`)
const { createOrGetProgressBar } = require(`./utils`)

const processImages = async (jobId, job, boundActionCreators) => {
try {
Expand All @@ -16,7 +17,7 @@ const processImages = async (jobId, job, boundActionCreators) => {
}

const jobsInFlight = new Map()
const scheduleJob = async (job, boundActionCreators) => {
const scheduleJob = async (job, boundActionCreators, reporter) => {
const inputPaths = job.inputPaths.filter(
inputPath => !fs.existsSync(path.join(job.outputDir, inputPath))
)
Expand Down Expand Up @@ -70,12 +71,16 @@ const scheduleJob = async (job, boundActionCreators) => {
{ name: `gatsby-plugin-sharp` }
)

const progressBar = createOrGetProgressBar(reporter)
const transformsCount = job.args.operations.length
progressBar.addImageToProcess(transformsCount)

const promise = new Promise((resolve, reject) => {
setImmediate(() => {
processImages(jobId, convertedJob, boundActionCreators).then(
resolve,
reject
)
processImages(jobId, convertedJob, boundActionCreators).then(result => {
progressBar.tick(transformsCount)
resolve(result)
}, reject)
})
})

Expand Down
58 changes: 57 additions & 1 deletion packages/gatsby-plugin-sharp/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
const ProgressBar = require(`progress`)

// TODO remove in V3
export function createProgress(message, reporter) {
function createGatsbyProgressOrFallbackToExternalProgressBar(
message,
reporter
) {
if (reporter && reporter.createProgress) {
return reporter.createProgress(message)
}
Expand All @@ -26,3 +29,56 @@ export function createProgress(message, reporter) {
},
}
}

let progressBar
let pendingImagesCounter = 0
let firstPass = true
const createOrGetProgressBar = reporter => {
if (!progressBar) {
progressBar = createGatsbyProgressOrFallbackToExternalProgressBar(
`Generating image thumbnails`,
reporter
)

const originalDoneFn = progressBar.done

// TODO this logic should be moved to the reporter.
// when done is called we remove the progressbar instance and reset all the things
// this will be called onPostBuild or when devserver is created
progressBar.done = () => {
originalDoneFn.call(progressBar)
progressBar = null
pendingImagesCounter = 0
}

progressBar.addImageToProcess = imageCount => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what we had in index file previously (moved it to utils because it's imported in few locations, so this avoids circular imports)

There is just addition of convenience addImageToProcess method to increase total (and start activity if needed) which would need to be copied to two places if it was not put here (or some other helper function)

if (pendingImagesCounter === 0) {
progressBar.start()
}
pendingImagesCounter += imageCount
progressBar.total = pendingImagesCounter
}

// when we create a progressBar for the second time so when .done() has been called before
// we create a modified tick function that automatically stops the progressbar when total is reached
// this is used for development as we're watching for changes
if (!firstPass) {
let progressBarCurrentValue = 0
const originalTickFn = progressBar.tick
progressBar.tick = (ticks = 1) => {
originalTickFn.call(progressBar, ticks)
progressBarCurrentValue += ticks

if (progressBarCurrentValue === pendingImagesCounter) {
progressBar.done()
}
}
}
firstPass = false
}

return progressBar
}

exports.createOrGetProgressBar = createOrGetProgressBar
exports.getProgressBar = () => progressBar