| /* | |
| * Copyright 2026 WebAssembly Community Group participants | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| #include "tools/fuzzing/fuzz-stats.h" | |
| #include "support/file_lock.h" | |
| #include <algorithm> | |
| #include <fstream> | |
| #include <iomanip> | |
| #include <iostream> | |
| #include <map> | |
| #include <mutex> | |
| #include <sstream> | |
| namespace wasm { | |
| namespace { | |
| std::string getStatsFilename() { | |
| if (const char* env = getenv("BINARYEN_FUZZ_STATS")) { | |
| if (env[0] != '\0') { | |
| std::string str(env); | |
| if (str == "1" || str == "true" || str == "on" || str == "yes") { | |
| return "fuzz-stats.txt"; | |
| } | |
| return str; | |
| } | |
| } | |
| return ""; | |
| } | |
| struct PatternStatsRecord { | |
| uint64_t occurrences = 0; | |
| uint64_t inModules = 0; | |
| uint64_t inFunctions = 0; | |
| }; | |
| std::mutex eventsMutex; | |
| std::map<std::string, std::map<int, uint64_t>> pendingEvents; | |
| void saveStats(bool hasModuleStats, | |
| uint64_t numFunctions, | |
| const std::map<std::string, uint64_t>* occurrences, | |
| const std::map<std::string, uint64_t>* funcMatches) { | |
| std::string filename = getStatsFilename(); | |
| if (filename.empty()) { | |
| return; | |
| } | |
| std::map<std::string, std::map<int, uint64_t>> localEvents; | |
| { | |
| std::lock_guard<std::mutex> lock(eventsMutex); | |
| localEvents.swap(pendingEvents); | |
| } | |
| if (!hasModuleStats && localEvents.empty()) { | |
| return; | |
| } | |
| // Lock file for safe concurrent updates across processes. | |
| FileLock lock(filename + ".lock"); | |
| // Read existing statistics from file if present. | |
| uint64_t totalModules = 0; | |
| uint64_t totalFunctions = 0; | |
| std::map<std::string, PatternStatsRecord> statsMap; | |
| std::map<std::string, std::map<int, uint64_t>> eventStatsMap; | |
| enum class Section { None, Patterns, Events }; | |
| { | |
| std::ifstream in(filename); | |
| if (in.is_open()) { | |
| std::string line; | |
| Section section = Section::None; | |
| std::string currentEvent; | |
| while (std::getline(in, line)) { | |
| if (!line.empty() && line.back() == '\r') { | |
| line.pop_back(); | |
| } | |
| if (line.empty() || line[0] == '#') { | |
| continue; | |
| } | |
| if (line == "Patterns:") { | |
| section = Section::Patterns; | |
| continue; | |
| } | |
| if (line == "Events:") { | |
| section = Section::Events; | |
| currentEvent.clear(); | |
| continue; | |
| } | |
| if (section == Section::Events) { | |
| if (line.rfind(" ", 0) == 0) { | |
| std::stringstream ss(line); | |
| int outcome = 0; | |
| char colon = 0; | |
| uint64_t count = 0; | |
| if (!currentEvent.empty() && (ss >> outcome >> colon) && | |
| colon == ':' && (ss >> count)) { | |
| eventStatsMap[currentEvent][outcome] = count; | |
| } | |
| } else if (line.rfind(" ", 0) == 0) { | |
| currentEvent = line.substr(2); | |
| if (!currentEvent.empty() && currentEvent.back() == ':') { | |
| currentEvent.pop_back(); | |
| } | |
| } | |
| continue; | |
| } | |
| std::stringstream ss(line); | |
| std::string key; | |
| if (ss >> key) { | |
| if (key == "Modules:") { | |
| ss >> totalModules; | |
| } else if (key == "Functions:") { | |
| ss >> totalFunctions; | |
| } else if (section == Section::Patterns) { | |
| std::string name = key; | |
| uint64_t occurrencesCount = 0; | |
| std::string perModStr, perFuncStr; | |
| uint64_t inMods = 0; | |
| std::string pctModStr; | |
| uint64_t inFuncs = 0; | |
| std::string pctFuncStr; | |
| if (ss >> occurrencesCount >> perModStr >> perFuncStr >> inMods >> | |
| pctModStr >> inFuncs >> pctFuncStr) { | |
| statsMap[name] = {occurrencesCount, inMods, inFuncs}; | |
| } else { | |
| statsMap[name].occurrences = occurrencesCount; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Accumulate current run into overall statistics. | |
| if (hasModuleStats) { | |
| totalModules += 1; | |
| totalFunctions += numFunctions; | |
| if (occurrences && funcMatches) { | |
| for (const auto& [name, count] : *occurrences) { | |
| auto& record = statsMap[name]; | |
| record.occurrences += count; | |
| record.inFunctions += funcMatches->at(name); | |
| if (count > 0) { | |
| record.inModules += 1; | |
| } | |
| } | |
| } | |
| } | |
| for (const auto& [eventName, outcomes] : localEvents) { | |
| for (const auto& [outcome, count] : outcomes) { | |
| eventStatsMap[eventName][outcome] += count; | |
| } | |
| } | |
| // Write updated statistics to a temporary file and atomically rename. | |
| std::string tmpFilename = filename + ".tmp"; | |
| { | |
| std::ofstream out(tmpFilename); | |
| if (!out.is_open()) { | |
| return; | |
| } | |
| out << "# Binaryen Fuzzing Statistics\n"; | |
| out << "Modules: " << totalModules << "\n"; | |
| out << "Functions: " << totalFunctions << "\n"; | |
| if (!statsMap.empty()) { | |
| out << "\nPatterns:\n"; | |
| out << "# " << std::left << std::setw(30) << "Name" << std::right | |
| << std::setw(12) << "Occurrences" << std::setw(14) << "Per Module" | |
| << std::setw(14) << "Per Function" << std::setw(10) << "Modules" | |
| << std::setw(12) << "% Modules" << std::setw(12) << "Functions" | |
| << std::setw(14) << "% Functions" | |
| << "\n"; | |
| for (const auto& [name, data] : statsMap) { | |
| double perMod = | |
| totalModules > 0 ? (double)data.occurrences / totalModules : 0.0; | |
| double perFunc = | |
| totalFunctions > 0 ? (double)data.occurrences / totalFunctions : 0.0; | |
| double pctMod = | |
| totalModules > 0 ? (100.0 * data.inModules) / totalModules : 0.0; | |
| double pctFunc = totalFunctions > 0 | |
| ? (100.0 * data.inFunctions) / totalFunctions | |
| : 0.0; | |
| std::ostringstream pctModStream, pctFuncStream; | |
| pctModStream << std::fixed << std::setprecision(2) << pctMod << "%"; | |
| pctFuncStream << std::fixed << std::setprecision(2) << pctFunc << "%"; | |
| out << " " << std::left << std::setw(30) << name << std::right | |
| << std::setw(12) << data.occurrences << std::setw(14) << std::fixed | |
| << std::setprecision(4) << perMod << std::setw(14) << std::fixed | |
| << std::setprecision(4) << perFunc << std::setw(10) | |
| << data.inModules << std::setw(12) << pctModStream.str() | |
| << std::setw(12) << data.inFunctions << std::setw(14) | |
| << pctFuncStream.str() << "\n"; | |
| } | |
| } | |
| if (!eventStatsMap.empty()) { | |
| out << "\nEvents:\n"; | |
| for (const auto& [eventName, outcomes] : eventStatsMap) { | |
| uint64_t totalEventCount = 0; | |
| for (const auto& [outcome, count] : outcomes) { | |
| totalEventCount += count; | |
| } | |
| out << " " << eventName << ":\n"; | |
| for (const auto& [outcome, count] : outcomes) { | |
| double pct = | |
| totalEventCount > 0 ? (100.0 * count) / totalEventCount : 0.0; | |
| std::ostringstream pctStream; | |
| pctStream << std::fixed << std::setprecision(2) << pct << "%"; | |
| out << " " << std::left << std::setw(8) | |
| << (std::to_string(outcome) + ":") << std::right << std::setw(12) | |
| << count << " (" << std::setw(7) << pctStream.str() << ")\n"; | |
| } | |
| } | |
| } | |
| } | |
| rename(tmpFilename.c_str(), filename.c_str()); | |
| if (getenv("BINARYEN_FUZZ_STATS_VERBOSE")) { | |
| std::cerr << "Fuzz stats updated in " << filename | |
| << " (Total Modules: " << totalModules | |
| << ", Total Functions: " << totalFunctions << ")\n"; | |
| } | |
| } | |
| struct EventFlusher { | |
| ~EventFlusher() { saveStats(false, 0, nullptr, nullptr); } | |
| } eventFlusher; | |
| } // namespace | |
| namespace FuzzStats { | |
| bool isEnabled() { | |
| static const bool enabled = !getStatsFilename().empty(); | |
| return enabled; | |
| } | |
| int recordEvent(const std::string& name, int outcome) { | |
| if (!isEnabled()) { | |
| return outcome; | |
| } | |
| std::lock_guard<std::mutex> lock(eventsMutex); | |
| pendingEvents[name][outcome]++; | |
| return outcome; | |
| } | |
| int recordEvent(const std::string& name, | |
| const char* file, | |
| int line, | |
| int outcome) { | |
| if (!isEnabled()) { | |
| return outcome; | |
| } | |
| const char* filename = file; | |
| for (const char* p = file; *p; ++p) { | |
| if (*p == '/' || *p == '\\') { | |
| filename = p + 1; | |
| } | |
| } | |
| return recordEvent(name + " (" + filename + ":" + std::to_string(line) + ")", | |
| outcome); | |
| } | |
| void save(uint64_t numFunctions, | |
| const std::map<std::string, uint64_t>& occurrences, | |
| const std::map<std::string, uint64_t>& funcMatches) { | |
| saveStats(true, numFunctions, &occurrences, &funcMatches); | |
| } | |
| } // namespace FuzzStats | |
| } // namespace wasm |