Implement exclude flags on CH using shared core
The core is fully contracted for each exclude flag and stored in a merged graph data structure.
This commit is contained in:
committed by
Patrick Niklaus
parent
4b75cb8b0e
commit
61c430c098
@@ -1,4 +1,6 @@
|
||||
#include "contractor/contractor.hpp"
|
||||
#include "contractor/contract_excludable_graph.hpp"
|
||||
#include "contractor/contracted_edge_container.hpp"
|
||||
#include "contractor/crc32_processor.hpp"
|
||||
#include "contractor/files.hpp"
|
||||
#include "contractor/graph_contractor.hpp"
|
||||
@@ -6,6 +8,7 @@
|
||||
|
||||
#include "extractor/compressed_edge_container.hpp"
|
||||
#include "extractor/edge_based_graph_factory.hpp"
|
||||
#include "extractor/files.hpp"
|
||||
#include "extractor/node_based_edge.hpp"
|
||||
|
||||
#include "storage/io.hpp"
|
||||
@@ -14,6 +17,8 @@
|
||||
|
||||
#include "util/exception.hpp"
|
||||
#include "util/exception_utils.hpp"
|
||||
#include "util/exclude_flag.hpp"
|
||||
#include "util/filtered_graph.hpp"
|
||||
#include "util/graph_loader.hpp"
|
||||
#include "util/integer_range.hpp"
|
||||
#include "util/log.hpp"
|
||||
@@ -42,6 +47,11 @@ int Contractor::Run()
|
||||
throw util::exception("Core factor must be between 0.0 to 1.0 (inclusive)" + SOURCE_REF);
|
||||
}
|
||||
|
||||
if (config.use_cached_priority)
|
||||
{
|
||||
util::Log(logWARNING) << "Using cached priorities is deprecated and they will be ignored.";
|
||||
}
|
||||
|
||||
TIMER_START(preparing);
|
||||
|
||||
util::Log() << "Reading node weights.";
|
||||
@@ -63,41 +73,36 @@ int Contractor::Run()
|
||||
// Contracting the edge-expanded graph
|
||||
|
||||
TIMER_START(contraction);
|
||||
std::vector<bool> is_core_node;
|
||||
std::vector<float> node_levels;
|
||||
if (config.use_cached_priority)
|
||||
|
||||
std::vector<std::vector<bool>> node_filters;
|
||||
{
|
||||
files::readLevels(config.GetPath(".osrm.level"), node_levels);
|
||||
extractor::EdgeBasedNodeDataContainer node_data;
|
||||
extractor::files::readNodeData(config.GetPath(".osrm.ebg_nodes"), node_data);
|
||||
|
||||
extractor::ProfileProperties properties;
|
||||
extractor::files::readProfileProperties(config.GetPath(".osrm.properties"), properties);
|
||||
|
||||
node_filters = util::excludeFlagsToNodeFilter(max_edge_id + 1, node_data, properties);
|
||||
}
|
||||
|
||||
util::DeallocatingVector<QueryEdge> contracted_edge_list;
|
||||
{ // own scope to not keep the contractor around
|
||||
auto contractor_graph = toContractorGraph(max_edge_id+1, std::move(edge_based_edge_list));
|
||||
std::tie(node_levels, is_core_node) = contractGraph(contractor_graph,
|
||||
std::move(node_levels),
|
||||
std::move(node_weights),
|
||||
config.core_factor);
|
||||
RangebasedCRC32 crc32_calculator;
|
||||
const unsigned checksum = crc32_calculator(edge_based_edge_list);
|
||||
|
||||
contracted_edge_list = toEdges<QueryEdge>(std::move(contractor_graph));
|
||||
}
|
||||
QueryGraph query_graph;
|
||||
std::vector<std::vector<bool>> edge_filters;
|
||||
std::vector<std::vector<bool>> cores;
|
||||
std::tie(query_graph, edge_filters, cores) =
|
||||
contractExcludableGraph(toContractorGraph(max_edge_id + 1, std::move(edge_based_edge_list)),
|
||||
std::move(node_weights),
|
||||
std::move(node_filters),
|
||||
config.core_factor);
|
||||
TIMER_STOP(contraction);
|
||||
|
||||
util::Log() << "Contracted graph has " << query_graph.GetNumberOfEdges() << " edges.";
|
||||
util::Log() << "Contraction took " << TIMER_SEC(contraction) << " sec";
|
||||
|
||||
{
|
||||
RangebasedCRC32 crc32_calculator;
|
||||
const unsigned checksum = crc32_calculator(contracted_edge_list);
|
||||
files::writeGraph(config.GetPath(".osrm.hsgr"), checksum, query_graph, edge_filters);
|
||||
|
||||
files::writeGraph(config.GetPath(".osrm.hsgr"),
|
||||
checksum,
|
||||
QueryGraph{max_edge_id + 1, std::move(contracted_edge_list)});
|
||||
}
|
||||
|
||||
files::writeCoreMarker(config.GetPath(".osrm.core"), is_core_node);
|
||||
if (!config.use_cached_priority)
|
||||
{
|
||||
files::writeLevels(config.GetPath(".osrm.level"), node_levels);
|
||||
}
|
||||
files::writeCoreMarker(config.GetPath(".osrm.core"), cores);
|
||||
|
||||
TIMER_STOP(preparing);
|
||||
|
||||
|
||||
@@ -1,23 +1,65 @@
|
||||
#include "contractor/contractor_dijkstra.hpp"
|
||||
#include "contractor/contractor_search.hpp"
|
||||
|
||||
#include "contractor/contractor_graph.hpp"
|
||||
#include "contractor/contractor_heap.hpp"
|
||||
|
||||
namespace osrm
|
||||
{
|
||||
namespace contractor
|
||||
{
|
||||
|
||||
ContractorDijkstra::ContractorDijkstra(const std::size_t heap_size) : heap(heap_size) {}
|
||||
namespace
|
||||
{
|
||||
void relaxNode(ContractorHeap &heap,
|
||||
const ContractorGraph &graph,
|
||||
const NodeID node,
|
||||
const EdgeWeight node_weight,
|
||||
const NodeID forbidden_node)
|
||||
{
|
||||
const short current_hop = heap.GetData(node).hop + 1;
|
||||
for (auto edge : graph.GetAdjacentEdgeRange(node))
|
||||
{
|
||||
const auto &data = graph.GetEdgeData(edge);
|
||||
if (!data.forward)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const NodeID to = graph.GetTarget(edge);
|
||||
BOOST_ASSERT(to != SPECIAL_NODEID);
|
||||
if (forbidden_node == to)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const EdgeWeight to_weight = node_weight + data.weight;
|
||||
|
||||
void ContractorDijkstra::Run(const unsigned number_of_targets,
|
||||
const int node_limit,
|
||||
const EdgeWeight weight_limit,
|
||||
const NodeID forbidden_node,
|
||||
const ContractorGraph &graph)
|
||||
// New Node discovered -> Add to Heap + Node Info Storage
|
||||
if (!heap.WasInserted(to))
|
||||
{
|
||||
heap.Insert(to, to_weight, ContractorHeapData{current_hop, false});
|
||||
}
|
||||
// Found a shorter Path -> Update weight
|
||||
else if (to_weight < heap.GetKey(to))
|
||||
{
|
||||
heap.DecreaseKey(to, to_weight);
|
||||
heap.GetData(to).hop = current_hop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void search(ContractorHeap &heap,
|
||||
const ContractorGraph &graph,
|
||||
const unsigned number_of_targets,
|
||||
const int node_limit,
|
||||
const EdgeWeight weight_limit,
|
||||
const NodeID forbidden_node)
|
||||
{
|
||||
int nodes = 0;
|
||||
unsigned number_of_targets_found = 0;
|
||||
while (!heap.Empty())
|
||||
{
|
||||
const NodeID node = heap.DeleteMin();
|
||||
BOOST_ASSERT(node != SPECIAL_NODEID);
|
||||
const auto node_weight = heap.GetKey(node);
|
||||
if (++nodes > node_limit)
|
||||
{
|
||||
@@ -38,59 +80,8 @@ void ContractorDijkstra::Run(const unsigned number_of_targets,
|
||||
}
|
||||
}
|
||||
|
||||
RelaxNode(node, node_weight, forbidden_node, graph);
|
||||
relaxNode(heap, graph, node, node_weight, forbidden_node);
|
||||
}
|
||||
}
|
||||
|
||||
void ContractorDijkstra::RelaxNode(const NodeID node,
|
||||
const EdgeWeight node_weight,
|
||||
const NodeID forbidden_node,
|
||||
const ContractorGraph &graph)
|
||||
{
|
||||
const short current_hop = heap.GetData(node).hop + 1;
|
||||
for (auto edge : graph.GetAdjacentEdgeRange(node))
|
||||
{
|
||||
const ContractorEdgeData &data = graph.GetEdgeData(edge);
|
||||
if (!data.forward)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const NodeID to = graph.GetTarget(edge);
|
||||
if (forbidden_node == to)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const EdgeWeight to_weight = node_weight + data.weight;
|
||||
|
||||
// New Node discovered -> Add to Heap + Node Info Storage
|
||||
if (!heap.WasInserted(to))
|
||||
{
|
||||
heap.Insert(to, to_weight, ContractorHeapData{current_hop, false});
|
||||
}
|
||||
// Found a shorter Path -> Update weight
|
||||
else if (to_weight < GetKey(to))
|
||||
{
|
||||
heap.DecreaseKey(to, to_weight);
|
||||
heap.GetData(to).hop = current_hop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ContractorDijkstra::Clear() { heap.Clear(); }
|
||||
|
||||
bool ContractorDijkstra::WasInserted(const NodeID node) const { return heap.WasInserted(node); }
|
||||
|
||||
void ContractorDijkstra::Insert(const NodeID node,
|
||||
const ContractorHeap::WeightType weight,
|
||||
const ContractorHeap::DataType &data)
|
||||
{
|
||||
heap.Insert(node, weight, data);
|
||||
}
|
||||
|
||||
ContractorHeap::WeightType ContractorDijkstra::GetKey(const NodeID node)
|
||||
{
|
||||
return heap.GetKey(node);
|
||||
}
|
||||
|
||||
} // namespace contractor
|
||||
} // namespace osrm
|
||||
+124
-125
@@ -1,6 +1,6 @@
|
||||
#include "contractor/graph_contractor.hpp"
|
||||
#include "contractor/contractor_dijkstra.hpp"
|
||||
#include "contractor/contractor_graph.hpp"
|
||||
#include "contractor/contractor_search.hpp"
|
||||
#include "contractor/query_edge.hpp"
|
||||
#include "util/deallocating_vector.hpp"
|
||||
#include "util/integer_range.hpp"
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <tbb/enumerable_thread_specific.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/parallel_invoke.h>
|
||||
#include <tbb/parallel_sort.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -29,10 +30,10 @@ namespace
|
||||
{
|
||||
struct ContractorThreadData
|
||||
{
|
||||
ContractorDijkstra dijkstra;
|
||||
ContractorHeap heap;
|
||||
std::vector<ContractorEdge> inserted_edges;
|
||||
std::vector<NodeID> neighbours;
|
||||
explicit ContractorThreadData(NodeID nodes) : dijkstra(nodes) {}
|
||||
explicit ContractorThreadData(NodeID nodes) : heap(nodes) {}
|
||||
};
|
||||
|
||||
struct ContractorNodeData
|
||||
@@ -42,34 +43,37 @@ struct ContractorNodeData
|
||||
using NodeLevel = float;
|
||||
|
||||
ContractorNodeData(std::size_t number_of_nodes,
|
||||
std::vector<NodePriority> priorities_,
|
||||
std::vector<bool> uncontracted_nodes_,
|
||||
std::vector<bool> contractable_,
|
||||
std::vector<EdgeWeight> weights_)
|
||||
: is_core(number_of_nodes, true), priorities(std::move(priorities_)),
|
||||
weights(std::move(weights_))
|
||||
: is_core(std::move(uncontracted_nodes_)), contractable(std::move(contractable_)),
|
||||
priorities(number_of_nodes), weights(std::move(weights_)), depths(number_of_nodes, 0)
|
||||
{
|
||||
// no cached priorities
|
||||
if (priorities.empty())
|
||||
if (contractable.empty())
|
||||
{
|
||||
depths.resize(number_of_nodes, 0);
|
||||
levels.resize(number_of_nodes);
|
||||
priorities.resize(number_of_nodes);
|
||||
contractable.resize(number_of_nodes, true);
|
||||
}
|
||||
if (is_core.empty())
|
||||
{
|
||||
is_core.resize(number_of_nodes, true);
|
||||
}
|
||||
}
|
||||
|
||||
void Renumber(const std::vector<NodeID> &old_to_new)
|
||||
{
|
||||
util::inplacePermutation(priorities.begin(), priorities.end(), old_to_new);
|
||||
util::inplacePermutation(weights.begin(), weights.end(), old_to_new);
|
||||
util::inplacePermutation(levels.begin(), levels.end(), old_to_new);
|
||||
util::inplacePermutation(is_core.begin(), is_core.end(), old_to_new);
|
||||
util::inplacePermutation(depths.begin(), depths.end(), old_to_new);
|
||||
tbb::parallel_invoke(
|
||||
[&] { util::inplacePermutation(priorities.begin(), priorities.end(), old_to_new); },
|
||||
[&] { util::inplacePermutation(weights.begin(), weights.end(), old_to_new); },
|
||||
[&] { util::inplacePermutation(is_core.begin(), is_core.end(), old_to_new); },
|
||||
[&] { util::inplacePermutation(contractable.begin(), contractable.end(), old_to_new); },
|
||||
[&] { util::inplacePermutation(depths.begin(), depths.end(), old_to_new); });
|
||||
}
|
||||
|
||||
std::vector<bool> is_core;
|
||||
std::vector<bool> contractable;
|
||||
std::vector<NodePriority> priorities;
|
||||
std::vector<EdgeWeight> weights;
|
||||
std::vector<NodeDepth> depths;
|
||||
std::vector<NodeLevel> levels;
|
||||
};
|
||||
|
||||
struct ContractionStats
|
||||
@@ -87,7 +91,8 @@ struct ContractionStats
|
||||
|
||||
struct RemainingNodeData
|
||||
{
|
||||
RemainingNodeData() : id(0), is_independent(false) {}
|
||||
RemainingNodeData() = default;
|
||||
RemainingNodeData(NodeID id, bool is_independent) : id(id), is_independent(is_independent) {}
|
||||
NodeID id : 31;
|
||||
bool is_independent : 1;
|
||||
};
|
||||
@@ -102,8 +107,7 @@ struct ThreadDataContainer
|
||||
auto &ref = data.local(exists);
|
||||
if (!exists)
|
||||
{
|
||||
// ref = std::make_shared<ContractorThreadData>(number_of_nodes);
|
||||
ref = std::make_shared<ContractorThreadData>(4000);
|
||||
ref = std::make_shared<ContractorThreadData>(number_of_nodes);
|
||||
}
|
||||
|
||||
return ref.get();
|
||||
@@ -130,14 +134,14 @@ inline bool Bias(const util::XORFastHash<> &fast_hash, const NodeID a, const Nod
|
||||
return a < b;
|
||||
}
|
||||
|
||||
template <bool RUNSIMULATION>
|
||||
template <bool RUNSIMULATION, typename ContractorGraph>
|
||||
void ContractNode(ContractorThreadData *data,
|
||||
const ContractorGraph &graph,
|
||||
const NodeID node,
|
||||
std::vector<EdgeWeight> &node_weights,
|
||||
ContractionStats *stats = nullptr)
|
||||
{
|
||||
auto &dijkstra = data->dijkstra;
|
||||
auto &heap = data->heap;
|
||||
std::size_t inserted_edges_size = data->inserted_edges.size();
|
||||
std::vector<ContractorEdge> &inserted_edges = data->inserted_edges;
|
||||
constexpr bool SHORTCUT_ARC = true;
|
||||
@@ -164,8 +168,8 @@ void ContractNode(ContractorThreadData *data,
|
||||
continue;
|
||||
}
|
||||
|
||||
dijkstra.Clear();
|
||||
dijkstra.Insert(source, 0, ContractorHeapData{});
|
||||
heap.Clear();
|
||||
heap.Insert(source, 0, ContractorHeapData{});
|
||||
EdgeWeight max_weight = 0;
|
||||
unsigned number_of_targets = 0;
|
||||
|
||||
@@ -230,9 +234,9 @@ void ContractNode(ContractorThreadData *data,
|
||||
continue;
|
||||
}
|
||||
max_weight = std::max(max_weight, path_weight);
|
||||
if (!dijkstra.WasInserted(target))
|
||||
if (!heap.WasInserted(target))
|
||||
{
|
||||
dijkstra.Insert(target, INVALID_EDGE_WEIGHT, ContractorHeapData{0, true});
|
||||
heap.Insert(target, INVALID_EDGE_WEIGHT, ContractorHeapData{0, true});
|
||||
++number_of_targets;
|
||||
}
|
||||
}
|
||||
@@ -240,12 +244,12 @@ void ContractNode(ContractorThreadData *data,
|
||||
if (RUNSIMULATION)
|
||||
{
|
||||
const int constexpr SIMULATION_SEARCH_SPACE_SIZE = 1000;
|
||||
dijkstra.Run(number_of_targets, SIMULATION_SEARCH_SPACE_SIZE, max_weight, node, graph);
|
||||
search(heap, graph, number_of_targets, SIMULATION_SEARCH_SPACE_SIZE, max_weight, node);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int constexpr FULL_SEARCH_SPACE_SIZE = 2000;
|
||||
dijkstra.Run(number_of_targets, FULL_SEARCH_SPACE_SIZE, max_weight, node, graph);
|
||||
search(heap, graph, number_of_targets, FULL_SEARCH_SPACE_SIZE, max_weight, node);
|
||||
}
|
||||
for (auto out_edge : graph.GetAdjacentEdgeRange(node))
|
||||
{
|
||||
@@ -259,7 +263,7 @@ void ContractNode(ContractorThreadData *data,
|
||||
continue;
|
||||
|
||||
const EdgeWeight path_weight = in_data.weight + out_data.weight;
|
||||
const EdgeWeight weight = dijkstra.GetKey(target);
|
||||
const EdgeWeight weight = heap.GetKey(target);
|
||||
if (path_weight < weight)
|
||||
{
|
||||
if (RUNSIMULATION)
|
||||
@@ -371,9 +375,9 @@ void RenumberGraph(ContractorGraph &graph, const std::vector<NodeID> &old_to_new
|
||||
|
||||
/* Reorder nodes for better locality during contraction */
|
||||
void RenumberData(std::vector<RemainingNodeData> &remaining_nodes,
|
||||
std::vector<NodeID> &new_to_old_node_id,
|
||||
ContractorNodeData &node_data,
|
||||
ContractorGraph &graph)
|
||||
std::vector<NodeID> &new_to_old_node_id,
|
||||
ContractorNodeData &node_data,
|
||||
ContractorGraph &graph)
|
||||
{
|
||||
std::vector<NodeID> current_to_new_node_id(graph.GetNumberOfNodes(), SPECIAL_NODEID);
|
||||
|
||||
@@ -407,7 +411,8 @@ void RenumberData(std::vector<RemainingNodeData> &remaining_nodes,
|
||||
RenumberGraph(graph, current_to_new_node_id);
|
||||
}
|
||||
|
||||
float EvaluateNodePriority(const ContractionStats &stats, const ContractorNodeData::NodeDepth node_depth)
|
||||
float EvaluateNodePriority(const ContractionStats &stats,
|
||||
const ContractorNodeData::NodeDepth node_depth)
|
||||
{
|
||||
// Result will contain the priority
|
||||
float result;
|
||||
@@ -476,14 +481,18 @@ bool UpdateNodeNeighbours(ContractorNodeData &node_data,
|
||||
// re-evaluate priorities of neighboring nodes
|
||||
for (const NodeID u : neighbours)
|
||||
{
|
||||
node_data.priorities[u] = EvaluateNodePriority(
|
||||
SimulateNodeContraction(data, graph, u, node_data.weights), node_data.depths[u]);
|
||||
if (node_data.contractable[u])
|
||||
{
|
||||
node_data.priorities[u] = EvaluateNodePriority(
|
||||
SimulateNodeContraction(data, graph, u, node_data.weights), node_data.depths[u]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsNodeIndependent(const util::XORFastHash<> &hash,
|
||||
const std::vector<float> &priorities,
|
||||
const std::vector<NodeID> &new_to_old_node_id,
|
||||
const ContractorGraph &graph,
|
||||
ContractorThreadData *const data,
|
||||
const NodeID node)
|
||||
@@ -509,7 +518,7 @@ bool IsNodeIndependent(const util::XORFastHash<> &hash,
|
||||
}
|
||||
// tie breaking
|
||||
if (std::abs(priority - target_priority) < std::numeric_limits<float>::epsilon() &&
|
||||
Bias(hash, node, target))
|
||||
Bias(hash, new_to_old_node_id[node], new_to_old_node_id[target]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -538,7 +547,7 @@ bool IsNodeIndependent(const util::XORFastHash<> &hash,
|
||||
}
|
||||
// tie breaking
|
||||
if (std::abs(priority - target_priority) < std::numeric_limits<float>::epsilon() &&
|
||||
Bias(hash, node, target))
|
||||
Bias(hash, new_to_old_node_id[node], new_to_old_node_id[target]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -548,15 +557,16 @@ bool IsNodeIndependent(const util::XORFastHash<> &hash,
|
||||
}
|
||||
}
|
||||
|
||||
LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
std::vector<float> cached_node_levels_,
|
||||
std::vector<EdgeWeight> node_weights_,
|
||||
double core_factor)
|
||||
std::vector<bool> contractGraph(ContractorGraph &graph,
|
||||
std::vector<bool> node_is_uncontracted_,
|
||||
std::vector<bool> node_is_contractable_,
|
||||
std::vector<EdgeWeight> node_weights_,
|
||||
double core_factor)
|
||||
{
|
||||
BOOST_ASSERT(node_weights_.size() == graph.GetNumberOfNodes());
|
||||
util::XORFastHash<> fast_hash;
|
||||
|
||||
// for the preperation we can use a big grain size, which is much faster (probably cache)
|
||||
const constexpr size_t InitGrainSize = 100000;
|
||||
const constexpr size_t PQGrainSize = 100000;
|
||||
// auto_partitioner will automatically increase the blocksize if we have
|
||||
// a lot of data. It is *important* for the last loop iterations
|
||||
@@ -575,56 +585,71 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
// Fill the map with an identiy mapping
|
||||
std::iota(new_to_old_node_id.begin(), new_to_old_node_id.end(), 0);
|
||||
|
||||
bool use_cached_node_priorities = !cached_node_levels_.empty();
|
||||
ContractorNodeData node_data{
|
||||
graph.GetNumberOfNodes(), std::move(cached_node_levels_), std::move(node_weights_)};
|
||||
ContractorNodeData node_data{graph.GetNumberOfNodes(),
|
||||
std::move(node_is_uncontracted_),
|
||||
std::move(node_is_contractable_),
|
||||
std::move(node_weights_)};
|
||||
|
||||
std::vector<RemainingNodeData> remaining_nodes(number_of_nodes);
|
||||
// initialize priorities in parallel
|
||||
tbb::parallel_for(tbb::blocked_range<NodeID>(0, number_of_nodes, InitGrainSize),
|
||||
[&remaining_nodes](const tbb::blocked_range<NodeID> &range) {
|
||||
for (auto x = range.begin(), end = range.end(); x != end; ++x)
|
||||
{
|
||||
remaining_nodes[x].id = x;
|
||||
}
|
||||
});
|
||||
std::vector<RemainingNodeData> remaining_nodes;
|
||||
remaining_nodes.reserve(number_of_nodes);
|
||||
for (auto node : util::irange<NodeID>(0, number_of_nodes))
|
||||
{
|
||||
if (node_data.is_core[node])
|
||||
{
|
||||
if (node_data.contractable[node])
|
||||
{
|
||||
remaining_nodes.emplace_back(node, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
node_data.priorities[node] =
|
||||
std::numeric_limits<ContractorNodeData::NodePriority>::max();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
node_data.priorities[node] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!use_cached_node_priorities)
|
||||
{
|
||||
util::UnbufferedLog log;
|
||||
log << "initializing node priorities...";
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(0, number_of_nodes, PQGrainSize),
|
||||
[&node_data, &graph, &thread_data_list](const tbb::blocked_range<NodeID> &range) {
|
||||
ContractorThreadData *data = thread_data_list.GetThreadData();
|
||||
for (auto x = range.begin(), end = range.end(); x != end; ++x)
|
||||
{
|
||||
node_data.priorities[x] = EvaluateNodePriority(
|
||||
SimulateNodeContraction(data, graph, x, node_data.weights),
|
||||
node_data.depths[x]);
|
||||
}
|
||||
});
|
||||
tbb::parallel_for(tbb::blocked_range<std::size_t>(0, remaining_nodes.size(), PQGrainSize),
|
||||
[&](const auto &range) {
|
||||
ContractorThreadData *data = thread_data_list.GetThreadData();
|
||||
for (auto x = range.begin(), end = range.end(); x != end; ++x)
|
||||
{
|
||||
auto node = remaining_nodes[x].id;
|
||||
BOOST_ASSERT(node_data.contractable[node]);
|
||||
node_data.priorities[node] = EvaluateNodePriority(
|
||||
SimulateNodeContraction(data, graph, node, node_data.weights),
|
||||
node_data.depths[node]);
|
||||
}
|
||||
});
|
||||
log << " ok.";
|
||||
}
|
||||
|
||||
util::Log() << "preprocessing " << number_of_nodes << " nodes...";
|
||||
auto number_of_core_nodes = std::max<std::size_t>(0, (1 - core_factor) * number_of_nodes);
|
||||
auto number_of_nodes_to_contract = remaining_nodes.size() - number_of_core_nodes;
|
||||
util::Log() << "preprocessing " << number_of_nodes_to_contract << " ("
|
||||
<< (number_of_nodes_to_contract / (float)number_of_nodes * 100.) << "%) nodes...";
|
||||
|
||||
util::UnbufferedLog log;
|
||||
util::Percent p(log, number_of_nodes);
|
||||
util::Percent p(log, remaining_nodes.size());
|
||||
|
||||
const util::XORFastHash<> hash;
|
||||
|
||||
unsigned current_level = 0;
|
||||
std::size_t next_renumbering = number_of_nodes * 0.65 * core_factor;
|
||||
while (remaining_nodes.size() > 1 &&
|
||||
number_of_contracted_nodes < static_cast<NodeID>(number_of_nodes * core_factor))
|
||||
std::size_t next_renumbering = number_of_nodes * 0.35;
|
||||
while (remaining_nodes.size() > number_of_core_nodes)
|
||||
{
|
||||
if (number_of_contracted_nodes > next_renumbering)
|
||||
if (remaining_nodes.size() < next_renumbering)
|
||||
{
|
||||
RenumberData(remaining_nodes, new_to_old_node_id, node_data, graph);
|
||||
log << "[renumbered]";
|
||||
// only one renumbering for now
|
||||
next_renumbering = number_of_nodes;
|
||||
next_renumbering = 0;
|
||||
}
|
||||
|
||||
tbb::parallel_for(
|
||||
@@ -635,8 +660,8 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
for (auto i = range.begin(), end = range.end(); i != end; ++i)
|
||||
{
|
||||
const NodeID node = remaining_nodes[i].id;
|
||||
remaining_nodes[i].is_independent =
|
||||
IsNodeIndependent(hash, node_data.priorities, graph, data, node);
|
||||
remaining_nodes[i].is_independent = IsNodeIndependent(
|
||||
hash, node_data.priorities, new_to_old_node_id, graph, data, node);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -649,21 +674,6 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
std::distance(remaining_nodes.begin(), begin_independent_nodes);
|
||||
auto end_independent_nodes_idx = remaining_nodes.size();
|
||||
|
||||
if (!use_cached_node_priorities)
|
||||
{
|
||||
// write out contraction level
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(
|
||||
begin_independent_nodes_idx, end_independent_nodes_idx, ContractGrainSize),
|
||||
[&](const auto &range) {
|
||||
for (auto position = range.begin(), end = range.end(); position != end;
|
||||
++position)
|
||||
{
|
||||
node_data.levels[remaining_nodes[position].id] = current_level;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// contract independent nodes
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(
|
||||
@@ -673,11 +683,17 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
for (auto position = range.begin(), end = range.end(); position != end; ++position)
|
||||
{
|
||||
const NodeID node = remaining_nodes[position].id;
|
||||
node_data.is_core[node] = false;
|
||||
ContractNode(data, graph, node, node_data.weights);
|
||||
}
|
||||
});
|
||||
|
||||
// core flags need to be set in serial since vector<bool> is not thread safe
|
||||
for (auto position :
|
||||
util::irange<std::size_t>(begin_independent_nodes_idx, end_independent_nodes_idx))
|
||||
{
|
||||
node_data.is_core[remaining_nodes[position].id] = false;
|
||||
}
|
||||
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(
|
||||
begin_independent_nodes_idx, end_independent_nodes_idx, DeleteGrainSize),
|
||||
@@ -691,12 +707,10 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
});
|
||||
|
||||
// make sure we really sort each block
|
||||
tbb::parallel_for(thread_data_list.data.range(),
|
||||
[&](const auto &range) {
|
||||
for (auto &data : range)
|
||||
tbb::parallel_sort(data->inserted_edges.begin(),
|
||||
data->inserted_edges.end());
|
||||
});
|
||||
tbb::parallel_for(thread_data_list.data.range(), [&](const auto &range) {
|
||||
for (auto &data : range)
|
||||
tbb::parallel_sort(data->inserted_edges.begin(), data->inserted_edges.end());
|
||||
});
|
||||
|
||||
// insert new edges
|
||||
for (auto &data : thread_data_list.data)
|
||||
@@ -706,7 +720,7 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
const EdgeID current_edge_ID = graph.FindEdge(edge.source, edge.target);
|
||||
if (current_edge_ID != SPECIAL_EDGEID)
|
||||
{
|
||||
ContractorGraph::EdgeData ¤t_data = graph.GetEdgeData(current_edge_ID);
|
||||
auto ¤t_data = graph.GetEdgeData(current_edge_ID);
|
||||
if (current_data.shortcut && edge.data.forward == current_data.forward &&
|
||||
edge.data.backward == current_data.backward)
|
||||
{
|
||||
@@ -724,22 +738,17 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
data->inserted_edges.clear();
|
||||
}
|
||||
|
||||
if (!use_cached_node_priorities)
|
||||
{
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(
|
||||
begin_independent_nodes_idx, end_independent_nodes_idx, NeighboursGrainSize),
|
||||
[&](
|
||||
const auto &range) {
|
||||
ContractorThreadData *data = thread_data_list.GetThreadData();
|
||||
for (auto position = range.begin(), end = range.end(); position != end;
|
||||
++position)
|
||||
{
|
||||
NodeID node = remaining_nodes[position].id;
|
||||
UpdateNodeNeighbours(node_data, data, graph, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<NodeID>(
|
||||
begin_independent_nodes_idx, end_independent_nodes_idx, NeighboursGrainSize),
|
||||
[&](const auto &range) {
|
||||
ContractorThreadData *data = thread_data_list.GetThreadData();
|
||||
for (auto position = range.begin(), end = range.end(); position != end; ++position)
|
||||
{
|
||||
NodeID node = remaining_nodes[position].id;
|
||||
UpdateNodeNeighbours(node_data, data, graph, node);
|
||||
}
|
||||
});
|
||||
|
||||
// remove contracted nodes from the pool
|
||||
BOOST_ASSERT(end_independent_nodes_idx - begin_independent_nodes_idx > 0);
|
||||
@@ -749,21 +758,11 @@ LevelAndCore contractGraph(ContractorGraph &graph,
|
||||
p.PrintStatus(number_of_contracted_nodes);
|
||||
++current_level;
|
||||
}
|
||||
log << "\n";
|
||||
|
||||
util::Log() << "[core] " << remaining_nodes.size() << " nodes " << graph.GetNumberOfEdges()
|
||||
<< " edges.";
|
||||
|
||||
node_data.Renumber(new_to_old_node_id);
|
||||
RenumberGraph(graph, new_to_old_node_id);
|
||||
|
||||
if (remaining_nodes.size() <= 2)
|
||||
{
|
||||
// in this case we don't need core markers since we fully contracted the graph
|
||||
node_data.is_core.clear();
|
||||
}
|
||||
|
||||
return LevelAndCore {std::move(node_data.levels), std::move(node_data.is_core)};
|
||||
return std::move(node_data.is_core);
|
||||
}
|
||||
|
||||
} // namespace contractor
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "updater/updater.hpp"
|
||||
|
||||
#include "util/exclude_flag.hpp"
|
||||
#include "util/log.hpp"
|
||||
#include "util/timing_util.hpp"
|
||||
|
||||
@@ -94,27 +95,6 @@ auto LoadAndUpdateEdgeExpandedGraph(const CustomizationConfig &config,
|
||||
return edge_based_graph;
|
||||
}
|
||||
|
||||
std::vector<std::vector<bool>>
|
||||
excludeFlagsToNodeFilter(const MultiLevelEdgeBasedGraph &graph,
|
||||
const extractor::EdgeBasedNodeDataContainer &node_data,
|
||||
const extractor::ProfileProperties &properties)
|
||||
{
|
||||
std::vector<std::vector<bool>> filters;
|
||||
for (auto mask : properties.excludable_classes)
|
||||
{
|
||||
if (mask != extractor::INAVLID_CLASS_DATA)
|
||||
{
|
||||
std::vector<bool> allowed_nodes(graph.GetNumberOfNodes(), true);
|
||||
for (const auto node : util::irange<NodeID>(0, graph.GetNumberOfNodes()))
|
||||
{
|
||||
allowed_nodes[node] = (node_data.GetClassData(node) & mask) == 0;
|
||||
}
|
||||
filters.push_back(std::move(allowed_nodes));
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
std::vector<CellMetric> customizeFilteredMetrics(const MultiLevelEdgeBasedGraph &graph,
|
||||
const partition::CellStorage &storage,
|
||||
const CellCustomizer &customizer,
|
||||
@@ -157,7 +137,7 @@ int Customizer::Run(const CustomizationConfig &config)
|
||||
util::Log() << "Loading partition data took " << TIMER_SEC(loading_data) << " seconds";
|
||||
|
||||
TIMER_START(cell_customize);
|
||||
auto filter = excludeFlagsToNodeFilter(graph, node_data, properties);
|
||||
auto filter = util::excludeFlagsToNodeFilter(graph.GetNumberOfNodes(), node_data, properties);
|
||||
auto metrics = customizeFilteredMetrics(graph, storage, CellCustomizer{mlp}, filter);
|
||||
TIMER_STOP(cell_customize);
|
||||
util::Log() << "Cells customization took " << TIMER_SEC(cell_customize) << " seconds";
|
||||
|
||||
@@ -16,15 +16,15 @@ BisectionGraphView::BisectionGraphView(const BisectionGraph &bisection_graph_)
|
||||
}
|
||||
|
||||
BisectionGraphView::BisectionGraphView(const BisectionGraph &bisection_graph_,
|
||||
const BisectionGraph::ConstNodeIterator begin_,
|
||||
const BisectionGraph::ConstNodeIterator end_)
|
||||
const BisectionGraph::ConstNodeIterator begin_,
|
||||
const BisectionGraph::ConstNodeIterator end_)
|
||||
: bisection_graph(bisection_graph_), begin(begin_), end(end_)
|
||||
{
|
||||
}
|
||||
|
||||
BisectionGraphView::BisectionGraphView(const BisectionGraphView &other_view,
|
||||
const BisectionGraph::ConstNodeIterator begin_,
|
||||
const BisectionGraph::ConstNodeIterator end_)
|
||||
const BisectionGraph::ConstNodeIterator begin_,
|
||||
const BisectionGraph::ConstNodeIterator end_)
|
||||
: BisectionGraphView(other_view.bisection_graph, begin_, end_)
|
||||
{
|
||||
}
|
||||
@@ -42,7 +42,10 @@ BisectionGraph::ConstNodeIterator BisectionGraphView::Begin() const { return beg
|
||||
|
||||
BisectionGraph::ConstNodeIterator BisectionGraphView::End() const { return end; }
|
||||
|
||||
const BisectionGraphView::NodeT &BisectionGraphView::Node(const NodeID nid) const { return *(begin + nid); }
|
||||
const BisectionGraphView::NodeT &BisectionGraphView::Node(const NodeID nid) const
|
||||
{
|
||||
return *(begin + nid);
|
||||
}
|
||||
|
||||
const BisectionGraphView::EdgeT &BisectionGraphView::Edge(const EdgeID eid) const
|
||||
{
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace
|
||||
|
||||
const auto constexpr INVALID_LEVEL = std::numeric_limits<DinicMaxFlow::Level>::max();
|
||||
|
||||
auto makeHasNeighborNotInCheck(const DinicMaxFlow::SourceSinkNodes &set, const BisectionGraphView &view)
|
||||
auto makeHasNeighborNotInCheck(const DinicMaxFlow::SourceSinkNodes &set,
|
||||
const BisectionGraphView &view)
|
||||
{
|
||||
return [&](const NodeID nid) {
|
||||
const auto is_not_contained = [&set](const BisectionEdge &edge) {
|
||||
|
||||
@@ -33,7 +33,8 @@ struct SpatialOrder
|
||||
|
||||
// Creates a spatial order of n * sources "first" and n * sink "last" node ids.
|
||||
// The slope determines the spatial order for sorting node coordinates.
|
||||
SpatialOrder makeSpatialOrder(const BisectionGraphView &view, const double ratio, const double slope)
|
||||
SpatialOrder
|
||||
makeSpatialOrder(const BisectionGraphView &view, const double ratio, const double slope)
|
||||
{
|
||||
struct NodeWithCoordinate
|
||||
{
|
||||
@@ -89,8 +90,10 @@ SpatialOrder makeSpatialOrder(const BisectionGraphView &view, const double ratio
|
||||
}
|
||||
|
||||
// Makes n cuts with different spatial orders and returns the best.
|
||||
DinicMaxFlow::MinCut
|
||||
bestMinCut(const BisectionGraphView &view, const std::size_t n, const double ratio, const double balance)
|
||||
DinicMaxFlow::MinCut bestMinCut(const BisectionGraphView &view,
|
||||
const std::size_t n,
|
||||
const double ratio,
|
||||
const double balance)
|
||||
{
|
||||
DinicMaxFlow::MinCut best;
|
||||
best.num_edges = -1;
|
||||
|
||||
@@ -149,7 +149,8 @@ RecursiveBisectionState::PrePartitionWithSCC(const std::size_t small_component_s
|
||||
}();
|
||||
|
||||
if (!has_small_component)
|
||||
views.push_back(BisectionGraphView(bisection_graph, bisection_graph.CEnd(), bisection_graph.CEnd()));
|
||||
views.push_back(
|
||||
BisectionGraphView(bisection_graph, bisection_graph.CEnd(), bisection_graph.CEnd()));
|
||||
|
||||
// apply scc as bisections, we need scc_level bits for this with scc_levels =
|
||||
// ceil(log_2(components))
|
||||
|
||||
+71
-8
@@ -268,12 +268,30 @@ void Storage::PopulateLayout(DataLayout &layout)
|
||||
reader.Skip<std::uint32_t>(1); // checksum
|
||||
auto num_nodes = reader.ReadVectorSize<contractor::QueryGraph::NodeArrayEntry>();
|
||||
auto num_edges = reader.ReadVectorSize<contractor::QueryGraph::EdgeArrayEntry>();
|
||||
auto num_metrics = reader.ReadElementCount64();
|
||||
|
||||
if (num_metrics > NUM_METRICS)
|
||||
{
|
||||
throw util::exception("Only " + std::to_string(NUM_METRICS) +
|
||||
" metrics are supported at the same time.");
|
||||
}
|
||||
|
||||
layout.SetBlockSize<unsigned>(DataLayout::HSGR_CHECKSUM, 1);
|
||||
layout.SetBlockSize<contractor::QueryGraph::NodeArrayEntry>(DataLayout::CH_GRAPH_NODE_LIST,
|
||||
num_nodes);
|
||||
layout.SetBlockSize<contractor::QueryGraph::EdgeArrayEntry>(DataLayout::CH_GRAPH_EDGE_LIST,
|
||||
num_edges);
|
||||
|
||||
for (const auto index : util::irange<std::size_t>(0, num_metrics))
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(
|
||||
static_cast<DataLayout::BlockID>(DataLayout::CH_EDGE_FILTER_0 + index), num_edges);
|
||||
}
|
||||
for (const auto index : util::irange<std::size_t>(num_metrics, NUM_METRICS))
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(
|
||||
static_cast<DataLayout::BlockID>(DataLayout::CH_EDGE_FILTER_0 + index), 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -282,6 +300,11 @@ void Storage::PopulateLayout(DataLayout &layout)
|
||||
0);
|
||||
layout.SetBlockSize<contractor::QueryGraph::EdgeArrayEntry>(DataLayout::CH_GRAPH_EDGE_LIST,
|
||||
0);
|
||||
for (const auto index : util::irange<std::size_t>(0, NUM_METRICS))
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(
|
||||
static_cast<DataLayout::BlockID>(DataLayout::CH_EDGE_FILTER_0 + index), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// load rsearch tree size
|
||||
@@ -313,12 +336,36 @@ void Storage::PopulateLayout(DataLayout &layout)
|
||||
{
|
||||
io::FileReader core_marker_file(config.GetPath(".osrm.core"),
|
||||
io::FileReader::VerifyFingerprint);
|
||||
const auto num_metrics = core_marker_file.ReadElementCount64();
|
||||
if (num_metrics > NUM_METRICS)
|
||||
{
|
||||
throw util::exception("Only " + std::to_string(NUM_METRICS) +
|
||||
" metrics are supported at the same time.");
|
||||
}
|
||||
|
||||
const auto number_of_core_markers = core_marker_file.ReadElementCount64();
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER, number_of_core_markers);
|
||||
for (const auto index : util::irange<std::size_t>(0, num_metrics))
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(
|
||||
static_cast<DataLayout::BlockID>(DataLayout::CH_CORE_MARKER_0 + index),
|
||||
number_of_core_markers);
|
||||
}
|
||||
for (const auto index : util::irange<std::size_t>(num_metrics, NUM_METRICS))
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(
|
||||
static_cast<DataLayout::BlockID>(DataLayout::CH_CORE_MARKER_0 + index), 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_0, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_1, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_2, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_3, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_4, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_5, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_6, 0);
|
||||
layout.SetBlockSize<unsigned>(DataLayout::CH_CORE_MARKER_7, 0);
|
||||
}
|
||||
|
||||
// load turn weight penalties
|
||||
@@ -577,8 +624,19 @@ void Storage::PopulateData(const DataLayout &layout, char *memory_ptr)
|
||||
util::vector_view<contractor::QueryGraphView::EdgeArrayEntry> edge_list(
|
||||
graph_edges_ptr, layout.num_entries[storage::DataLayout::CH_GRAPH_EDGE_LIST]);
|
||||
|
||||
std::vector<util::vector_view<bool>> edge_filter;
|
||||
for (auto index : util::irange<std::size_t>(0, NUM_METRICS))
|
||||
{
|
||||
auto block_id =
|
||||
static_cast<DataLayout::BlockID>(storage::DataLayout::CH_EDGE_FILTER_0 + index);
|
||||
auto data_ptr = layout.GetBlockPtr<unsigned, true>(memory_ptr, block_id);
|
||||
auto num_entries = layout.num_entries[block_id];
|
||||
edge_filter.emplace_back(data_ptr, num_entries);
|
||||
}
|
||||
|
||||
contractor::QueryGraphView graph_view(std::move(node_list), std::move(edge_list));
|
||||
contractor::files::readGraph(config.GetPath(".osrm.hsgr"), *checksum, graph_view);
|
||||
contractor::files::readGraph(
|
||||
config.GetPath(".osrm.hsgr"), *checksum, graph_view, edge_filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -872,12 +930,17 @@ void Storage::PopulateData(const DataLayout &layout, char *memory_ptr)
|
||||
|
||||
if (boost::filesystem::exists(config.GetPath(".osrm.core")))
|
||||
{
|
||||
auto core_marker_ptr =
|
||||
layout.GetBlockPtr<unsigned, true>(memory_ptr, storage::DataLayout::CH_CORE_MARKER);
|
||||
util::vector_view<bool> is_core_node(
|
||||
core_marker_ptr, layout.num_entries[storage::DataLayout::CH_CORE_MARKER]);
|
||||
std::vector<util::vector_view<bool>> cores;
|
||||
for (auto index : util::irange<std::size_t>(0, NUM_METRICS))
|
||||
{
|
||||
auto block_id =
|
||||
static_cast<DataLayout::BlockID>(storage::DataLayout::CH_CORE_MARKER_0 + index);
|
||||
auto data_ptr = layout.GetBlockPtr<unsigned, true>(memory_ptr, block_id);
|
||||
auto num_entries = layout.num_entries[block_id];
|
||||
cores.emplace_back(data_ptr, num_entries);
|
||||
}
|
||||
|
||||
contractor::files::readCoreMarker(config.GetPath(".osrm.core"), is_core_node);
|
||||
contractor::files::readCoreMarker(config.GetPath(".osrm.core"), cores);
|
||||
}
|
||||
|
||||
// load profile properties
|
||||
|
||||
Reference in New Issue
Block a user