cachelib/cachebench/util/NandWrites.cpp (262 lines of code) (raw):
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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 "cachelib/cachebench/util/NandWrites.h"
#include <folly/Format.h>
#include <folly/String.h>
#include <folly/Subprocess.h>
#include <folly/json.h>
#include <folly/logging/xlog.h>
#include <algorithm>
#include <string>
#include <vector>
namespace facebook {
namespace hw {
// Simple wrapper around folly::Subprocess.
class SubprocessWrapper : public Process {
public:
SubprocessWrapper(const std::vector<std::string>& argv,
const folly::Subprocess::Options& options,
const char* executable = nullptr,
const std::vector<std::string>* env = nullptr)
: subprocess_(argv, options, executable, env) {}
virtual ~SubprocessWrapper() {}
virtual std::pair<std::string, std::string> communicate() {
return subprocess_.communicate();
}
virtual folly::ProcessReturnCode wait() { return subprocess_.wait(); }
private:
folly::Subprocess subprocess_;
};
std::shared_ptr<Process> ProcessFactory::createProcess(
const std::vector<std::string>& argv,
const folly::Subprocess::Options& options,
const char* executable,
const std::vector<std::string>* env) const {
return std::make_shared<SubprocessWrapper>(argv, options, executable, env);
}
namespace {
void printCmd(const std::vector<std::string>& argv) {
auto s =
std::accumulate(argv.begin(),
argv.end(),
std::string(""),
[](const auto& a, const auto& b) { return a + " " + b; });
XLOG(DBG) << "Running command: " << s;
}
bool runNvmeCmd(const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const std::vector<std::string>& args,
std::string& out) {
std::vector<std::string> argv{nvmePath.str()};
std::copy(args.begin(), args.end(), std::back_inserter(argv));
printCmd(argv);
// Note that we can't just provide a command line here since then Subprocess
// will use the shell, and this code cannot use the shell since it is invoked
// by a setuid root binary in some contexts.
try {
auto proc =
processFactory->createProcess(argv,
folly::Subprocess::Options().pipeStdout(),
nvmePath.data(),
nullptr /* env */);
XDCHECK(proc);
const auto& [stdout, stderr] = proc->communicate();
const auto& rc = proc->wait();
bool success = rc.exitStatus() == 0;
if (success) {
XLOG(DBG) << "Got output: " << stdout;
out = stdout;
}
return success;
} catch (const folly::SubprocessSpawnError& e) {
XLOG(ERR) << e.what();
return false;
}
}
// Get the "bytes written" line in the `nvme` output for a device, as a vector
// of space-delimited fields.
//
// Runs `nvme` with the given arguments, looks for the first line containing
// the string "ritten", and then extracts and returns the given (space
// delimited) fields. If no matching line is found, returns an empty vector.
std::vector<std::string> getBytesWrittenLine(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const std::vector<std::string>& args) {
std::string out;
if (!runNvmeCmd(processFactory, nvmePath, args, out)) {
XLOG(ERR) << "Failed to run nvme command!";
return {};
}
// The existing NandWrite code expects a line that matches the pattern
// /ritten/, so that's what we do here. We just use the first matching
// line.
std::vector<folly::StringPiece> lines;
folly::split("\n", out, lines, true /* ignoreEmpty */);
for (const auto& line : lines) {
if (line.find("ritten") != std::string::npos) {
std::vector<std::string> fields;
folly::split(" ", line, fields, true /* ignoreEmpty */);
return fields;
}
}
XLOG(ERR) << "No matching line found in nvme output!";
return {};
}
// Get the "write count" for a drive using `nvme`. Note that different vendors
// use different units for this value, hence the "factor" argument; see the
// functions below.
std::optional<uint64_t> getBytesWritten(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const std::vector<std::string>& args,
const size_t fieldNum,
const uint64_t factor) {
std::vector<std::string> fields =
getBytesWrittenLine(processFactory, nvmePath, args);
XLOG(DBG) << "got fields: " << folly::join(",", fields);
if (fields.size() <= fieldNum) {
XLOG(ERR) << "Unexpected number of fields in line! Got " << fields.size()
<< " fields, but expected at least " << fieldNum + 1 << ".";
return std::nullopt;
}
fields[fieldNum].erase(
std::remove(fields[fieldNum].begin(), fields[fieldNum].end(), ','),
fields[fieldNum].end());
return std::stoll(fields[fieldNum], 0 /* pos */, 0 /* base */) * factor;
}
// The output for a Samsung device looks like:
//
// clang-format off
// ...
// [015:000] PhysicallyWrittenBytes : 5357954930143232
// ...
// clang-format on
//
// Note that Samsung supports JSON output, but for simplicity we parse
// all the vendor output the same way.
std::optional<uint64_t> samsungWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
// For Samsung devices, the returned count is already in bytes.
return getBytesWritten(processFactory,
nvmePath,
{"samsung", "vs-smart-add-log", devicePath.str()},
3 /* field num */,
1 /* factor */);
}
// The output for a LiteOn device looks like:
//
// clang-format off
// ...
// Physical(NAND) bytes written : 157,035,510,104,064
// ...
// clang-format on
std::optional<uint64_t> liteonWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
// For LiteOn devices, the returned count is already in bytes.
return getBytesWritten(processFactory,
nvmePath,
{"liteon", "vs-smart-add-log", devicePath.str()},
4 /* field num */,
1 /* factor */);
}
// The output for a Seagate device looks like this:
//
// clang-format off
// Seagate Extended SMART Information :
// Description Ext-Smart-Id Ext-Smart-Value
// --------------------------------------------------------------------------------
// ...
// Physical (NAND) bytes written 60137 0x000000000000000000000002d0cdc01b
// ...
// clang-format on
std::optional<uint64_t> seagateWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
// For Segate, the output is a count of 500 KiB blocks written.
//
// XXX The code in NandWrites.cpp assumes this, but the name of the attribute
// in the output is "bytes written" -- so which is it?
return getBytesWritten(processFactory,
nvmePath,
{"seagate", "vs-smart-add-log", devicePath.str()},
5 /* field num */,
500 * 1024 /* factor */);
}
// The output for a Toshiba device looks like:
//
// clang-format off
// Vendor Log Page 0xCA for NVME device:nvme0 namespace-id:ffffffff
// Total data written to NAND : 1133997.9 GiB
// ...
// clang-format on
std::optional<uint64_t> toshibaWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
// We expect the units to be one of 'KiB', 'MiB', 'GiB', or 'TiB'.
const auto& fields =
getBytesWrittenLine(processFactory,
nvmePath,
{"toshiba", "vs-smart-add-log", devicePath.str()});
// There should be 8 fields in the "data written" line.
constexpr size_t kExpectedFieldCount = 8;
if (fields.size() != kExpectedFieldCount) {
XLOG(ERR) << "Wrong number of fields! Got " << fields.size()
<< " fields, but expected exactly " << kExpectedFieldCount << ".";
return std::nullopt;
}
const auto& value = std::stoll(fields[6]);
const auto& units = fields[7];
if (units == "KiB") {
return value * 1024;
} else if (units == "MiB") {
return value * 1024 * 1024;
} else if (units == "GiB") {
return value * 1024 * 1024 * 1024;
} else if (units == "TiB") {
return value * 1024 * 1024 * 1024 * 1024;
}
XLOG(ERR) << "Unrecognized units " << units << " in nvme output!";
return std::nullopt;
}
// The output for an Intel device looks like:
//
// clang-format off
// Additional Smart Log for NVME device:nvme0 namespace-id:ffffffff
// key normalized raw
// ...
// nand_bytes_written : 100% sectors: 224943088
// ...
// clang-format on
//
// Note that Intel supports JSON output, but for simplicity we parse
// all the vendor output the same way.
std::optional<uint64_t> intelWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
// For Intel devices, the output is in number of 32 MB pages.
//
// XXX The code in NandWrites assumes that the output is a number of 32 MB
// pages, but the actual `nvme` output for an Intel device says "sectors" and
// the `nvme list` output lists a sector size of 4 KiB.
return getBytesWritten(processFactory,
nvmePath,
{"intel", "smart-log-add", devicePath.str()},
4 /* field num */,
32 * 1024 * 1024 /* factor */);
}
// The output for an WDC device looks like:
//
// clang-format off
// Additional Smart Log for NVME device:nvme0 namespace-id:ffffffff
// key normalized raw
// ...
// Physical media units written - 0 2068700589752320
// ...
// clang-format on
//
std::optional<uint64_t> wdcWriteBytes(
const std::shared_ptr<ProcessFactory>& processFactory,
const folly::StringPiece nvmePath,
const folly::StringPiece devicePath) {
// For WDC devices, the output is in number of bytes.
//
return getBytesWritten(processFactory,
nvmePath,
{"wdc", "vs-smart-add-log", devicePath.str()},
7 /* field num */,
1 /* factor */);
}
// I don't have access to hosts with Liteon, or SKHMS flash drives that I
// can use to test this code, so I've left these functions commented out for
// now.
//
// uint64_t skhmsWriteBytes(const folly::StringPiece& device) {
// // For SKHMS, the output is in 512 byte pages.
// return getWriteCount(device, {"skhms", "vs-skhms-smart-log"}, 4) * 512;
// }
std::optional<uint64_t> notImplemented(const std::string& vendorName) {
throw std::invalid_argument(
folly::sformat("Function not implemented for vendor {}", vendorName));
}
// Gets the output of `nvme list` for the given device.
std::optional<std::string> getDeviceModelNumber(
std::shared_ptr<ProcessFactory> processFactory,
const folly::StringPiece& nvmePath,
const folly::StringPiece& devicePath) {
std::string out;
if (!runNvmeCmd(processFactory, nvmePath, {"list", "-o", "json"}, out)) {
XLOG(ERR) << "Failed to run nvme command!";
return std::nullopt;
}
try {
const auto& obj = folly::parseJson(out);
const auto& devices = obj["Devices"];
for (const auto& device : devices) {
XLOG(DBG) << "Considering device " << device["DevicePath"].asString();
if (device["DevicePath"].asString() == devicePath) {
XLOG(DBG) << "Device matched, returning model number "
<< device["ModelNumber"].asString();
return device["ModelNumber"].asString();
}
}
} catch (const folly::json::parse_error& e) {
XLOG(ERR) << e.what();
}
// No matching device in the nvme output.
return std::nullopt;
}
} // anonymous namespace
// TODO: add unit tests
uint64_t nandWriteBytes(const folly::StringPiece& deviceName,
const folly::StringPiece& nvmePath,
std::shared_ptr<ProcessFactory> processFactory) {
const auto& devicePath = folly::sformat("/dev/{}", deviceName);
auto modelNumber = getDeviceModelNumber(processFactory, nvmePath, devicePath);
if (!modelNumber) {
throw std::invalid_argument(
folly::sformat("Failed to get device info for device {}", deviceName));
}
folly::toLowerAscii(modelNumber.value());
static const std::map<std::string, std::function<std::optional<uint64_t>(
const std::shared_ptr<ProcessFactory>&,
const folly::StringPiece&,
const folly::StringPiece&)>>
vendorMap{{"samsung", samsungWriteBytes},
{"mz1lb960hbjr-", samsungWriteBytes},
// The Samsung PM983a doesn't include Samsung in the model
// number at this time, but it's a Samsung device.
{"liteon", liteonWriteBytes},
{"ssstc", liteonWriteBytes},
{"intel", intelWriteBytes},
{"seagate", seagateWriteBytes},
// The Seagate XM1441 doesn't include SEAGATE in the model
// number, but it's a Segate device.
{"xm1441-", seagateWriteBytes},
{"skhms", [](const auto&, const auto&,
const auto&) { return notImplemented("SKHMS"); }},
{"toshiba", toshibaWriteBytes},
{"wus4bb019d4m9e7", wdcWriteBytes},
{"wus4bb019djese7", wdcWriteBytes},
{"wus4bb038djese7", wdcWriteBytes}};
for (const auto& [vendor, func] : vendorMap) {
XLOG(DBG) << "Looking for vendor " << vendor << " in device model string \""
<< modelNumber.value() << "\".";
if (modelNumber.value().find(vendor) != std::string::npos) {
XLOG(DBG) << "Matched vendor " << vendor;
const auto& bytesWritten = func(processFactory, nvmePath, devicePath);
if (!bytesWritten) {
// Throw an exception to maintain the same contract as the old version
// of this code.
//
// TODO: update the method to return an optional instead?
throw std::invalid_argument(folly::sformat(
"Failed to get bytes written for device {}", deviceName));
}
return bytesWritten.value();
}
}
// We got a model string but didn't match the vendor.
throw std::invalid_argument(folly::sformat(
"Vendor not recogized in device model number {}", modelNumber.value()));
}
} // namespace hw
} // namespace facebook