Skip to content

Commit 9debdd3

Browse files
authored
Merge pull request webpack#8650 from webpack/performance/webpack-sources
Caching of utf-8 conversion, memory improvement, refactoring
2 parents 865011f + c5ad6df commit 9debdd3

10 files changed

+340
-202
lines changed

declarations.d.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,101 @@ declare module "@webassemblyjs/ast" {
220220
export function isFuncImportDescr(n: Node): boolean;
221221
}
222222

223+
declare module "webpack-sources" {
224+
type MapOptions = { columns?: boolean; module?: boolean };
225+
226+
export abstract class Source {
227+
size(): number;
228+
229+
map(options?: MapOptions): Object;
230+
231+
sourceAndMap(
232+
options?: MapOptions
233+
): {
234+
source: string | Buffer;
235+
map: Object;
236+
};
237+
238+
updateHash(hash: import("./lib/util/createHash").Hash): void;
239+
240+
source(): string | Buffer;
241+
}
242+
243+
export class RawSource extends Source {
244+
constructor(source: string | Buffer);
245+
246+
// TODO remove internals
247+
_value: string | Buffer;
248+
}
249+
250+
export class OriginalSource extends Source {
251+
constructor(source: string | Buffer, name: string);
252+
253+
// TODO remove internals
254+
_value: string | Buffer;
255+
_name: string;
256+
}
257+
258+
export class ReplaceSource extends Source {
259+
constructor(source: Source, name?: string);
260+
261+
replace(start: number, end: number, newValue: string, name?: string): void;
262+
insert(pos: number, newValue: string, name?: string): void;
263+
264+
// TODO remove internals
265+
_name: string;
266+
_source: string;
267+
_replacements: {
268+
start: number;
269+
end: number;
270+
content: string;
271+
insertIndex: number;
272+
name: string;
273+
}[];
274+
}
275+
276+
export class SourceMapSource extends Source {
277+
constructor(
278+
source: string,
279+
name: string,
280+
sourceMap: Object | string,
281+
originalSource?: string,
282+
innerSourceMap?: Object
283+
);
284+
}
285+
286+
export class ConcatSource extends Source {
287+
constructor(...args: (string | Source)[]);
288+
289+
children: (string | Source)[];
290+
291+
add(item: string | Source): void;
292+
}
293+
294+
export class PrefixSource extends Source {
295+
constructor(prefix: string, source: string | Source);
296+
297+
_source: string | Source;
298+
_prefix: string;
299+
}
300+
301+
export class CachedSource extends Source {
302+
constructor(source: Source);
303+
304+
_source: Source;
305+
_cachedSource: string | Buffer;
306+
_cachedBuffer: Buffer;
307+
_cachedSize: number;
308+
_cachedMaps: Object;
309+
}
310+
311+
export class SizeOnlySource extends Source {
312+
constructor(size: number);
313+
314+
_size: number;
315+
}
316+
}
317+
223318
// This "hack" is needed because typescript doesn't support recursive type definitions
224319
// It's referenced from "ruleSet-conditions" in schemas/WebpackOptions.json
225320
interface RuleSetConditionsRecursive

lib/Compilation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,8 @@ class Compilation {
541541
this.builtModules = new WeakSet();
542542
/** @private @type {Map<Module, Callback[]>} */
543543
this._rebuildingModules = new Map();
544-
/** @type {WeakSet<Source>} */
545-
this.emittedAssets = new WeakSet();
544+
/** @type {Set<string>} */
545+
this.emittedAssets = new Set();
546546
/** @type {SortableSet<string>} */
547547
this.fileDependencies = new SortableSet();
548548
/** @type {SortableSet<string>} */

lib/Compiler.js

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
AsyncParallelHook,
1616
AsyncSeriesHook
1717
} = require("tapable");
18+
const { SizeOnlySource } = require("webpack-sources");
1819

1920
const Cache = require("./Cache");
2021
const Compilation = require("./Compilation");
@@ -27,6 +28,7 @@ const Stats = require("./Stats");
2728
const Watching = require("./Watching");
2829
const { makePathsRelative } = require("./util/identifier");
2930

