Skip to content

Commit

Permalink
When used in Eleventy, this adds more insight into image processing c…
Browse files Browse the repository at this point in the history
…ost during build. Adds per-image image processing output when not in Eleventy’s quiet mode. Adds an after-build summary message.
  • Loading branch information
zachleat committed May 10, 2024
1 parent 06c9505 commit e958119
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 11 deletions.
68 changes: 66 additions & 2 deletions img.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ const getImageSize = require("image-size");
const sharp = require("sharp");
const brotliSize = require("brotli-size");
const { RemoteAssetCache, queue } = require("@11ty/eleventy-fetch");
const { TemplatePath } = require("@11ty/eleventy-utils");

const svgHook = require("./src/format-hooks/svg.js");
const MemoryCache = require("./src/memory-cache.js");
const DiskCache = require("./src/disk-cache.js");
const BuildLogger = require("./src/build-logger.js");
const Util = require("./src/util.js");

const debug = require("debug")("Eleventy:Image");

const KEYS = {
requested: "requested"
};

const GLOBAL_OPTIONS = {
widths: ["auto"],
formats: ["webp", "jpeg"], // "png", "svg", "avif"
Expand Down Expand Up @@ -539,7 +545,7 @@ class Image {
let fullStats = this.getFullStats(metadata);
for(let outputFormat in fullStats) {
for(let stat of fullStats[outputFormat]) {
if(this.options.useCache && diskCache.isCached(stat.outputPath)){
if(this.options.useCache && diskCache.isCached(stat.outputPath, input, this.options.generatedVia !== KEYS.requested)){
// Cached images already exist in output
let contents;
if(this.options.dryRun) {
Expand Down Expand Up @@ -716,6 +722,8 @@ class ImagePath {
/* Size Cache */
let memCache = new MemoryCache();
let diskCache = new DiskCache();
let deferCount = 0;
let buildLogger = new BuildLogger();

/* Queue */
let processingQueue = new PQueue({
Expand All @@ -727,18 +735,72 @@ processingQueue.on("active", () => {

function queueImage(src, opts) {
let img = new Image(src, opts);
let eleventyConfig = opts?.eleventyConfig;
let key;
let resolvedOptions = img.options;

if(typeof eleventyConfig?.logger?.logWithOptions === "function") {
if(opts.generatedVia !== KEYS.requested) {
buildLogger.setupOnce(eleventyConfig, () => {
// before build
deferCount = 0;
memCache.resetCount();
diskCache.resetCount();
}, () => {
// after build
let [memoryCacheHit] = memCache.getCount();
let [diskCacheHit, diskCacheMiss] = diskCache.getCount();

let cachedCount = memoryCacheHit + diskCacheHit;
let optimizedCount = diskCacheMiss + diskCacheHit + memoryCacheHit + deferCount;

let msg = [];
msg.push(`${optimizedCount} ${optimizedCount !== 1 ? "images" : "image"} optimized`);

if(cachedCount > 0 || deferCount > 0) {
let innerMsg = [];
if(cachedCount > 0) {
innerMsg.push(`${cachedCount} cached`);
}
if(deferCount > 0) {
innerMsg.push(`${deferCount} deferred`);
}
msg.push(` (${innerMsg.join(", ")})`);
}

eleventyConfig?.logger?.logWithOptions({
message: msg.join(""),
prefix: "[11ty/eleventy-img]",
force: true,
color: "green",
});
});
}
}

if(opts.transformOnRequest) {
deferCount++;
}

if(resolvedOptions.useCache) {
// we don’t know the output format yet, but this hash is just for the in memory cache
key = img.getInMemoryCacheKey();
let cached = memCache.get(key);
let cached = memCache.get(key, !opts.transformOnRequest && opts.generatedVia !== KEYS.requested);
if(cached) {
return cached;
}
}

if(typeof eleventyConfig?.logger?.logWithOptions === "function") {
if(!resolvedOptions.statsOnly) {
let logSrc = path.isAbsolute(src) ? TemplatePath.addLeadingDotSlash(path.relative(path.resolve("."), src)) : src;
eleventyConfig.logger.logWithOptions({
message: `Processing ${logSrc} (${opts.generatedVia})`,
prefix: "[11ty/eleventy-img]"
});
}
}

debug("Processing %o (in-memory cache miss), options: %o", src, opts);

let promise = processingQueue.add(async () => {
Expand All @@ -754,6 +816,7 @@ function queueImage(src, opts) {
}

// Fetch remote image to operate on it
// `remoteImageMetadata` is no longer required for statsOnly on remote images
src = await img.getInput();
}

Expand Down Expand Up @@ -803,6 +866,7 @@ module.exports.statsSync = Image.statsSync;
module.exports.statsByDimensionsSync = Image.statsByDimensionsSync;
module.exports.getFormats = Image.getFormatsArray;
module.exports.getWidths = Image.getValidWidths;
module.exports.keys = KEYS;

module.exports.getHash = function getHash(src, options) {
let img = new Image(src, options);
Expand Down
23 changes: 23 additions & 0 deletions src/build-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class BuildLogger {
constructor() {
this.hasAssigned = false;
}

setupOnce(eleventyConfig, beforeCallback, afterCallback) {
if(this.hasAssigned) {
return;
}

this.hasAssigned = true;

eleventyConfig.on("eleventy.before", beforeCallback);
eleventyConfig.on("eleventy.after", afterCallback);

eleventyConfig.on("eleventy.reset", () => {
this.hasAssigned = false;
beforeCallback(); // we run this on reset because the before callback will have disappeared (as the config reset)
});
}
}

module.exports = BuildLogger;
29 changes: 27 additions & 2 deletions src/disk-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@ const fs = require("fs");
class DiskCache {
constructor() {
this.hitCounter = 0;
this.missCounter = 0;
this.inputs = new Map();
}

isCached(path) {
resetCount() {
this.hitCounter = 0;
this.missCounter = 0;
}

getCount() {
return [this.hitCounter, this.missCounter];
}

isCached(path, input, incrementCounts = true) {
// Disk cache runs once per output file, so we only want to increment counts once per input
if(this.inputs.has(input)) {
incrementCounts = false;
}
this.inputs.set(input, true);

if(fs.existsSync(path)) {
this.hitCounter++;
if(incrementCounts) {
this.hitCounter++;
}

// debug("Images re-used (via disk cache): %o", this.hitCounter);
return true;
}

if(incrementCounts) {
this.inputs.set(input, true);
this.missCounter++;
}

return false;
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/global-options.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
const path = require("path");

function getGlobalOptions(directories, options, via) {
function getGlobalOptions(eleventyConfig, options, via) {
let directories = eleventyConfig.directories;
let globalOptions = Object.assign({
packages: {
image: require("../"),
},
outputDir: path.join(directories.output, options.urlPath || ""),
}, options);

// globalOptions.eleventyConfig = eleventyConfig;
globalOptions.directories = directories;
globalOptions.generatedVia = via;

Object.defineProperty(globalOptions, "eleventyConfig", {
value: eleventyConfig,
enumerable: false,
});

return globalOptions;
}

Expand Down
7 changes: 7 additions & 0 deletions src/image-attrs-to-posthtml-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ async function imageAttributesToPosthtmlNode(attributes, instanceOptions, global
}
}

let cfg = globalPluginOptions.eleventyConfig;
let options = Object.assign({}, globalPluginOptions, instanceOptions);

Object.defineProperty(options, "eleventyConfig", {
value: cfg,
enumerable: false,
});

let metadata = await eleventyImage(attributes.src, options);
let imageAttributes = Object.assign({}, globalPluginOptions.defaultAttributes, attributes);

Expand Down
20 changes: 18 additions & 2 deletions src/memory-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ class MemoryCache {
constructor() {
this.cache = {};
this.hitCounter = 0;
this.missCounter = 0;
}

resetCount() {
this.hitCounter = 0;
this.missCounter = 0;
}

getCount() {
return [this.hitCounter, this.missCounter];
}

add(key, results) {
Expand All @@ -14,15 +24,21 @@ class MemoryCache {
debug("Unique images processed: %o", Object.keys(this.cache).length);
}

get(key) {
get(key, incrementCounts = false) {
if(this.cache[key]) {
this.hitCounter++;
if(incrementCounts) {
this.hitCounter++;
}
// debug("Images re-used (via in-memory cache): %o", this.hitCounter);

// may return promise
return this.cache[key].results;
}

if(incrementCounts) {
this.missCounter++;
}

return false;
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/on-request-during-serve-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const fs = require("fs");
const { TemplatePath } = require("@11ty/eleventy-utils");

const eleventyImage = require("../img.js");
const KEYS = eleventyImage.keys;
const Util = require("./util.js");

const debug = require("debug")("Eleventy:Image");
Expand Down Expand Up @@ -37,15 +38,24 @@ function eleventyImageOnRequestDuringServePlugin(eleventyConfig, options = {}) {
let opts = Object.assign({}, defaultOptions, options, {
widths: [width || "auto"],
formats: [imageFormat || "auto"],
transformOnRequest: false, // use the built images so we don’t go in a loop

dryRun: true,
cacheOptions: {
// We *do* want to write files to .cache for re-use here.
dryRun: false
}
},

transformOnRequest: false, // use the built images so we don’t go in a loop
generatedVia: KEYS.requested
});

if(eleventyConfig) {
Object.defineProperty(opts, "eleventyConfig", {
value: eleventyConfig,
enumerable: false,
});
}

debug( `%o transformed on request to %o at %o width.`, src, imageFormat, width );

try {
Expand Down
2 changes: 1 addition & 1 deletion src/transform-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function eleventyImageTransformPlugin(eleventyConfig, options = {}) {

// Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above.
// Devs can pass in the same object to both if they want!
let opts = getGlobalOptions(eleventyConfig.directories, options, "transform");
let opts = getGlobalOptions(eleventyConfig, options, "transform");

eleventyConfig.addJavaScriptFunction("__private_eleventyImageTransformConfigurationOptions", () => {
return opts;
Expand Down
2 changes: 1 addition & 1 deletion src/webc-options-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function eleventyWebcOptionsPlugin(eleventyConfig, options = {}) {
// Notably, global options are not shared automatically with the `eleventyImageTransformPlugin` below.
// Devs can pass in the same object to both if they want!
eleventyConfig.addJavaScriptFunction("__private_eleventyImageConfigurationOptions", () => {
return getGlobalOptions(eleventyConfig.directories, options, "webc");
return getGlobalOptions(eleventyConfig, options, "webc");
});

if(options.transformOnRequest !== false) {
Expand Down

0 comments on commit e958119

Please sign in to comment.