Skip to content

Commit b62ed2f

Browse files
davidaurelioFacebook Github Bot 5
authored andcommitted
Add support for module groups to iOS Random Access Bundle format
Summary: Adds the possibility to specify an array of files (group roots) that are used to bundle modules outside of the “startup section” into bigger groups by colocating their code. A require call for any grouped module will load all modules in that group. Files contained by multiple groups are deoptimized (i.e. bundled as individual script) Reviewed By: martinbigio Differential Revision: D3841780 fbshipit-source-id: 8d37782792efd66b5f557c7567489f68c9b229d8
1 parent 9ff4d31 commit b62ed2f

File tree

6 files changed

+306
-24
lines changed

6 files changed

+306
-24
lines changed

local-cli/bundle/output/unbundle/as-indexed-file.js

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,20 @@ function saveAsIndexedFile(bundle, options, log) {
3232
} = options;
3333

3434
log('start');
35-
const {startupModules, lazyModules} = bundle.getUnbundle();
35+
const {startupModules, lazyModules, groups} = bundle.getUnbundle();
3636
log('finish');
3737

38+
const moduleGroups = ModuleGroups(groups, lazyModules);
3839
const startupCode = joinModules(startupModules);
3940

4041
log('Writing unbundle output to:', bundleOutput);
4142
const writeUnbundle = writeBuffers(
4243
fs.createWriteStream(bundleOutput),
43-
buildTableAndContents(startupCode, lazyModules, encoding)
44+
buildTableAndContents(startupCode, lazyModules, moduleGroups, encoding)
4445
).then(() => log('Done writing unbundle output'));
4546

4647
const sourceMap =
47-
buildSourceMapWithMetaData({startupModules, lazyModules});
48+
buildSourceMapWithMetaData({startupModules, lazyModules, moduleGroups});
4849

