Coverage Report

Created: 2026-02-09 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmInstrumentation.cxx
Line
Count
Source
1
#include "cmInstrumentation.h"
2
3
#include <algorithm>
4
#include <chrono>
5
#include <ctime>
6
#include <iomanip>
7
#include <set>
8
#include <sstream>
9
#include <stdexcept>
10
#include <utility>
11
12
#include <cm/memory>
13
#include <cm/optional>
14
#include <cmext/algorithm>
15
16
#include <cm3p/json/reader.h>
17
#include <cm3p/json/version.h>
18
#include <cm3p/json/writer.h>
19
#include <cm3p/uv.h>
20
21
#include "cmsys/Directory.hxx"
22
#include "cmsys/FStream.hxx"
23
#include "cmsys/RegularExpression.hxx"
24
#include "cmsys/SystemInformation.hxx"
25
26
#include "cmCMakePath.h"
27
#include "cmCryptoHash.h"
28
#include "cmFileLock.h"
29
#include "cmFileLockResult.h"
30
#include "cmGeneratorTarget.h"
31
#include "cmGlobalGenerator.h"
32
#include "cmInstrumentationQuery.h"
33
#include "cmJSONState.h"
34
#include "cmList.h"
35
#include "cmLocalGenerator.h"
36
#include "cmState.h"
37
#include "cmStringAlgorithms.h"
38
#include "cmSystemTools.h"
39
#include "cmTimestamp.h"
40
#include "cmUVProcessChain.h"
41
#include "cmValue.h"
42
#include "cmake.h"
43
44
using LoadQueriesAfter = cmInstrumentation::LoadQueriesAfter;
45
46
std::map<std::string, std::string> cmInstrumentation::cdashSnippetsMap = {
47
  {
48
    "configure",
49
    "configure",
50
  },
51
  {
52
    "generate",
53
    "configure",
54
  },
55
  {
56
    "compile",
57
    "build",
58
  },
59
  {
60
    "link",
61
    "build",
62
  },
63
  {
64
    "custom",
65
    "build",
66
  },
67
  {
68
    "build",
69
    "skip",
70
  },
71
  {
72
    "cmakeBuild",
73
    "build",
74
  },
75
  {
76
    "cmakeInstall",
77
    "build",
78
  },
79
  {
80
    "install",
81
    "build",
82
  },
83
  {
84
    "ctest",
85
    "build",
86
  },
87
  {
88
    "test",
89
    "test",
90
  }
91
};
92
93
cmInstrumentation::cmInstrumentation(std::string const& binary_dir,
94
                                     LoadQueriesAfter loadQueries)
95
0
{
96
0
  this->binaryDir = binary_dir;
97
0
  this->timingDirv1 = cmStrCat(this->binaryDir, "/.cmake/instrumentation/v1");
98
0
  this->cdashDir = cmStrCat(this->timingDirv1, "/cdash");
99
0
  this->dataDir = cmStrCat(this->timingDirv1, "/data");
100
0
  if (cm::optional<std::string> configDir =
101
0
        cmSystemTools::GetCMakeConfigDirectory()) {
102
0
    this->userTimingDirv1 = cmStrCat(configDir.value(), "/instrumentation/v1");
103
0
  }
104
0
  if (loadQueries == LoadQueriesAfter::Yes) {
105
0
    this->LoadQueries();
106
0
  }
107
0
}
108
109
void cmInstrumentation::LoadQueries()
110
0
{
111
0
  auto const readJSONQueries = [this](std::string const& dir) {
112
0
    if (cmSystemTools::FileIsDirectory(dir) && this->ReadJSONQueries(dir)) {
113
0
      this->hasQuery = true;
114
0
    }
115
0
  };
116
0
  readJSONQueries(cmStrCat(this->timingDirv1, "/query"));
117
0
  readJSONQueries(cmStrCat(this->timingDirv1, "/query/generated"));
118
0
  if (!this->userTimingDirv1.empty()) {
119
0
    readJSONQueries(cmStrCat(this->userTimingDirv1, "/query"));
120
0
  }
121
0
}
122
123
void cmInstrumentation::CheckCDashVariable()
124
0
{
125
0
  std::string envVal;
126
0
  if (cmSystemTools::GetEnv("CTEST_USE_INSTRUMENTATION", envVal) &&
127
0
      !cmIsOff(envVal)) {
128
0
    std::set<cmInstrumentationQuery::Option> options_ = {
129
0
      cmInstrumentationQuery::Option::CDashSubmit
130
0
    };
131
0
    if (cmSystemTools::GetEnv("CTEST_USE_VERBOSE_INSTRUMENTATION", envVal) &&
132
0
        !cmIsOff(envVal)) {
133
0
      options_.insert(cmInstrumentationQuery::Option::CDashVerbose);
134
0
    }
135
0
    std::set<cmInstrumentationQuery::Hook> hooks_;
136
0
    this->WriteJSONQuery(options_, hooks_, {});
137
0
  }
138
0
}
139
140
cmsys::SystemInformation& cmInstrumentation::GetSystemInformation()
141
0
{
142
0
  if (!this->systemInformation) {
143
0
    this->systemInformation = cm::make_unique<cmsys::SystemInformation>();
144
0
  }
145
0
  return *this->systemInformation;
146
0
}
147
148
bool cmInstrumentation::ReadJSONQueries(std::string const& directory)
149
0
{
150
0
  cmsys::Directory d;
151
0
  bool result = false;
152
0
  if (d.Load(directory)) {
153
0
    for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
154
0
      std::string fpath = d.GetFilePath(i);
155
0
      if (cmHasLiteralSuffix(fpath, ".json")) {
156
0
        result = true;
157
0
        this->ReadJSONQuery(fpath);
158
0
      }
159
0
    }
160
0
  }
161
0
  return result;
162
0
}
163
164
void cmInstrumentation::ReadJSONQuery(std::string const& file)
165
0
{
166
0
  auto query = cmInstrumentationQuery();
167
0
  query.ReadJSON(file, this->errorMsg, this->options, this->hooks,
168
0
                 this->callbacks);
169
0
  if (this->HasOption(cmInstrumentationQuery::Option::CDashVerbose)) {
170
0
    this->AddOption(cmInstrumentationQuery::Option::CDashSubmit);
171
0
  }
172
0
  if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) {
173
0
    this->AddHook(cmInstrumentationQuery::Hook::PrepareForCDash);
174
0
    this->AddOption(cmInstrumentationQuery::Option::DynamicSystemInformation);
175
0
  }
