blob: 7f25f38a11298812b0bb1b7e47c8495e43dfcd32 [file]
/*
* 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