Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/blockingqueue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class BlockingQueue
totalSize -= item.size;

lock.unlock();
itemsNotFull.notify_all();
itemsNotFull.notify_one();
}

return std::move(item.value);
Expand Down
171 changes: 159 additions & 12 deletions src/build.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
#include <string>
#include <memory>
#include <map>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>

#include <string.h>

Expand Down Expand Up @@ -162,10 +166,14 @@ static size_t normalizeEOL(char* data, size_t size)
return result;
}

static std::vector<char> readFile(FileStream& in)
static std::vector<char> readFile(FileStream& in, size_t sizeHint = 0)
{
std::vector<char> result;

// pre-allocate if size hint is available to avoid O(n^2) reallocation
if (sizeHint > 0)
result.reserve(sizeHint);

// read file as is
char buffer[65536];
size_t readsize;
Expand Down Expand Up @@ -646,18 +654,33 @@ void buildAppendFilePart(BuildContext* context, const char* path, unsigned int s

bool buildAppendFile(BuildContext* context, const char* path, uint64_t timeStamp, uint64_t fileSize)
{
FileStream in(path, "rb");
if (!in)
{
context->output->error("Error reading file %s\n", path);
return false;
}

try
{
std::vector<char> contents = convertToUTF8(readFile(in));
// Try optimized Windows read first (uses FILE_FLAG_SEQUENTIAL_SCAN for better prefetching)
std::vector<char> contents = readFileOptimized(path);

appendFilePart(context, path, 0, contents.empty() ? 0 : &contents[0], contents.size(), timeStamp, fileSize, &contents);
// Fallback to FileStream if optimized read failed (e.g., on non-Windows or special files)
if (contents.empty() && fileSize > 0)
{
FileStream in(path, "rb");
if (!in)
{
context->output->error("Error reading file %s\n", path);
return false;
}
contents = readFile(in, static_cast<size_t>(fileSize));
}

// Normalize EOL and convert to UTF8
if (!contents.empty())
{
size_t size = normalizeEOL(&contents[0], contents.size());
contents.resize(size);
}

contents = convertToUTF8(std::move(contents));

appendFilePart(context, path, 0, contents.empty() ? nullptr : &contents[0], contents.size(), timeStamp, fileSize, &contents);

return true;
}
Expand Down Expand Up @@ -739,6 +762,102 @@ unsigned int buildFinish(BuildContext* context)
return result;
}

// Parallel file reader for overlapping I/O with processing
struct ReadAheadBuffer
{
struct ReadFile
{
std::string path;
std::vector<char> contents;
uint64_t timeStamp;
uint64_t fileSize;
bool ready;
bool error;

ReadFile() : timeStamp(0), fileSize(0), ready(false), error(false) {}
};

std::vector<ReadFile> files;
std::atomic<size_t> nextToRead;
std::atomic<size_t> nextToConsume;
std::mutex mutex;
std::condition_variable cv;
std::atomic<bool> done;
size_t windowSize;

ReadAheadBuffer(size_t fileCount, size_t window = 64)
: files(fileCount), nextToRead(0), nextToConsume(0), done(false), windowSize(window)
{
}

void readerThread()
{
while (!done)
{
size_t idx = nextToRead.fetch_add(1);
if (idx >= files.size())
break;

// Wait if we're too far ahead of consumer
while (idx >= nextToConsume + windowSize && !done)
std::this_thread::yield();

if (done) break;

ReadFile& rf = files[idx];

// Read file
std::vector<char> contents = readFileOptimized(rf.path.c_str());
if (contents.empty() && rf.fileSize > 0)
{
// Fallback
FileStream in(rf.path.c_str(), "rb");
if (in)
contents = readFile(in, static_cast<size_t>(rf.fileSize));
}

// Normalize EOL
if (!contents.empty())
{
size_t size = normalizeEOL(&contents[0], contents.size());
contents.resize(size);
}

// Convert to UTF8
contents = convertToUTF8(std::move(contents));

{
std::lock_guard<std::mutex> lock(mutex);
rf.contents = std::move(contents);
rf.ready = true;
}
cv.notify_all();
}
}

bool getFile(size_t idx, std::vector<char>& contents)
{
if (idx >= files.size()) return false;

ReadFile& rf = files[idx];

std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&] { return rf.ready || done; });

if (!rf.ready) return false;

contents = std::move(rf.contents);
nextToConsume = idx + 1;
return !rf.error;
}

void stop()
{
done = true;
cv.notify_all();
}
};

