blob: aaaa090549b5626ecd054f67662c9f555d5b5cc4 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "diagnostics/cros_healthd/routines/memory/memory.h"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/json/json_writer.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/system/sys_info.h>
#include <re2/re2.h>
#include "diagnostics/common/mojo_utils.h"
#include "diagnostics/cros_healthd/routines/memory/memory_constants.h"
namespace diagnostics {
namespace {
namespace executor_ipc = chromeos::cros_healthd_executor::mojom;
namespace mojo_ipc = chromeos::cros_healthd::mojom;
// Approximate number of microseconds per byte of memory tested. Derived from
// testing on a nami device.
constexpr double kMicrosecondsPerByte = 0.20;
// Regex to parse out the version of memtester used.
constexpr char kMemtesterVersionRegex[] = R"(memtester version (.+))";
// Regex to parse out the amount of memory tested.
constexpr char kMemtesterBytesTestedRegex[] = R"(got \d+MB \((\d+) bytes\))";
// Regex to parse out a particular subtest and its result.
constexpr char kMemtesterSubtestRegex[] = R"((.+)\s*: (.+))";
// Failure messages in subtests will begin with this pattern.
constexpr char kMemtesterSubtestFailurePattern[] = "FAILURE:";
// Takes a |raw_string|, potentially with backspace characters ('\b'), and
// processes the backspaces in the string like the console would. For example,
// if |raw_string| was "Hello, Worlb\bd\n", this function would return
// "Hello, World\n". This function operates a single character at a time, and
// should only be used for small inputs.
std::string ProcessBackspaces(const std::string& raw_string) {
std::string processed;
for (const char& c : raw_string) {
if (c == '\b') {
// std::string::pop_back() causes undefined behavior if the string is
// empty. We never expect to call this method on an empty string - that
// would indicate |raw_string| was invalid.
DCHECK(!processed.empty());
processed.pop_back();
} else {
processed.push_back(c);
}
}
return processed;
}
} // namespace
MemoryRoutine::MemoryRoutine(Context* context,
const base::TickClock* tick_clock)
: context_(context),
status_(mojo_ipc::DiagnosticRoutineStatusEnum::kReady) {
DCHECK(context_);
if (tick_clock) {
tick_clock_ = tick_clock;
} else {
default_tick_clock_ = std::make_unique<base::DefaultTickClock>();
tick_clock_ = default_tick_clock_.get();
}
DCHECK(tick_clock_);
}
MemoryRoutine::~MemoryRoutine() = default;
void MemoryRoutine::Start() {
DCHECK_EQ(status_, mojo_ipc::DiagnosticRoutineStatusEnum::kReady);
// Estimate the routine's duration based on the amount of free memory.
expected_duration_us_ =
base::SysInfo::AmountOfAvailablePhysicalMemory() * kMicrosecondsPerByte;
start_ticks_ = tick_clock_->NowTicks();
status_ = mojo_ipc::DiagnosticRoutineStatusEnum::kRunning;
status_message_ = kMemoryRoutineRunningMessage;
context_->executor()->RunMemtester(base::BindOnce(
&MemoryRoutine::DetermineRoutineResult, weak_ptr_factory_.GetWeakPtr()));
}
// The memory routine can only be started.
void MemoryRoutine::Resume() {}
void MemoryRoutine::Cancel() {}
void MemoryRoutine::PopulateStatusUpdate(mojo_ipc::RoutineUpdate* response,
bool include_output) {
DCHECK(response);
// Because the memory routine is non-interactive, we will never include a user
// message.
mojo_ipc::NonInteractiveRoutineUpdate update;
update.status = status_;
update.status_message = status_message_;
response->routine_update_union->set_noninteractive_update(update.Clone());
if (include_output && !output_dict_.DictEmpty()) {
std::string json;
base::JSONWriter::WriteWithOptions(
output_dict_, base::JSONWriter::Options::OPTIONS_PRETTY_PRINT, &json);
response->output =
CreateReadOnlySharedMemoryRegionMojoHandle(base::StringPiece(json));
}
// If the routine has finished, set the progress percent to 100 and don't take
// the amount of time ran into account.
if (status_ == mojo_ipc::DiagnosticRoutineStatusEnum::kPassed ||
status_ == mojo_ipc::DiagnosticRoutineStatusEnum::kFailed) {
response->progress_percent = 100;
return;
}
if (status_ == mojo_ipc::DiagnosticRoutineStatusEnum::kReady) {
// The routine has not started.
response->progress_percent = 0;
return;
}
// Cap the progress at 99, in case it's taking longer than the estimated
// time.
base::TimeDelta elapsed_time = tick_clock_->NowTicks() - start_ticks_;
response->progress_percent =
std::min<int64_t>(99, static_cast<int64_t>(elapsed_time.InMicroseconds() /
expected_duration_us_ * 100));
}
mojo_ipc::DiagnosticRoutineStatusEnum MemoryRoutine::GetStatus() {
return status_;
}
void MemoryRoutine::DetermineRoutineResult(
executor_ipc::ProcessResultPtr process) {
ParseMemtesterOutput(process->out);
int32_t ret = process->return_code;
if (ret == EXIT_SUCCESS) {
status_message_ = kMemoryRoutineSucceededMessage;
status_ = mojo_ipc::DiagnosticRoutineStatusEnum::kPassed;
return;
}
std::string status_message;
if (ret & MemtesterErrorCodes::kAllocatingLockingInvokingError)
status_message += kMemoryRoutineAllocatingLockingInvokingFailureMessage;
if (ret & MemtesterErrorCodes::kStuckAddressTestError)
status_message += kMemoryRoutineStuckAddressTestFailureMessage;
if (ret & MemtesterErrorCodes::kOtherTestError)
status_message += kMemoryRoutineOtherTestFailureMessage;
status_message_ = std::move(status_message);
status_ = mojo_ipc::DiagnosticRoutineStatusEnum::kFailed;
}
void MemoryRoutine::ParseMemtesterOutput(const std::string& raw_output) {
std::vector<std::string> lines = base::SplitString(
raw_output, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// The following strings are used to hold values matched from regexes.
std::string version;
std::string bytes_tested_str;
std::string subtest_name;
std::string subtest_result;
// Holds the integer value for the number of bytes tested, converted from
// |bytes_tested_str|.
uint64_t bytes_tested;
// Holds the results of all subtests.
base::Value subtest_dict(base::Value::Type::DICTIONARY);
// Holds all the parsed output from memtester.
base::Value result_dict(base::Value::Type::DICTIONARY);
for (int index = 0; index < lines.size(); index++) {
std::string line = ProcessBackspaces(lines[index]);
if (RE2::FullMatch(line, kMemtesterVersionRegex, &version)) {
result_dict.SetStringKey("memtesterVersion", version);
} else if (RE2::PartialMatch(line, kMemtesterBytesTestedRegex,
&bytes_tested_str) &&
base::StringToUint64(bytes_tested_str, &bytes_tested)) {
result_dict.SetIntKey("bytesTested", static_cast<int>(bytes_tested));
} else if (RE2::FullMatch(line, kMemtesterSubtestRegex, &subtest_name,
&subtest_result) &&
!base::StartsWith(line, kMemtesterSubtestFailurePattern,
base::CompareCase::SENSITIVE)) {
// Process |subtest_name| so it's formatted like a JSON key - remove
// spaces and make sure the first letter is lowercase. For example, Stuck
// Address should become stuckAddress.
base::RemoveChars(subtest_name, " ", &subtest_name);
subtest_name[0] = std::tolower(subtest_name[0]);
subtest_dict.SetStringKey(subtest_name, subtest_result);
}
}
if (!subtest_dict.DictEmpty())
result_dict.SetKey("subtests", std::move(subtest_dict));
if (!result_dict.DictEmpty())
output_dict_.SetKey("resultDetails", std::move(result_dict));
}
} // namespace diagnostics