blob: 9117af1598810dac048330806a685923f09d73b6 [file] [log] [blame]
// Copyright 2014 Google Inc. All Rights Reserved.
//
// 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 "syzygy/testing/metrics.h"
#include <limits>
#include "base/command_line.h"
#include "base/environment.h"
#include "base/path_service.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/time/time.h"
#include "base/win/scoped_handle.h"
#include "syzygy/version/syzygy_version.h"
namespace testing {
namespace {
// The metrics will be written to a log file with the following extension,
// directly alongside the executable that is emitting metrics. By convention
// all test executables are in the root of the configuration output directory.
static const wchar_t kMetricsFileName[] = L"metrics.csv";
// The maximum allowable size of the metrics file. This prevents local
// developers metric files from growing monotonically and getting out of
// control. This needs to be amply large for the set of metrics that will be
// emitted in a single run of unittests.
static const size_t kMetricsFileMaxSize = 1 * 1024 * 1024;
// The environment variable where the metrics are configured.
static const char kMetricsEnvVar[] = "SYZYGY_UNITTEST_METRICS";
// Switches for controlling metrics.
static const char kEmitToLog[] = "emit-to-log";
static const char kEmitToWaterfall[] = "emit-to-waterfall";
// The current build configuration.
static const char kBuildConfig[] =
#ifdef _COVERAGE_BUILD
"Coverage";
#else
#ifdef NDEBUG
#ifdef OFFICIAL_BUILD
"Official";
#else
"Release";
#endif // OFFICIAL_BUILD
#else
"Debug";
#endif // NDEBUG
#endif // _COVERAGE_BUILD
// A global lock used by metrics.
base::Lock metrics_lock;
// Global metrics configuration. This is initialized from the environment.
struct MetricsConfiguration {
bool emit_to_log;
bool emit_to_waterfall;
} metrics_config = { false, false };
// Parses metrics configuration from the environment.
void ParseMetricsConfiguration() {
base::Environment* env = base::Environment::Create();
DCHECK_NE(static_cast<base::Environment*>(nullptr), env);
// If there's no environment variable then there's nothing to parse.
std::string s;
if (!env->GetVar(kMetricsEnvVar, &s))
return;
// Build a command line object that we can use for parsing. Prefix the
// environment variable with a dummy executable name.
std::wstring cmd_line(L"foo.exe ");
cmd_line.append(base::UTF8ToWide(s));
base::CommandLine cmd = base::CommandLine::FromString(cmd_line);
metrics_config.emit_to_log = cmd.HasSwitch(kEmitToLog);
metrics_config.emit_to_waterfall = cmd.HasSwitch(kEmitToWaterfall);
}
// Generates the path to the metrics log.
base::FilePath GetMetricsLogPath() {
base::FilePath exe_dir;
PathService::Get(base::DIR_EXE, &exe_dir);
return exe_dir.Append(kMetricsFileName);
}
// Opens a file for exclusive writing. This tries in a loop but can fail
// permanently. Returns an empty handle on failure.
bool OpenForExclusiveWrite(const base::FilePath& path,
base::win::ScopedHandle* handle) {
DCHECK_NE(static_cast<base::win::ScopedHandle*>(nullptr), handle);
DWORD wait = 1; // In milliseconds.
for (size_t retries = 0; retries <= 10; ++retries) {
handle->Set(::CreateFile(
path.value().c_str(),
FILE_APPEND_DATA,
0, // Exclusive access.
nullptr, // No security attributes.
OPEN_ALWAYS, // Create existing file, or create a new one.
FILE_ATTRIBUTE_NORMAL,
nullptr));
if (handle->IsValid())
return true;
::Sleep(wait);
wait++;
}
return false;
}
// Deletes the specified file if its size exceeds the given
// threshold. Returns true on success, false otherwise.
bool DeleteFileIfTooLarge(const base::FilePath& path, size_t max_size) {
// Get the file size in a retry loop.
int64_t file_size = 0;
DWORD wait = 1;
bool got_size = false;
for (size_t retries = 0; retries <= 10; ++retries) {
if (!base::PathExists(path))
return true;
if (base::GetFileSize(path, &file_size)) {
got_size = true;
break;
}
::Sleep(wait);
wait++;
}
if (!got_size) {
LOG(ERROR) << "Unable to determine metric file size: " << path.value();
return false;
}
// If the file doesn't need to be deleted then return.
if (file_size <= static_cast<int64_t>(max_size))
return true;
// Try to delete the file in a retry loop.
wait = 1;
for (size_t retries = 0; retries <= 10; ++retries) {
if (base::DeleteFile(path, false)) {
LOG(INFO) << "Delete large metric file: " << path.value();
return true;
}
::Sleep(wait);
wait++;
}
LOG(ERROR) << "Unable to delete large metric file: " << path.value();
return false;
}
// Emits a single line of data to the log file. Logs an error if this fails,
// succeeds silently.
void EmitLineToMetricsFile(const base::StringPiece& line) {
base::FilePath path = GetMetricsLogPath();
if (!DeleteFileIfTooLarge(path, kMetricsFileMaxSize))
return;
base::win::ScopedHandle handle;
if (!OpenForExclusiveWrite(path, &handle)) {
LOG(ERROR) << "Failed to acquire handle to metrics log.";
return;
}
DCHECK(handle.IsValid());
DWORD bytes_written = 0;
if (!::WriteFile(handle.Get(),
line.data(),
static_cast<DWORD>(line.size()),
&bytes_written,
nullptr) ||
bytes_written != line.size()) {
LOG(ERROR) << "Failed to write line to metrics log.";
}
}
struct ScopedInfoLogLevel {
ScopedInfoLogLevel() : level(0) {
level = logging::GetMinLogLevel();
logging::SetMinLogLevel(logging::LOG_INFO);
}
~ScopedInfoLogLevel() {
logging::SetMinLogLevel(level);
}
int level;
};
// Emits a metric that will appear on the waterfall console.
void EmitMetricToWaterfall(const base::StringPiece& name,
const std::string& value) {
ScopedInfoLogLevel scoped_info_log_level;
LOG(INFO) << "Emitting metric to waterfall\n\n"
<< "@@@STEP_TEXT@" << name << " = " << value << "@@@\n";
}
// Emit a metric in a simple human readable format.
void EmitMetricToLogging(const base::StringPiece& name,
const std::string& value) {
ScopedInfoLogLevel scoped_info_log_level;
LOG(INFO) << "PERF: " << name << "=" << value;
}
void EmitMetric(const base::StringPiece& name, const std::string& value) {
base::AutoLock auto_lock(metrics_lock);
// Ensure the metric configuration is parsed from the environment.
ParseMetricsConfiguration();
// Build the CSV record.
base::Time time = base::Time::Now();
const version::SyzygyVersion& version = version::kSyzygyVersion;
std::string record = base::StringPrintf(
"%lld, " // Time (in microseconds since epoch)
"%d.%d.%d.%d, " // Version
"%s, " // GitHash
"%s, " // Config
"%s, " // MetricName
"%s\n", // MetricValue
time.ToInternalValue(),
version.major(), version.minor(), version.build(), version.patch(),
version.last_change().c_str(),
kBuildConfig,
name.data(),
value.data());
// Emit the record to the log.
if (metrics_config.emit_to_log)
EmitLineToMetricsFile(record);
// And also emit it to the log or waterfall.
if (metrics_config.emit_to_waterfall) {
EmitMetricToWaterfall(name, value);
} else {
EmitMetricToLogging(name, value);
}
}
} // namespace
void EmitMetric(const base::StringPiece& name, int64_t value) {
std::string s = base::StringPrintf("%lld", value);
EmitMetric(name, s);
}
void EmitMetric(const base::StringPiece& name, uint64_t value) {
std::string s = base::StringPrintf("%llu", value);
EmitMetric(name, s);
}
void EmitMetric(const base::StringPiece& name, double value) {
// Convert the metric value to a string.
std::string s = base::StringPrintf(
"%.*e", std::numeric_limits<double>::digits10, value);
EmitMetric(name, s);
}
} // namespace testing