Coverage Report

Created: 2026-06-15 07:03

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