| 1 | //===- DTLTO.cpp - Integrated Distributed ThinLTO implementation ----------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // \file |
| 9 | // This file implements support functions for Integrated Distributed ThinLTO, |
| 10 | // focusing on preparing complilation jobs for distribution. |
| 11 | // |
| 12 | //===----------------------------------------------------------------------===// |
| 13 | |
| 14 | #include "llvm/DTLTO/DTLTO.h" |
| 15 | |
| 16 | #include "llvm/ADT/STLExtras.h" |
| 17 | #include "llvm/ADT/ScopeExit.h" |
| 18 | #include "llvm/ADT/SmallString.h" |
| 19 | #include "llvm/ADT/StringExtras.h" |
| 20 | #include "llvm/ADT/StringRef.h" |
| 21 | #include "llvm/LTO/LTO.h" |
| 22 | #include "llvm/Support/FileSystem.h" |
| 23 | #include "llvm/Support/MemoryBufferRef.h" |
| 24 | #include "llvm/Support/Path.h" |
| 25 | #include "llvm/Support/Process.h" |
| 26 | #include "llvm/Support/TimeProfiler.h" |
| 27 | #include "llvm/Support/raw_ostream.h" |
| 28 | |
| 29 | #include <string> |
| 30 | |
| 31 | using namespace llvm; |
| 32 | |
| 33 | // Remove temporary files created to enable distribution. |
| 34 | void lto::DTLTO::cleanup() { |
| 35 | if (!SaveTemps) { |
| 36 | // Remove one file, report error if any. |
| 37 | auto removeFile = [](StringRef FileName) -> void { |
| 38 | std::error_code EC = sys::fs::remove(path: FileName, IgnoreNonExisting: true); |
| 39 | if (EC && |
| 40 | EC != std::make_error_code(e: std::errc::no_such_file_or_directory)) |
| 41 | errs() << "warning: could not remove the file '" << FileName |
| 42 | << "': " << EC.message() << "\n" ; |
| 43 | }; |
| 44 | |
| 45 | TimeTraceScope JobScope("Remove DTLTO temporary files" ); |
| 46 | for (const auto &Name : CleanupList) |
| 47 | removeFile(Name); |
| 48 | // Clean the CleanupList for safety. |
| 49 | CleanupList.clear(); |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // Runs the DTLTO thin link phase, producing per-module summary indices, |
| 54 | // import lists, and cache keys for distribution. |
| 55 | Error lto::DTLTO::performThinLink() { |
| 56 | size_t NumTasks = getMaxTasks(); |
| 57 | SummaryIndexFiles.resize(new_size: NumTasks); |
| 58 | ImportsFilesList.resize(new_size: NumTasks); |
| 59 | CacheKeysList.resize(new_size: NumTasks); |
| 60 | |
| 61 | lto::Config &Cfg = getConfig(); |
| 62 | Cfg.GetSummaryIndexOutputStream = |
| 63 | [&](size_t task) -> std::unique_ptr<raw_svector_ostream> { |
| 64 | return std::make_unique<raw_svector_ostream>(args&: SummaryIndexFiles[task]); |
| 65 | }; |
| 66 | Cfg.GetCacheKeyOutputString = [&](size_t task) -> std::string & { |
| 67 | return CacheKeysList[task]; |
| 68 | }; |
| 69 | Cfg.GetImportsListOutputArray = |
| 70 | [&](size_t task) -> std::vector<std::string> & { |
| 71 | return ImportsFilesList[task]; |
| 72 | }; |
| 73 | return Base::run(AddStream: AddStreamFunc, Cache: {}); |
| 74 | } |
| 75 | |
| 76 | // Runs the DTLTO pipeline. |
| 77 | LLVM_ABI Error lto::DTLTO::run(AddStreamFn AddStream, FileCache CacheParam) { |
| 78 | scope_exit CleanUp([this]() { cleanup(); }); |
| 79 | |
| 80 | AddStreamFunc = AddStream; |
| 81 | Cache = std::move(CacheParam); |
| 82 | Conf.Dtlto = 1; |
| 83 | UID = itostr(X: sys::Process::getProcessId()); |
| 84 | |
| 85 | if (Error Err = performThinLink()) |
| 86 | return Err; |
| 87 | |
| 88 | ThinLTOTaskOffset = RegularLTO.ParallelCodeGenParallelismLevel; |
| 89 | DistributorParams.TargetTriple = RegularLTO.CombinedModule->getTargetTriple(); |
| 90 | |
| 91 | if (Error Err = prepareDtltoJobs()) |
| 92 | return Err; |
| 93 | if (Error Err = serializeLTOInputs()) |
| 94 | return Err; |
| 95 | if (Error Err = performCodegen()) |
| 96 | return Err; |
| 97 | if (Error Err = addObjectFilesToLink()) |
| 98 | return Err; |
| 99 | return Error::success(); |
| 100 | } |
| 101 | |
| 102 | // Probes the LTO cache for a compiled native object for the given job. |
| 103 | Error lto::DTLTO::checkCacheHit(Job &J) { |
| 104 | if (!Cache.isValid()) |
| 105 | return Error::success(); |
| 106 | |
| 107 | TimeTraceScope TimeScope("Check cache for DTLTO" , J.SummaryIndexPath); |
| 108 | |
| 109 | auto CacheAddStreamExp = Cache(J.Task, J.CacheKey, J.ModuleID); |
| 110 | if (Error Err = CacheAddStreamExp.takeError()) |
| 111 | return Err; |
| 112 | AddStreamFn &CacheAddStream = *CacheAddStreamExp; |
| 113 | // If CacheAddStream is null, we have a cache hit and at this point |
| 114 | // object file is already passed back to the linker. |
| 115 | if (!CacheAddStream) { |
| 116 | J.Cached = true; // Cache hit, mark the job as cached. |
| 117 | CachedJobs.fetch_add(i: 1); |
| 118 | } else { |
| 119 | // If CacheAddStream is not null, we have a cache miss and we need to |
| 120 | // run the backend for codegen. Save cache 'add stream' |
| 121 | // function for a later use. |
| 122 | J.CacheAddStream = std::move(CacheAddStream); |
| 123 | } |
| 124 | return Error::success(); |
| 125 | } |
| 126 | |
| 127 | // Prepares a single DTLTO backend compilation job for a ThinLTO module. |
| 128 | Error lto::DTLTO::prepareDtltoJob(StringRef ModulePath, unsigned Task) { |
| 129 | assert(Task >= ThinLTOTaskOffset && Task - ThinLTOTaskOffset < Jobs.size() && |
| 130 | "Task index out of range for Jobs" ); |
| 131 | assert(Task < SummaryIndexFiles.size() && "Task index out of range" ); |
| 132 | |
| 133 | SString ObjFilePath = |
| 134 | sys::path::parent_path(path: DistributorParams.LinkerOutputFile); |
| 135 | sys::path::append(path&: ObjFilePath, a: sys::path::stem(path: ModulePath) + "." + |
| 136 | itostr(X: Task) + "." + UID + ".native.o" ); |
| 137 | |
| 138 | SString SummaryIndexPathStr = ObjFilePath; |
| 139 | SummaryIndexPathStr += ".thinlto.bc" ; |
| 140 | SString ImportsPathStr = ModulePath; |
| 141 | ImportsPathStr += ".imports" ; |
| 142 | |
| 143 | Job &J = Jobs[Task - ThinLTOTaskOffset]; |
| 144 | J = {.Task: Task, |
| 145 | .ModuleID: ModulePath, |
| 146 | .NativeObjectPath: Saver.save(S: ObjFilePath.str()), |
| 147 | .SummaryIndexPath: Saver.save(S: SummaryIndexPathStr.str()), |
| 148 | .ImportsPath: Saver.save(S: ImportsPathStr.str()), |
| 149 | .ImportsFilesList: ImportsFilesList[Task], |
| 150 | .CacheKey: CacheKeysList[Task], |
| 151 | .CacheAddStream: nullptr, |
| 152 | .Cached: false}; |
| 153 | |
| 154 | if (Error Err = checkCacheHit(J)) |
| 155 | return Err; |
| 156 | if (!J.Cached) { |
| 157 | InputModuleIDsToSerialize.insert(V: J.ModuleID); |
| 158 | for (StringRef ImportPath : J.ImportsFilesList) |
| 159 | InputModuleIDsToSerialize.insert(V: ImportPath); |
| 160 | |
| 161 | TimeTraceScope JobScope("Emit individual index for DTLTO" , |
| 162 | J.SummaryIndexPath); |
| 163 | if (Error Err = save(Buffer: SummaryIndexFiles[Task], Path: J.SummaryIndexPath)) |
| 164 | return Err; |
| 165 | } |
| 166 | if (OnIndexWriteCb) |
| 167 | OnIndexWriteCb(J.SummaryIndexPath.str()); |
| 168 | |
| 169 | if (ShouldEmitImportFiles) |
| 170 | if (Error Err = save(Buffer: join(R&: J.ImportsFilesList, Separator: "\n" ), Path: J.ImportsPath)) |
| 171 | return Err; |
| 172 | |
| 173 | if (!SaveTemps) { |
| 174 | if (!J.Cached) |
| 175 | addToCleanup(Filename: J.NativeObjectPath.str()); |
| 176 | if (!ShouldEmitIndexFiles) |
| 177 | addToCleanup(Filename: J.SummaryIndexPath.str()); |
| 178 | if (!ShouldEmitImportFiles) |
| 179 | addToCleanup(Filename: J.ImportsPath.str()); |
| 180 | } |
| 181 | return Error::success(); |
| 182 | } |
| 183 | |
| 184 | // Derive a set of Clang options that will be shared/common for all DTLTO |
| 185 | // backend compilations. |
| 186 | void lto::DTLTO::buildCommonRemoteCompilerOptions() { |
| 187 | const lto::Config &C = getConfig(); |
| 188 | auto &Ops = DistributorParams.CodegenOptions; |
| 189 | |
| 190 | Ops.push_back(Elt: Saver.save(S: "-O" + Twine(C.OptLevel))); |
| 191 | |
| 192 | if (C.Options.EmitAddrsig) |
| 193 | Ops.push_back(Elt: "-faddrsig" ); |
| 194 | if (C.Options.FunctionSections) |
| 195 | Ops.push_back(Elt: "-ffunction-sections" ); |
| 196 | if (C.Options.DataSections) |
| 197 | Ops.push_back(Elt: "-fdata-sections" ); |
| 198 | |
| 199 | if (C.RelocModel == Reloc::PIC_) |
| 200 | // Clang doesn't have -fpic for all triples. |
| 201 | if (!DistributorParams.TargetTriple.isOSBinFormatCOFF()) |
| 202 | Ops.push_back(Elt: "-fpic" ); |
| 203 | |
| 204 | // Turn on/off warnings about profile cfg mismatch (default on) |
| 205 | // --lto-pgo-warn-mismatch. |
| 206 | if (!C.PGOWarnMismatch) { |
| 207 | Ops.push_back(Elt: "-mllvm" ); |
| 208 | Ops.push_back(Elt: "-no-pgo-warn-mismatch" ); |
| 209 | } |
| 210 | |
| 211 | // Enable sample-based profile guided optimizations. |
| 212 | // Sample profile file path --lto-sample-profile=<value>. |
| 213 | if (!C.SampleProfile.empty()) { |
| 214 | Ops.push_back(Elt: Saver.save(S: "-fprofile-sample-use=" + Twine(C.SampleProfile))); |
| 215 | DistributorParams.CommonInputs.insert(V: C.SampleProfile); |
| 216 | } |
| 217 | |
| 218 | // We don't know which of options will be used by Clang. |
| 219 | Ops.push_back(Elt: "-Wno-unused-command-line-argument" ); |
| 220 | |
| 221 | // Forward any supplied options. |
| 222 | if (!DistributorParams.RemoteCompilerArgs.empty()) |
| 223 | for (auto &a : DistributorParams.RemoteCompilerArgs) |
| 224 | Ops.push_back(Elt: a); |
| 225 | } |
| 226 | |
| 227 | // Initializes DTLTO state and prepares a job for each ThinLTO module. |
| 228 | Error lto::DTLTO::prepareDtltoJobs() { |
| 229 | auto &ModuleMap = |
| 230 | ThinLTO.ModulesToCompile ? *ThinLTO.ModulesToCompile : ThinLTO.ModuleMap; |
| 231 | |
| 232 | InputModuleIDsToSerialize.clear(); |
| 233 | |
| 234 | if (ModuleMap.empty()) |
| 235 | return Error::success(); |
| 236 | |
| 237 | Jobs.resize(N: ModuleMap.size()); |
| 238 | |
| 239 | for (auto [I, Mod] : enumerate(First&: ModuleMap)) |
| 240 | if (Error E = prepareDtltoJob(ModulePath: Mod.first, Task: ThinLTOTaskOffset + I)) |
| 241 | return E; |
| 242 | |
| 243 | return Error::success(); |
| 244 | } |
| 245 | |
| 246 | // Runs the DTLTO code generation phase. Must be invoked after thinLink(). |
| 247 | Error lto::DTLTO::performCodegen() { |
| 248 | if (Jobs.empty()) |
| 249 | return Error::success(); |
| 250 | // Build common remote compiler options. |
| 251 | buildCommonRemoteCompilerOptions(); |
| 252 | |
| 253 | DistributionDriver Distributor(DistributorParams, Jobs, SaveTemps, |
| 254 | [&](StringRef S) { addToCleanup(Filename: S); }); |
| 255 | |
| 256 | if (CachedJobs.load() < Jobs.size()) { |
| 257 | if (Error E = Distributor()) |
| 258 | return E; |
| 259 | } |
| 260 | return Error::success(); |
| 261 | } |
| 262 | |
| 263 | // Adds compiled object files to the link for each non-cached job. |
| 264 | Error lto::DTLTO::addObjectFilesToLink() { |
| 265 | TimeTraceScope FilesScope("Add DTLTO files to the link" ); |
| 266 | for (auto &Job : Jobs) { |
| 267 | if (!Job.CacheKey.empty() && Job.Cached) { |
| 268 | assert(Cache.isValid()); |
| 269 | continue; |
| 270 | } |
| 271 | // Load the native object from a file into a memory buffer |
| 272 | // and store its contents in the output buffer. |
| 273 | auto ObjFileMbOrErr = |
| 274 | MemoryBuffer::getFile(Filename: Job.NativeObjectPath, /*IsText=*/false, |
| 275 | /*RequiresNullTerminator=*/false); |
| 276 | if (std::error_code EC = ObjFileMbOrErr.getError()) |
| 277 | return make_error<StringError>( |
| 278 | Args: BCError + "cannot open native object file: " + Job.NativeObjectPath + |
| 279 | ": " + EC.message(), |
| 280 | Args: inconvertibleErrorCode()); |
| 281 | |
| 282 | MemoryBufferRef ObjFileMbRef = ObjFileMbOrErr->get()->getMemBufferRef(); |
| 283 | if (Cache.isValid()) { |
| 284 | // Cache hits are taken care of earlier. At this point, we could only |
| 285 | // have cache misses. |
| 286 | assert(Job.CacheAddStream); |
| 287 | // Obtain a file stream for a storing a cache entry. |
| 288 | auto CachedFileStreamOrErr = Job.CacheAddStream(Job.Task, Job.ModuleID); |
| 289 | if (!CachedFileStreamOrErr) |
| 290 | return joinErrors( |
| 291 | E1: CachedFileStreamOrErr.takeError(), |
| 292 | E2: createStringError(EC: inconvertibleErrorCode(), |
| 293 | Fmt: "Cannot get a cache file stream: %s" , |
| 294 | Vals: Job.NativeObjectPath.data())); |
| 295 | // Store a file buffer into the cache stream. |
| 296 | auto &CacheStream = *(CachedFileStreamOrErr->get()); |
| 297 | *(CacheStream.OS) << ObjFileMbRef.getBuffer(); |
| 298 | if (Error Err = CacheStream.commit()) |
| 299 | return Err; |
| 300 | } else { |
| 301 | if (AddBuffer) { |
| 302 | AddBuffer(Job.Task, Job.ModuleID, std::move(ObjFileMbOrErr.get())); |
| 303 | } else { |
| 304 | auto StreamOrErr = AddStreamFunc(Job.Task, Job.ModuleID); |
| 305 | if (Error Err = StreamOrErr.takeError()) |
| 306 | return Err; |
| 307 | auto &Stream = *StreamOrErr->get(); |
| 308 | *Stream.OS << ObjFileMbRef.getBuffer(); |
| 309 | if (Error Err = Stream.commit()) |
| 310 | return Err; |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | return Error::success(); |
| 315 | } |
| 316 | |