Files
netris-cdc-file-transfer/cdc_indexer/main.cc
ljusten 23fcd5ef1d Improve cdc_fuse_fs and path (#2)
Improve cdc_fuse_fs and path

Improves the error handling in path so that std:error_codes are not
assumed to be of system category, and also that their messages are
displayed. Also improves debug messages in GameletComponent.
2022-11-15 12:53:02 +01:00

436 lines
17 KiB
C++

// Copyright 2022 Google LLC
//
// 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 <sys/stat.h>
#include <sys/types.h>
#include <algorithm>
#include <cstdio>
#include <iostream>
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/flags/usage.h"
#include "absl/flags/usage_config.h"
#include "absl/random/random.h"
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/str_format.h"
#include "absl_helper/jedec_size_flag.h"
#include "cdc_indexer/indexer.h"
#include "common/errno_mapping.h"
#include "common/path.h"
ABSL_FLAG(std::vector<std::string>, inputs, std::vector<std::string>(),
"List of input files or directory to read from.");
ABSL_FLAG(uint32_t, num_threads, 0,
"How many threads should read files in parallel, use 0 to "
"auto-dertermine the best concurrency for this machine.");
ABSL_FLAG(cdc_ft::JedecSize, min_chunk_size, cdc_ft::JedecSize(0),
"The minimum chunk size to size the files into. Defaults to half of "
"the average chunk size. Supports common unit suffixes K, M, G.");
ABSL_FLAG(cdc_ft::JedecSize, avg_chunk_size, cdc_ft::JedecSize(256 << 10),
"The average chunk size to size the files into. Supports common "
"unit suffixes K, M, G.");
ABSL_FLAG(cdc_ft::JedecSize, max_chunk_size, cdc_ft::JedecSize(0),
"The maximum chunk size to size the files into. Defaults to twice "
"the average chunk size. Supports common unit suffixes K, M, G.");
ABSL_FLAG(cdc_ft::JedecSize, read_block_size, cdc_ft::JedecSize(0),
"The block size to read the input file(s) from disk. Defaults to the "
"value of --max_chunk_size. Supports common unit suffixes K, M, G.");
ABSL_FLAG(std::string, hash, "blake3",
"Which hash function to use. Supported values are \"blake3\" and "
"\"null\".");
ABSL_FLAG(std::string, results_file, "",
"File name to append results to in CVS format.");
ABSL_FLAG(std::string, description, "",
"A descriptive string of the experiment that was run. If given, this "
"will be prepended literally to the results_file. Multiple columns "
"can be separated with commas.");
namespace cdc_ft {
namespace {
const char* GearTable() {
// The following macros are defined in indexer.h.
#if CDC_GEAR_TABLE == CDC_GEAR_32BIT
return "32 bit";
#elif CDC_GEAR_TABLE == CDC_GEAR_64BIT
return "64 bit";
#else
#error "Unknown gear table"
return "unknown";
#endif
}
void SetupFlagsHelp() {
absl::SetProgramUsageMessage(
"CDC indexer to measure and report data redundancy.");
absl::FlagsUsageConfig fuc;
// Filter flags to show when the --help flag is set.
fuc.contains_help_flags = [](absl::string_view f) {
return absl::EndsWith(f, "main.cc");
};
absl::SetFlagsUsageConfig(fuc);
}
// Prints a human-readable representation of the given size, such as "4 KB".
template <typename T>
std::string HumanBytes(T size, int precision = 0) {
const size_t threshold = 2048;
if (size < 1024)
return absl::StrFormat("%d bytes", static_cast<size_t>(size));
double s = static_cast<double>(size) / 1024;
std::string units = "KB";
if (s > threshold) {
s /= 1024;
units = "MB";
}
if (s > threshold) {
s /= 1024;
units = "GB";
}
if (s > threshold) {
s /= 1024;
units = "TB";
}
if (s > threshold) {
s /= 1024;
units = "PB";
}
return absl::StrFormat("%.*f %s", precision, s, units);
}
// Prints a human-readable representation of a duration as minutes and seconds
// in the format "m:ss".
std::string HumanDuration(const absl::Duration& d) {
auto sec = absl::ToInt64Seconds(d);
return absl::StrFormat("%02d:%02d", sec / 60, std::abs(sec) % 60);
}
std::string HashTypeToString(IndexerConfig::HashType type) {
switch (type) {
case IndexerConfig::HashType::kNull:
return "(no hashing)";
case IndexerConfig::HashType::kBlake3:
return "BLAKE3";
default:
return "unknown";
}
}
// Prints progress information on stdout.
void ShowProgress(const Indexer::OpStats& stats) {
static absl::Time op_start = absl::Now();
static absl::Time last_progress = op_start;
static size_t last_total_bytes = 0;
auto now = absl::Now();
auto elapsed = now - last_progress;
if (elapsed < absl::Milliseconds(500)) return;
double bps =
(stats.total_bytes - last_total_bytes) / absl::ToDoubleSeconds(elapsed);
double dedup_pct = (stats.total_bytes - stats.unique_bytes) /
static_cast<double>(stats.total_bytes) * 100.0;
std::cout << '\r' << HumanDuration(now - op_start) << " " << std::setw(2)
<< HumanBytes(stats.total_bytes, 2) << " in " << stats.total_files
<< " files processed at " << HumanBytes(bps, 1) << "/s"
<< ", " << static_cast<int>(dedup_pct) << "% deduplication"
<< std::flush;
last_progress = now;
last_total_bytes = stats.total_bytes;
}
void ShowSummary(const IndexerConfig& cfg, const Indexer::OpStats& stats,
absl::Duration elapsed) {
const int title_w = 20;
const int num_w = 16;
double dedup_pct = (stats.total_bytes - stats.unique_bytes) /
static_cast<double>(stats.total_bytes) * 100.0;
double bps = stats.total_bytes / absl::ToDoubleSeconds(elapsed);
std::cout << "Chunk size (min/avg/max): " << HumanBytes(cfg.min_chunk_size)
<< " / " << HumanBytes(cfg.avg_chunk_size) << " / "
<< HumanBytes(cfg.max_chunk_size)
<< " | Hash: " << HashTypeToString(cfg.hash_type)
<< " | Threads: " << cfg.num_threads << std::endl;
std::cout << "gear_table: " << GearTable() << " | mask_s: 0x" << std::hex
<< cfg.mask_s << " | mask_l: 0x" << cfg.mask_l << std::dec
<< std::endl;
std::cout << std::setw(title_w) << "Duration:" << std::setw(num_w)
<< HumanDuration(elapsed) << std::endl;
std::cout << std::setw(title_w) << "Total files:" << std::setw(num_w)
<< stats.total_files << std::endl;
std::cout << std::setw(title_w) << "Total chunks:" << std::setw(num_w)
<< stats.total_chunks << std::endl;
std::cout << std::setw(title_w) << "Unique chunks:" << std::setw(num_w)
<< stats.unique_chunks << std::endl;
std::cout << std::setw(title_w) << "Total data:" << std::setw(num_w)
<< HumanBytes(stats.total_bytes, 2) << std::endl;
std::cout << std::setw(title_w) << "Unique data:" << std::setw(num_w)
<< HumanBytes(stats.unique_bytes, 2) << std::endl;
std::cout << std::setw(title_w) << "Throughput:" << std::setw(num_w - 2)
<< HumanBytes(bps, 2) << "/s" << std::endl;
std::cout << std::setw(title_w) << "Avg. chunk size:" << std::setw(num_w)
<< HumanBytes(static_cast<double>(stats.unique_bytes) /
stats.unique_chunks)
<< std::endl;
std::cout << std::setw(title_w) << "Deduplication:" << std::setw(num_w - 1)
<< std::setprecision(4) << dedup_pct << "%" << std::endl;
}
void ShowChunkSize(size_t size, uint64_t cnt, uint64_t max_count,
uint64_t total_count) {
const int key_w = 7;
const int hbar_w = 40;
const int num_w = 10;
const int pct_w = 2;
double pct = 100.0 * static_cast<double>(cnt) / total_count;
double hscale = static_cast<double>(cnt) / max_count;
int blocks = round(hscale * hbar_w);
std::cout << std::setw(key_w) << HumanBytes(size) << " ";
for (int i = 0; i < blocks; i++) std::cout << "#";
for (int i = hbar_w - blocks; i > 0; i--) std::cout << " ";
std::cout << " " << std::setw(num_w) << cnt << " (" << std::setw(pct_w)
<< round(pct) << "%)" << std::endl;
}
std::vector<size_t> ChunkSizeBuckets(const IndexerConfig& cfg,
const Indexer::ChunkSizeMap& sizes,
size_t fixed_min_size,
size_t fixed_max_size,
uint64_t* max_count_out,
uint64_t* total_count_out) {
size_t min_size = 1u << 31;
size_t max_size = 0;
uint64_t max_count = 0;
uint64_t total_count = 0, found_count = 0;
uint64_t outside_min_max_count = 0;
std::vector<size_t> buckets;
// Find out min/max chunk sizes
for (auto [chunk_size, count] : sizes) {
if (chunk_size < min_size) min_size = chunk_size;
if (chunk_size > max_size) max_size = chunk_size;
if (count > max_count) max_count = count;
if (chunk_size < fixed_min_size) outside_min_max_count += count;
if (fixed_max_size > 0 && chunk_size > fixed_max_size)
outside_min_max_count += count;
total_count += count;
}
if (fixed_min_size > 0) min_size = fixed_min_size;
// Use steps of powers of two until min. chunk size is reached.
uint64_t size;
uint64_t pow_end_size = std::min(cfg.min_chunk_size, max_size);
for (size = min_size; size < pow_end_size; size <<= 1) {
buckets.push_back(size);
auto it = sizes.find(size);
if (it != sizes.end()) found_count += it->second;
}
if (fixed_max_size > max_size) max_size = fixed_max_size;
// Use step increments of max_chunk_size_step afterwards.
for (; size <= max_size; size += cfg.max_chunk_size_step) {
buckets.push_back(size);
auto it = sizes.find(size);
if (it != sizes.end()) found_count += it->second;
}
// Make sure we found every bucket.
assert(total_count == found_count + outside_min_max_count);
if (max_count_out) *max_count_out = max_count;
if (total_count_out) *total_count_out = total_count;
return buckets;
}
void ShowChunkSizes(const IndexerConfig& cfg,
const Indexer::ChunkSizeMap& sizes) {
uint64_t max_count = 0;
uint64_t total_count = 0;
auto buckets = ChunkSizeBuckets(cfg, sizes, 0, 0, &max_count, &total_count);
for (auto size : buckets) {
auto it = sizes.find(size);
uint64_t cnt = it != sizes.end() ? it->second : 0;
ShowChunkSize(size, cnt, max_count, total_count);
}
}
absl::Status WriteResultsFile(const std::string& filepath,
const std::string& description,
const IndexerConfig& cfg,
const Indexer::OpStats& stats,
const Indexer::ChunkSizeMap& sizes) {
bool exists = path::FileExists(filepath);
std::FILE* fout = std::fopen(filepath.c_str(), "a");
if (!fout) {
return ErrnoToCanonicalStatus(errno, "Couldn't write to file '%s'",
filepath);
}
path::FileCloser closer(fout);
static constexpr int num_columns = 15;
static const char* columns[num_columns] = {
"gear_table",
"mask_s",
"mask_l",
"Min chunk size [KiB]",
"Avg chunk size [KiB]",
"Max chunk size [KiB]",
"Read speed [MiB/s]",
"Files",
"Total chunks",
"Unique chunks",
"Total size [MiB]",
"Unique size [MiB]",
"Dedup size [MiB]",
"Dedup ratio",
"Res avg chunk size [KiB]",
};
auto buckets = ChunkSizeBuckets(cfg, sizes, cfg.min_chunk_size,
cfg.max_chunk_size, nullptr, nullptr);
// Write column headers this is a new file.
if (!exists) {
// Write empty columns corresponding to the no. of given columns.
int desc_cols = description.empty() ? 0 : 1;
desc_cols += std::count(description.begin(), description.end(), ',');
for (int i = 0; i < desc_cols; i++) {
std::fprintf(fout, i == 0 ? "Description," : ",");
}
// Write fixed column headers.
for (int i = 0; i < num_columns; i++) {
std::fprintf(fout, "%s,", columns[i]);
}
// Write chunk distribution column headers
for (auto size : buckets) {
std::fprintf(fout, "%s,", HumanBytes(size).c_str());
}
std::fprintf(fout, "\n");
}
// Count allow chunks below min_chunk_size and above max_chunk_size as they
// won't be included in the buckets list automatically.
uint64_t below_min_cnt = 0, above_max_cnt = 0;
for (auto [chunk_size, count] : sizes) {
if (chunk_size < cfg.min_chunk_size) below_min_cnt += count;
if (chunk_size > cfg.max_chunk_size) above_max_cnt += count;
}
static constexpr double mib = static_cast<double>(1 << 20);
// Write user-supplied description
if (!description.empty()) std::fprintf(fout, "%s,", description.c_str());
// Write chunking params.
std::fprintf(fout, "%s,0x%zx,0x%zx,", GearTable(), cfg.mask_s, cfg.mask_l);
std::fprintf(fout, "%zu,%zu,%zu,", cfg.min_chunk_size >> 10,
cfg.avg_chunk_size >> 10, cfg.max_chunk_size >> 10);
// Write speed, files, chunks.
double mibps =
(stats.total_bytes / mib) / absl::ToDoubleSeconds(stats.elapsed);
std::fprintf(fout, "%f,%zu,%zu,%zu,", mibps, stats.total_files,
stats.total_chunks, stats.unique_chunks);
// Write total and unique sizes.
std::fprintf(fout, "%f,%f,%f,", stats.total_bytes / mib,
stats.unique_bytes / mib,
(stats.total_bytes - stats.unique_bytes) / mib);
// Write dedup ratio and avg. chunk size.
double dedup_ratio = (stats.total_bytes - stats.unique_bytes) /
static_cast<double>(stats.total_bytes);
size_t avg_size = stats.unique_bytes / stats.unique_chunks;
std::fprintf(fout, "%f,%zu,", dedup_ratio, avg_size >> 10);
// Write chunk distribution
size_t index = 0;
for (auto size : buckets) {
auto it = sizes.find(size);
uint64_t cnt = it != sizes.end() ? it->second : 0;
if (index == 0) {
cnt += below_min_cnt;
} else if (index + 1 == buckets.size()) {
cnt += above_max_cnt;
}
++index;
std::fprintf(fout, "%f,", static_cast<double>(cnt) / stats.unique_chunks);
}
std::fprintf(fout, "\n");
return absl::OkStatus();
}
IndexerConfig::HashType GetHashType(const std::string name) {
if (name == "null") return IndexerConfig::HashType::kNull;
if (name == "blake3") return IndexerConfig::HashType::kBlake3;
std::cerr << "Unknown hash type: \"" << name << "\"" << std::endl;
return IndexerConfig::HashType::kUndefined;
}
} // namespace
} // namespace cdc_ft
int main(int argc, char* argv[]) {
cdc_ft::SetupFlagsHelp();
absl::ParseCommandLine(argc, argv);
std::vector<std::string> inputs = absl::GetFlag(FLAGS_inputs);
if (inputs.empty()) {
std::cout << "Execute the following command to get help on the usage:"
<< std::endl
<< argv[0] << " --help" << std::endl;
return 0;
}
cdc_ft::IndexerConfig cfg;
cfg.num_threads = absl::GetFlag(FLAGS_num_threads);
cfg.min_chunk_size = absl::GetFlag(FLAGS_min_chunk_size).Size();
cfg.avg_chunk_size = absl::GetFlag(FLAGS_avg_chunk_size).Size();
cfg.max_chunk_size = absl::GetFlag(FLAGS_max_chunk_size).Size();
cfg.read_block_size = absl::GetFlag(FLAGS_read_block_size).Size();
cfg.hash_type = cdc_ft::GetHashType(absl::GetFlag(FLAGS_hash));
if (!cfg.min_chunk_size) cfg.min_chunk_size = cfg.avg_chunk_size >> 1;
if (!cfg.max_chunk_size) cfg.max_chunk_size = cfg.avg_chunk_size << 1;
if (!cfg.read_block_size) cfg.read_block_size = cfg.max_chunk_size;
cfg.max_chunk_size_step = std::max<size_t>(cfg.min_chunk_size >> 2, 1024u);
assert(cfg.avg_chunk_size > 0);
assert(cfg.avg_chunk_size > cfg.min_chunk_size);
assert(cfg.avg_chunk_size < cfg.max_chunk_size);
assert(cfg.hash_type != cdc_ft::IndexerConfig::HashType::kUndefined);
cdc_ft::Indexer idx;
std::cout << "Starting indexer on " << inputs.size() << " inputs."
<< std::endl;
static absl::Time start = absl::Now();
absl::Status res = idx.Run(cfg, inputs, cdc_ft::ShowProgress);
auto elapsed = absl::Now() - start;
std::cout << std::endl;
if (res.ok()) {
std::cout << "Operation succeeded." << std::endl << std::endl;
cdc_ft::ShowSummary(idx.Config(), idx.Stats(), elapsed);
std::cout << std::endl;
cdc_ft::ShowChunkSizes(idx.Config(), idx.ChunkSizes());
std::string results_file = absl::GetFlag(FLAGS_results_file);
if (!results_file.empty()) {
res = cdc_ft::WriteResultsFile(
results_file, absl::GetFlag(FLAGS_description), idx.Config(),
idx.Stats(), idx.ChunkSizes());
if (!res.ok())
std::cerr << "Failed to write results to '" << results_file
<< "': " << res.message() << std::endl;
}
} else {
std::cerr << "Error: (" << res.code() << ") " << res.message() << std::endl;
}
return static_cast<int>(res.code());
}