void buildProject(Output* output, const char* path)
{
output->print("Building %s:\n", path);
Expand All @@ -765,11 +884,39 @@ void buildProject(Output* output, const char* path)
BuildContext* builder = buildStart(output, tempPath.c_str(), files.size());
if (!builder) return;

for (auto& f: files)
// Use parallel read-ahead for better I/O throughput
unsigned int numReaders = std::max(2u, std::thread::hardware_concurrency() / 2);
ReadAheadBuffer readAhead(files.size(), numReaders * 8);

// Initialize file info
for (size_t i = 0; i < files.size(); ++i)
{
buildAppendFile(builder, f.path.c_str(), f.timeStamp, f.fileSize);
readAhead.files[i].path = files[i].path;
readAhead.files[i].timeStamp = files[i].timeStamp;
readAhead.files[i].fileSize = files[i].fileSize;
}

// Start reader threads
std::vector<std::thread> readers;
for (unsigned int i = 0; i < numReaders; ++i)
readers.emplace_back(&ReadAheadBuffer::readerThread, &readAhead);

// Consume files in order
for (size_t i = 0; i < files.size(); ++i)
{
std::vector<char> contents;
if (readAhead.getFile(i, contents))
{
appendFilePart(builder, files[i].path.c_str(), 0,
contents.empty() ? nullptr : &contents[0], contents.size(),
files[i].timeStamp, files[i].fileSize, &contents);
}
}

readAhead.stop();
for (auto& t : readers)
t.join();

buildFinish(builder);
}

Expand Down
5 changes: 5 additions & 0 deletions src/fileutil.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#pragma once

#include <string>
#include <vector>
#include <functional>

#include <stdio.h>
Expand All @@ -28,3 +29,7 @@ bool getFileAttributes(const char* path, uint64_t* mtime, uint64_t* size);
FILE* openFile(const char* path, const char* mode);

bool watchDirectory(const char* path, const std::function<void (const char* name)>& callback);

