Skip to content

Commit 230388c

Browse files
committed
[wasm-split] Add a multi-split mode
Add a mode that splits a module into arbitrarily many parts based on a simple manifest file. This is currently implemented by splitting out one module at a time in a loop, but this could change in the future if splitting out all the modules at once would improve the quality of the output.
1 parent 202dce6 commit 230388c

5 files changed

Lines changed: 344 additions & 3 deletions

File tree

src/tools/wasm-split/split-options.cpp

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ std::ostream& operator<<(std::ostream& o, WasmSplitOptions::Mode& mode) {
6161
case WasmSplitOptions::Mode::Split:
6262
o << "split";
6363
break;
64+
case WasmSplitOptions::Mode::MultiSplit:
65+
o << "multi-split";
66+
break;
6467
case WasmSplitOptions::Mode::Instrument:
6568
o << "instrument";
6669
break;
@@ -91,7 +94,14 @@ WasmSplitOptions::WasmSplitOptions()
9194
"Split an input module into two output modules. The default mode.",
9295
WasmSplitOption,
9396
Options::Arguments::Zero,
94-
[&](Options* o, const std::string& arugment) { mode = Mode::Split; })
97+
[&](Options* o, const std::string& argument) { mode = Mode::Split; })
98+
.add(
99+
"--multi-split",
100+
"",
101+
"Split an input module into an arbitrary number of output modules.",
102+
WasmSplitOption,
103+
Options::Arguments::Zero,
104+
[&](Options* o, const std::string& argument) { mode = Mode::MultiSplit; })
95105
.add(
96106
"--instrument",
97107
"",
@@ -151,6 +161,25 @@ WasmSplitOptions::WasmSplitOptions()
151161
[&](Options* o, const std::string& argument) {
152162
splitFuncs = parseNameList(argument);
153163
})
164+
.add(
165+
"--manifest",
166+
"",
167+
"File describing the functions to be split into each module. Each "
168+
"section separated by a blank line begins with the base name of an "
169+
"output module, which is followed by a list of functions to place in "
170+
"that module, one per line.",
171+
WasmSplitOption,
172+
{Mode::MultiSplit},
173+
Options::Arguments::One,
174+
[&](Options* o, const std::string& argument) { manifestFile = argument; })
175+
.add("--out-prefix",
176+
"",
177+
"Prefix prepended to module names in the manifest file to create "
178+
"output file names.",
179+
WasmSplitOption,
180+
{Mode::MultiSplit},
181+
Options::Arguments::One,
182+
[&](Options* o, const std::string& argument) { outPrefix = argument; })
154183
.add("--primary-output",
155184
"-o1",
156185
"Output file for the primary module.",
@@ -313,7 +342,7 @@ WasmSplitOptions::WasmSplitOptions()
313342
"-g",
314343
"Emit names section in wasm binary (or full debuginfo in wast)",
315344
WasmSplitOption,
316-
{Mode::Split, Mode::Instrument},
345+
{Mode::Split, Mode::MultiSplit, Mode::Instrument},
317346
Options::Arguments::Zero,
318347
[&](Options* o, const std::string& arguments) {
319348
passOptions.debugInfo = true;
@@ -322,7 +351,7 @@ WasmSplitOptions::WasmSplitOptions()
322351
"-o",
323352
"Output file.",
324353
WasmSplitOption,
325-
{Mode::Instrument, Mode::MergeProfiles},
354+
{Mode::Instrument, Mode::MergeProfiles, Mode::MultiSplit},
326355
Options::Arguments::One,
327356
[&](Options* o, const std::string& argument) { output = argument; })
328357
.add("--unescape",
@@ -407,6 +436,7 @@ bool WasmSplitOptions::validate() {
407436
}
408437
switch (mode) {
409438
case Mode::Split:
439+
case Mode::MultiSplit:
410440
case Mode::Instrument:
411441
if (inputFiles.size() > 1) {
412442
fail("Cannot have more than one input file.");

src/tools/wasm-split/split-options.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const std::string DEFAULT_PROFILE_EXPORT("__write_profile");
2626
struct WasmSplitOptions : ToolOptions {
2727
enum class Mode : unsigned {
2828
Split,
29+
MultiSplit,
2930
Instrument,
3031
MergeProfiles,
3132
PrintProfile,
@@ -68,6 +69,9 @@ struct WasmSplitOptions : ToolOptions {
6869
std::string secondaryMemoryName;
6970
std::string exportPrefix;
7071

72+
std::string manifestFile;
73+
std::string outPrefix;
74+
7175
// A hack to ensure the split and instrumented modules have the same table
7276
// size when using Emscripten's SPLIT_MODULE mode with dynamic linking. TODO:
7377
// Figure out a more elegant solution for that use case and remove this.

src/tools/wasm-split/wasm-split.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,82 @@ void splitModule(const WasmSplitOptions& options) {
362362
writeModule(*secondary, options.secondaryOutput, options);
363363
}
364364

365+
void multiSplitModule(const WasmSplitOptions& options) {
366+
if (options.manifestFile.empty()) {
367+
Fatal() << "--multi-split requires --manifest";
368+
}
369+
if (options.output.empty()) {
370+
Fatal() << "--multi-split requires --output";
371+
}
372+
373+
std::ifstream manifest(options.manifestFile);
374+
if (!manifest.is_open()) {
375+
Fatal() << "File not found: " << options.manifestFile;
376+
}
377+
378+
Module wasm;
379+
parseInput(wasm, options);
380+
381+
std::map<std::string, std::unordered_set<std::string>> moduleFuncs;
382+
std::string currModule;
383+
std::unordered_set<std::string>* currFuncs = nullptr;
384+
std::unordered_map<std::string, std::string> funcModules;
385+
386+
std::string line;
387+
bool newSection = true;
388+
while (std::getline(manifest, line)) {
389+
if (line.empty()) {
390+
newSection = true;
391+
continue;
392+
}
393+
if (newSection) {
394+
currModule = line;
395+
currFuncs = &moduleFuncs[line];
396+
newSection = false;
397+
continue;
398+
}
399+
assert(currFuncs);
400+
currFuncs->insert(line);
401+
auto [it, inserted] = funcModules.insert({line, currModule});
402+
if (!inserted && it->second != currModule) {
403+
Fatal() << "Function " << line << "cannot be assigned to module "
404+
<< currModule << "; it is already assigned to module "
405+
<< it->second << '\n';
406+
}
407+
if (inserted && !options.quiet && !wasm.getFunctionOrNull(line)) {
408+
std::cerr << "warning: Function " << line << " does not exist\n";
409+
}
410+
}
411+
412+
ModuleSplitting::Config config;
413+
config.usePlaceholders = false;
414+
config.importNamespace = "";
415+
config.minimizeNewExportNames = true;
416+
for (auto& func : wasm.functions) {
417+
config.primaryFuncs.insert(func->name);
418+
}
419+
for (auto& [mod, funcs] : moduleFuncs) {
420+
if (options.verbose) {
421+
std::cerr << "Splitting module " << mod << '\n';
422+
}
423+
if (!options.quiet && funcs.empty()) {
424+
std::cerr << "warning: Module " << mod << " will be empty\n";
425+
}
426+
for (auto& func : funcs) {
427+
config.primaryFuncs.erase(Name(func));
428+
}
429+
auto splitResults = ModuleSplitting::splitFunctions(wasm, config);
430+
// TODO: symbolMap, placeholderMap, emitModuleNames
431+
// TODO: Support --emit-text and use .wast in that case.
432+
auto moduleName = options.outPrefix + mod + ".wasm";
433+
PassRunner runner(&*splitResults.secondary);
434+
runner.add("remove-unused-module-elements");
435+
runner.run();
436+
writeModule(*splitResults.secondary, moduleName, options);
437+
}
438+
writeModule(wasm, options.output, options);
439+
}
440+
365441
void mergeProfiles(const WasmSplitOptions& options) {
366442
// Read the initial profile. We will merge other profiles into this one.
367443
ProfileData data = readProfile(options.inputFiles[0]);
@@ -503,6 +579,9 @@ int main(int argc, const char* argv[]) {
503579
case WasmSplitOptions::Mode::Split:
504580
splitModule(options);
505581
break;
582+
case WasmSplitOptions::Mode::MultiSplit:
583+
multiSplitModule(options);
584+
break;
506585
case WasmSplitOptions::Mode::Instrument:
507586
instrumentModule(options);
508587
break;

0 commit comments

Comments
 (0)