176
0
  if (!this->errorMsg.empty()) {
177
0
    cmSystemTools::Error(cmStrCat(
178
0
      "Could not load instrumentation queries from ",
179
0
      cmSystemTools::GetParentDirectory(file), ":\n", this->errorMsg));
180
0
  }
181
0
}
182
183
bool cmInstrumentation::HasErrors() const
184
0
{
185
0
  return !this->errorMsg.empty();
186
0
}
187
188
void cmInstrumentation::WriteJSONQuery(
189
  std::set<cmInstrumentationQuery::Option> const& options_,
190
  std::set<cmInstrumentationQuery::Hook> const& hooks_,
191
  std::vector<std::vector<std::string>> const& callbacks_)
192
0
{
193
0
  Json::Value root;
194
0
  root["version"] = 1;
195
0
  root["options"] = Json::arrayValue;
196
0
  for (auto const& option : options_) {
197
0
    root["options"].append(cmInstrumentationQuery::OptionString[option]);
198
0
  }
199
0
  root["hooks"] = Json::arrayValue;
200
0
  for (auto const& hook : hooks_) {
201
0
    root["hooks"].append(cmInstrumentationQuery::HookString[hook]);
202
0
  }
203
0
  root["callbacks"] = Json::arrayValue;
204
0
  for (auto const& callback : callbacks_) {
205
0
    root["callbacks"].append(cmInstrumentation::GetCommandStr(callback));
206
0
  }
207
0
  this->WriteInstrumentationJson(
208
0
    root, "query/generated",
209
0
    cmStrCat("query-", this->writtenJsonQueries++, ".json"));
210
0
}
211
212
void cmInstrumentation::AddCustomContent(std::string const& name,
213
                                         Json::Value const& contents)
214
0
{
215
0
  this->customContent[name] = contents;
216
0
}
217
218
void cmInstrumentation::WriteCMakeContent(
219
  std::unique_ptr<cmGlobalGenerator> const& gg)
220
0
{
221
0
  Json::Value root;
222
0
  root["targets"] = this->DumpTargets(gg);
223
0
  root["custom"] = this->customContent;
224
0
  root["project"] =
225
0
    gg->GetCMakeInstance()->GetCacheDefinition("CMAKE_PROJECT_NAME").GetCStr();
226
0
  this->WriteInstrumentationJson(
227
0
    root, "data/content",
228
0
    cmStrCat("cmake-", this->ComputeSuffixTime(), ".json"));
229
0
}
230
231
Json::Value cmInstrumentation::DumpTargets(
232
  std::unique_ptr<cmGlobalGenerator> const& gg)
233
0
{
234
0
  Json::Value targets = Json::objectValue;
235
0
  std::vector<cmGeneratorTarget*> targetList;
236
0
  for (auto const& lg : gg->GetLocalGenerators()) {
237
0
    cm::append(targetList, lg->GetGeneratorTargets());
238
0
  }
239
0
  for (cmGeneratorTarget* gt : targetList) {
240
0
    if (this->IsInstrumentableTargetType(gt->GetType())) {
241
0
      Json::Value target = Json::objectValue;
242
0
      auto labels = gt->GetSafeProperty("LABELS");
243
0
      target["labels"] = Json::arrayValue;
244
0
      for (auto const& item : cmList(labels)) {
245
0
        target["labels"].append(item);
246
0
      }
247
0
      target["type"] = cmState::GetTargetTypeName(gt->GetType()).c_str();
248
0
      targets[gt->GetName()] = target;
249
0
    }
250
0
  }
251
0
  return targets;
252
0
}
253
254
std::string cmInstrumentation::GetFileByTimestamp(
255
  cmInstrumentation::LatestOrOldest order, std::string const& dataSubdir,
256
  std::string const& exclude)
257
0
{
258
0
  std::string fullDir = cmStrCat(this->dataDir, '/', dataSubdir);
259
0
  std::string result;
260
0
  if (cmSystemTools::FileExists(fullDir)) {
261
0
    cmsys::Directory d;
262
0
    if (d.Load(fullDir)) {
263
0
      for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
264
0
        std::string fname = d.GetFileName(i);
265
0
        if (fname != "." && fname != ".." && fname != exclude &&
266
0
            (result.empty() ||
267
0
             (order == LatestOrOldest::Latest && fname > result) ||
268
0
             (order == LatestOrOldest::Oldest && fname < result))) {
269
0
          result = fname;
270
0
        }
271
0
      }
272
0
    }
273
0
  }
274
0
  return result;
275
0
}
276
277
void cmInstrumentation::RemoveOldFiles(std::string const& dataSubdir)
278
0
{
279
0
  std::string const dataSubdirPath = cmStrCat(this->dataDir, '/', dataSubdir);
280
0
  std::string oldIndex =
281
0
    this->GetFileByTimestamp(LatestOrOldest::Oldest, "index");
282
0
  if (!oldIndex.empty()) {
283
0
    oldIndex = cmStrCat(this->dataDir, "/index/", oldIndex);
284
0
  }
285
0
  if (cmSystemTools::FileExists(dataSubdirPath)) {
286
0
    std::string latestFile =
287
0
      this->GetFileByTimestamp(LatestOrOldest::Latest, dataSubdir);
288
0
    cmsys::Directory d;
289
0
    if (d.Load(dataSubdirPath)) {
290
0
      for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
291
0
        std::string fname = d.GetFileName(i);
292
0
        std::string fpath = d.GetFilePath(i);
293
0
        if (fname != "." && fname != ".." && fname < latestFile) {
294
0
          if (!oldIndex.empty()) {
295
0
            int compare;
296
0
            cmSystemTools::FileTimeCompare(oldIndex, fpath, &compare);
297
0
            if (compare == 1) {
298
0
              continue;
299
0
            }
300
0
          }
301
0
          cmSystemTools::RemoveFile(fpath);
302
0
        }
303
0
      }
304
0
    }
305
0
  }
