Skip to content

Commit e60ca69

Browse files
authored
Merge pull request #85440 from rjmansfield/fix-wmo-opt-record-paths
Fix optimization record paths in multi-threaded WMO mode
2 parents 8f4d1b1 + 1f5fb75 commit e60ca69

File tree

5 files changed

+236
-38
lines changed

5 files changed

+236
-38
lines changed

lib/Frontend/ArgsToFrontendOutputsConverter.cpp

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -286,23 +286,41 @@ SupplementaryOutputPathsComputer::computeOutputPaths() const {
286286
// For other cases, process the paths normally
287287
std::vector<SupplementaryOutputPaths> outputPaths;
288288
unsigned i = 0;
289-
bool hadError = InputsAndOutputs.forEachInputProducingSupplementaryOutput(
290-
[&](const InputFile &input) -> bool {
291-
if (auto suppPaths = computeOutputPathsForOneInput(
292-
OutputFiles[i], (*pathsFromUser)[i], input)) {
293-
++i;
294-
outputPaths.push_back(*suppPaths);
295-
return false;
296-
}
297-
return true;
298-
});
289+
bool hadError = false;
290+
291+
// In multi-threaded WMO with supplementary output file map, we have paths
292+
// for all inputs, so process them all through computeOutputPathsForOneInput
293+
if (!InputsAndOutputs.hasPrimaryInputs() && OutputFiles.size() > 1 &&
294+
pathsFromUser->size() == InputsAndOutputs.inputCount()) {
295+
hadError = InputsAndOutputs.forEachInput([&](const InputFile &input) -> bool {
296+
if (auto suppPaths = computeOutputPathsForOneInput(
297+
OutputFiles[i], (*pathsFromUser)[i], input)) {
298+
++i;
299+
outputPaths.push_back(*suppPaths);
300+
return false;
301+
}
302+
return true;
303+
});
304+
} else {
305+
// Standard path: process inputs that produce supplementary output
306+
hadError = InputsAndOutputs.forEachInputProducingSupplementaryOutput(
307+
[&](const InputFile &input) -> bool {
308+
if (auto suppPaths = computeOutputPathsForOneInput(
309+
OutputFiles[i], (*pathsFromUser)[i], input)) {
310+
++i;
311+
outputPaths.push_back(*suppPaths);
312+
return false;
313+
}
314+
return true;
315+
});
316+
}
299317
if (hadError)
300318
return std::nullopt;
301319

302-
// In WMO mode compute supplementary output paths for optimization record
303-
// data (opt-remarks). We need one path per LLVMModule that will be created as
304-
// part of wmo.
305-
if (!InputsAndOutputs.hasPrimaryInputs()) {
320+
// In WMO mode without supplementary output file map, compute supplementary
321+
// output paths for optimization records for inputs beyond the first one.
322+
if (!InputsAndOutputs.hasPrimaryInputs() && OutputFiles.size() > 1 &&
323+
pathsFromUser->size() != InputsAndOutputs.inputCount()) {
306324
unsigned i = 0;
307325
InputsAndOutputs.forEachInput([&](const InputFile &input) -> bool {
308326
// First input is already computed.
@@ -448,8 +466,9 @@ SupplementaryOutputPathsComputer::getSupplementaryOutputPathsFromArguments()
448466
sop.APIDescriptorOutputPath = (*apiDescriptorOutput)[moduleIndex];
449467
sop.ConstValuesOutputPath = (*constValuesOutput)[moduleIndex];
450468
sop.ModuleSemanticInfoOutputPath = (*moduleSemanticInfoOutput)[moduleIndex];
451-
sop.YAMLOptRecordPath = (*optRecordOutput)[moduleIndex];
452-
sop.BitstreamOptRecordPath = (*optRecordOutput)[moduleIndex];
469+
// Optimization record paths are per-file in multi-threaded WMO, like IR
470+
sop.YAMLOptRecordPath = (*optRecordOutput)[i];
471+
sop.BitstreamOptRecordPath = (*optRecordOutput)[i];
453472
sop.SILOutputPath = (*silOutput)[silOutputIndex];
454473
sop.LLVMIROutputPath = (*irOutput)[i];
455474
result.push_back(sop);
@@ -478,18 +497,28 @@ SupplementaryOutputPathsComputer::getSupplementaryFilenamesFromArguments(
478497
paths.emplace_back();
479498
return paths;
480499
}
481-
// Special handling for SIL and IR output paths: allow multiple paths per file
482-
// type
483-
else if ((pathID == options::OPT_sil_output_path ||
484-
pathID == options::OPT_ir_output_path) &&
500+
// Special handling for IR and optimization record output paths: allow multiple paths per file
501+
// type. Note: SIL is NOT included here because in WMO mode, SIL is generated once
502+
// per module, not per source file.
503+
else if ((pathID == options::OPT_ir_output_path ||
504+
pathID == options::OPT_save_optimization_record_path) &&
485505
paths.size() > N) {
486-
// For parallel compilation, we can have multiple SIL/IR output paths
506+
// For parallel compilation, we can have multiple IR/opt-record output paths
487507
// so return all the paths.
488508
return paths;
489509
}
490510

491-
if (paths.empty())
511+
if (paths.empty()) {
512+
// For IR and optimization records in multi-threaded WMO, we need one entry per input file.
513+
// Check if WMO is enabled and we have multiple output files (multi-threaded WMO).
514+
if ((pathID == options::OPT_ir_output_path ||
515+
pathID == options::OPT_save_optimization_record_path) &&
516+
Args.hasArg(options::OPT_whole_module_optimization) &&
517+
OutputFiles.size() > 1) {
518+
return std::vector<std::string>(OutputFiles.size(), std::string());
519+
}
492520
return std::vector<std::string>(N, std::string());
521+
}
493522

494523
Diags.diagnose(SourceLoc(), diag::error_wrong_number_of_arguments,
495524
Args.getLastArg(pathID)->getOption().getPrefixedName(), N,
@@ -864,5 +893,28 @@ SupplementaryOutputPathsComputer::readSupplementaryOutputFileMap() const {
864893
if (hadError)
865894
return std::nullopt;
866895

896+
// In multi-threaded WMO mode, we need to read supplementary output paths
897+
// for all inputs beyond the first one (which was already processed above).
898+
// Entries in the map are optional, so if an input is missing, the regular
899+
// WMO path generation logic will handle it.
900+
if (!InputsAndOutputs.hasPrimaryInputs() && OutputFiles.size() > 1) {
901+
InputsAndOutputs.forEachInput([&](const InputFile &input) -> bool {
902+
// Skip the first input, which was already processed above
903+
if (InputsAndOutputs.firstInput().getFileName() == input.getFileName()) {
904+
return false;
905+
}
906+
907+
// Check if this input has an entry in the supplementary output file map
908+
const TypeToPathMap *mapForInput =
909+
OFM->getOutputMapForInput(input.getFileName());
910+
if (mapForInput) {
911+
// Entry exists, use it
912+
outputPaths.push_back(createFromTypeToPathMap(mapForInput));
913+
}
914+
// If no entry exists, skip - the regular WMO logic will generate paths
915+
return false;
916+
});
917+
}
918+
867919
return outputPaths;
868920
}

lib/Frontend/CompilerInvocation.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,8 +3231,12 @@ static bool ParseSILArgs(SILOptions &Opts, ArgList &Args,
32313231
if (const Arg *A = Args.getLastArg(OPT_save_optimization_record_passes))
32323232
Opts.OptRecordPasses = A->getValue();
32333233

3234-
if (const Arg *A = Args.getLastArg(OPT_save_optimization_record_path))
3235-
Opts.OptRecordFile = A->getValue();
3234+
// Only use getLastArg for single -save-optimization-record-path.
3235+
// With multiple paths (multi-threaded WMO), FrontendTool will populate
3236+
// OptRecordFile and AuxOptRecordFiles from command-line arguments.
3237+
auto allOptRecordPaths = Args.getAllArgValues(OPT_save_optimization_record_path);
3238+
if (allOptRecordPaths.size() == 1)
3239+
Opts.OptRecordFile = allOptRecordPaths[0];
32363240

32373241
// If any of the '-g<kind>', except '-gnone', is given,
32383242
// tell the SILPrinter to print debug info as well

lib/FrontendTool/FrontendTool.cpp

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,8 @@ static bool writeAPIDescriptorIfNeeded(CompilerInstance &Instance) {
731731
static bool performCompileStepsPostSILGen(
732732
CompilerInstance &Instance, std::unique_ptr<SILModule> SM,
733733
ModuleOrSourceFile MSF, const PrimarySpecificPaths &PSPs, int &ReturnValue,
734-
FrontendObserver *observer, ArrayRef<const char *> CommandLineArgs);
734+
FrontendObserver *observer, ArrayRef<const char *> CommandLineArgs,
735+
ArrayRef<PrimarySpecificPaths> auxPSPs = {});
735736

736737
bool swift::performCompileStepsPostSema(
737738
CompilerInstance &Instance, int &ReturnValue, FrontendObserver *observer,
@@ -749,14 +750,48 @@ bool swift::performCompileStepsPostSema(
749750
PSPs.SupplementaryOutputs.YAMLOptRecordPath :
750751
PSPs.SupplementaryOutputs.BitstreamOptRecordPath;
751752
}
753+
754+
auto populateOptRecordPathsFromCmdLine = [&]() {
755+
auto optRecordPaths = collectSupplementaryOutputPaths(
756+
CommandLineArgs, options::OPT_save_optimization_record_path);
757+
if (!optRecordPaths.empty()) {
758+
// Set the first path. With multiple paths, CompilerInvocation leaves
759+
// OptRecordFile empty, so we populate it here along with aux paths.
760+
SILOpts.OptRecordFile = optRecordPaths[0];
761+
if (optRecordPaths.size() > 1) {
762+
SILOpts.AuxOptRecordFiles.assign(optRecordPaths.begin() + 1,
763+
optRecordPaths.end());
764+
}
765+
}
766+
};
767+
752768
if (!auxPSPs.empty()) {
753769
assert(SILOpts.AuxOptRecordFiles.empty());
770+
// Check if ALL auxPSPs have optimization record paths populated
771+
bool allHaveOptRecordPaths = true;
754772
for (const auto &auxFile: auxPSPs) {
755-
SILOpts.AuxOptRecordFiles.push_back(
756-
SILOpts.OptRecordFormat == llvm::remarks::Format::YAML ?
757-
auxFile.SupplementaryOutputs.YAMLOptRecordPath :
758-
auxFile.SupplementaryOutputs.BitstreamOptRecordPath);
773+
bool hasPath = SILOpts.OptRecordFormat == llvm::remarks::Format::YAML ?
774+
!auxFile.SupplementaryOutputs.YAMLOptRecordPath.empty() :
775+
!auxFile.SupplementaryOutputs.BitstreamOptRecordPath.empty();
776+
if (!hasPath) {
777+
allHaveOptRecordPaths = false;
778+
break;
779+
}
759780
}
781+
782+
if (allHaveOptRecordPaths) {
783+
for (const auto &auxFile: auxPSPs) {
784+
SILOpts.AuxOptRecordFiles.push_back(
785+
SILOpts.OptRecordFormat == llvm::remarks::Format::YAML ?
786+
auxFile.SupplementaryOutputs.YAMLOptRecordPath :
787+
auxFile.SupplementaryOutputs.BitstreamOptRecordPath);
788+
}
789+
} else {
790+
populateOptRecordPathsFromCmdLine();
791+
}
792+
} else {
793+
assert(SILOpts.AuxOptRecordFiles.empty());
794+
populateOptRecordPathsFromCmdLine();
760795
}
761796
return SILOpts;
762797
};
@@ -781,7 +816,7 @@ bool swift::performCompileStepsPostSema(
781816
&irgenOpts);
782817
return performCompileStepsPostSILGen(Instance, std::move(SM), mod, PSPs,
783818
ReturnValue, observer,
784-
CommandLineArgs);
819+
CommandLineArgs, auxPSPs);
785820
}
786821

787822

@@ -1983,7 +2018,8 @@ static bool generateCode(CompilerInstance &Instance, StringRef OutputFilename,
19832018
static bool performCompileStepsPostSILGen(
19842019
CompilerInstance &Instance, std::unique_ptr<SILModule> SM,
19852020
ModuleOrSourceFile MSF, const PrimarySpecificPaths &PSPs, int &ReturnValue,
1986-
FrontendObserver *observer, ArrayRef<const char *> CommandLineArgs) {
2021+
FrontendObserver *observer, ArrayRef<const char *> CommandLineArgs,
2022+
ArrayRef<PrimarySpecificPaths> auxPSPs) {
19872023
const auto &Invocation = Instance.getInvocation();
19882024
const auto &opts = Invocation.getFrontendOptions();
19892025
FrontendOptions::ActionType Action = opts.RequestedAction;
@@ -2156,10 +2192,38 @@ static bool performCompileStepsPostSILGen(
21562192
std::vector<std::string> ParallelOutputFilenames =
21572193
opts.InputsAndOutputs.copyOutputFilenames();
21582194

2159-
// Collect IR output paths from command line arguments
2160-
std::vector<std::string> ParallelIROutputFilenames =
2161-
collectSupplementaryOutputPaths(CommandLineArgs,
2162-
options::OPT_ir_output_path);
2195+
// Collect IR output paths - check if supplementary output file map has paths,
2196+
// otherwise fall back to command line arguments
2197+
std::vector<std::string> ParallelIROutputFilenames;
2198+
if (!auxPSPs.empty()) {
2199+
// Check if the first file (PSPs) and ALL auxPSPs have IR output paths populated
2200+
bool allHaveIRPaths = !PSPs.SupplementaryOutputs.LLVMIROutputPath.empty();
2201+
if (allHaveIRPaths) {
2202+
for (const auto &auxFile : auxPSPs) {
2203+
if (auxFile.SupplementaryOutputs.LLVMIROutputPath.empty()) {
2204+
allHaveIRPaths = false;
2205+
break;
2206+
}
2207+
}
2208+
}
2209+
2210+
if (allHaveIRPaths) {
2211+
// Paths are in supplementary output file map - include first file + aux files
2212+
ParallelIROutputFilenames.push_back(PSPs.SupplementaryOutputs.LLVMIROutputPath);
2213+
for (const auto &auxFile : auxPSPs) {
2214+
ParallelIROutputFilenames.push_back(
2215+
auxFile.SupplementaryOutputs.LLVMIROutputPath);
2216+
}
2217+
} else {
2218+
// Fall back to command line arguments
2219+
ParallelIROutputFilenames = collectSupplementaryOutputPaths(
2220+
CommandLineArgs, options::OPT_ir_output_path);
2221+
}
2222+
} else {
2223+
// No auxPSPs, use command line arguments
2224+
ParallelIROutputFilenames = collectSupplementaryOutputPaths(
2225+
CommandLineArgs, options::OPT_ir_output_path);
2226+
}
21632227

21642228
llvm::GlobalVariable *HashGlobal;
21652229
cas::SwiftCASOutputBackend *casBackend =
@@ -2310,10 +2374,13 @@ collectSupplementaryOutputPaths(ArrayRef<const char *> Args,
23102374
StringRef arg = Args[i];
23112375
StringRef optionName;
23122376

2313-
if (OptionID == options::OPT_sil_output_path) {
2314-
optionName = "-sil-output-path";
2315-
} else if (OptionID == options::OPT_ir_output_path) {
2377+
// Note: SIL is NOT included here because in WMO mode, SIL is generated once
2378+
// per module, not per source file. Only IR and optimization records can have
2379+
// per-file outputs in multi-threaded WMO.
2380+
if (OptionID == options::OPT_ir_output_path) {
23162381
optionName = "-ir-output-path";
2382+
} else if (OptionID == options::OPT_save_optimization_record_path) {
2383+
optionName = "-save-optimization-record-path";
23172384
} else {
23182385
continue;
23192386
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Test that frontend properly handles supplementary output file maps with
2+
// optimization records in multi-threaded WMO mode
3+
4+
// RUN: %empty-directory(%t)
5+
6+
// RUN: echo 'public func funcA() -> Int { return 42 }' > %t/file_a.swift
7+
// RUN: echo 'public func funcB() -> String { return "hello" }' > %t/file_b.swift
8+
9+
// RUN: echo '{' > %t/output-file-map.json
10+
// RUN: echo ' "%/t/file_a.swift": {' >> %t/output-file-map.json
11+
// RUN: echo ' "object": "%/t/file_a.o",' >> %t/output-file-map.json
12+
// RUN: echo ' "yaml-opt-record": "%/t/file_a.opt.yaml",' >> %t/output-file-map.json
13+
// RUN: echo ' "llvm-ir": "%/t/file_a.ll"' >> %t/output-file-map.json
14+
// RUN: echo ' },' >> %t/output-file-map.json
15+
// RUN: echo ' "%/t/file_b.swift": {' >> %t/output-file-map.json
16+
// RUN: echo ' "object": "%/t/file_b.o",' >> %t/output-file-map.json
17+
// RUN: echo ' "yaml-opt-record": "%/t/file_b.opt.yaml",' >> %t/output-file-map.json
18+
// RUN: echo ' "llvm-ir": "%/t/file_b.ll"' >> %t/output-file-map.json
19+
// RUN: echo ' }' >> %t/output-file-map.json
20+
// RUN: echo '}' >> %t/output-file-map.json
21+
22+
// RUN: %target-swift-frontend -c %/t/file_a.swift %/t/file_b.swift \
23+
// RUN: -wmo -num-threads 2 -O -module-name TestModule \
24+
// RUN: -supplementary-output-file-map %t/output-file-map.json \
25+
// RUN: -o %t/file_a.o -o %t/file_b.o
26+
27+
// RUN: ls %t/file_a.o
28+
// RUN: ls %t/file_b.o
29+
// RUN: ls %t/file_a.opt.yaml
30+
// RUN: ls %t/file_b.opt.yaml
31+
// RUN: ls %t/file_a.ll
32+
// RUN: ls %t/file_b.ll
33+
34+
// RUN: grep -q "funcA" %t/file_a.opt.yaml
35+
// RUN: grep -q "funcB" %t/file_b.opt.yaml
36+
// RUN: grep -q "funcA" %t/file_a.ll
37+
// RUN: grep -q "funcB" %t/file_b.ll
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Test that frontend properly handles multiple supplementary output paths
2+
// using command line options in multi-threaded WMO mode.
3+
4+
// RUN: %empty-directory(%t)
5+
6+
// RUN: echo 'public func functionA() -> Int { return 42 }' > %t/FileA.swift
7+
// RUN: echo 'public func functionB() -> String { return "hello" }' > %t/FileB.swift
8+
9+
// RUN: %target-swift-frontend -c %t/FileA.swift %t/FileB.swift \
10+
// RUN: -wmo -num-threads 2 -O -module-name TestModule \
11+
// RUN: -save-optimization-record-path %t/FileA.opt.yaml \
12+
// RUN: -save-optimization-record-path %t/FileB.opt.yaml \
13+
// RUN: -ir-output-path %t/FileA.ll \
14+
// RUN: -ir-output-path %t/FileB.ll \
15+
// RUN: -sil-output-path %t/TestModule.sil \
16+
// RUN: -o %t/FileA.o -o %t/FileB.o
17+
18+
// RUN: ls %t/FileA.opt.yaml
19+
// RUN: ls %t/FileB.opt.yaml
20+
21+
// RUN: ls %t/FileA.ll
22+
// RUN: ls %t/FileB.ll
23+
24+
// RUN: ls %t/TestModule.sil
25+
26+
// RUN: ls %t/FileA.o
27+
// RUN: ls %t/FileB.o
28+
29+
// RUN: grep -q "functionA" %t/FileA.ll
30+
// RUN: grep -q "functionB" %t/FileB.ll
31+
32+
// In multi-threaded WMO, each source file should generate its own optimization record file
33+
// RUN: grep -q "functionA" %t/FileA.opt.yaml
34+
// RUN: grep -q "functionB" %t/FileB.opt.yaml
35+
36+
// Verify the SIL output contains both functions (whole module)
37+
// RUN: grep -q "functionA" %t/TestModule.sil
38+
// RUN: grep -q "functionB" %t/TestModule.sil

0 commit comments

Comments
 (0)