| // Copyright (c) 2012 The Chromium 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 "chrome/browser/performance_monitor/database.h" |
| |
| #include "base/file_util.h" |
| #include "base/files/file_path.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/path_service.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/performance_monitor/key_builder.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "third_party/leveldatabase/src/include/leveldb/db.h" |
| #include "third_party/leveldatabase/src/include/leveldb/iterator.h" |
| #include "third_party/leveldatabase/src/include/leveldb/write_batch.h" |
| |
| namespace performance_monitor { |
| namespace { |
| const char kDbDir[] = "Performance Monitor Databases"; |
| const char kRecentDb[] = "Recent Metrics"; |
| const char kMaxValueDb[] = "Max Value Metrics"; |
| const char kEventDb[] = "Events"; |
| const char kStateDb[] = "Configuration"; |
| const char kActiveIntervalDb[] = "Active Interval"; |
| const char kMetricDb[] = "Metrics"; |
| const double kDefaultMaxValue = 0.0; |
| |
| // If the db is quiet for this number of minutes, then it is considered down. |
| const base::TimeDelta kActiveIntervalTimeout() { |
| return base::TimeDelta::FromMinutes(5); |
| } |
| |
| TimeRange ActiveIntervalToTimeRange(const std::string& start_time, |
| const std::string& end_time) { |
| int64 start_time_int = 0; |
| int64 end_time_int = 0; |
| base::StringToInt64(start_time, &start_time_int); |
| base::StringToInt64(end_time, &end_time_int); |
| return TimeRange(base::Time::FromInternalValue(start_time_int), |
| base::Time::FromInternalValue(end_time_int)); |
| } |
| |
| double StringToDouble(const std::string& s) { |
| double value = 0.0; |
| if (!base::StringToDouble(s, &value)) |
| LOG(ERROR) << "Failed to convert " << s << " to double."; |
| return value; |
| } |
| |
| // Returns an event from the given JSON string; the scoped_ptr will be NULL if |
| // we are unable to properly parse the JSON. |
| scoped_ptr<Event> EventFromJSON(const std::string& data) { |
| Value* value = base::JSONReader::Read(data); |
| DictionaryValue* dict = NULL; |
| if (!value || !value->GetAsDictionary(&dict)) |
| return scoped_ptr<Event>(); |
| |
| return Event::FromValue(scoped_ptr<DictionaryValue>(dict)); |
| } |
| |
| } // namespace |
| |
| const char Database::kDatabaseSequenceToken[] = |
| "_performance_monitor_db_sequence_token_"; |
| |
| TimeRange::TimeRange() { |
| } |
| |
| TimeRange::TimeRange(base::Time start_time, base::Time end_time) |
| : start(start_time), |
| end(end_time) { |
| } |
| |
| TimeRange::~TimeRange() { |
| } |
| |
| base::Time Database::SystemClock::GetTime() { |
| return base::Time::Now(); |
| } |
| |
| // Static |
| scoped_ptr<Database> Database::Create(base::FilePath path) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| if (path.empty()) { |
| CHECK(PathService::Get(chrome::DIR_USER_DATA, &path)); |
| path = path.AppendASCII(kDbDir); |
| } |
| scoped_ptr<Database> database; |
| if (!base::DirectoryExists(path) && !base::CreateDirectory(path)) |
| return database.Pass(); |
| database.reset(new Database(path)); |
| |
| // If the database did not initialize correctly, return a NULL scoped_ptr. |
| if (!database->valid_) |
| database.reset(); |
| return database.Pass(); |
| } |
| |
| bool Database::AddStateValue(const std::string& key, const std::string& value) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| UpdateActiveInterval(); |
| leveldb::Status insert_status = state_db_->Put(write_options_, key, value); |
| return insert_status.ok(); |
| } |
| |
| std::string Database::GetStateValue(const std::string& key) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::string result; |
| state_db_->Get(read_options_, key, &result); |
| return result; |
| } |
| |
| bool Database::AddEvent(const Event& event) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| UpdateActiveInterval(); |
| std::string value; |
| base::JSONWriter::Write(event.data(), &value); |
| std::string key = key_builder_->CreateEventKey(event.time(), event.type()); |
| leveldb::Status status = event_db_->Put(write_options_, key, value); |
| return status.ok(); |
| } |
| |
| std::vector<TimeRange> Database::GetActiveIntervals(const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::vector<TimeRange> results; |
| std::string start_key = key_builder_->CreateActiveIntervalKey(start); |
| std::string end_key = key_builder_->CreateActiveIntervalKey(end); |
| scoped_ptr<leveldb::Iterator> it(active_interval_db_->NewIterator( |
| read_options_)); |
| it->Seek(start_key); |
| // If the interator is valid, we check the previous value in case we jumped |
| // into the middle of an active interval. If the iterator is not valid, then |
| // the key may be in the current active interval. |
| if (it->Valid()) |
| it->Prev(); |
| else |
| it->SeekToLast(); |
| if (it->Valid() && it->value().ToString() > start_key) { |
| results.push_back(ActiveIntervalToTimeRange(it->key().ToString(), |
| it->value().ToString())); |
| } |
| |
| for (it->Seek(start_key); |
| it->Valid() && it->key().ToString() < end_key; |
| it->Next()) { |
| results.push_back(ActiveIntervalToTimeRange(it->key().ToString(), |
| it->value().ToString())); |
| } |
| return results; |
| } |
| |
| Database::EventVector Database::GetEvents(EventType type, |
| const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| EventVector events; |
| std::string start_key = |
| key_builder_->CreateEventKey(start, EVENT_UNDEFINED); |
| std::string end_key = |
| key_builder_->CreateEventKey(end, EVENT_NUMBER_OF_EVENTS); |
| leveldb::WriteBatch invalid_entries; |
| scoped_ptr<leveldb::Iterator> it(event_db_->NewIterator(read_options_)); |
| for (it->Seek(start_key); |
| it->Valid() && it->key().ToString() <= end_key; |
| it->Next()) { |
| if (type != EVENT_UNDEFINED) { |
| EventType key_type = |
| key_builder_->EventKeyToEventType(it->key().ToString()); |
| if (key_type != type) |
| continue; |
| } |
| scoped_ptr<Event> event = EventFromJSON(it->value().ToString()); |
| if (!event.get()) { |
| invalid_entries.Delete(it->key()); |
| LOG(ERROR) << "Found invalid event in the database. JSON: '" |
| << it->value().ToString() |
| << "'. Erasing event from the database."; |
| continue; |
| } |
| events.push_back(linked_ptr<Event>(event.release())); |
| } |
| event_db_->Write(write_options_, &invalid_entries); |
| return events; |
| } |
| |
| Database::EventTypeSet Database::GetEventTypes(const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| EventTypeSet results; |
| std::string start_key = |
| key_builder_->CreateEventKey(start, EVENT_UNDEFINED); |
| std::string end_key = |
| key_builder_->CreateEventKey(end, EVENT_NUMBER_OF_EVENTS); |
| scoped_ptr<leveldb::Iterator> it(event_db_->NewIterator(read_options_)); |
| for (it->Seek(start_key); |
| it->Valid() && it->key().ToString() <= end_key; |
| it->Next()) { |
| EventType key_type = |
| key_builder_->EventKeyToEventType(it->key().ToString()); |
| results.insert(key_type); |
| } |
| return results; |
| } |
| |
| bool Database::AddMetric(const std::string& activity, |
| const Metric& metric) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| if (!metric.IsValid()) { |
| DLOG(ERROR) << "Metric to be added is invalid. Type: " << metric.type |
| << ", Time: " << metric.time.ToInternalValue() |
| << ", Value: " << metric.value << ". Ignoring."; |
| return false; |
| } |
| |
| UpdateActiveInterval(); |
| std::string recent_key = |
| key_builder_->CreateRecentKey(metric.time, metric.type, activity); |
| std::string metric_key = |
| key_builder_->CreateMetricKey(metric.time, metric.type, activity); |
| std::string recent_map_key = |
| key_builder_->CreateRecentMapKey(metric.type, activity); |
| // Use recent_map_ to quickly find the key that must be removed. |
| RecentMap::iterator old_it = recent_map_.find(recent_map_key); |
| if (old_it != recent_map_.end()) |
| recent_db_->Delete(write_options_, old_it->second); |
| recent_map_[recent_map_key] = recent_key; |
| leveldb::Status recent_status = |
| recent_db_->Put(write_options_, recent_key, metric.ValueAsString()); |
| leveldb::Status metric_status = |
| metric_db_->Put(write_options_, metric_key, metric.ValueAsString()); |
| |
| bool max_value_success = |
| UpdateMaxValue(activity, metric.type, metric.ValueAsString()); |
| return recent_status.ok() && metric_status.ok() && max_value_success; |
| } |
| |
| bool Database::UpdateMaxValue(const std::string& activity, |
| MetricType metric, |
| const std::string& value) { |
| std::string max_value_key( |
| key_builder_->CreateMaxValueKey(metric, activity)); |
| bool has_key = ContainsKey(max_value_map_, max_value_key); |
| if ((has_key && StringToDouble(value) > max_value_map_[max_value_key]) || |
| !has_key) { |
| max_value_map_[max_value_key] = StringToDouble(value); |
| return max_value_db_->Put(write_options_, max_value_key, value).ok(); |
| } |
| |
| return true; |
| } |
| |
| Database::MetricTypeSet Database::GetActiveMetrics(const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::string recent_start_key = key_builder_->CreateRecentKey( |
| start, static_cast<MetricType>(0), std::string()); |
| std::string recent_end_key = key_builder_->CreateRecentKey( |
| end, METRIC_NUMBER_OF_METRICS, std::string()); |
| std::string recent_end_of_time_key = key_builder_->CreateRecentKey( |
| clock_->GetTime(), METRIC_NUMBER_OF_METRICS, std::string()); |
| |
| MetricTypeSet active_metrics; |
| // Get all the guaranteed metrics. |
| scoped_ptr<leveldb::Iterator> recent_it( |
| recent_db_->NewIterator(read_options_)); |
| for (recent_it->Seek(recent_start_key); |
| recent_it->Valid() && recent_it->key().ToString() <= recent_end_key; |
| recent_it->Next()) { |
| RecentKey split_key = |
| key_builder_->SplitRecentKey(recent_it->key().ToString()); |
| active_metrics.insert(split_key.type); |
| } |
| // Get all the possible metrics (metrics that may have been updated after |
| // |end|). |
| MetricTypeSet possible_metrics; |
| for (recent_it->Seek(recent_end_key); |
| recent_it->Valid() && |
| recent_it->key().ToString() <= recent_end_of_time_key; |
| recent_it->Next()) { |
| RecentKey split_key = |
| key_builder_->SplitRecentKey(recent_it->key().ToString()); |
| possible_metrics.insert(split_key.type); |
| } |
| MetricTypeSet::iterator possible_it; |
| scoped_ptr<leveldb::Iterator> metric_it( |
| metric_db_->NewIterator(read_options_)); |
| for (possible_it = possible_metrics.begin(); |
| possible_it != possible_metrics.end(); |
| ++possible_it) { |
| std::string metric_start_key = |
| key_builder_->CreateMetricKey(start, *possible_it,std::string()); |
| std::string metric_end_key = |
| key_builder_->CreateMetricKey(end, *possible_it, std::string()); |
| metric_it->Seek(metric_start_key); |
| // Stats in the timerange from any activity makes the metric active. |
| if (metric_it->Valid() && metric_it->key().ToString() <= metric_end_key) { |
| active_metrics.insert(*possible_it); |
| } |
| } |
| |
| return active_metrics; |
| } |
| |
| std::set<std::string> Database::GetActiveActivities(MetricType metric_type, |
| const base::Time& start) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::set<std::string> results; |
| std::string start_key = key_builder_->CreateRecentKey( |
| start, static_cast<MetricType>(0), std::string()); |
| scoped_ptr<leveldb::Iterator> it(recent_db_->NewIterator(read_options_)); |
| for (it->Seek(start_key); it->Valid(); it->Next()) { |
| RecentKey split_key = |
| key_builder_->SplitRecentKey(it->key().ToString()); |
| if (split_key.type == metric_type) |
| results.insert(split_key.activity); |
| } |
| return results; |
| } |
| |
| double Database::GetMaxStatsForActivityAndMetric(const std::string& activity, |
| MetricType metric) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::string max_value_key( |
| key_builder_->CreateMaxValueKey(metric, activity)); |
| if (ContainsKey(max_value_map_, max_value_key)) |
| return max_value_map_[max_value_key]; |
| return kDefaultMaxValue; |
| } |
| |
| bool Database::GetRecentStatsForActivityAndMetric(const std::string& activity, |
| MetricType metric_type, |
| Metric* metric) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| std::string recent_map_key = |
| key_builder_->CreateRecentMapKey(metric_type, activity); |
| if (!ContainsKey(recent_map_, recent_map_key)) |
| return false; |
| std::string recent_key = recent_map_[recent_map_key]; |
| |
| std::string result; |
| leveldb::Status status = recent_db_->Get(read_options_, recent_key, &result); |
| if (status.ok()) |
| *metric = Metric(metric_type, |
| key_builder_->SplitRecentKey(recent_key).time, |
| result); |
| return status.ok(); |
| } |
| |
| scoped_ptr<Database::MetricVector> Database::GetStatsForActivityAndMetric( |
| const std::string& activity, |
| MetricType metric_type, |
| const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| scoped_ptr<MetricVector> results(new MetricVector()); |
| std::string start_key = |
| key_builder_->CreateMetricKey(start, metric_type, activity); |
| std::string end_key = |
| key_builder_->CreateMetricKey(end, metric_type, activity); |
| leveldb::WriteBatch invalid_entries; |
| scoped_ptr<leveldb::Iterator> it(metric_db_->NewIterator(read_options_)); |
| for (it->Seek(start_key); |
| it->Valid() && it->key().ToString() <= end_key; |
| it->Next()) { |
| MetricKey split_key = |
| key_builder_->SplitMetricKey(it->key().ToString()); |
| if (split_key.activity == activity) { |
| Metric metric(metric_type, split_key.time, it->value().ToString()); |
| if (!metric.IsValid()) { |
| invalid_entries.Delete(it->key()); |
| LOG(ERROR) << "Found bad metric in the database. Type: " |
| << metric.type << ", Time: " << metric.time.ToInternalValue() |
| << ", Value: " << metric.value |
| << ". Erasing metric from database."; |
| continue; |
| } |
| results->push_back(metric); |
| } |
| } |
| metric_db_->Write(write_options_, &invalid_entries); |
| return results.Pass(); |
| } |
| |
| Database::MetricVectorMap Database::GetStatsForMetricByActivity( |
| MetricType metric_type, |
| const base::Time& start, |
| const base::Time& end) { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| MetricVectorMap results; |
| std::string start_key = |
| key_builder_->CreateMetricKey(start, metric_type, std::string()); |
| std::string end_key = |
| key_builder_->CreateMetricKey(end, metric_type, std::string()); |
| leveldb::WriteBatch invalid_entries; |
| scoped_ptr<leveldb::Iterator> it(metric_db_->NewIterator(read_options_)); |
| for (it->Seek(start_key); |
| it->Valid() && it->key().ToString() <= end_key; |
| it->Next()) { |
| MetricKey split_key = key_builder_->SplitMetricKey(it->key().ToString()); |
| if (!results[split_key.activity].get()) { |
| results[split_key.activity] = |
| linked_ptr<MetricVector >(new MetricVector()); |
| } |
| Metric metric(metric_type, split_key.time, it->value().ToString()); |
| if (!metric.IsValid()) { |
| invalid_entries.Delete(it->key()); |
| LOG(ERROR) << "Found bad metric in the database. Type: " |
| << metric.type << ", Time: " << metric.time.ToInternalValue() |
| << ", Value: " << metric.value |
| << ". Erasing metric from database."; |
| continue; |
| } |
| results[split_key.activity]->push_back(metric); |
| } |
| metric_db_->Write(write_options_, &invalid_entries); |
| return results; |
| } |
| |
| Database::Database(const base::FilePath& path) |
| : key_builder_(new KeyBuilder()), |
| path_(path), |
| read_options_(leveldb::ReadOptions()), |
| write_options_(leveldb::WriteOptions()), |
| valid_(false) { |
| if (!InitDBs()) |
| return; |
| LoadRecents(); |
| LoadMaxValues(); |
| clock_ = scoped_ptr<Clock>(new SystemClock()); |
| valid_ = true; |
| } |
| |
| Database::~Database() { |
| } |
| |
| bool Database::InitDBs() { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| leveldb::Options open_options; |
| open_options.max_open_files = 0; // Use minimum. |
| open_options.create_if_missing = true; |
| |
| // TODO (rdevlin.cronin): This code is ugly. Fix it. |
| recent_db_ = SafelyOpenDatabase(open_options, |
| kRecentDb, |
| true); // fix if damaged |
| max_value_db_ = SafelyOpenDatabase(open_options, |
| kMaxValueDb, |
| true); // fix if damaged |
| state_db_ = SafelyOpenDatabase(open_options, |
| kStateDb, |
| true); // fix if damaged |
| active_interval_db_ = SafelyOpenDatabase(open_options, |
| kActiveIntervalDb, |
| true); // fix if damaged |
| metric_db_ = SafelyOpenDatabase(open_options, |
| kMetricDb, |
| true); // fix if damaged |
| event_db_ = SafelyOpenDatabase(open_options, |
| kEventDb, |
| true); // fix if damaged |
| return recent_db_ && max_value_db_ && state_db_ && |
| active_interval_db_ && metric_db_ && event_db_; |
| } |
| |
| scoped_ptr<leveldb::DB> Database::SafelyOpenDatabase( |
| const leveldb::Options& options, |
| const std::string& path, |
| bool fix_if_damaged) { |
| #if defined(OS_POSIX) |
| std::string name = path_.AppendASCII(path).value(); |
| #elif defined(OS_WIN) |
| std::string name = WideToUTF8(path_.AppendASCII(path).value()); |
| #endif |
| |
| leveldb::DB* database; |
| leveldb::Status status = leveldb::DB::Open(options, name, &database); |
| // If all goes well, return the database. |
| if (status.ok()) |
| return scoped_ptr<leveldb::DB>(database); |
| |
| // Return NULL and print the error if we either didn't find the database and |
| // don't want to create it, or if we don't want to try to fix it. |
| if ((status.IsNotFound() && !options.create_if_missing) || !fix_if_damaged) { |
| LOG(ERROR) << status.ToString(); |
| return scoped_ptr<leveldb::DB>(); |
| } |
| // Otherwise, we have an error (corruption, io error, or a not found error |
| // even if we tried to create it). |
| // |
| // First, we try again. |
| LOG(ERROR) << "Database error: " << status.ToString() << ". Trying again."; |
| status = leveldb::DB::Open(options, name, &database); |
| // If we fail on corruption, we can try to repair it. |
| if (status.IsCorruption()) { |
| LOG(ERROR) << "Database corrupt (second attempt). Trying to repair."; |
| status = leveldb::RepairDB(name, options); |
| // If the repair succeeds and we can open the database, return the |
| // database. Otherwise, continue on. |
| if (status.ok()) { |
| status = leveldb::DB::Open(options, name, &database); |
| if (status.ok()) |
| return scoped_ptr<leveldb::DB>(database); |
| } |
| LOG(ERROR) << "Repair failed. Deleting database."; |
| } |
| // Next, try to delete and recreate the database. Return NULL if we fail |
| // on either of these steps. |
| status = leveldb::DestroyDB(name, options); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to delete database. " << status.ToString(); |
| return scoped_ptr<leveldb::DB>(); |
| } |
| // If we don't have the create_if_missing option, add it (it's safe to |
| // assume this is okay, since we have permission to |fix_if_damaged|). |
| if (!options.create_if_missing) { |
| leveldb::Options create_options(options); |
| create_options.create_if_missing = true; |
| status = leveldb::DB::Open(create_options, name, &database); |
| } else { |
| status = leveldb::DB::Open(options, name, &database); |
| } |
| // There's nothing else we can try at this point. |
| if (status.ok()) |
| return scoped_ptr<leveldb::DB>(database); |
| // Return the database if we succeeded, or NULL on failure. |
| LOG(ERROR) << "Failed to recreate database. " << status.ToString(); |
| return scoped_ptr<leveldb::DB>(); |
| } |
| |
| bool Database::Close() { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| metric_db_.reset(); |
| event_db_.reset(); |
| recent_db_.reset(); |
| max_value_db_.reset(); |
| state_db_.reset(); |
| active_interval_db_.reset(); |
| start_time_key_.clear(); |
| return true; |
| } |
| |
| void Database::LoadRecents() { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| recent_map_.clear(); |
| scoped_ptr<leveldb::Iterator> it(recent_db_->NewIterator(read_options_)); |
| for (it->SeekToFirst(); it->Valid(); it->Next()) { |
| RecentKey split_key = key_builder_->SplitRecentKey(it->key().ToString()); |
| recent_map_[key_builder_-> |
| CreateRecentMapKey(split_key.type, split_key.activity)] = |
| it->key().ToString(); |
| } |
| } |
| |
| void Database::LoadMaxValues() { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| max_value_map_.clear(); |
| scoped_ptr<leveldb::Iterator> it(max_value_db_->NewIterator(read_options_)); |
| for (it->SeekToFirst(); it->Valid(); it->Next()) { |
| max_value_map_[it->key().ToString()] = |
| StringToDouble(it->value().ToString()); |
| } |
| } |
| |
| // TODO(chebert): Only update the active interval under certian circumstances |
| // eg. every 10 times or when forced. |
| void Database::UpdateActiveInterval() { |
| CHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| base::Time current_time = clock_->GetTime(); |
| std::string end_time; |
| // If the last update was too long ago. |
| if (start_time_key_.empty() || |
| current_time - last_update_time_ > kActiveIntervalTimeout()) { |
| start_time_key_ = key_builder_->CreateActiveIntervalKey(current_time); |
| end_time = start_time_key_; |
| } else { |
| end_time = key_builder_->CreateActiveIntervalKey(clock_->GetTime()); |
| } |
| last_update_time_ = current_time; |
| active_interval_db_->Put(write_options_, start_time_key_, end_time); |
| } |
| |
| } // namespace performance_monitor |