306
0
}
307
308
void cmInstrumentation::ClearGeneratedQueries()
309
0
{
310
0
  std::string dir = cmStrCat(this->timingDirv1, "/query/generated");
311
0
  if (cmSystemTools::FileIsDirectory(dir)) {
312
0
    cmSystemTools::RemoveADirectory(dir);
313
0
  }
314
0
}
315
316
bool cmInstrumentation::HasQuery() const
317
0
{
318
0
  return this->hasQuery;
319
0
}
320
321
bool cmInstrumentation::HasOption(cmInstrumentationQuery::Option option) const
322
0
{
323
0
  return (this->options.find(option) != this->options.end());
324
0
}
325
326
bool cmInstrumentation::HasHook(cmInstrumentationQuery::Hook hook) const
327
0
{
328
0
  return (this->hooks.find(hook) != this->hooks.end());
329
0
}
330
331
int cmInstrumentation::CollectTimingData(cmInstrumentationQuery::Hook hook)
332
0
{
333
  // Don't run collection if hook is disabled
334
0
  if (hook != cmInstrumentationQuery::Hook::Manual && !this->HasHook(hook)) {
335
0
    return 0;
336
0
  }
337
338
  // Touch index file immediately to claim snippets
339
0
  std::string suffix_time = ComputeSuffixTime();
340
0
  std::string const& index_name = cmStrCat("index-", suffix_time, ".json");
341
0
  std::string index_path = cmStrCat(this->dataDir, "/index/", index_name);
342
0
  cmSystemTools::Touch(index_path, true);
343
344
  // Gather Snippets
345
0
  using snippet = std::pair<std::string, std::string>;
346
0
  std::vector<snippet> files;
347
0
  cmsys::Directory d;
348
0
  std::string last_index_name =
349
0
    this->GetFileByTimestamp(LatestOrOldest::Latest, "index", index_name);
350
0
  if (d.Load(this->dataDir)) {
351
0
    for (unsigned int i = 0; i < d.GetNumberOfFiles(); i++) {
352
0
      std::string fpath = d.GetFilePath(i);
353
0
      std::string fname = d.GetFile(i);
354
0
      if (fname.rfind('.', 0) == 0 || d.FileIsDirectory(i)) {
355
0
        continue;
356
0
      }
357
0
      files.push_back(snippet(std::move(fname), std::move(fpath)));
358
0
    }
359
0
  }
360
361
  // Build Json Object
362
0
  Json::Value index(Json::objectValue);
363
0
  index["snippets"] = Json::arrayValue;
364
0
  index["hook"] = cmInstrumentationQuery::HookString[hook];
365
0
  index["dataDir"] = this->dataDir;
366
0
  index["buildDir"] = this->binaryDir;
367
0
  index["version"] = 1;
368
0
  if (this->HasOption(
369
0
        cmInstrumentationQuery::Option::StaticSystemInformation)) {
370
0
    this->InsertStaticSystemInformation(index);
371
0
  }
372
0
  for (auto const& file : files) {
373
0
    if (last_index_name.empty()) {
374
0
      index["snippets"].append(file.first);
375
0
    } else {
376
0
      int compare;
377
0
      std::string last_index_path =
378
0
        cmStrCat(this->dataDir, "/index/", last_index_name);
379
0
      cmSystemTools::FileTimeCompare(file.second, last_index_path, &compare);
380
0
      if (compare == 1) {
381
0
        index["snippets"].append(file.first);
382
0
      }
383
0
    }
384
0
  }
385
386
  // Parse snippets into the Google trace file
387
0
  if (this->HasOption(cmInstrumentationQuery::Option::Trace)) {
388
0
    std::string trace_name = cmStrCat("trace-", suffix_time, ".json");
389
0
    this->WriteTraceFile(index, trace_name);
390
0
    index["trace"] = cmStrCat("trace/", trace_name);
391
0
  }
392
393
  // Write index file
394
0
  this->WriteInstrumentationJson(index, "data/index", index_name);
395
396
  // Execute callbacks
397
0
  for (auto& cb : this->callbacks) {
398
0
    cmSystemTools::RunSingleCommand(cmStrCat(cb, " \"", index_path, '"'),
399
0
                                    nullptr, nullptr, nullptr, nullptr,
400
0
                                    cmSystemTools::OUTPUT_PASSTHROUGH);
401
0
  }
402
403
  // Special case for CDash collation
404
0
  if (this->HasOption(cmInstrumentationQuery::Option::CDashSubmit)) {
405
0
    this->PrepareDataForCDash(this->dataDir, index_path);
406
0
  }
407
408
  // Delete files
409
0
  for (auto const& f : index["snippets"]) {
410
0
    cmSystemTools::RemoveFile(cmStrCat(this->dataDir, '/', f.asString()));
411
0
  }
412
0
  cmSystemTools::RemoveFile(index_path);
413
414
  // Delete old content and trace files
415
0
  this->RemoveOldFiles("content");
416
0
  this->RemoveOldFiles("trace");
417
418
0
  return 0;
419
0
}
420
421
void cmInstrumentation::InsertDynamicSystemInformation(
422
  Json::Value& root, std::string const& prefix)
423
0
{
424
0
  Json::Value data;
425
0
  double memory;
426
0
  double load;
427
0
  this->GetDynamicSystemInformation(memory, load);
428
0
  if (!root.isMember("dynamicSystemInformation")) {
429
0
    root["dynamicSystemInformation"] = Json::objectValue;
430
0
  }
431
0
  root["dynamicSystemInformation"][cmStrCat(prefix, "HostMemoryUsed")] =
432
0
    memory;
433
0
  root["dynamicSystemInformation"][cmStrCat(prefix, "CPULoadAverage")] =
434
0
    load > 0 ? Json::Value(load) : Json::nullValue;
435
0
}
436
437
void cmInstrumentation::GetDynamicSystemInformation(double& memory,
438
                                                    double& load)
