blob: 878bf8c27f6cfd5fa20c9e13adb55fb40705f58e [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/variations/variations_associated_data.h"
#include <map>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/memory/singleton.h"
#include "base/metrics/field_trial_param_associator.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_split.h"
#include "base/synchronization/lock.h"
namespace variations {
namespace {
// The internal singleton accessor for the map, used to keep it thread-safe.
class GroupMapAccessor {
public:
struct VariationEntry {
VariationID id;
TimeWindow time_window;
};
typedef std::map<ActiveGroupId, VariationEntry, ActiveGroupIdCompare>
GroupToIDMap;
// Retrieve the singleton.
static GroupMapAccessor* GetInstance() {
return base::Singleton<GroupMapAccessor,
base::LeakySingletonTraits<GroupMapAccessor>>::get();
}
GroupMapAccessor(const GroupMapAccessor&) = delete;
GroupMapAccessor& operator=(const GroupMapAccessor&) = delete;
// Ensures that |group_identifier| is associated with only one non-trigger,
// trigger, or signed-in key.
void ValidateID(IDCollectionKey key,
ActiveGroupId group_identifier,
VariationID id) {
static_assert(ID_COLLECTION_COUNT == 6,
"If you add a new collection key, add handling code here!");
#if DCHECK_IS_ON()
for (int i = 0; i < ID_COLLECTION_COUNT; ++i) {
IDCollectionKey other_key = static_cast<IDCollectionKey>(i);
if (key == other_key) {
continue;
}
VariationID other_id = GetID(other_key, group_identifier);
// For a GOOGLE_APP key, validate that all other collections with this
// |group_identifier| have the same associated ID.
if (key == GOOGLE_APP) {
DCHECK(other_id == EMPTY_ID || other_id == id);
continue;
}
// The ID should not be registered under a different non-GOOGLE_APP
// IDCollectionKey.
if (other_key != GOOGLE_APP) {
DCHECK_EQ(EMPTY_ID, other_id);
}
}
#endif // DCHECK_IS_ON()
}
void AssociateID(IDCollectionKey key,
ActiveGroupId group_identifier,
VariationID id,
TimeWindow time_window) {
ValidateID(key, group_identifier, id);
base::AutoLock scoped_lock(lock_);
GroupToIDMap* group_to_id_map = GetGroupToIDMap(key);
(*group_to_id_map)[group_identifier] = {id, time_window};
}
VariationID GetID(IDCollectionKey key,
ActiveGroupId group_identifier,
std::optional<base::Time> current_time = std::nullopt) {
base::AutoLock scoped_lock(lock_);
GroupToIDMap* group_to_id_map = GetGroupToIDMap(key);
GroupToIDMap::const_iterator it = group_to_id_map->find(group_identifier);
if (it == group_to_id_map->end() ||
(current_time.has_value() &&
(*current_time < it->second.time_window.start() ||
*current_time > it->second.time_window.end()))) {
return EMPTY_ID;
}
return it->second.id;
}
void ClearAllMapsForTesting() {
base::AutoLock scoped_lock(lock_);
for (int i = 0; i < ID_COLLECTION_COUNT; ++i) {
GroupToIDMap* map = GetGroupToIDMap(static_cast<IDCollectionKey>(i));
DCHECK(map);
map->clear();
}
}
base::Time GetNextTimeWindowEvent(base::Time current_time) const {
base::AutoLock scoped_lock(lock_);
base::Time next_event = base::Time::Max();
// This double loop is O(N) where N is the number of field trials having an
// associated variations ID, which should be in the order of 10s at most.
for (const auto& id_map : group_to_id_maps_) {
for (const auto& [id, entry] : id_map) {
// Update the next time window event if the start or end time is after
// 'current_time' but also before `next_event`.
if (entry.time_window.start() > current_time &&
entry.time_window.start() < next_event) {
next_event = entry.time_window.start();
}
if (entry.time_window.end() > current_time &&
entry.time_window.end() < next_event) {
next_event = entry.time_window.end();
}
}
}
return next_event;
}
private:
friend struct base::DefaultSingletonTraits<GroupMapAccessor>;
// Retrieves the GroupToIDMap for |key|.
GroupToIDMap* GetGroupToIDMap(IDCollectionKey key) {
return &group_to_id_maps_[key];
}
GroupMapAccessor() { group_to_id_maps_.resize(ID_COLLECTION_COUNT); }
~GroupMapAccessor() = default;
mutable base::Lock lock_;
std::vector<GroupToIDMap> group_to_id_maps_;
};
} // namespace
TimeWindow::TimeWindow(base::Time start, base::Time end)
: start_(start), end_(end) {
CHECK_LT(start_, end_);
}
void AssociateGoogleVariationID(IDCollectionKey key,
std::string_view trial_name,
std::string_view group_name,
VariationID variation_id,
TimeWindow time_window) {
AssociateGoogleVariationID(key, MakeActiveGroupId(trial_name, group_name),
variation_id, time_window);
}
void AssociateGoogleVariationID(IDCollectionKey key,
ActiveGroupId active_group_id,
VariationID variation_id,
TimeWindow time_window) {
GroupMapAccessor::GetInstance()->AssociateID(key, active_group_id,
variation_id, time_window);
}
VariationID GetGoogleVariationID(IDCollectionKey key,
std::string_view trial_name,
std::string_view group_name,
std::optional<base::Time> current_time) {
return GetGoogleVariationID(
key, MakeActiveGroupId(trial_name, group_name), current_time);
}
VariationID GetGoogleVariationID(
IDCollectionKey key,
ActiveGroupId active_group_id,
std::optional<base::Time> current_time) {
return GroupMapAccessor::GetInstance()->GetID(key, active_group_id,
current_time);
}
base::Time GetNextTimeWindowEvent(base::Time current_time) {
return GroupMapAccessor::GetInstance()->GetNextTimeWindowEvent(current_time);
}
// Functions below are exposed for testing explicitly behind this namespace.
// They simply wrap existing functions in this file.
namespace testing {
void ClearAllVariationIDs() {
GroupMapAccessor::GetInstance()->ClearAllMapsForTesting();
}
void ClearAllVariationParams() {
base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting();
}
} // namespace testing
} // namespace variations