blob: fcfeb33b7ae31b75b37436e907409ed807ce79f5 [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/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.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;
// Validates the specified mapping. A `group_identifier` is allowed to only
// have one `key`, except in the case of `GOOGLE_APP`. A `GOOGLE_APP` key may
// coexist with another provided they have the same `id` value.
bool 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!");
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);
if (other_id == EMPTY_ID) {
continue;
}
// In the case of a GOOGLE_APP key, another entry is allowed provided that
// the IDs match.
if (key == GOOGLE_APP || other_key == GOOGLE_APP) {
if (other_id != id) {
return false;
}
continue;
}
// `group_identifier` may not be registered under multiple non-GOOGLE_APP
// IDCollectionKey values.
return false;
}
return true;
}
void AssociateID(IDCollectionKey key,
ActiveGroupId group_identifier,
VariationID id,
TimeWindow time_window) {
if (!ValidateID(key, group_identifier, id)) {
// In the case of a validation failure, omit the association and instead
// report the issue via a crash dump, to notify us of a problem with the
// server-side configs.
SCOPED_CRASH_KEY_NUMBER("InvalidVariationsID", "id", id);
base::debug::DumpWithoutCrashing();
return;
}
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() &&
!it->second.time_window.Contains(*current_time))) {
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 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
// `time` but also before `next_event`.
if (entry.time_window.start() > time &&
entry.time_window.start() < next_event) {
next_event = entry.time_window.start();
}
if (entry.time_window.end() > 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
void AssociateGoogleVariationID(base::PassKey<VariationsSeedProcessor> pass_key,
IDCollectionKey key,
ActiveGroupId active_group_id,
VariationID variation_id,
TimeWindow time_window) {
GroupMapAccessor::GetInstance()->AssociateID(key, active_group_id,
variation_id, time_window);
}
void AssociateGoogleVariationID(base::PassKey<SyntheticTrialRegistry> pass_key,
IDCollectionKey key,
ActiveGroupId active_group_id,
VariationID variation_id,
TimeWindow time_window) {
GroupMapAccessor::GetInstance()->AssociateID(key, active_group_id,
variation_id, time_window);
}
void AssociateGoogleVariationIDForTesting(IDCollectionKey key,
std::string_view trial_name,
std::string_view group_name,
VariationID variation_id,
TimeWindow time_window) {
GroupMapAccessor::GetInstance()->AssociateID(
key, MakeActiveGroupId(trial_name, group_name), 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 test {
void ClearAllVariationIDs() {
GroupMapAccessor::GetInstance()->ClearAllMapsForTesting();
}
void ClearAllVariationParams() {
base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting();
}
} // namespace test
} // namespace variations