439
0
{
440
0
  cmsys::SystemInformation& info = this->GetSystemInformation();
441
0
  if (!this->ranSystemChecks) {
442
0
    info.RunCPUCheck();
443
0
    info.RunMemoryCheck();
444
0
    this->ranSystemChecks = true;
445
0
  }
446
0
  memory = (double)info.GetHostMemoryUsed();
447
0
  load = info.GetLoadAverage();
448
0
}
449
450
void cmInstrumentation::InsertStaticSystemInformation(Json::Value& root)
451
0
{
452
0
  cmsys::SystemInformation& info = this->GetSystemInformation();
453
0
  if (!this->ranOSCheck) {
454
0
    info.RunOSCheck();
455
0
    this->ranOSCheck = true;
456
0
  }
457
0
  if (!this->ranSystemChecks) {
458
0
    info.RunCPUCheck();
459
0
    info.RunMemoryCheck();
460
0
    this->ranSystemChecks = true;
461
0
  }
462
0
  Json::Value infoRoot;
463
0
  infoRoot["familyId"] = info.GetFamilyID();
464
0
  infoRoot["hostname"] = info.GetHostname();
465
0
  infoRoot["is64Bits"] = info.Is64Bits();
466
0
  infoRoot["modelId"] = info.GetModelID();
467
0
  infoRoot["modelName"] = info.GetModelName();
468
0
  infoRoot["numberOfLogicalCPU"] = info.GetNumberOfLogicalCPU();
469
0
  infoRoot["numberOfPhysicalCPU"] = info.GetNumberOfPhysicalCPU();
470
0
  infoRoot["OSName"] = info.GetOSName();
471
0
  infoRoot["OSPlatform"] = info.GetOSPlatform();
472
0
  infoRoot["OSRelease"] = info.GetOSRelease();
473
0
  infoRoot["OSVersion"] = info.GetOSVersion();
474
0
  infoRoot["processorAPICID"] = info.GetProcessorAPICID();
475
0
  infoRoot["processorCacheSize"] = info.GetProcessorCacheSize();
476
0
  infoRoot["processorClockFrequency"] =
477
0
    (double)info.GetProcessorClockFrequency();
478
0
  infoRoot["processorName"] = info.GetExtendedProcessorName();
479
0
  infoRoot["totalPhysicalMemory"] =
480
0
    static_cast<Json::Value::UInt64>(info.GetTotalPhysicalMemory());
481
0
  infoRoot["totalVirtualMemory"] =
482
0
    static_cast<Json::Value::UInt64>(info.GetTotalVirtualMemory());
483
0
  infoRoot["vendorID"] = info.GetVendorID();
484
0
  infoRoot["vendorString"] = info.GetVendorString();
485
486
  // Record fields unable to be determined as null JSON objects.
487
0
  for (std::string const& field : infoRoot.getMemberNames()) {
488
0
    if ((infoRoot[field].isNumeric() && infoRoot[field].asInt64() <= 0) ||
489
0
        (infoRoot[field].isString() && infoRoot[field].asString().empty())) {
490
0
      infoRoot[field] = Json::nullValue;
491
0
    }
492
0
  }
493
0
  root["staticSystemInformation"] = infoRoot;
494
0
}
495
496
void cmInstrumentation::InsertTimingData(
497
  Json::Value& root, std::chrono::steady_clock::time_point steadyStart,
498
  std::chrono::system_clock::time_point systemStart)
499
0
{
500
0
  uint64_t timeStart = std::chrono::duration_cast<std::chrono::milliseconds>(
501
0
                         systemStart.time_since_epoch())
502
0
                         .count();
503
0
  uint64_t duration = std::chrono::duration_cast<std::chrono::milliseconds>(
504
0
                        std::chrono::steady_clock::now() - steadyStart)
505
0
                        .count();
506
0
  root["timeStart"] = static_cast<Json::Value::UInt64>(timeStart);
507
0
  root["duration"] = static_cast<Json::Value::UInt64>(duration);
508
0
}
509
510
Json::Value cmInstrumentation::ReadJsonSnippet(std::string const& file_name)
511
0
{
512
0
  Json::CharReaderBuilder builder;
513
0
  builder["collectComments"] = false;
514
0
  cmsys::ifstream ftmp(
515
0
    cmStrCat(this->timingDirv1, "/data/", file_name).c_str());
516
0
  Json::Value snippetData;
517
0
  builder["collectComments"] = false;
518
519
0
  if (!Json::parseFromStream(builder, ftmp, &snippetData, nullptr)) {
520
#if JSONCPP_VERSION_HEXA < 0x01070300
521
    snippetData = Json::Value::null;
522
#else
523
0
    snippetData = Json::Value::nullSingleton();
524
0
#endif
525
0
  }
526
527
0
  ftmp.close();
528
0
  return snippetData;
529
0
}
530
531
void cmInstrumentation::WriteInstrumentationJson(Json::Value& root,
532
                                                 std::string const& subdir,
533
                                                 std::string const& file_name)
534
0
{
535
0
  Json::StreamWriterBuilder wbuilder;
536
0
  wbuilder["indentation"] = "\t";
537
0
  std::unique_ptr<Json::StreamWriter> JsonWriter =
538
0
    std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
539
0
  std::string const& directory = cmStrCat(this->timingDirv1, '/', subdir);
540
0
  cmSystemTools::MakeDirectory(directory);
541
542
0
  cmsys::ofstream ftmp(cmStrCat(directory, '/', file_name).c_str());
543
0
  if (!ftmp.good()) {
544
0
    throw std::runtime_error(std::string("Unable to open: ") + file_name);
545
0
  }
546
547
0
  try {
548
0
    JsonWriter->write(root, &ftmp);
549
0
    ftmp << "\n";
550
0
    ftmp.close();
551
0
  } catch (std::ios_base::failure& fail) {
552
0
    cmSystemTools::Error(cmStrCat("Failed to write JSON: ", fail.what()));
553
0
  } catch (...) {
554
0
    cmSystemTools::Error("Error writing JSON output for instrumentation.");
555
0
  }
556
0
}
557
558
std::string cmInstrumentation::InstrumentTest(
559
  std::string const& name, std::string const& command,
560
  std::vector<std::string> const& args, int64_t result,
561
  std::chrono::steady_clock::time_point steadyStart,
562
  std::chrono::system_clock::time_point systemStart, std::string config)