31+
/** @typedef {import("webpack-sources").Source} Source */
3032
/** @typedef {import("../declarations/WebpackOptions").Entry} Entry */
3133
/** @typedef {import("../declarations/WebpackOptions").OutputOptions} OutputOptions */
3234
/** @typedef {import("../declarations/WebpackOptions").WatchOptions} WatchOptions */
@@ -161,6 +163,11 @@ class Compiler {
161163

162164
/** @type {boolean} */
163165
this.watchMode = false;
166+
167+
/** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */
168+
this._assetEmittingSourceCache = new WeakMap();
169+
/** @private @type {Map<string, number>} */
170+
this._assetEmittingWrittenFiles = new Map();
164171
}
165172

166173
/**
@@ -327,19 +334,70 @@ class Compiler {
327334
outputPath,
328335
targetFile
329336
);
330-
if (source.existsAt === targetPath) {
331-
compilation.emittedAssets.delete(source);
332-
return callback();
337+
338+
// check if the target file has already been written by this Compiler
339+
const targetFileGeneration = this._assetEmittingWrittenFiles.get(
340+
targetPath
341+
);
342+
343+
// create an cache entry for this Source if not already existing
344+
let cacheEntry = this._assetEmittingSourceCache.get(source);
345+
if (cacheEntry === undefined) {
346+
cacheEntry = {
347+
sizeOnlySource: undefined,
348+
writtenTo: new Map()
349+
};
350+
this._assetEmittingSourceCache.set(source, cacheEntry);
351+
}
352+
353+
// if the target file has already been written
354+
if (targetFileGeneration !== undefined) {
355+
// check if the Source has been written to this target file
356+
const writtenGeneration = cacheEntry.writtenTo.get(targetPath);
357+
if (writtenGeneration === targetFileGeneration) {
358+
// if yes, we skip writing the file
359+
// as it's already there
360+
// (we assume one doesn't remove files while the Compiler is running)
361+
return callback();
362+
}
333363
}
334-
let content = source.source();
335364

336-
if (!Buffer.isBuffer(content)) {
337-
content = Buffer.from(content, "utf8");
365+
// get the binary (Buffer) content from the Source
366+
/** @type {Buffer} */
367+
let content;
368+
if (typeof source.buffer === "function") {
369+
content = source.buffer();
370+
} else {
371+
const bufferOrString = source.source();
372+
if (Buffer.isBuffer(bufferOrString)) {
373+
content = bufferOrString;
374+
} else {
375+
content = Buffer.from(bufferOrString, "utf8");
376+
}
338377
}
339378

340-
source.existsAt = targetPath;
341-
compilation.emittedAssets.add(source);
342-
this.outputFileSystem.writeFile(targetPath, content, callback);
379+
// Create a replacement resource which only allows to ask for size
380+
// This allows to GC all memory allocated by the Source
381+
// (expect when the Source is stored in any other cache)
382+
cacheEntry.sizeOnlySource = new SizeOnlySource(content.length);
383+
compilation.assets[file] = cacheEntry.sizeOnlySource;
384+
385+
// Write the file to output file system
386+
this.outputFileSystem.writeFile(targetPath, content, err => {
387+
if (err) return callback(err);
388+
389+
// information marker that the asset has been emitted
390+
compilation.emittedAssets.add(file);
391+
392+
// cache the information that the Source has been written to that ___location
393+
const newGeneration =
394+
targetFileGeneration === undefined
395+
? 1
396+
: targetFileGeneration + 1;
397+
cacheEntry.writtenTo.set(targetPath, newGeneration);
398+
this._assetEmittingWrittenFiles.set(targetPath, newGeneration);
399+
callback();
400+
});
343401
};
344402

