Skip to content

Commit

Permalink
build-system: Introduce CSS Cache 🚀 (#33313)
Browse files Browse the repository at this point in the history
* Introduce cache for CSS.

Reuses the same infrastructure built for the esbuild cache.

* address raghu comments

* add file extensions
  • Loading branch information
samouri authored Mar 30, 2021
1 parent 582c330 commit 3a0a403
Showing 8 changed files with 234 additions and 143 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ firebase/**
out/**
test/coverage/**
.babel-cache/**
.css-cache/**

# Code directories
build-system/tasks/visual-diff/snippets/*.js
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ build/
.amp-dep-check
extensions/**/dist/**
c
.css-cache/
/dist
dist.3p
dist.tools
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.babel-cache/**
.css-cache/**
.github/ISSUE_TEMPLATE/**
**/package*.json
**/node_modules/**
98 changes: 3 additions & 95 deletions build-system/common/esbuild-babel.js
Original file line number Diff line number Diff line change
@@ -15,77 +15,19 @@
*/

const babel = require('@babel/core');
const crypto = require('crypto');
const fs = require('fs-extra');
const path = require('path');
const {TransformCache, batchedRead, md5} = require('./transform-cache');

/**
* Directory where the babel filecache lives.
*/
const CACHE_DIR = path.resolve(__dirname, '..', '..', '.babel-cache');

/**
* Cache for storing transformed files on both memory and on disk.
*/
class BabelTransformCache {
constructor() {
fs.ensureDirSync(CACHE_DIR);

/** @type {Map<string, Promise<Buffer|string>>} */
this.map = new Map();

/** @type {Set<string>} */
this.fsCache = new Set(fs.readdirSync(CACHE_DIR));
}

/**
* @param {string} hash
* @return {null|Promise<Buffer|string>}
*/
get(hash) {
const cached = this.map.get(hash);
if (cached) {
return cached;
}
if (this.fsCache.has(hash)) {
const transformedPromise = fs.readFile(path.join(CACHE_DIR, hash));
this.map.set(hash, transformedPromise);
return transformedPromise;
}
return null;
}

/**
* @param {string} hash
* @param {Promise<string>} transformPromise
*/
set(hash, transformPromise) {
if (this.map.has(hash)) {
throw new Error(
`Read race occured. Attempting to transform a file twice.`
);
}

this.map.set(hash, transformPromise);
transformPromise.then((contents) => {
fs.outputFile(path.join(CACHE_DIR, hash), contents);
});
}
}

/**
* Used to cache babel transforms done by esbuild.
* @const {!BabelTransformCache}
* @const {!TransformCache}
*/
const transformCache = new BabelTransformCache();

/**
* Used to cache file reads done by esbuild, since it can issue multiple
* "loads" per file. This batches consecutive reads into a single, and then
* clears its cache item for the next load.
* @private @const {!Map<string, Promise<{hash: string, contents: string}>>}
*/
const readCache = new Map();
const transformCache = new TransformCache(CACHE_DIR, '.js');

/**
* Creates a babel plugin for esbuild for the given caller. Optionally enables
@@ -102,40 +44,6 @@ function getEsbuildBabelPlugin(
preSetup = () => {},
postLoad = () => {}
) {
function md5(...args) {
if (!enableCache) {
return '';
}
const hash = crypto.createHash('md5');
for (const a of args) {
hash.update(a);
}
return hash.digest('hex');
}

/**
* @param {string} path
* @param {string} optionsHash
* @return {{contents: string, hash: string}}
*/
function batchedRead(path, optionsHash) {
let read = readCache.get(path);
if (!read) {
read = fs.promises
.readFile(path)
.then((contents) => ({
contents,
hash: md5(contents, optionsHash),
}))
.finally(() => {
readCache.delete(path);
});
readCache.set(path, read);
}

return read;
}

function transformContents(contents, hash, babelOptions) {
if (enableCache) {
const cached = transformCache.get(hash);
128 changes: 128 additions & 0 deletions build-system/common/transform-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const crypto = require('crypto');
const fs = require('fs-extra');
const path = require('path');

/**
* Cache for storing transformed files on both memory and on disk.
*/
class TransformCache {
constructor(cacheDir, fileExtension) {
/** @type {string} */
this.fileExtension = fileExtension;

/** @type {string} */
this.cacheDir = cacheDir;
fs.ensureDirSync(cacheDir);

/** @type {Map<string, Promise<Buffer|string>>} */
this.transformMap = new Map();

/** @type {Set<string>} */
this.fsCache = new Set(fs.readdirSync(cacheDir));
}

/**
* @param {string} hash
* @return {null|Promise<Buffer|string>}
*/
get(hash) {
const cached = this.transformMap.get(hash);
if (cached) {
return cached;
}
const filename = hash + this.fileExtension;
if (this.fsCache.has(filename)) {
const transformedPromise = fs.readFile(
path.join(this.cacheDir, filename)
);
this.transformMap.set(hash, transformedPromise);
return transformedPromise;
}
return null;
}

/**
* @param {string} hash
* @param {Promise<string>} transformPromise
*/
set(hash, transformPromise) {
if (this.transformMap.has(hash)) {
throw new Error(
`Read race occured. Attempting to transform a file twice.`
);
}

this.transformMap.set(hash, transformPromise);
const filepath = path.join(this.cacheDir, hash) + this.fileExtension;
transformPromise.then((contents) => {
fs.outputFile(filepath, contents);
});
}
}

/**
* Returns the md5 hash of provided args.
*
* @param {...(string|Buffer)} args
* @return {string}
*/
function md5(...args) {
const hash = crypto.createHash('md5');
for (const a of args) {
hash.update(a);
}
return hash.digest('hex');
}

/**
* Used to cache file reads, since some (esbuild) will have multiple
* "loads" per file. This batches consecutive reads into a single, and then
* clears its cache item for the next load.
* @private @const {!Map<string, Promise<{hash: string, contents: string}>>}
*/
const readCache = new Map();

/**
* Returns the string contents and hash of the file at the specified path.
* If multiple reads are requested for the same file before the first read has completed,
* the result will be reused.
*
* @param {string} path
* @param {string=} optionsHash
* @return {{contents: string, hash: string}}
*/
function batchedRead(path, optionsHash) {
let read = readCache.get(path);
if (!read) {
read = fs.promises
.readFile(path)
.then((contents) => ({
contents,
hash: md5(contents, optionsHash ?? ''),
}))
.finally(() => {
readCache.delete(path);
});
readCache.set(path, read);
}

return read;
}

module.exports = {TransformCache, batchedRead, md5};
1 change: 1 addition & 0 deletions build-system/tasks/clean.js
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ async function clean() {
const pathsToDelete = [
'.amp-dep-check',
'.babel-cache',
'.css-cache',
'build',
'extensions/**/dist',
'build-system/server/new-server/transforms/dist',
12 changes: 6 additions & 6 deletions build-system/tasks/css/init-sync.js
Original file line number Diff line number Diff line change
@@ -15,17 +15,17 @@
*/
'use strict';

const {transformCss} = require('./jsify-css');
const {transformCssString} = require('./jsify-css');

/**
* Wrapper for the asynchronous transformCss that is used by transformCssSync()
* Wrapper for the asynchronous transformCssString that is used by transformCssSync()
* in build-system/tasks/css/jsify-css-sync.js.
* @return {function(string, !Object=, !Object=): ReturnType<transformCss>}
*
* @return {function(string, !Object=, !Object=): ReturnType<transformCssString>}
*/
function init() {
return function (cssStr, opt_cssnano, opt_filename) {
return Promise.resolve(transformCss(cssStr, opt_cssnano, opt_filename));
return function (cssStr, opt_filename) {
return Promise.resolve(transformCssString(cssStr, opt_filename));
};
}
module.exports = init;
Loading

0 comments on commit 3a0a403

Please sign in to comment.