563
0
{
564
  // Store command info
565
0
  Json::Value root(this->preTestStats);
566
0
  std::string command_str = cmStrCat(command, ' ', GetCommandStr(args));
567
0
  root["version"] = 1;
568
0
  root["command"] = command_str;
569
0
  root["role"] = "test";
570
0
  root["testName"] = name;
571
0
  root["result"] = static_cast<Json::Value::Int64>(result);
572
0
  root["config"] = config;
573
0
  root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
574
575
  // Post-Command
576
0
  this->InsertTimingData(root, steadyStart, systemStart);
577
0
  if (this->HasOption(
578
0
        cmInstrumentationQuery::Option::DynamicSystemInformation)) {
579
0
    this->InsertDynamicSystemInformation(root, "after");
580
0
  }
581
582
0
  cmsys::SystemInformation& info = this->GetSystemInformation();
583
0
  std::chrono::system_clock::time_point endTime =
584
0
    systemStart + std::chrono::milliseconds(root["duration"].asUInt64());
585
0
  std::string file_name = cmStrCat(
586
0
    "test-",
587
0
    this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), '-',
588
0
    this->ComputeSuffixTime(endTime), ".json");
589
0
  this->WriteInstrumentationJson(root, "data", file_name);
590
0
  return file_name;
591
0
}
592
593
void cmInstrumentation::GetPreTestStats()
594
0
{
595
0
  if (this->HasOption(
596
0
        cmInstrumentationQuery::Option::DynamicSystemInformation)) {
597
0
    this->InsertDynamicSystemInformation(this->preTestStats, "before");
598
0
  }
599
0
}
600
601
int cmInstrumentation::InstrumentCommand(
602
  std::string command_type, std::vector<std::string> const& command,
603
  std::function<int()> const& callback,
604
  cm::optional<std::map<std::string, std::string>> data,
605
  cm::optional<std::map<std::string, std::string>> arrayData,
606
  LoadQueriesAfter reloadQueriesAfterCommand)
607
0
{
608
609
  // Always begin gathering data for configure in case cmake_instrumentation
610
  // command creates a query
611
0
  if (!this->hasQuery && reloadQueriesAfterCommand == LoadQueriesAfter::No) {
612
0
    return callback();
613
0
  }
614
615
  // Store command info
616
0
  Json::Value root(Json::objectValue);
617
0
  Json::Value commandInfo(Json::objectValue);
618
0
  std::string command_str = GetCommandStr(command);
619
620
0
  if (!command_str.empty()) {
621
0
    root["command"] = command_str;
622
0
  }
623
0
  root["version"] = 1;
624
625
  // Pre-Command
626
0
  auto steady_start = std::chrono::steady_clock::now();
627
0
  auto system_start = std::chrono::system_clock::now();
628
0
  double preConfigureMemory = 0;
629
0
  double preConfigureLoad = 0;
630
0
  if (this->HasOption(
631
0
        cmInstrumentationQuery::Option::DynamicSystemInformation)) {
632
0
    this->InsertDynamicSystemInformation(root, "before");
633
0
  } else if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) {
634
0
    this->GetDynamicSystemInformation(preConfigureMemory, preConfigureLoad);
635
0
  }
636
637
  // Execute Command
638
0
  int ret = callback();
639
640
  // Exit early if configure didn't generate a query
641
0
  if (reloadQueriesAfterCommand == LoadQueriesAfter::Yes) {
642
0
    this->LoadQueries();
643
0
    if (!this->HasQuery()) {
644
0
      return ret;
645
0
    }
646
0
    if (this->HasOption(
647
0
          cmInstrumentationQuery::Option::DynamicSystemInformation)) {
648
0
      root["dynamicSystemInformation"] = Json::objectValue;
649
0
      root["dynamicSystemInformation"]["beforeHostMemoryUsed"] =
650
0
        preConfigureMemory;
651
0
      root["dynamicSystemInformation"]["beforeCPULoadAverage"] =
652
0
        preConfigureLoad;
653
0
    }
654
0
  }
655
656
  // Post-Command
657
0
  this->InsertTimingData(root, steady_start, system_start);
658
0
  if (this->HasOption(
659
0
        cmInstrumentationQuery::Option::DynamicSystemInformation)) {
660
0
    this->InsertDynamicSystemInformation(root, "after");
661
0
  }
662
663
  // Gather additional data
664
0
  if (data.has_value()) {
665
0
    for (auto const& item : data.value()) {
666
0
      if (item.first == "role" && !item.second.empty()) {
667
0
        command_type = item.second;
668
0
      } else if (item.first == "showOnly") {
669
0
        root[item.first] = item.second == "1" ? true : false;
670
0
      } else if (!item.second.empty()) {
671
0
        root[item.first] = item.second;
672
0
      }
673
0
    }
674
0
  }
675
676
  // See SpawnBuildDaemon(); this data is currently meaningless for build.
677
0
  root["result"] = command_type == "build" ? Json::nullValue : ret;
678
679
  // Create empty config entry if config not found
680
0
  if (!root.isMember("config") &&
681
0
      (command_type == "compile" || command_type == "link" ||
682
0
       command_type == "custom" || command_type == "install")) {
683
0
    root["config"] = "";
684
0
  }
685
686
0
  if (arrayData.has_value()) {
687
0
    for (auto const& item : arrayData.value()) {
688
0
      if (item.first == "targetLabels" && command_type != "link") {
689
0
        continue;
690
0
      }
691
0
      root[item.first] = Json::arrayValue;
692
0
      std::stringstream ss(item.second);
693
0
      std::string element;
694
0
      while (getline(ss, element, ',')) {
695
0
        root[item.first].append(element);
696
0
      }
697
0
      if (item.first == "outputs") {
698
0
        root["outputSizes"] = Json::arrayValue;
699
0
        for (auto const& output : root["outputs"]) {
700
0
          root["outputSizes"].append(
701
0
            static_cast<Json::Value::UInt64>(cmSystemTools::FileLength(
702
0
              cmStrCat(this->binaryDir, '/', output.asCString()))));
703
0
        }
704
0
      }
705
0
    }
706
0
  }
707
0
  root["role"] = command_type;
708
0
  root["workingDir"] = cmSystemTools::GetLogicalWorkingDirectory();
709
710
0
  auto addCMakeContent = [this](Json::Value& root_) -> void {
711
0
    std::string contentFile =
712
0
      this->GetFileByTimestamp(LatestOrOldest::Latest, "content");
713
0
    if (!contentFile.empty()) {
714
0
      root_["cmakeContent"] = cmStrCat("content/", contentFile);
715
0
    }
716
0
  };
717
  // Don't insert path to CMake content until generate time