4950
return Promise.all([
5051
writeUnbundle,
@@ -84,7 +85,7 @@ function entryOffset(n) {
8485
return (2 + n * 2) * SIZEOF_UINT32;
8586
}
8687

87-
function buildModuleTable(startupCode, buffers) {
88+
function buildModuleTable(startupCode, buffers, moduleGroups) {
8889
// table format:
8990
// - num_entries: uint_32 number of entries
9091
// - startup_code_len: uint_32 length of the startup section
@@ -94,7 +95,8 @@ function buildModuleTable(startupCode, buffers) {
9495
// - module_offset: uint_32 offset into the modules blob
9596
// - module_length: uint_32 length of the module code in bytes
9697

97-
const maxId = buffers.reduce((max, {id}) => Math.max(max, id), 0);
98+
const moduleIds = Array.from(moduleGroups.modulesById.keys());
99+
const maxId = moduleIds.reduce((max, id) => Math.max(max, id));
98100
const numEntries = maxId + 1;
99101
const table = new Buffer(entryOffset(numEntries)).fill(0);
100102

@@ -107,32 +109,59 @@ function buildModuleTable(startupCode, buffers) {
107109
// entries
108110
let codeOffset = startupCode.length;
109111
buffers.forEach(({id, buffer}) => {
110-
const offset = entryOffset(id);
111-
// module_offset
112-
table.writeUInt32LE(codeOffset, offset);
113-
// module_length
114-
table.writeUInt32LE(buffer.length, offset + SIZEOF_UINT32);
112+
const idsInGroup = moduleGroups.groups.has(id)
113+
? [id].concat(Array.from(moduleGroups.groups.get(id)))
114+
: [id];
115+
116+
idsInGroup.forEach(moduleId => {
117+
const offset = entryOffset(moduleId);
118+
// module_offset
119+
table.writeUInt32LE(codeOffset, offset);
120+
// module_length
121+
table.writeUInt32LE(buffer.length, offset + SIZEOF_UINT32);
122+
});
115123
codeOffset += buffer.length;
116124
});
117125

118126
return table;
119127
}
120128

121-
function buildModuleBuffers(modules, encoding) {
122-
return modules.map(
123-
module => moduleToBuffer(module.id, module.code, encoding));
129+
function groupCode(rootCode, moduleGroup, modulesById) {
130+
if (!moduleGroup || !moduleGroup.size) {
131+
return rootCode;
132+
}
133+
const code = [rootCode];
134+
for (const id of moduleGroup) {
135+
code.push(modulesById.get(id).code);
136+
}
137+
138+
return code.join('\n');
139+
}
140+
141+
function buildModuleBuffers(modules, moduleGroups, encoding) {
142+
return modules
143+
.filter(m => !moduleGroups.modulesInGroups.has(m.id))
144+
.map(({id, code}) => moduleToBuffer(
145+
id,
146+
groupCode(
147+
code,
148+
moduleGroups.groups.get(id),
149+
moduleGroups.modulesById,
150+
),
151+
encoding
152+
));
124153
}
125154

126-
function buildTableAndContents(startupCode, modules, encoding) {
155+
function buildTableAndContents(startupCode, modules, moduleGroups, encoding) {
127156
// file contents layout:
128157
// - magic number char[4] 0xE5 0xD1 0x0B 0xFB (0xFB0BD1E5 uint32 LE)
129158
// - offset table table see `buildModuleTables`
130159
// - code blob char[] null-terminated code strings, starting with
131160
// the startup code
132161

133162
const startupCodeBuffer = nullTerminatedBuffer(startupCode, encoding);
134-
const moduleBuffers = buildModuleBuffers(modules, encoding);
135-
const table = buildModuleTable(startupCodeBuffer, moduleBuffers);
163+
const moduleBuffers = buildModuleBuffers(modules, moduleGroups, encoding);
164+
const table = buildModuleTable(startupCodeBuffer, moduleBuffers, moduleGroups);
136165

137166
return [
138167
fileHeader,
@@ -141,4 +170,18 @@ function buildTableAndContents(startupCode, modules, encoding) {
141170
].concat(moduleBuffers.map(({buffer}) => buffer));
142171
}
143172

173+
function ModuleGroups(groups, modules) {
174+
return {
175+
groups,
176+
modulesById: new Map(modules.map(m => [m.id, m])),
177+
modulesInGroups: new Set(concat(groups.values())),
178+
};
179+
}
180+
181+
function * concat(iterators) {
182+
for (const it of iterators) {
183+
yield * it;
184+
}
185+
}
186+
144187
module.exports = saveAsIndexedFile;

local-cli/bundle/output/unbundle/build-unbundle-sourcemap-with-metadata.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
const {combineSourceMaps, joinModules} = require('./util');
1212

13-
module.exports = ({startupModules, lazyModules}) => {
13+
module.exports = ({startupModules, lazyModules, moduleGroups}) => {
1414
const startupModule = {
1515
code: joinModules(startupModules),
1616
map: combineSourceMaps({modules: startupModules}),
1717
};
1818
return combineSourceMaps({
1919
modules: [startupModule].concat(lazyModules),
20+
moduleGroups,
2021
withCustomOffsets: true,
2122
});
2223
};

local-cli/bundle/output/unbundle/util.js

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const wrapperEnd = wrappedCode => wrappedCode.indexOf('{') + 1;
3131

3232
const Section = (line, column, map) => ({map, offset: {line, column}});
3333

34-
function combineSourceMaps({modules, withCustomOffsets}) {
34+
function combineSourceMaps({modules, withCustomOffsets, moduleGroups}) {
3535
let offsets;
3636
const sections = [];
3737
const sourceMap = {
@@ -45,13 +45,41 @@ function combineSourceMaps({modules, withCustomOffsets}) {
4545

4646
let line = 0;
4747
modules.forEach(({code, id, map, name}) => {
48-
const hasOffset = withCustomOffsets && id != null;
49-
const column = hasOffset ? wrapperEnd(code) : 0;
48+
let column = 0;
49+
let hasOffset = false;
50+
let group;
51+
let groupLines = 0;
52+
53+
if (withCustomOffsets) {
54+
if (moduleGroups && moduleGroups.modulesInGroups.has(id)) {
55+
// this is a module appended to another module
56+
return;
57+
}
58+
59+
if (moduleGroups && moduleGroups.groups.has(id)) {
60+
group = moduleGroups.groups.get(id);
61+
const otherModules = Array.from(group).map(
62+
moduleId => moduleGroups.modulesById.get(moduleId));
63+
otherModules.forEach(m => {
64+
groupLines += countLines(m.code);
65+
});
66+
map = combineSourceMaps({
67+
modules: [{code, id, map, name}].concat(otherModules),
68+
});
69+
}
70+
71+
hasOffset = id != null;
72+
column = wrapperEnd(code);
73+
}
74+
5075
sections.push(Section(line, column, map || lineToLineSourceMap(code, name)));
5176
if (hasOffset) {
5277
offsets[id] = line;
78+
for (const moduleId of group || []) {
79+
offsets[moduleId] = line;
80+
}
5381
}
54-
line += countLines(code);
82+
line += countLines(code) + groupLines;
5583
});
5684

5785
return sourceMap;

packager/react-packager/src/Bundler/Bundle.js

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const crypto = require('crypto');
1717
const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL=';
1818

1919
class Bundle extends BundleBase {
20-
constructor({sourceMapUrl, dev, minify} = {}) {
20+
constructor({sourceMapUrl, dev, minify, ramGroups} = {}) {
2121
super();
2222
this._sourceMap = false;
2323
this._sourceMapUrl = sourceMapUrl;
@@ -26,6 +26,7 @@ class Bundle extends BundleBase {
2626
this._dev = dev;
2727
this._minify = minify;
2828

29+
this._ramGroups = ramGroups;
2930
this._ramBundle = null; // cached RAM Bundle
3031
}
3132

@@ -111,9 +112,17 @@ class Bundle extends BundleBase {
111112
// separate modules we need to preload from the ones we don't
112113
const [startupModules, lazyModules] = partition(modules, shouldPreload);
113114

115+
const ramGroups = this._ramGroups;
116+
let groups;
114117
this._ramBundle = {
115118
startupModules,
116119
lazyModules,
120+
get groups() {
121+
if (!groups) {
122+
groups = createGroups(ramGroups || [], lazyModules);
123+
}
124+
return groups;
125+
}
117126
};
118127
}
119128

@@ -265,6 +274,10 @@ class Bundle extends BundleBase {
265274
].join('\n');
266275
}
267276

277+
setRamGroups(ramGroups) {
278+
this._ramGroups = ramGroups;
279+
}
280+
268281
toJSON() {
269282
this.assertFinalized('Cannot serialize bundle unless finalized');
270283

@@ -318,4 +331,89 @@ function partition(array, predicate) {
318331
return [included, excluded];
319332
}
320333

334+
function * filter(iterator, predicate) {
335+
for (const value of iterator) {
336+
if (predicate(value)) {
337+
yield value;
338+
}
339+
}
340+
}
341+
342+
function * subtree(moduleTransport, moduleTransportsByPath, seen = new Set()) {
343+
seen.add(moduleTransport.id);
344+
for (const [, {path}] of moduleTransport.meta.dependencyPairs) {
345+
const dependency = moduleTransportsByPath.get(path);
346+
if (dependency && !seen.has(dependency.id)) {
347+
yield dependency.id;
348+
yield * subtree(dependency, moduleTransportsByPath, seen);
349+
}
350+
}
351+
}
352+
353+
class ArrayMap extends Map {
354+
get(key) {
355+
let array = super.get(key);
356+
if (!array) {
357+
array = [];
358+
this.set(key, array);
359+
}
360+
return array;
361+
}
362+
}
363+
364+
function createGroups(ramGroups, lazyModules) {
365+
// build two maps that allow to lookup module data
366+
// by path or (numeric) module id;
367+
const byPath = new Map();
368+
const byId = new Map();
369+
lazyModules.forEach(m => {
370+
byPath.set(m.sourcePath, m);
371+
byId.set(m.id, m.sourcePath);
372+
});
373+
374+
// build a map of group root IDs to an array of module IDs in the group
375+
const result = new Map(
376+
ramGroups
377+
.map(modulePath => {
378+
const root = byPath.get(modulePath);
379+
if (!root) {
380+
throw Error(`Group root ${modulePath} is not part of the bundle`);
381+
}
382+
return [
383+
root.id,
384+
// `subtree` yields the IDs of all transitive dependencies of a module
385+
new Set(subtree(byPath.get(root.sourcePath), byPath)),
386+
];
387+
})
388+
);
389+
390+
if (ramGroups.length > 1) {
391+
// build a map of all grouped module IDs to an array of group root IDs
392+
const all = new ArrayMap();
393+
for (const [parent, children] of result) {
394+
for (const module of children) {
395+
all.get(module).push(parent);
396+
}
397+
}
398+
399+
// find all module IDs that are part of more than one group
400+
const doubles = filter(all, ([, parents]) => parents.length > 1);
401+
for (const [moduleId, parents] of doubles) {
402+
// remove them from their groups
403+
parents.forEach(p => result.get(p).delete(moduleId));
404+
405+
// print a warning for each removed module
406+
const parentNames = parents.map(byId.get, byId);
407+
const lastName = parentNames.pop();
408+
console.warn(
409+
`Module ${byId.get(moduleId)} belongs to groups ${
410+
parentNames.join(', ')}, and ${lastName
411+
}. Removing it from all groups.`
412+
);
413+
}
414+
}
415+
416+
return result;
417+
}
418+
321419
module.exports = Bundle;

0 commit comments

Comments
 (0)