src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
129 / 145
89.0%
Branches:
9 / 16, 18 excl
56.2%
Functions:
7 / 8
87.5%

global/compile-stats.spice
Line Branch Exec Source
1 // Copyright (c) 2021-2026 ChilliBits. All rights reserved.
2
3 // Std imports
4 import "std/io/file";
5 import "std/io/filepath";
6 import "std/os/system";
7 import "std/text/json/json-value";
8 import "std/text/json/json-serializer";
9 import "std/bindings/llvm/llvm" as llvm;
10
11 // Own imports
12 import "@/driver";
13 import "@/exception/compiler-error";
14 import "@/exception/fatal-error";
15 import "@/global/global-resource-manager";
16 import "@/source-file";
17 import "@/util/test-util";
18
19 // Version of the compile statistics format. Adding fields does not require a version bump
20 public const unsigned long COMPILE_STATS_FORMAT_VERSION = 1l;
21
22 /**
23 * Append a number field to a JSON object
24 *
25 * @param object JSON object
26 * @param key Field key
27 * @param value Field value
28 */
29 24 p addNumberField(JsonValue* object, string key, unsigned long value) {
30 24 object.addObjectField(String(key), __new<JsonValue>(value));
31 24 }
32
33 /**
34 * Append a string field to a JSON object
35 *
36 * @param object JSON object
37 * @param key Field key
38 * @param value Field value
39 */
40 5 p addStringField(JsonValue* object, string key, const String& value) {
41 5 object.addObjectField(String(key), __new<JsonValue>(value));
42 5 }
43
44 /**
45 * Get the name of an opt level, like it is passed on the command line (without the dash)
46 *
47 * @param optLevel Opt level
48 * @return Name of the opt level
49 */
50 5 f<string> getOptLevelName(OptLevel optLevel) {
51
4/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
✓ Branch 5 taken 1 time.
5 switch optLevel {
52 2 case OptLevel::O0: { return "O0"; }
53 ✗ case OptLevel::O1: { return "O1"; }
54 ✗ case OptLevel::O2: { return "O2"; }
55 1 case OptLevel::O3: { return "O3"; }
56 1 case OptLevel::Os: { return "Os"; }
57 1 default: { return "Oz"; }
58 }
59 ✗ }
60
61 /**
62 * Build the compile statistics of the whole compilation as JSON object. The stage times are summed over all source files.
63 * The format is documented in media/specs/benchmarks.md.
64 *
65 * @param resourceManager Global resource manager of the compilation
66 * @param totalMicros Wall time of the whole compilation in microseconds
67 * @return Compile statistics as JSON object (heap-allocated, release it with deleteJsonValue)
68 */
69 1 public f<JsonValue*> buildCompileStats(GlobalResourceManager& resourceManager, unsigned long totalMicros) {
70 1 const CliOptions& cliOptions = resourceManager.cliOptions;
71
72 // Sum up the stage times and counters of all source files
73 1 TimerOutput times;
74 1 unsigned long typeCheckerRuns = 0l;
75 1 unsigned long irFunctionCount = 0l;
76 1 unsigned long irInstructionCount = 0l;
77
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
4 foreach SourceFile* sourceFile : resourceManager.sourceFilesInCreationOrder {
78 3 const TimerOutput& fileTimes = sourceFile.compilerOutput.times;
79 3 times.lexerAndParser += fileTimes.lexerAndParser;
80 3 times.importCollector += fileTimes.importCollector;
81 3 times.symbolTableBuilder += fileTimes.symbolTableBuilder;
82 3 times.typeCheckerPre += fileTimes.typeCheckerPre;
83 3 times.typeCheckerPost += fileTimes.typeCheckerPost;
84 3 times.irGenerator += fileTimes.irGenerator;
85 3 times.irOptimizer += fileTimes.irOptimizer;
86 3 times.bitcodeLinker += fileTimes.bitcodeLinker;
87 3 times.ltoOptimizer += fileTimes.ltoOptimizer;
88 3 times.objectEmitter += fileTimes.objectEmitter;
89 3 typeCheckerRuns += cast<unsigned long>(sourceFile.totalTypeCheckerRuns);
90 3 irFunctionCount += sourceFile.compilerOutput.irFunctionCount;
91 3 irInstructionCount += sourceFile.compilerOutput.irInstructionCount;
92 3 }
93
94 1 result = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
95 1 addNumberField(result, "version", COMPILE_STATS_FORMAT_VERSION);
96
97 // Compiler
98 1 JsonValue* compiler = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
99 1 addStringField(compiler, "version", String(__get_build_var<string>("SPICE_VERSION", "dev")));
100 1 addStringField(compiler, "gitHash", String(__get_build_var<string>("SPICE_GIT_HASH", "dev")));
101 1 addStringField(compiler, "llvmVersion", String(llvm::getVersionString()));
102 1 result.addObjectField(String("compiler"), compiler);
103
104 // Options, that influence the measured values
105 1 JsonValue* options = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
106 1 addStringField(options, "optLevel", String(getOptLevelName(cliOptions.optLevel)));
107 1 options.addObjectField(String("lto"), __new<JsonValue>(cliOptions.useLTO));
108 1 addStringField(options, "backend", String(cliOptions.backend == Backend::TPDE ? "tpde" : "llvm"));
109 1 addNumberField(options, "jobs", resourceManager.getCompileJobCount());
110 1 result.addObjectField(String("options"), options);
111
112 // Wall times of the compilation
113 1 JsonValue* wallTime = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
114 1 addNumberField(wallTime, "frontEnd", resourceManager.frontEndTimer.getDurationInMicros());
115 1 addNumberField(wallTime, "middleEnd", resourceManager.middleEndTimer.getDurationInMicros());
116 1 addNumberField(wallTime, "backEnd", resourceManager.backEndTimer.getDurationInMicros());
117 1 addNumberField(wallTime, "linker", resourceManager.linkerTimer.getDurationInMicros());
118 1 addNumberField(wallTime, "total", totalMicros);
119 1 result.addObjectField(String("wallTime"), wallTime);
120
121 // Stage times, summed over all source files
122 1 JsonValue* stages = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
123 1 addNumberField(stages, "lexerAndParser", times.lexerAndParser);
124 1 addNumberField(stages, "importCollector", times.importCollector);
125 1 addNumberField(stages, "symbolTableBuilder", times.symbolTableBuilder);
126 1 addNumberField(stages, "typeCheckerPre", times.typeCheckerPre);
127 1 addNumberField(stages, "typeCheckerPost", times.typeCheckerPost);
128 1 addNumberField(stages, "irGenerator", times.irGenerator);
129 1 addNumberField(stages, "irOptimizer", times.irOptimizer);
130 1 addNumberField(stages, "bitcodeLinker", times.bitcodeLinker);
131 1 addNumberField(stages, "ltoOptimizer", times.ltoOptimizer);
132 1 addNumberField(stages, "objectEmitter", times.objectEmitter);
133 1 result.addObjectField(String("stages"), stages);
134
135 // Counters
136 1 JsonValue* counters = __new<JsonValue>(JsonValueKind::JSON_OBJECT);
137 1 addNumberField(counters, "sourceFiles", resourceManager.sourceFiles.getSize());
138 1 addNumberField(counters, "lines", resourceManager.getTotalLineCount());
139 1 addNumberField(counters, "types", resourceManager.typeRegistry.getTypeCount());
140 1 addNumberField(counters, "typeCheckerRuns", typeCheckerRuns);
141 1 addNumberField(counters, "irFunctions", irFunctionCount);
142 1 addNumberField(counters, "irInstructions", irInstructionCount);
143 1 result.addObjectField(String("counters"), counters);
144
145 // Peak memory usage of the compiler process. The linker runs in a process of its own and is not included
146 1 addNumberField(result, "peakMemoryBytes", getPeakMemoryUsage());
147 1 }
148
149 /**
150 * Write the compile statistics of the whole compilation as JSON to the file, that was requested with --dump-stats
151 *
152 * @param resourceManager Global resource manager of the compilation
153 * @param totalMicros Wall time of the whole compilation in microseconds
154 */
155 ✗ public p writeCompileStats(GlobalResourceManager& resourceManager, unsigned long totalMicros) {
156 ✗ JsonValue* stats = buildCompileStats(resourceManager, totalMicros);
157 ✗ JsonSerializer serializer = JsonSerializer(/*pretty=*/true);
158 ✗ const String content = serializer.serialize(stats) + "\n";
159 ✗ deleteJsonValue(stats);
160
161 ✗ const FilePath& statsFile = resourceManager.cliOptions.dump.statsFile;
162 // writeFile reports a failed write as ok(false)
163 ✗ Result<bool> writeResult = writeFile(statsFile.toString(), content.getRaw());
164
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if !writeResult.isOk() || !writeResult.unwrap() {
165 ✗ const String messageSuffix = "Failed to write compile statistics: " + statsFile.toString();
166 ✗ const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, messageSuffix.getRaw());
167 ✗ abortWithError(error.getMessage());
168 ✗ }
169 ✗ }
170
171 // ─── tests ────────────────────────────────────────────────────────────────────
172
173 // Value of a number field of a JSON object
174 11 f<unsigned long> getNumberField(JsonValue* object, string key) {
175 11 JsonValue* field = object.getField(key);
176 11 return field.getUnsignedInteger();
177 }
178
179 1 #[test]
180 f<bool> testGetOptLevelName() {
181 1 assert getOptLevelName(OptLevel::O0) == "O0";
182 1 assert getOptLevelName(OptLevel::O3) == "O3";
183 1 assert getOptLevelName(OptLevel::Os) == "Os";
184 1 assert getOptLevelName(OptLevel::Oz) == "Oz";
185 1 return true;
186 }
187
188 1 #[test]
189 f<bool> testBuildCompileStats() {
190 1 const FilePath testDir = makeUniqueTestDir("spice-compile-stats-test");
191 1 const FilePath mainPath = testDir / "main.spice";
192 // The import collector of each file runs the front end of its import, so the stages nest
193 1 writeTestFile(mainPath, "import \"first\" as first;\n\nf<int> main() {\n return first::getFirst();\n}\n");
194 1 writeTestFile(testDir / "first.spice", "import \"second\" as second;\n\npublic f<int> getFirst() {\n return second::getSecond();\n}\n");
195 1 writeTestFile(testDir / "second.spice", "public f<int> getSecond() {\n return 0;\n}\n");
196
197 1 unsigned long version;
198 1 unsigned long sourceFiles;
199 1 unsigned long irInstructions;
200 1 unsigned long total;
201 1 unsigned long frontAndMiddleEndWallTime;
202 1 unsigned long frontAndMiddleEndStageTime;
203 {
204 // Source files look up an LLVM target, so use the native one
205 1 CliOptions cliOptions;
206 1 cliOptions.targetTriple = llvm::normalizeTargetTriple(llvm::getDefaultTargetTriple());
207 1 cliOptions.isNativeTarget = true;
208
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
1 cliOptions.mainSourceFile = mainPath;
209 1 GlobalResourceManager resourceManager = GlobalResourceManager(cliOptions);
210 1 SourceFile* mainFile = resourceManager.createSourceFile(nil<SourceFile*>, String(MAIN_FILE_NAME), mainPath, false);
211 1 mainFile.runFrontEnd();
212 1 mainFile.runMiddleEnd();
213
214 1 JsonValue* stats = buildCompileStats(resourceManager, 42l);
215 1 version = getNumberField(stats, "version");
216 1 JsonValue* counters = stats.getField("counters");
217 1 sourceFiles = getNumberField(counters, "sourceFiles");
218 1 irInstructions = getNumberField(counters, "irInstructions");
219 1 JsonValue* wallTime = stats.getField("wallTime");
220 1 total = getNumberField(wallTime, "total");
221 1 frontAndMiddleEndWallTime = getNumberField(wallTime, "frontEnd") + getNumberField(wallTime, "middleEnd");
222 1 JsonValue* stages = stats.getField("stages");
223 1 frontAndMiddleEndStageTime = 0l;
224 1 const string[5] frontAndMiddleEndStages = ["lexerAndParser", "importCollector", "symbolTableBuilder", "typeCheckerPre", "typeCheckerPost"];
225
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 time.
6 for unsigned int i = 0; i < 5; i++ {
226 5 frontAndMiddleEndStageTime += getNumberField(stages, frontAndMiddleEndStages[i]);
227 5 }
228 1 deleteJsonValue(stats);
229 1 }
230
231 // Clean up before asserting, so that a failure does not leave the files behind
232 1 removeTestDir(testDir);
233 1 assert version == COMPILE_STATS_FORMAT_VERSION;
234 1 assert sourceFiles == 3l;
235 1 assert irInstructions == 0l; // The back end did not run
236 1 assert total == 42l;
237 // Each stage is measured exclusively, so the nested stages are not counted twice
238 1 assert frontAndMiddleEndStageTime <= frontAndMiddleEndWallTime;
239 1 return true;
240 1 }
241