718
0
  if (command_type != "configure") {
719
0
    addCMakeContent(root);
720
0
  }
721
722
  // Write Json
723
0
  cmsys::SystemInformation& info = this->GetSystemInformation();
724
0
  std::chrono::system_clock::time_point endTime =
725
0
    system_start + std::chrono::milliseconds(root["duration"].asUInt64());
726
0
  std::string const& file_name = cmStrCat(
727
0
    command_type, '-',
728
0
    this->ComputeSuffixHash(cmStrCat(command_str, info.GetProcessId())), '-',
729
0
    this->ComputeSuffixTime(endTime), ".json");
730
731
  // Don't write configure snippet until generate time
732
0
  if (command_type == "configure") {
733
0
    this->configureSnippetData = root;
734
0
    this->configureSnippetName = file_name;
735
0
  } else {
736
    // Add reference to CMake content and write out configure snippet after
737
    // generate
738
0
    if (command_type == "generate") {
739
0
      addCMakeContent(this->configureSnippetData);
740
0
      this->WriteInstrumentationJson(this->configureSnippetData, "data",
741
0
                                     this->configureSnippetName);
742
0
    }
743
0
    this->WriteInstrumentationJson(root, "data", file_name);
744
0
  }
745
0
  return ret;
746
0
}
747
748
std::string cmInstrumentation::GetCommandStr(
749
  std::vector<std::string> const& args)
750
0
{
751
0
  std::string command_str;
752
0
  for (size_t i = 0; i < args.size(); ++i) {
753
0
    if (args[i].find(' ') != std::string::npos) {
754
0
      command_str = cmStrCat(command_str, '"', args[i], '"');
755
0
    } else {
756
0
      command_str = cmStrCat(command_str, args[i]);
757
0
    }
758
0
    if (i < args.size() - 1) {
759
0
      command_str = cmStrCat(command_str, ' ');
760
0
    }
761
0
  }
762
0
  return command_str;
763
0
}
764
765
std::string cmInstrumentation::ComputeSuffixHash(
766
  std::string const& command_str)
767
0
{
768
0
  cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
769
0
  std::string hash = hasher.HashString(command_str);
770
0
  hash.resize(20, '0');
771
0
  return hash;
772
0
}
773
774
std::string cmInstrumentation::ComputeSuffixTime(
775
  cm::optional<std::chrono::system_clock::time_point> time)
776
0
{
777
0
  std::chrono::milliseconds ms =
778
0
    std::chrono::duration_cast<std::chrono::milliseconds>(
779
0
      (time.has_value() ? time.value() : std::chrono::system_clock::now())
780
0
        .time_since_epoch());
781
0
  std::chrono::seconds s =
782
0
    std::chrono::duration_cast<std::chrono::seconds>(ms);
783
784
0
  std::time_t ts = s.count();
785
0
  std::size_t tms = ms.count() % 1000;
786
787
0
  cmTimestamp cmts;
788
0
  std::ostringstream ss;
789
0
  ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
790
0
     << std::setfill('0') << std::setw(4) << tms;
791
0
  return ss.str();
792
0
}
793
794
bool cmInstrumentation::IsInstrumentableTargetType(
795
  cmStateEnums::TargetType type)
796
0
{
797
0
  return type == cmStateEnums::TargetType::EXECUTABLE ||
798
0
    type == cmStateEnums::TargetType::SHARED_LIBRARY ||
799
0
    type == cmStateEnums::TargetType::STATIC_LIBRARY ||
800
0
    type == cmStateEnums::TargetType::MODULE_LIBRARY ||
801
0
    type == cmStateEnums::TargetType::OBJECT_LIBRARY;
802
0
}
803
804
/*
805
 * Called by ctest --start-instrumentation.
806
 *
807
 * This creates a detached process which waits for the parent process (i.e.,
808
 * the build system) to die before running the postBuild hook. In this way, the
809
 * postBuild hook triggers after every invocation of the build system,
810
 * regardless of whether the build passed or failed.
811
 */
812
int cmInstrumentation::SpawnBuildDaemon()
813
0
{
814
  // Do not inherit handles from the parent process, so that the daemon is
815
  // fully detached. This helps prevent deadlock between the two.
816
0
  uv_disable_stdio_inheritance();
817
818
  // preBuild Hook
819
0
  if (this->LockBuildDaemon()) {
820
    // Release lock before spawning the build daemon, to prevent blocking it.
821
0
    this->lock.Release();
822
0
    this->CollectTimingData(cmInstrumentationQuery::Hook::PreBuild);
823
0
  }
824
825
  // postBuild Hook
826
0
  auto ppid = uv_os_getppid();
827
0
  if (ppid) {
828
0
    std::vector<std::string> args;
829
0
    args.push_back(cmSystemTools::GetCTestCommand());
830
0
    args.push_back("--wait-and-collect-instrumentation");
831
0
    args.push_back(this->binaryDir);
832
0
    args.push_back(std::to_string(ppid));
833
0
    auto builder = cmUVProcessChainBuilder().SetDetached().AddCommand(args);
834
0
    auto chain = builder.Start();
835
0
    uv_run(&chain.GetLoop(), UV_RUN_DEFAULT);
836
0
  }
837
0
  return 0;
838
0
}
839
840
// Prevent multiple build daemons from running simultaneously
841
bool cmInstrumentation::LockBuildDaemon()
842
0
{
843
0
  std::string const lockFile = cmStrCat(this->timingDirv1, "/.build.lock");
844
0
  if (!cmSystemTools::FileExists(lockFile)) {
845
0
    cmSystemTools::Touch(lockFile, true);
846
0
  }
847
0
  return this->lock.Lock(lockFile, 0).IsOk();
848
0
}
849
850
/*
851
 * Always called by ctest --wait-and-collect-instrumentation in a detached
852
 * process. Waits for the given PID to end before running the postBuild hook.
853
 *
854
 * See SpawnBuildDaemon()
855
 */
