Coverage Report

Created: 2026-07-14 07:09

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