// Read file directly into vector using optimized Windows API (FILE_FLAG_SEQUENTIAL_SCAN)
// Returns empty vector on failure
std::vector<char> readFileOptimized(const char* path);
7 changes: 7 additions & 0 deletions src/fileutil_posix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ static bool watchDirectoryFSEvent(const char* path, const std::function<void (co

#endif

// POSIX stub - returns empty to trigger fallback to standard file reading
std::vector<char> readFileOptimized(const char* path)
{
(void)path;
return std::vector<char>();
}

bool watchDirectory(const char* path, const std::function<void (const char* name)>& callback)
{
#if defined(__linux__)
Expand Down
49 changes: 49 additions & 0 deletions src/fileutil_win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,55 @@ FILE* openFile(const char* path, const char* mode)
return _wfopen(wpath.c_str(), wmode);
}

// Read file directly into vector using Windows API with FILE_FLAG_SEQUENTIAL_SCAN
std::vector<char> readFileOptimized(const char* path)
{
std::vector<char> result;

// Get full path with long path prefix
std::wstring wpath = fromUtf8(isFullPath(path) ? path : normalizePath(getCurrentDirectory().c_str(), path).c_str());
wpath.insert(0, L"\\\\?\\");
std::replace(wpath.begin(), wpath.end(), '/', '\\');

// Open file with sequential scan hint for better prefetching
HANDLE hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);

if (hFile == INVALID_HANDLE_VALUE)
return result;

// Get file size
LARGE_INTEGER fileSize;
if (!GetFileSizeEx(hFile, &fileSize))
{
CloseHandle(hFile);
return result;
}

// Handle empty files
if (fileSize.QuadPart == 0)
{
CloseHandle(hFile);
return result;
}

// Pre-allocate and read directly into vector
size_t totalSize = static_cast<size_t>(fileSize.QuadPart);
result.resize(totalSize);

DWORD bytesRead = 0;
BOOL success = ReadFile(hFile, &result[0], static_cast<DWORD>(totalSize), &bytesRead, NULL);
CloseHandle(hFile);

if (!success || bytesRead != totalSize)
{
result.clear();
return result;
}

return result;
}

bool watchDirectory(const char* path, const std::function<void (const char* name)>& callback)
{
HANDLE h = CreateFileW(fromUtf8(path).c_str(), FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
Expand Down
12 changes: 6 additions & 6 deletions src/project.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#include <memory>
#include <algorithm>
#include <stdexcept>
#include <map>
#include <unordered_map>
#include <string>

static std::string getHomePath()
Expand Down Expand Up @@ -147,7 +147,7 @@ static bool extractSuffix(const std::string& str, const char* prefix, std::strin
return false;
}

static std::shared_ptr<Regex> createRegexCached(const std::string& query, std::map<std::string, std::shared_ptr<Regex>>& regexCache)
static std::shared_ptr<Regex> createRegexCached(const std::string& query, std::unordered_map<std::string, std::shared_ptr<Regex>>& regexCache)
{
auto p = regexCache.insert(std::make_pair(query, std::shared_ptr<Regex>()));

Expand All @@ -157,7 +157,7 @@ static std::shared_ptr<Regex> createRegexCached(const std::string& query, std::m
return p.first->second;
}

static std::shared_ptr<Regex> createOrRegexCached(const std::vector<std::string>& list, std::map<std::string, std::shared_ptr<Regex>>& regexCache)
static std::shared_ptr<Regex> createOrRegexCached(const std::vector<std::string>& list, std::unordered_map<std::string, std::shared_ptr<Regex>>& regexCache)
{
if (list.empty()) return std::shared_ptr<Regex>();

Expand All @@ -170,7 +170,7 @@ static std::shared_ptr<Regex> createOrRegexCached(const std::vector<std::string>
}

static std::unique_ptr<ProjectGroup> buildGroup(std::unique_ptr<ProjectGroup> group, const std::vector<std::string>& include, const std::vector<std::string>& exclude,
std::map<std::string, std::shared_ptr<Regex>>& regexCache)
std::unordered_map<std::string, std::shared_ptr<Regex>>& regexCache)
{
group->include = createOrRegexCached(include, regexCache);
group->exclude = createOrRegexCached(exclude, regexCache);
Expand All @@ -179,7 +179,7 @@ static std::unique_ptr<ProjectGroup> buildGroup(std::unique_ptr<ProjectGroup> gr
}

static std::unique_ptr<ProjectGroup> parseGroup(std::ifstream& in, const char* file, unsigned int& lineId, ProjectGroup* parent,
std::map<std::string, std::shared_ptr<Regex>>& regexCache, const char* pathBase)
std::unordered_map<std::string, std::shared_ptr<Regex>>& regexCache, const char* pathBase)
{
std::string line, suffix;
std::vector<std::string> include, exclude;
Expand Down Expand Up @@ -251,7 +251,7 @@ std::unique_ptr<ProjectGroup> parseProject(Output* output, const char* file)
std::string pathBase = normalizePath(getCurrentDirectory().c_str(), (std::string(file) + "/..").c_str());

unsigned int line = 0;
std::map<std::string, std::shared_ptr<Regex>> regexCache;
std::unordered_map<std::string, std::shared_ptr<Regex>> regexCache;

try
{
Expand Down
7 changes: 2 additions & 5 deletions src/stringutil.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ inline const char* findLineStart(const char* begin, const char* pos)

inline const char* findLineEnd(const char* pos, const char* end)
{
for (const char* s = pos; s != end; ++s)
if (*s == '\n')
return s;

return end;
const char* nl = static_cast<const char*>(memchr(pos, '\n', end - pos));
return nl ? nl : end;
}

inline unsigned int countLines(const char* begin, const char* end)
Expand Down