856
int cmInstrumentation::CollectTimingAfterBuild(int ppid)
857
0
{
858
  // Check if another process is already instrumenting the build.
859
  // This lock will be released when the process exits at the end of the build.
860
0
  if (!this->LockBuildDaemon()) {
861
0
    return 0;
862
0
  }
863
0
  std::function<int()> waitForBuild = [ppid]() -> int {
864
0
    while (0 == uv_kill(ppid, 0)) {
865
0
      cmSystemTools::Delay(100);
866
0
    };
867
    // FIXME(#27331): Investigate a cross-platform solution to obtain the exit
868
    // code given the `ppid` above.
869
0
    return 0;
870
0
  };
871
0
  int ret = this->InstrumentCommand(
872
0
    "build", {}, [waitForBuild]() { return waitForBuild(); }, cm::nullopt,
873
0
    cm::nullopt, LoadQueriesAfter::Yes);
874
0
  this->CollectTimingData(cmInstrumentationQuery::Hook::PostBuild);
875
0
  return ret;
876
0
}
877
878
void cmInstrumentation::AddHook(cmInstrumentationQuery::Hook hook)
879
0
{
880
0
  this->hooks.insert(hook);
881
0
}
882
883
void cmInstrumentation::AddOption(cmInstrumentationQuery::Option option)
884
0
{
885
0
  this->options.insert(option);
886
0
}
887
888
std::string const& cmInstrumentation::GetCDashDir() const
889
0
{
890
0
  return this->cdashDir;
891
0
}
892
893
std::string const& cmInstrumentation::GetDataDir() const
894
0
{
895
0
  return this->dataDir;
896
0
}
897
898
/** Copy the snippets referred to by an index file to a separate
899
 * directory where they will be parsed for submission to CDash.
900
 **/
901
void cmInstrumentation::PrepareDataForCDash(std::string const& data_dir,
902
                                            std::string const& index_path)
903
0
{
904
0
  cmSystemTools::MakeDirectory(this->cdashDir);
905
0
  cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/configure"));
906
0
  cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build"));
907
0
  cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/commands"));
908
0
  cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/build/targets"));
909
0
  cmSystemTools::MakeDirectory(cmStrCat(this->cdashDir, "/test"));
910
911
0
  Json::Value root;
912
0
  std::string error_msg;
913
0
  cmJSONState parseState = cmJSONState(index_path, &root);
914
0
  if (!parseState.errors.empty()) {
915
0
    cmSystemTools::Error(parseState.GetErrorMessage(true));
916
0
    return;
917
0
  }
918
919
0
  if (!root.isObject()) {
920
0
    error_msg =
921
0
      cmStrCat("Expected index file ", index_path, " to contain an object");
922
0
    cmSystemTools::Error(error_msg);
923
0
    return;
924
0
  }
925
926
0
  if (!root.isMember("snippets")) {
927
0
    error_msg = cmStrCat("Expected index file ", index_path,
928
0
                         " to have a key 'snippets'");
929
0
    cmSystemTools::Error(error_msg);
930
0
    return;
931
0
  }
932
933
0
  std::string dst_dir;
934
0
  Json::Value snippets = root["snippets"];
935
0
  for (auto const& snippet : snippets) {
936
    // Parse the role of this snippet.
937
0
    std::string snippet_str = snippet.asString();
938
0
    std::string snippet_path = cmStrCat(data_dir, '/', snippet_str);
939
0
    Json::Value snippet_root;
940
0
    parseState = cmJSONState(snippet_path, &snippet_root);
941
0
    if (!parseState.errors.empty()) {
942
0
      cmSystemTools::Error(parseState.GetErrorMessage(true));
943
0
      continue;
944
0
    }
945
0
    if (!snippet_root.isObject()) {
946
0
      error_msg = cmStrCat("Expected snippet file ", snippet_path,
947
0
                           " to contain an object");
948
0
      cmSystemTools::Error(error_msg);
949
0
      continue;
950
0
    }
951
0
    if (!snippet_root.isMember("role")) {
952
0
      error_msg = cmStrCat("Expected snippet file ", snippet_path,
953
0
                           " to have a key 'role'");
954
0
      cmSystemTools::Error(error_msg);
955
0
      continue;
956
0
    }
957
958
0
    std::string snippet_role = snippet_root["role"].asString();
959
0
    auto map_element = this->cdashSnippetsMap.find(snippet_role);
960
0
    if (map_element == this->cdashSnippetsMap.end()) {
961
0
      std::string message =
962
0
        "Unexpected snippet type encountered: " + snippet_role;
963
0
      cmSystemTools::Message(message, "Warning");
964
0
      continue;
965
0
    }
966
967
0
    if (map_element->second == "skip") {
968
0
      continue;
969
0
    }
970
971
0
    if (map_element->second == "build") {
972
      // We organize snippets on a per-target basis (when possible)
973
      // for Build.xml.
974
0
      if (snippet_root.isMember("target")) {
975
0
        dst_dir = cmStrCat(this->cdashDir, "/build/targets/",
976
0
                           snippet_root["target"].asString());
977
0
        cmSystemTools::MakeDirectory(dst_dir);
978
0
      } else {
979
0
        dst_dir = cmStrCat(this->cdashDir, "/build/commands");
980
0
      }
981
0
    } else {
982
0
      dst_dir = cmStrCat(this->cdashDir, '/', map_element->second);
983
0
    }
984
985
0
    std::string dst = cmStrCat(dst_dir, '/', snippet_str);
986
0
    cmsys::Status copied = cmSystemTools::CopyFileAlways(snippet_path, dst);
987
0
    if (!copied) {
988
0
      error_msg = cmStrCat("Failed to copy ", snippet_path, " to ", dst);
989
0
      cmSystemTools::Error(error_msg);
990
0
    }
991
0
  }
992
0
}
993
994
void cmInstrumentation::WriteTraceFile(Json::Value const& index,
995
                                       std::string const& trace_name)