345403
if (targetFile.match(/\/|\\/)) {

lib/NormalModule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ class NormalModule extends Module {
574574
}
575575

576576
const handleParseError = e => {
577-
const source = this._source.source();
577+
const source = /** @type {string} */ (this._source.source());
578578
const error = new ModuleParseError(source, e);
579579
this.markModuleAsErrored(error);
580580
this._initBuildHash(compilation);

lib/stats/DefaultStatsFactoryPlugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ const SIMPLE_EXTRACTORS = {
282282
return names;
283283
}, /** @type {Set<string>} */ (new Set()))
284284
).sort(compareIds);
285-
object.emitted = compilation.emittedAssets.has(asset.source);
285+
object.emitted = compilation.emittedAssets.has(asset.name);
286286
},
287287
performance: (object, asset) => {
288288
object.isOverSizeLimit = SizeLimitsPlugin.isOverSizeLimit(asset.source);

lib/util/registerExternalSerializer.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ register(
3838
*/
3939
serialize(source, { write }) {
4040
write(source._source);
41+
write(source._cachedBuffer);
4142
write(source._cachedSource);
4243
write(source._cachedSize);
4344
write(source._cachedMaps);
@@ -49,6 +50,7 @@ register(
4950
*/
5051
deserialize({ read }) {
5152
const source = new CachedSource(read());
53+
source._cachedBuffer = read();
5254
source._cachedSource = read();
5355
source._cachedSize = read();
5456
source._cachedMaps = read();
@@ -148,9 +150,7 @@ register(
148150
serialize(source, { write }) {
149151
write(source._source);
150152
write(source._name);
151-
const replacements = /** @type {TODO} */ (Array.from(
152-
source.replacements
153-
));
153+
const replacements = Array.from(source._replacements);
154154
replacements.sort((a, b) => {
155155
return a.insertIndex - b.insertIndex;
156156
});
@@ -171,7 +171,6 @@ register(
171171
const source = new ReplaceSource(read(), read());
172172
const len = read();
173173
for (let i = 0; i < len; i++) {
174-
// @ts-ignore TODO signature is missing one argument in typings
175174
source.replace(read(), read(), read(), read());
176175
}
177176
return source;

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@
2929
"tapable": "^1.1.0",
3030
"terser-webpack-plugin": "^1.2.1",
3131
"watchpack": "2.0.0-beta.2",
32-
"webpack-sources": "^1.3.0"
32+
"webpack-sources": "2.0.0-beta.0"
3333
},
3434
"devDependencies": {
3535
"@types/node": "^10.12.3",
3636
"@types/tapable": "^1.0.1",
37-
"@types/webpack-sources": "^0.1.4",
3837
"benchmark": "^2.1.1",
3938
"bundle-loader": "~0.5.0",
4039
"codacy-coverage": "^3.1.0",

test/HotModuleReplacementPlugin.test.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,32 +99,25 @@ describe("HotModuleReplacementPlugin", () => {
9999
}, 120000);
100100

101101
it("should correct working when entry is Object and key is a number", done => {
102+
const outputPath = path.join(__dirname, "js", "HotModuleReplacementPlugin");
102103
const entryFile = path.join(
103-
__dirname,
104-
"js",
105-
"HotModuleReplacementPlugin",
104+
outputPath,
106105
"entry.js"
107106
);
108107
const statsFile3 = path.join(
109-
__dirname,
110-
"js",
111-
"HotModuleReplacementPlugin",
108+
outputPath,
112109
"HotModuleReplacementPlugin.test.stats3.txt"
113110
);
114111
const statsFile4 = path.join(
115-
__dirname,
116-
"js",
117-
"HotModuleReplacementPlugin",
112+
outputPath,
118113
"HotModuleReplacementPlugin.test.stats4.txt"
119114
);
120115
const recordsFile = path.join(
121-
__dirname,
122-
"js",
123-
"HotModuleReplacementPlugin",
116+
outputPath,
124117
"records.json"
125118
);
126119
try {
127-
mkdirp.sync(path.join(__dirname, "js", "HotModuleReplacementPlugin"));
120+
mkdirp.sync(outputPath);
128121
} catch (e) {
129122
// empty
130123
}
@@ -141,7 +134,7 @@ describe("HotModuleReplacementPlugin", () => {
141134
},
142135
recordsPath: recordsFile,
143136
output: {
144-
path: path.join(__dirname, "js", "HotModuleReplacementPlugin")
137+
path: outputPath
145138
},
146139
plugins: [new webpack.HotModuleReplacementPlugin()],
147140
optimization: {
@@ -163,7 +156,7 @@ describe("HotModuleReplacementPlugin", () => {
163156
if (err) throw err;
164157
fs.writeFileSync(statsFile3, stats.toString());
165158
const result = JSON.parse(
166-
stats.compilation.assets[`${hash}.hot-update.json`].source()
159+
fs.readFileSync(path.join(outputPath, `${hash}.hot-update.json`), "utf-8")
167160
)["c"];
168161
expect(result).toEqual([chunkName]);
169162
done();

0 commit comments

Comments
 (0)