Skip to content

Commit 8525aaf

Browse files
committed
Eliminate duplicate css files using hashing.
Parsing css files is by far the most time consuming part of the aggregator. Over 90% of the wall time is used parsing css into ASTs. In a test project with around 250 css files, with many duplicates, parsing takes around 14 seconds on s 2,5GHz Core i5 M processer, while all the other tasks combined take less than two seconds. Therefore we want to do as little parsing as possible. The changes in this commit compares the xxHash of each file with a set of hashes of already processed files. Duplicates are then eliminated before they are handed over to the css parser. This results in a factor 8 speedup of the aggregation function on the test project, which now completes in around two seconds.
1 parent 906627d commit 8525aaf

File tree

2 files changed

+49
-27
lines changed

2 files changed

+49
-27
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"tslint": "latest"
4747
},
4848
"dependencies": {
49-
"css": "^2.2.1"
49+
"css": "^2.2.1",
50+
"xxhashjs": "^0.2.1"
5051
}
5152
}

src/cssAggregator.ts

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { workspace } from 'vscode'
22
import { parse, Rule } from 'css';
33
import { readFile } from 'fs'
4+
let XXH = require('xxhashjs').h32;
45

56
function flatten<T>(nestedArray: T[][]): T[] {
67
if (nestedArray.length === 0) {
@@ -28,8 +29,11 @@ function findClassName(selector: string): string {
2829

2930
export default function () {
3031

32+
let startTime = process.hrtime();
33+
let cssHashSet = new Set();
34+
3135
return workspace.findFiles('**/*.css', '').then(uris => {
32-
36+
3337
let cssTextPromises = uris.map(uri =>
3438
<PromiseLike<string>>new Promise<string>((resolve, reject) =>
3539
readFile(uri.fsPath, workspace.getConfiguration('files').get('encoding'), (err, data) => {
@@ -42,40 +46,57 @@ export default function () {
4246
)
4347
);
4448

45-
let cssASTPromises = cssTextPromises.map(
46-
cssTextPromise => cssTextPromise.then(cssText => parse(cssText), console.log)
47-
);
49+
let uniqueCssTextsPromise = Promise.all(cssTextPromises).then(
50+
cssTexts => cssTexts.filter(cssText => {
51+
let hash = XXH(cssText, 0x1337).toNumber();
52+
if (cssHashSet.has(hash)) {
53+
return false;
54+
} else {
55+
cssHashSet.add(hash);
56+
return true;
57+
}
58+
})
59+
);
60+
61+
let cssASTsPromise = uniqueCssTextsPromise.then(
62+
uniqueCssTexts => uniqueCssTexts.map(cssText => parse(cssText)),
63+
console.log
64+
);
4865

49-
let rulesPromises = cssASTPromises.map(
50-
cssASTPromise => cssASTPromise.then(cssAST =>
51-
<Rule[]>(cssAST.stylesheet.rules.filter(node => (<Rule>node).type === 'rule')),
52-
console.log)
53-
);
66+
let rulesPromise = cssASTsPromise.then(cssASTs => {
67+
if (cssASTs.length > 0) {
68+
return flatten(cssASTs.map(
69+
cssAST => <Rule[]>(cssAST.stylesheet.rules.filter(node => (<Rule>node).type === 'rule'))
70+
))
71+
} else {
72+
return [];
73+
}
74+
}, console.log);
5475

55-
let selectorsPromises = rulesPromises.map(
56-
rulesPromise => rulesPromise.then(rules => {
76+
let selectorsPromise = rulesPromise.then(rules => {
5777
if (rules.length > 0) {
5878
return flatten(rules.map(rule => rule.selectors));
5979
} else {
6080
return [];
6181
}
62-
}, console.log)
63-
);
82+
}, console.log);
6483

65-
let cssClassesPromises = selectorsPromises.map(
66-
selectorsPromise => selectorsPromise.then(selectors =>
84+
let cssClassesPromise = selectorsPromise.then(selectors =>
6785
selectors.map(selector => findClassName(selector)).filter(value => value !== ""),
68-
console.log)
69-
);
86+
console.log
87+
);
88+
89+
return cssClassesPromise.then(cssClasses => {
90+
let uniqueCssClasses = Array.from(new Set(cssClasses));
91+
92+
let elapsedTime = process.hrtime(startTime);
93+
94+
console.log(`Elapsed time: ${elapsedTime[0]} s ${Math.trunc(elapsedTime[1] / 1e6)} ms`);
95+
console.log(`Files processed: ${cssHashSet.size}`);
96+
console.log(`cssClasses discovered: ${uniqueCssClasses.length}`);
97+
7098

71-
return Promise.all(cssClassesPromises).then(cssClassesInAllFiles => {
72-
let allcssClasses = Array.from(new Set(
73-
cssClassesInAllFiles
74-
.filter(cssClasses => cssClasses.length > 0)
75-
.reduce((p, c) => p.concat(c))
76-
));
77-
78-
return allcssClasses;
99+
return uniqueCssClasses;
100+
}, console.log);
79101
}, console.log);
80-
}, console.log);
81102
}

0 commit comments

Comments
 (0)