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 | 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 | 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 | ✗ | 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 | 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 | 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 |