| /* |
| * 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 |