/* * Copyright 2020 WebAssembly Community Group participants * * 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. */ // wasm-split: Split a module in two or instrument a module to inform future // splitting. #include "ir/module-splitting.h" #include "ir/module-utils.h" #include "ir/names.h" #include "pass.h" #include "support/file.h" #include "support/name.h" #include "support/path.h" #include "support/utilities.h" #include "tool-options.h" #include "wasm-binary.h" #include "wasm-builder.h" #include "wasm-io.h" #include "wasm-type.h" #include "wasm-validator.h" #include using namespace wasm; namespace { const std::string DEFAULT_PROFILE_EXPORT("__write_profile"); std::set parseNameList(const std::string& list) { std::set names; std::istringstream stream(list); for (std::string name; std::getline(stream, name, ',');) { names.insert(name); } return names; } struct WasmSplitOptions : ToolOptions { enum class Mode : unsigned { Split, Instrument, MergeProfiles, }; Mode mode = Mode::Split; constexpr static size_t NumModes = static_cast(Mode::MergeProfiles) + 1; bool verbose = false; bool emitBinary = true; bool symbolMap = false; bool placeholderMap = false; // TODO: Remove this. See the comment in wasm-binary.h. bool emitModuleNames = false; std::string profileFile; std::string profileExport = DEFAULT_PROFILE_EXPORT; std::set keepFuncs; std::set splitFuncs; std::vector inputFiles; std::string output; std::string primaryOutput; std::string secondaryOutput; std::string importNamespace; std::string placeholderNamespace; std::string exportPrefix; // A hack to ensure the split and instrumented modules have the same table // size when using Emscripten's SPLIT_MODULE mode with dynamic linking. TODO: // Figure out a more elegant solution for that use case and remove this. int initialTableSize = -1; // The options that are valid for each mode. std::array, NumModes> validOptions; std::vector usedOptions; WasmSplitOptions(); WasmSplitOptions& add(const std::string& longName, const std::string& shortName, const std::string& description, std::vector&& modes, Arguments arguments, const Action& action); WasmSplitOptions& add(const std::string& longName, const std::string& shortName, const std::string& description, Arguments arguments, const Action& action); bool validate(); void parse(int argc, const char* argv[]); }; WasmSplitOptions::WasmSplitOptions() : ToolOptions("wasm-split", "Split a module into a primary module and a secondary " "module, or instrument a module to gather a profile that " "can inform future splitting, or manage such profiles. Options " "that are only accepted in particular modes are marked with " "the accepted \"[]\" in their descriptions.") { (*this) .add("--split", "", "Split an input module into two output modules. The default mode.", Options::Arguments::Zero, [&](Options* o, const std::string& arugment) { mode = Mode::Split; }) .add( "--instrument", "", "Instrument an input module to allow it to generate a profile that can" " be used to guide splitting.", Options::Arguments::Zero, [&](Options* o, const std::string& argument) { mode = Mode::Instrument; }) .add("--merge-profiles", "", "Merge multiple profiles for the same module into a single profile.", Options::Arguments::Zero, [&](Options* o, const std::string& argument) { mode = Mode::MergeProfiles; }) .add( "--profile", "", "The profile to use to guide splitting.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { profileFile = argument; }) .add("--keep-funcs", "", "Comma-separated list of functions to keep in the primary module, " "regardless of any profile.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { keepFuncs = parseNameList(argument); }) .add("--split-funcs", "", "Comma-separated list of functions to split into the secondary " "module, regardless of any profile. If there is no profile, then " "this defaults to all functions defined in the module.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { splitFuncs = parseNameList(argument); }) .add("--primary-output", "-o1", "Output file for the primary module.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { primaryOutput = argument; }) .add("--secondary-output", "-o2", "Output file for the secondary module.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { secondaryOutput = argument; }) .add("--symbolmap", "", "Write a symbol map file for each of the output modules.", {Mode::Split}, Options::Arguments::Zero, [&](Options* o, const std::string& argument) { symbolMap = true; }) .add( "--placeholdermap", "", "Write a file mapping placeholder indices to the function names.", {Mode::Split}, Options::Arguments::Zero, [&](Options* o, const std::string& argument) { placeholderMap = true; }) .add("--import-namespace", "", "The namespace from which to import objects from the primary " "module into the secondary module.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { importNamespace = argument; }) .add("--placeholder-namespace", "", "The namespace from which to import placeholder functions into " "the primary module.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { placeholderNamespace = argument; }) .add( "--export-prefix", "", "An identifying prefix to prepend to new export names created " "by module splitting.", {Mode::Split}, Options::Arguments::One, [&](Options* o, const std::string& argument) { exportPrefix = argument; }) .add("--profile-export", "", "The export name of the function the embedder calls to write the " "profile into memory. Defaults to `__write_profile`.", {Mode::Instrument}, Options::Arguments::One, [&](Options* o, const std::string& argument) { profileExport = argument; }) .add( "--emit-module-names", "", "Emit module names, even if not emitting the rest of the names section. " "Can help differentiate the modules in stack traces. This option will be " "removed once simpler ways of naming modules are widely available. See " "https://bugs.chromium.org/p/v8/issues/detail?id=11808.", {Mode::Split, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { emitModuleNames = true; }) .add("--initial-table", "", "A hack to ensure the split and instrumented modules have the same " "table size when using Emscripten's SPLIT_MODULE mode with dynamic " "linking. TODO: Figure out a more elegant solution for that use " "case and remove this.", {Mode::Split, Mode::Instrument}, Options::Arguments::One, [&](Options* o, const std::string& argument) { initialTableSize = std::stoi(argument); }) .add("--emit-text", "-S", "Emit text instead of binary for the output file or files.", {Mode::Split, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& argument) { emitBinary = false; }) .add("--debuginfo", "-g", "Emit names section in wasm binary (or full debuginfo in wast)", {Mode::Split, Mode::Instrument}, Options::Arguments::Zero, [&](Options* o, const std::string& arguments) { passOptions.debugInfo = true; }) .add("--output", "-o", "Output file.", {Mode::Instrument, Mode::MergeProfiles}, Options::Arguments::One, [&](Options* o, const std::string& argument) { output = argument; }) .add("--verbose", "-v", "Verbose output mode. Prints the functions that will be kept " "and split out when splitting a module.", Options::Arguments::Zero, [&](Options* o, const std::string& argument) { verbose = true; quiet = false; }) .add_positional("INFILES", Options::Arguments::N, [&](Options* o, const std::string& argument) { inputFiles.push_back(argument); }); } std::ostream& operator<<(std::ostream& o, WasmSplitOptions::Mode& mode) { switch (mode) { case WasmSplitOptions::Mode::Split: o << "split"; break; case WasmSplitOptions::Mode::Instrument: o << "instrument"; break; case WasmSplitOptions::Mode::MergeProfiles: o << "merge-profiles"; break; } return o; } WasmSplitOptions& WasmSplitOptions::add(const std::string& longName, const std::string& shortName, const std::string& description, std::vector&& modes, Arguments arguments, const Action& action) { // Insert the valid modes at the beginning of the description. std::stringstream desc; if (modes.size()) { desc << '['; std::string sep = ""; for (Mode m : modes) { validOptions[static_cast(m)].insert(longName); desc << sep << m; sep = ", "; } desc << "] "; } desc << description; ToolOptions::add( longName, shortName, desc.str(), arguments, [&, action, longName](Options* o, const std::string& argument) { usedOptions.push_back(longName); action(o, argument); }); return *this; } WasmSplitOptions& WasmSplitOptions::add(const std::string& longName, const std::string& shortName, const std::string& description, Arguments arguments, const Action& action) { // Add an option valid in all modes. for (unsigned i = 0; i < NumModes; ++i) { validOptions[i].insert(longName); } return add(longName, shortName, description, {}, arguments, action); } bool WasmSplitOptions::validate() { bool valid = true; auto fail = [&](auto msg) { std::cerr << "error: " << msg << "\n"; valid = false; }; // Validate the positional arguments. if (inputFiles.size() == 0) { fail("no input file"); } switch (mode) { case Mode::Split: case Mode::Instrument: if (inputFiles.size() > 1) { fail("Cannot have more than one input file."); } break; case Mode::MergeProfiles: // Any number >= 1 allowed. break; } // Validate that all used options are allowed in the current mode. for (std::string& opt : usedOptions) { if (!validOptions[static_cast(mode)].count(opt)) { std::stringstream msg; msg << "Option " << opt << " cannot be used in " << mode << " mode."; fail(msg.str()); } } if (mode == Mode::Split) { std::vector impossible; std::set_intersection(keepFuncs.begin(), keepFuncs.end(), splitFuncs.begin(), splitFuncs.end(), std::inserter(impossible, impossible.end())); for (auto& func : impossible) { fail(std::string("Cannot both keep and split out function ") + func.c_str()); } } return valid; } void WasmSplitOptions::parse(int argc, const char* argv[]) { ToolOptions::parse(argc, argv); // Since --quiet is defined in ToolOptions but --verbose is defined here, // --quiet doesn't know to unset --verbose. Fix it up here. if (quiet && verbose) { verbose = false; } } void parseInput(Module& wasm, const WasmSplitOptions& options) { options.applyFeatures(wasm); ModuleReader reader; reader.setProfile(options.profile); try { reader.read(options.inputFiles[0], wasm); } catch (ParseException& p) { p.dump(std::cerr); std::cerr << '\n'; Fatal() << "error parsing wasm"; } catch (std::bad_alloc&) { Fatal() << "error building module, std::bad_alloc (possibly invalid " "request for silly amounts of memory)"; } if (options.passOptions.validate && !WasmValidator().validate(wasm)) { Fatal() << "error validating input"; } } // Add a global monotonic counter and a timestamp global for each function, code // at the beginning of each function to set its timestamp, and a new exported // function for dumping the profile data. struct Instrumenter : public Pass { PassRunner* runner = nullptr; Module* wasm = nullptr; const std::string& profileExport; uint64_t moduleHash; Name counterGlobal; std::vector functionGlobals; Instrumenter(const std::string& profileExport, uint64_t moduleHash); void run(PassRunner* runner, Module* wasm) override; void addGlobals(); void instrumentFuncs(); void addProfileExport(); }; Instrumenter::Instrumenter(const std::string& profileExport, uint64_t moduleHash) : profileExport(profileExport), moduleHash(moduleHash) {} void Instrumenter::run(PassRunner* runner, Module* wasm) { this->runner = runner; this->wasm = wasm; addGlobals(); instrumentFuncs(); addProfileExport(); } void Instrumenter::addGlobals() { // Create fresh global names (over-reserves, but that's ok) counterGlobal = Names::getValidGlobalName(*wasm, "monotonic_counter"); functionGlobals.reserve(wasm->functions.size()); ModuleUtils::iterDefinedFunctions(*wasm, [&](Function* func) { functionGlobals.push_back(Names::getValidGlobalName( *wasm, std::string(func->name.c_str()) + "_timestamp")); }); // Create and add new globals auto addGlobal = [&](Name name) { auto global = Builder::makeGlobal( name, Type::i32, Builder(*wasm).makeConst(Literal::makeZero(Type::i32)), Builder::Mutable); global->hasExplicitName = true; wasm->addGlobal(std::move(global)); }; addGlobal(counterGlobal); for (auto& name : functionGlobals) { addGlobal(name); } } void Instrumenter::instrumentFuncs() { // Inject the following code at the beginning of each function to advance the // monotonic counter and set the function's timestamp if it hasn't already // been set. // // (if (i32.eqz (global.get $timestamp)) // (block // (global.set $monotonic_counter // (i32.add // (global.get $monotonic_counter) // (i32.const 1) // ) // ) // (global.set $timestamp // (global.get $monotonic_counter) // ) // ) // ) Builder builder(*wasm); auto globalIt = functionGlobals.begin(); ModuleUtils::iterDefinedFunctions(*wasm, [&](Function* func) { func->body = builder.makeSequence( builder.makeIf( builder.makeUnary(EqZInt32, builder.makeGlobalGet(*globalIt, Type::i32)), builder.makeSequence( builder.makeGlobalSet( counterGlobal, builder.makeBinary(AddInt32, builder.makeGlobalGet(counterGlobal, Type::i32), builder.makeConst(Literal::makeOne(Type::i32)))), builder.makeGlobalSet( *globalIt, builder.makeGlobalGet(counterGlobal, Type::i32)))), func->body, func->body->type); ++globalIt; }); } // wasm-split profile format: // // The wasm-split profile is a binary format designed to be simple to produce // and consume. It is comprised of: // // 1. An 8-byte module hash // // 2. A 4-byte timestamp for each defined function // // The module hash is meant to guard against bugs where the module that was // instrumented and the module that is being split are different. The timestamps // are non-zero for functions that were called during the instrumented run and 0 // otherwise. Functions with smaller non-zero timestamps were called earlier in // the instrumented run than funtions with larger timestamps. void Instrumenter::addProfileExport() { // Create and export a function to dump the profile into a given memory // buffer. The function takes the available address and buffer size as // arguments and returns the total size of the profile. It only actually // writes the profile if the given space is sufficient to hold it. auto name = Names::getValidFunctionName(*wasm, profileExport); auto writeProfile = Builder::makeFunction( name, Signature({Type::i32, Type::i32}, Type::i32), {}); writeProfile->hasExplicitName = true; writeProfile->setLocalName(0, "addr"); writeProfile->setLocalName(1, "size"); // Calculate the size of the profile: // 8 bytes module hash + // 4 bytes for the timestamp for each function const size_t profileSize = 8 + 4 * functionGlobals.size(); // Create the function body Builder builder(*wasm); auto getAddr = [&]() { return builder.makeLocalGet(0, Type::i32); }; auto getSize = [&]() { return builder.makeLocalGet(1, Type::i32); }; auto hashConst = [&]() { return builder.makeConst(int64_t(moduleHash)); }; auto profileSizeConst = [&]() { return builder.makeConst(int32_t(profileSize)); }; // Write the hash followed by all the time stamps Expression* writeData = builder.makeStore(8, 0, 1, getAddr(), hashConst(), Type::i64); uint32_t offset = 8; for (const auto& global : functionGlobals) { writeData = builder.blockify( writeData, builder.makeStore(4, offset, 1, getAddr(), builder.makeGlobalGet(global, Type::i32), Type::i32)); offset += 4; } writeProfile->body = builder.makeSequence( builder.makeIf(builder.makeBinary(GeUInt32, getSize(), profileSizeConst()), writeData), profileSizeConst()); // Create an export for the function wasm->addFunction(std::move(writeProfile)); wasm->addExport( Builder::makeExport(profileExport, name, ExternalKind::Function)); // Also make sure there is a memory with enough pages to write into size_t pages = (profileSize + Memory::kPageSize - 1) / Memory::kPageSize; if (!wasm->memory.exists) { wasm->memory.exists = true; wasm->memory.initial = pages; wasm->memory.max = pages; } else if (wasm->memory.initial < pages) { wasm->memory.initial = pages; if (wasm->memory.max < pages) { wasm->memory.max = pages; } } // TODO: export the memory if it is not already exported. } uint64_t hashFile(const std::string& filename) { auto contents(read_file>(filename, Flags::Binary)); size_t digest = 0; // Don't use `hash` or `rehash` - they aren't deterministic between executions for (char c : contents) { hash_combine(digest, c); } return uint64_t(digest); } void adjustTableSize(Module& wasm, int initialSize) { if (initialSize < 0) { return; } if (wasm.tables.empty()) { Fatal() << "--initial-table used but there is no table"; } auto& table = wasm.tables.front(); if ((uint64_t)initialSize < table->initial) { Fatal() << "Specified initial table size too small, should be at least " << table->initial; } if ((uint64_t)initialSize > table->max) { Fatal() << "Specified initial table size larger than max table size " << table->max; } table->initial = initialSize; } void writeModule(Module& wasm, std::string filename, const WasmSplitOptions& options) { ModuleWriter writer; writer.setBinary(options.emitBinary); writer.setDebugInfo(options.passOptions.debugInfo); if (options.emitModuleNames) { writer.setEmitModuleName(true); } writer.write(wasm, filename); } void instrumentModule(const WasmSplitOptions& options) { Module wasm; parseInput(wasm, options); // Check that the profile export name is not already taken if (wasm.getExportOrNull(options.profileExport) != nullptr) { Fatal() << "error: Export " << options.profileExport << " already exists."; } uint64_t moduleHash = hashFile(options.inputFiles[0]); PassRunner runner(&wasm, options.passOptions); Instrumenter(options.profileExport, moduleHash).run(&runner, &wasm); adjustTableSize(wasm, options.initialTableSize); // Write the output modules writeModule(wasm, options.output, options); } struct ProfileData { uint64_t hash; std::vector timestamps; }; // See "wasm-split profile format" above for more information. ProfileData readProfile(const std::string& file) { auto profileData = read_file>(file, Flags::Binary); size_t i = 0; auto readi32 = [&]() { if (i + 4 > profileData.size()) { Fatal() << "Unexpected end of profile data in " << file; } uint32_t i32 = 0; i32 |= uint32_t(uint8_t(profileData[i++])); i32 |= uint32_t(uint8_t(profileData[i++])) << 8; i32 |= uint32_t(uint8_t(profileData[i++])) << 16; i32 |= uint32_t(uint8_t(profileData[i++])) << 24; return i32; }; uint64_t hash = readi32(); hash |= uint64_t(readi32()) << 32; std::vector timestamps; while (i < profileData.size()) { timestamps.push_back(readi32()); } return {hash, timestamps}; } void writeSymbolMap(Module& wasm, std::string filename) { PassOptions options; options.arguments["symbolmap"] = filename; PassRunner runner(&wasm, options); runner.add("symbolmap"); runner.run(); } void writePlaceholderMap(const std::map placeholderMap, std::string filename) { Output output(filename, Flags::Text); auto& o = output.getStream(); for (auto pair : placeholderMap) { o << pair.first << ':' << pair.second << '\n'; } } void splitModule(const WasmSplitOptions& options) { Module wasm; parseInput(wasm, options); std::set keepFuncs; if (options.profileFile.size()) { // Use the profile to initialize `keepFuncs`. uint64_t hash = hashFile(options.inputFiles[0]); ProfileData profile = readProfile(options.profileFile); if (profile.hash != hash) { Fatal() << "error: checksum in profile does not match module checksum. " << "The split module must be the original module that was " << "instrumented to generate the profile."; } size_t i = 0; ModuleUtils::iterDefinedFunctions(wasm, [&](Function* func) { if (i >= profile.timestamps.size()) { Fatal() << "Unexpected end of profile data"; } if (profile.timestamps[i++] > 0) { keepFuncs.insert(func->name); } }); if (i != profile.timestamps.size()) { Fatal() << "Unexpected extra profile data"; } } // Add in the functions specified with --keep-funcs for (auto& func : options.keepFuncs) { if (!options.quiet && wasm.getFunctionOrNull(func) == nullptr) { std::cerr << "warning: function " << func << " does not exist\n"; } keepFuncs.insert(func); } // Remove the functions specified with --remove-funcs for (auto& func : options.splitFuncs) { auto* function = wasm.getFunctionOrNull(func); if (!options.quiet && function == nullptr) { std::cerr << "warning: function " << func << " does not exist\n"; } if (function && function->imported()) { if (!options.quiet) { std::cerr << "warning: cannot split out imported function " << func << "\n"; } } else { keepFuncs.erase(func); } } if (!options.quiet && keepFuncs.size() == 0) { std::cerr << "warning: not keeping any functions in the primary module\n"; } // If warnings are enabled, check that any functions are being split out. if (!options.quiet) { std::set splitFuncs; ModuleUtils::iterDefinedFunctions(wasm, [&](Function* func) { if (keepFuncs.count(func->name) == 0) { splitFuncs.insert(func->name); } }); if (splitFuncs.size() == 0) { std::cerr << "warning: not splitting any functions out to the secondary module\n"; } // Dump the kept and split functions if we are verbose if (options.verbose) { auto printCommaSeparated = [&](auto funcs) { for (auto it = funcs.begin(); it != funcs.end(); ++it) { if (it != funcs.begin()) { std::cout << ", "; } std::cout << *it; } }; std::cout << "Keeping functions: "; printCommaSeparated(keepFuncs); std::cout << "\n"; std::cout << "Splitting out functions: "; printCommaSeparated(splitFuncs); std::cout << "\n"; } } // Actually perform the splitting ModuleSplitting::Config config; config.primaryFuncs = std::move(keepFuncs); if (options.importNamespace.size()) { config.importNamespace = options.importNamespace; } if (options.placeholderNamespace.size()) { config.placeholderNamespace = options.placeholderNamespace; } if (options.exportPrefix.size()) { config.newExportPrefix = options.exportPrefix; } config.minimizeNewExportNames = !options.passOptions.debugInfo; auto splitResults = ModuleSplitting::splitFunctions(wasm, config); auto& secondary = splitResults.secondary; adjustTableSize(wasm, options.initialTableSize); adjustTableSize(*secondary, options.initialTableSize); if (options.symbolMap) { writeSymbolMap(wasm, options.primaryOutput + ".symbols"); writeSymbolMap(*secondary, options.secondaryOutput + ".symbols"); } if (options.placeholderMap) { writePlaceholderMap(splitResults.placeholderMap, options.primaryOutput + ".placeholders"); } // Set the names of the split modules. This can help differentiate them in // stack traces. if (options.emitModuleNames) { if (!wasm.name) { wasm.name = Path::getBaseName(options.primaryOutput); } secondary->name = Path::getBaseName(options.secondaryOutput); } // write the output modules writeModule(wasm, options.primaryOutput, options); writeModule(*secondary, options.secondaryOutput, options); } void mergeProfiles(const WasmSplitOptions& options) { // Read the initial profile. We will merge other profiles into this one. ProfileData data = readProfile(options.inputFiles[0]); // In verbose mode, we want to find profiles that don't contribute to the // merged profile. To do that, keep track of how many profiles each function // appears in. If any profile contains only functions that appear in multiple // profiles, it could be dropped. std::vector numProfiles; if (options.verbose) { numProfiles.resize(data.timestamps.size()); for (size_t t = 0; t < data.timestamps.size(); ++t) { if (data.timestamps[t]) { numProfiles[t] = 1; } } } // Read all the other profiles, taking the minimum nonzero timestamp for each // function. for (size_t i = 1; i < options.inputFiles.size(); ++i) { ProfileData newData = readProfile(options.inputFiles[i]); if (newData.hash != data.hash) { Fatal() << "Checksum in profile " << options.inputFiles[i] << " does not match hash in profile " << options.inputFiles[0]; } if (newData.timestamps.size() != data.timestamps.size()) { Fatal() << "Profile " << options.inputFiles[i] << " incompatible with profile " << options.inputFiles[0]; } for (size_t t = 0; t < data.timestamps.size(); ++t) { if (data.timestamps[t] && newData.timestamps[t]) { data.timestamps[t] = std::min(data.timestamps[t], newData.timestamps[t]); } else if (newData.timestamps[t]) { data.timestamps[t] = newData.timestamps[t]; } if (options.verbose && newData.timestamps[t]) { ++numProfiles[t]; } } } // Check for useless profiles. if (options.verbose) { for (const auto& file : options.inputFiles) { bool useless = true; ProfileData newData = readProfile(file); for (size_t t = 0; t < newData.timestamps.size(); ++t) { if (newData.timestamps[t] && numProfiles[t] == 1) { useless = false; break; } } if (useless) { std::cout << "Profile " << file << " only includes functions included in other profiles.\n"; } } } // Write the combined profile. BufferWithRandomAccess buffer; buffer << data.hash; for (size_t t = 0; t < data.timestamps.size(); ++t) { buffer << uint32_t(data.timestamps[t]); } Output out(options.output, Flags::Binary); buffer.writeTo(out.getStream()); } } // anonymous namespace int main(int argc, const char* argv[]) { WasmSplitOptions options; options.parse(argc, argv); if (!options.validate()) { Fatal() << "Invalid command line arguments"; } switch (options.mode) { case WasmSplitOptions::Mode::Split: splitModule(options); break; case WasmSplitOptions::Mode::Instrument: instrumentModule(options); break; case WasmSplitOptions::Mode::MergeProfiles: mergeProfiles(options); break; } }