996
0
{
997
0
  std::vector<std::string> snippets = std::vector<std::string>();
998
0
  for (auto const& f : index["snippets"]) {
999
0
    snippets.push_back(f.asString());
1000
0
  }
1001
  // Reverse-sort snippets by timeEnd (timeStart + duration) as a
1002
  // prerequisite for AssignTargetToTraceThread().
1003
0
  auto extractSnippetTimestamp = [](std::string file) -> std::string {
1004
0
    cmsys::RegularExpression snippetTimeRegex(
1005
0
      "[A-Za-z]+-[A-Za-z0-9]+-([0-9T\\-]+)\\.json");
1006
0
    cmsys::RegularExpressionMatch matchA;
1007
0
    if (snippetTimeRegex.find(file.c_str(), matchA)) {
1008
0
      return matchA.match(1);
1009
0
    }
1010
0
    return "";
1011
0
  };
1012
0
  std::sort(
1013
0
    snippets.begin(), snippets.end(),
1014
0
    [extractSnippetTimestamp](std::string snippetA, std::string snippetB) {
1015
0
      return extractSnippetTimestamp(snippetA) >
1016
0
        extractSnippetTimestamp(snippetB);
1017
0
    });
1018
1019
0
  std::string traceDir = cmStrCat(this->timingDirv1, "/data/trace/");
1020
0
  std::string traceFile = cmStrCat(traceDir, trace_name);
1021
0
  cmSystemTools::MakeDirectory(traceDir);
1022
0
  cmsys::ofstream traceStream;
1023
0
  Json::StreamWriterBuilder wbuilder;
1024
0
  wbuilder["indentation"] = "\t";
1025
0
  std::unique_ptr<Json::StreamWriter> jsonWriter =
1026
0
    std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
1027
0
  traceStream.open(traceFile.c_str(), std::ios::out | std::ios::trunc);
1028
0
  if (!traceStream.good()) {
1029
0
    throw std::runtime_error(std::string("Unable to open: ") + traceFile);
1030
0
  }
1031
0
  traceStream << "[";
1032
1033
  // Append trace events from single snippets. Prefer writing to the output
1034
  // stream incrementally over building up a Json::arrayValue in memory for
1035
  // large traces.
1036
0
  std::vector<uint64_t> workers = std::vector<uint64_t>();
1037
0
  Json::Value traceEvent;
1038
0
  Json::Value snippetData;
1039
0
  for (size_t i = 0; i < snippets.size(); i++) {
1040
0
    snippetData = this->ReadJsonSnippet(snippets[i]);
1041
0
    traceEvent = this->BuildTraceEvent(workers, snippetData);
1042
0
    try {
1043
0
      if (i > 0) {
1044
0
        traceStream << ",";
1045
0
      }
1046
0
      jsonWriter->write(traceEvent, &traceStream);
1047
0
      if (i % 50 == 0 || i == snippets.size() - 1) {
1048
0
        traceStream.flush();
1049
0
        traceStream.clear();
1050
0
      }
1051
0
    } catch (std::ios_base::failure& fail) {
1052
0
      cmSystemTools::Error(
1053
0
        cmStrCat("Failed to write to Google trace file: ", fail.what()));
1054
0
    } catch (...) {
1055
0
      cmSystemTools::Error("Error writing Google trace output.");
1056
0
    }
1057
0
  }
1058
1059
0
  try {
1060
0
    traceStream << "]\n";
1061
0
    traceStream.close();
1062
0
  } catch (...) {
1063
0
    cmSystemTools::Error("Error writing Google trace output.");
1064
0
  }
1065
0
}
1066
1067
Json::Value cmInstrumentation::BuildTraceEvent(std::vector<uint64_t>& workers,
1068
                                               Json::Value const& snippetData)
1069
0
{
1070
0
  Json::Value snippetTraceEvent;
1071
1072
  // Provide a useful trace event name depending on what data is available
1073
  // from the snippet.
1074
0
  std::string nameSuffix;
1075
0
  if (snippetData["role"] == "compile") {
1076
0
    nameSuffix = snippetData["source"].asString();
1077
0
  } else if (snippetData["role"] == "link") {
1078
0
    nameSuffix = snippetData["target"].asString();
1079
0
  } else if (snippetData["role"] == "install") {
1080
0
    cmCMakePath workingDir(snippetData["workingDir"].asCString());
1081
0
    nameSuffix = workingDir.GetFileName().String();
1082
0
  } else if (snippetData["role"] == "custom") {
1083
0
    nameSuffix = snippetData["command"].asString();
1084
0
  } else if (snippetData["role"] == "test") {
1085
0
    nameSuffix = snippetData["testName"].asString();
1086
0
  }
1087
0
  if (!nameSuffix.empty()) {
1088
0
    snippetTraceEvent["name"] =
1089
0
      cmStrCat(snippetData["role"].asString(), ": ", nameSuffix);
1090
0
  } else {
1091
0
    snippetTraceEvent["name"] = snippetData["role"].asString();
1092
0
  }
1093
1094
0
  snippetTraceEvent["cat"] = snippetData["role"];
1095
0
  snippetTraceEvent["ph"] = "X";
1096
0
  snippetTraceEvent["args"] = snippetData;
1097
1098
  // Time in the Trace Event Format is stored in microseconds
1099
  // but the snippet files store time in milliseconds.
1100
0
  snippetTraceEvent["ts"] = snippetData["timeStart"].asUInt64() * 1000;
1101
0
  snippetTraceEvent["dur"] = snippetData["duration"].asUInt64() * 1000;
1102
1103
  // Assign an arbitrary PID, since this data isn't useful for the
1104
  // visualization in our case.
1105
0
  snippetTraceEvent["pid"] = 0;
1106
  // Assign TID of 0 for snippets which will have other snippet data
1107
  // visualized "underneath" them. (For others, start from 1.)
1108
0
  if (snippetData["role"] == "build" || snippetData["role"] == "cmakeBuild" ||
1109
0
      snippetData["role"] == "ctest" ||
1110
0
      snippetData["role"] == "cmakeInstall") {
1111
0
    snippetTraceEvent["tid"] = 0;
1112
0
  } else {
1113
0
    snippetTraceEvent["tid"] = static_cast<Json::Value::UInt64>(
1114
0
      AssignTargetToTraceThread(workers, snippetData["timeStart"].asUInt64(),
1115
0
                                snippetData["duration"].asUInt64()));
1116
0
  }
1117
1118
0
  return snippetTraceEvent;
1119
0
}
1120
1121
size_t cmInstrumentation::AssignTargetToTraceThread(
1122
  std::vector<uint64_t>& workers, uint64_t timeStart, uint64_t duration)
1123
0
{
1124
0
  for (size_t i = 0; i < workers.size(); i++) {
1125
0
    if (workers[i] >= timeStart + duration) {
1126
0
      workers[i] = timeStart;
1127
0
      return i + 1;
1128
0
    }
1129
0
  }
1130
0
  workers.push_back(timeStart);
1131
0
  return workers.size();
1132
0
}