329 lines
13 KiB
C++
329 lines
13 KiB
C++
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// Model the pinned FILE* buffering boundary, not NOR page operations or time.
|
|
// The production transaction and CRC implementation are compiled below.
|
|
class FileSystem;
|
|
struct Stream {
|
|
FileSystem* fs;
|
|
std::string path;
|
|
bool writing;
|
|
size_t buffer_size = 4096, position = 0, backend_position = 0, cursor = 0;
|
|
std::vector<uint8_t> buffer;
|
|
};
|
|
class File {
|
|
std::shared_ptr<Stream> stream;
|
|
public:
|
|
File() = default;
|
|
File(FileSystem* fs, const char* path, bool writing)
|
|
: stream(std::make_shared<Stream>(Stream{fs, path, writing})) {}
|
|
explicit operator bool() const { return bool(stream); }
|
|
bool setBufferSize(size_t);
|
|
size_t size() const;
|
|
size_t read(uint8_t*, size_t);
|
|
size_t write(const uint8_t*, size_t);
|
|
void flush();
|
|
void close() { flush(); stream.reset(); }
|
|
};
|
|
class FileSystem {
|
|
public:
|
|
using Files = std::map<std::string, std::vector<uint8_t>>;
|
|
Files files;
|
|
unsigned reads = 0, writes = 0, backend_reads = 0, backend_writes = 0;
|
|
unsigned config_calls = 0, closes = 0, renames = 0;
|
|
unsigned fail_config = 0, short_read_call = 0, fail_rename = 0;
|
|
size_t largest_read = 0, largest_backend_read = 0, max_write = SIZE_MAX;
|
|
size_t backend_read_bytes = 0, reader_buffer_size = 0;
|
|
bool fail_open_read = false, fail_open_write = false;
|
|
std::vector<Files> rename_snapshots;
|
|
bool exists(const char* path) { return files.count(path) != 0; }
|
|
bool remove(const char* path) { return files.erase(path) != 0; }
|
|
bool rename(const char* from, const char* to) {
|
|
++renames;
|
|
if (renames == fail_rename || !exists(from) || exists(to)) return false;
|
|
files[to] = files.at(from);
|
|
files.erase(from);
|
|
rename_snapshots.push_back(files);
|
|
return true;
|
|
}
|
|
File open(const char* path, const char* mode, bool = false) {
|
|
const bool writing = *mode == 'w';
|
|
if (writing ? fail_open_write : fail_open_read || !exists(path)) return File();
|
|
if (writing) files[path].clear();
|
|
return File(this, path, writing);
|
|
}
|
|
};
|
|
bool File::setBufferSize(size_t size) {
|
|
if (!stream) return false;
|
|
if (++stream->fs->config_calls == stream->fs->fail_config) return false;
|
|
assert(stream->buffer.empty() && stream->position == 0);
|
|
stream->buffer_size = size;
|
|
if (!stream->writing) stream->fs->reader_buffer_size = size;
|
|
return true;
|
|
}
|
|
size_t File::size() const { return stream ? stream->fs->files.at(stream->path).size() : 0; }
|
|
void File::flush() {
|
|
if (!stream || !stream->writing || stream->buffer.empty()) return;
|
|
auto& fs = *stream->fs;
|
|
++fs.backend_writes;
|
|
auto& target = fs.files.at(stream->path);
|
|
target.insert(target.end(), stream->buffer.begin(), stream->buffer.end());
|
|
stream->buffer.clear();
|
|
}
|
|
size_t File::write(const uint8_t* data, size_t length) {
|
|
if (!stream || !stream->writing) return 0;
|
|
auto& fs = *stream->fs;
|
|
++fs.writes;
|
|
const size_t accepted = std::min(length, fs.max_write);
|
|
size_t copied = 0;
|
|
while (copied < accepted) {
|
|
const size_t n = std::min(accepted - copied, stream->buffer_size - stream->buffer.size());
|
|
stream->buffer.insert(stream->buffer.end(), data + copied, data + copied + n);
|
|
copied += n;
|
|
if (stream->buffer.size() == stream->buffer_size) flush();
|
|
}
|
|
return accepted;
|
|
}
|
|
size_t File::read(uint8_t* data, size_t length) {
|
|
if (!stream || stream->writing) return 0;
|
|
auto& fs = *stream->fs;
|
|
++fs.reads;
|
|
fs.largest_read = std::max(fs.largest_read, length);
|
|
if (fs.reads == fs.short_read_call) --length;
|
|
const auto& source = fs.files.at(stream->path);
|
|
size_t copied = 0;
|
|
while (copied < length) {
|
|
if (stream->cursor == stream->buffer.size()) {
|
|
const size_t available = source.size() - std::min(source.size(), stream->backend_position);
|
|
const size_t n = std::min(stream->buffer_size, available);
|
|
if (!n) break;
|
|
++fs.backend_reads;
|
|
fs.backend_read_bytes += n;
|
|
fs.largest_backend_read = std::max(fs.largest_backend_read, n);
|
|
stream->buffer.assign(source.begin() + stream->backend_position,
|
|
source.begin() + stream->backend_position + n);
|
|
stream->backend_position += n;
|
|
stream->cursor = 0;
|
|
}
|
|
const size_t n = std::min(length - copied, stream->buffer.size() - stream->cursor);
|
|
memcpy(data + copied, stream->buffer.data() + stream->cursor, n);
|
|
stream->cursor += n;
|
|
copied += n;
|
|
}
|
|
stream->position += copied;
|
|
return copied;
|
|
}
|
|
#define FILESYSTEM FileSystem
|
|
unsigned fixture_commit_calls = 0;
|
|
#include "transaction_under_test.h"
|
|
using Writer = mesh::ContactFileTransaction;
|
|
using Progress = Writer::CommitProgress;
|
|
|
|
std::vector<uint8_t> image(size_t bytes) {
|
|
std::vector<uint8_t> result(bytes);
|
|
for (size_t i = 0; i < bytes; ++i) result[i] = (i * 37 + i / 251) & 255;
|
|
return result;
|
|
}
|
|
void writeImage(Writer& writer, const std::vector<uint8_t>& bytes) {
|
|
for (size_t offset = 0; offset < bytes.size();) {
|
|
const size_t count = std::min(size_t(152), bytes.size() - offset);
|
|
assert(writer.write(bytes.data() + offset, count) == count);
|
|
offset += count;
|
|
}
|
|
}
|
|
unsigned effectiveBudget(unsigned requested) {
|
|
#if defined(ESP32_PLATFORM)
|
|
return std::max(1U, std::min(8U, requested));
|
|
#else
|
|
(void)requested;
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
struct Measurement {
|
|
unsigned passes, logical_reads, backend_reads, max_pass_reads, max_pass_refills;
|
|
size_t max_pass_backend_bytes, reader_buffer_size;
|
|
};
|
|
Measurement verify(size_t bytes, unsigned budget, bool use_default = false) {
|
|
FileSystem fs;
|
|
const auto old = image(21), next = image(bytes);
|
|
fs.files["/contacts3"] = old;
|
|
Writer writer(&fs, "/contacts3");
|
|
assert(writer);
|
|
writeImage(writer, next);
|
|
unsigned passes = 0, max_reads = 0, max_refills = 0, data_passes = 0;
|
|
size_t max_backend_bytes = 0;
|
|
#if defined(ESP32_PLATFORM)
|
|
// Selection is made once in Open, independently of the later read loop.
|
|
const size_t selected = !use_default && budget >= 8 ? 512 : 128;
|
|
#endif
|
|
Progress result = Progress::Pending;
|
|
while (result == Progress::Pending) {
|
|
const unsigned before_reads = fs.reads, before_backend = fs.backend_reads;
|
|
const size_t before_backend_bytes = fs.backend_read_bytes;
|
|
const bool was_ready = writer.readyToPublish();
|
|
result = use_default ? writer.serviceCommit() : writer.serviceCommit(true, budget);
|
|
assert(++passes < 10000);
|
|
const unsigned reads = fs.reads - before_reads, refills = fs.backend_reads - before_backend;
|
|
max_reads = std::max(max_reads, reads);
|
|
max_refills = std::max(max_refills, refills);
|
|
const size_t backend_bytes = fs.backend_read_bytes - before_backend_bytes;
|
|
max_backend_bytes = std::max(max_backend_bytes, backend_bytes);
|
|
const unsigned bound = use_default ? 1 : effectiveBudget(budget);
|
|
assert(reads <= bound);
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(refills <= (64 * bound + selected - 1) / selected);
|
|
assert(backend_bytes <= 512); // FILE refill admissions, not NOR operations or time
|
|
#endif
|
|
if (reads) {
|
|
++data_passes;
|
|
assert(!writer.readyToPublish()); // close/CRC comparison is a later pass
|
|
}
|
|
if (result == Progress::Pending) assert(fs.files.at("/contacts3") == old);
|
|
else assert(was_ready); // publication never shares a CRC read pass
|
|
}
|
|
assert(result == Progress::Succeeded);
|
|
const unsigned bound = use_default ? 1 : effectiveBudget(budget);
|
|
assert(data_passes == (bytes + 64 * bound - 1) / (64 * bound));
|
|
assert(passes == data_passes + 4);
|
|
assert(fs.files.at("/contacts3") == next);
|
|
assert(fs.reads == (bytes + 63) / 64);
|
|
assert(fs.largest_read <= 64);
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(fs.reader_buffer_size == selected);
|
|
assert(fs.largest_backend_read <= selected);
|
|
assert(fs.backend_reads == (bytes + selected - 1) / selected);
|
|
#endif
|
|
return {passes, fs.reads, fs.backend_reads, max_reads, max_refills,
|
|
max_backend_bytes, fs.reader_buffer_size};
|
|
}
|
|
void limitsAndTails() {
|
|
for (size_t bytes : {size_t(0), size_t(1), size_t(63), size_t(64), size_t(65),
|
|
size_t(127), size_t(128), size_t(129), size_t(511),
|
|
size_t(512), size_t(513), size_t(152 * 347)}) {
|
|
verify(bytes, 1, true);
|
|
for (unsigned budget : {0U, 1U, 2U, 8U, 9U, UINT32_MAX}) verify(bytes, budget);
|
|
}
|
|
FileSystem fs;
|
|
Writer writer(&fs, "/sync");
|
|
writeImage(writer, image(347 * 152));
|
|
fixture_commit_calls = 0;
|
|
assert(writer.commit());
|
|
assert(fixture_commit_calls == 829); // actual commit() drains default-one passes
|
|
#if defined(ESP32_PLATFORM)
|
|
assert(fs.reader_buffer_size == 128 && fs.backend_reads == 413);
|
|
#endif
|
|
}
|
|
void readFailureAndCorruption() {
|
|
const auto old = image(19), next = image(1536);
|
|
for (unsigned failed_read = 1; failed_read <= 16; ++failed_read) {
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = old;
|
|
Writer writer(&fs, "/contacts3");
|
|
writeImage(writer, next);
|
|
fs.short_read_call = failed_read;
|
|
Progress result = Progress::Pending;
|
|
for (unsigned i = 0; result == Progress::Pending && i < 100; ++i)
|
|
result = writer.serviceCommit(true, 8);
|
|
assert(result == Progress::Failed);
|
|
assert(fs.files.at("/contacts3") == old && !fs.exists("/contacts3.tmp"));
|
|
}
|
|
for (unsigned corruption : {0U, 63U, 64U, 127U, 511U, 512U, 1535U}) {
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = old;
|
|
Writer writer(&fs, "/contacts3");
|
|
writeImage(writer, next);
|
|
assert(writer.serviceCommit(true, 8) == Progress::Pending); // flush
|
|
fs.files.at("/contacts3.tmp")[corruption] ^= 1;
|
|
Progress result = Progress::Pending;
|
|
while (result == Progress::Pending) result = writer.serviceCommit(true, 8);
|
|
assert(result == Progress::Failed && fs.files.at("/contacts3") == old);
|
|
}
|
|
}
|
|
void cancellationAndCrash() {
|
|
const auto old = image(19), next = image(1536);
|
|
for (unsigned stop = 0; stop < 8; ++stop) {
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = old;
|
|
Writer writer(&fs, "/contacts3");
|
|
writeImage(writer, next);
|
|
for (unsigned i = 0; i < stop; ++i) {
|
|
const auto result = writer.serviceCommit(true, 8);
|
|
if (result == Progress::Succeeded) break;
|
|
}
|
|
if (fs.files.at("/contacts3") == old) {
|
|
const unsigned reads = fs.reads;
|
|
assert(writer.serviceCommit(false, 8) == Progress::Failed);
|
|
assert(fs.reads == reads && !fs.exists("/contacts3.tmp"));
|
|
}
|
|
// Simulate boot from the durable map before any destructor can run.
|
|
FileSystem reboot;
|
|
reboot.files = fs.files;
|
|
assert(Writer::recover(&reboot, "/contacts3"));
|
|
assert(reboot.files.at("/contacts3") == old || reboot.files.at("/contacts3") == next);
|
|
}
|
|
for (unsigned failure : {0U, 1U, 2U, 3U}) {
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = old;
|
|
Writer writer(&fs, "/contacts3");
|
|
writeImage(writer, next);
|
|
fs.fail_rename = failure;
|
|
Progress result = Progress::Pending;
|
|
while (result == Progress::Pending) result = writer.serviceCommit(true, 8);
|
|
assert(result == (failure == 1 || failure == 2 ? Progress::Failed : Progress::Succeeded));
|
|
assert(fs.files.at("/contacts3") == (result == Progress::Succeeded ? next : old));
|
|
for (const auto& snapshot : fs.rename_snapshots) {
|
|
FileSystem reboot;
|
|
reboot.files = snapshot;
|
|
assert(Writer::recover(&reboot, "/contacts3"));
|
|
assert(reboot.files.at("/contacts3") == old || reboot.files.at("/contacts3") == next);
|
|
}
|
|
}
|
|
}
|
|
void setupFailures() {
|
|
for (unsigned fail_config : {1U, 2U}) {
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = image(19);
|
|
fs.fail_config = fail_config;
|
|
Writer writer(&fs, "/contacts3");
|
|
if (fail_config == 1) { assert(!writer); continue; }
|
|
writeImage(writer, image(512));
|
|
assert(writer.serviceCommit(true, 8) == Progress::Pending);
|
|
assert(writer.serviceCommit(true, 8) == Progress::Failed);
|
|
assert(fs.reads == 0 && fs.files.at("/contacts3") == image(19));
|
|
}
|
|
FileSystem fs;
|
|
fs.files["/contacts3"] = image(19);
|
|
Writer writer(&fs, "/contacts3");
|
|
writeImage(writer, image(512));
|
|
fs.fail_open_read = true;
|
|
assert(writer.serviceCommit(true, 8) == Progress::Pending);
|
|
assert(writer.serviceCommit(true, 8) == Progress::Failed);
|
|
assert(fs.reads == 0 && fs.files.at("/contacts3") == image(19));
|
|
}
|
|
int main() {
|
|
limitsAndTails();
|
|
readFailureAndCorruption();
|
|
cancellationAndCrash();
|
|
#if defined(ESP32_PLATFORM)
|
|
setupFailures();
|
|
#endif
|
|
const auto one = verify(347 * 152, 1), eight = verify(347 * 152, 8);
|
|
printf("{\"one_passes\":%u,\"eight_passes\":%u,\"logical_reads\":%u,"
|
|
"\"backend_reads\":%u,\"max_pass_reads\":%u,\"max_pass_refills\":%u,"
|
|
"\"max_pass_backend_bytes\":%zu,\"reader_buffer_size\":%zu,"
|
|
"\"one_backend_reads\":%u,\"one_reader_buffer_size\":%zu}\n",
|
|
one.passes, eight.passes, eight.logical_reads, eight.backend_reads,
|
|
eight.max_pass_reads, eight.max_pass_refills,
|
|
eight.max_pass_backend_bytes, eight.reader_buffer_size,
|
|
one.backend_reads, one.reader_buffer_size);
|
|
}
|