blob: a2b8c13622309a14f11ff6ea070f601043ca3e45 [file] [log] [blame]
Thomas Vachuska4f1a60c2014-10-28 13:39:07 -07001/*
2 * Copyright 2014 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
alshabib339a3d92014-09-26 17:54:32 -070016package org.onlab.onos.store.topology.impl;
17
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080018import com.google.common.base.Supplier;
19import com.google.common.base.Suppliers;
alshabib339a3d92014-09-26 17:54:32 -070020import com.google.common.collect.ImmutableMap;
21import com.google.common.collect.ImmutableSet;
22import com.google.common.collect.ImmutableSetMultimap;
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080023
alshabib339a3d92014-09-26 17:54:32 -070024import org.onlab.graph.DijkstraGraphSearch;
25import org.onlab.graph.GraphPathSearch;
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080026import org.onlab.graph.GraphPathSearch.Result;
alshabib339a3d92014-09-26 17:54:32 -070027import org.onlab.graph.TarjanGraphSearch;
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080028import org.onlab.graph.TarjanGraphSearch.SCCResult;
alshabib339a3d92014-09-26 17:54:32 -070029import org.onlab.onos.net.AbstractModel;
30import org.onlab.onos.net.ConnectPoint;
31import org.onlab.onos.net.DefaultPath;
32import org.onlab.onos.net.DeviceId;
33import org.onlab.onos.net.Link;
34import org.onlab.onos.net.Path;
35import org.onlab.onos.net.provider.ProviderId;
36import org.onlab.onos.net.topology.ClusterId;
37import org.onlab.onos.net.topology.DefaultTopologyCluster;
38import org.onlab.onos.net.topology.DefaultTopologyVertex;
39import org.onlab.onos.net.topology.GraphDescription;
40import org.onlab.onos.net.topology.LinkWeight;
41import org.onlab.onos.net.topology.Topology;
42import org.onlab.onos.net.topology.TopologyCluster;
43import org.onlab.onos.net.topology.TopologyEdge;
44import org.onlab.onos.net.topology.TopologyGraph;
45import org.onlab.onos.net.topology.TopologyVertex;
46
47import java.util.ArrayList;
48import java.util.List;
49import java.util.Map;
50import java.util.Set;
51
52import static com.google.common.base.MoreObjects.toStringHelper;
53import static com.google.common.collect.ImmutableSetMultimap.Builder;
Thomas Vachuska6acd3bb2014-11-09 23:44:22 -080054import static org.onlab.onos.core.CoreService.CORE_PROVIDER_ID;
Thomas Vachuska57126fe2014-11-11 17:13:24 -080055import static org.onlab.onos.net.Link.State.ACTIVE;
56import static org.onlab.onos.net.Link.State.INACTIVE;
alshabib339a3d92014-09-26 17:54:32 -070057import static org.onlab.onos.net.Link.Type.INDIRECT;
58
59/**
60 * Default implementation of the topology descriptor. This carries the
61 * backing topology data.
62 */
63public class DefaultTopology extends AbstractModel implements Topology {
64
65 private static final DijkstraGraphSearch<TopologyVertex, TopologyEdge> DIJKSTRA =
66 new DijkstraGraphSearch<>();
67 private static final TarjanGraphSearch<TopologyVertex, TopologyEdge> TARJAN =
68 new TarjanGraphSearch<>();
69
alshabib339a3d92014-09-26 17:54:32 -070070 private final long time;
Thomas Vachuska6b7920d2014-11-25 19:48:39 -080071 private final long computeCost;
alshabib339a3d92014-09-26 17:54:32 -070072 private final TopologyGraph graph;
73
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080074 private final Supplier<SCCResult<TopologyVertex, TopologyEdge>> clusterResults;
75 private final Supplier<ImmutableMap<DeviceId, Result<TopologyVertex, TopologyEdge>>> results;
76 private final Supplier<ImmutableSetMultimap<PathKey, Path>> paths;
alshabib339a3d92014-09-26 17:54:32 -070077
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080078 private final Supplier<ImmutableMap<ClusterId, TopologyCluster>> clusters;
79 private final Supplier<ImmutableSet<ConnectPoint>> infrastructurePoints;
80 private final Supplier<ImmutableSetMultimap<ClusterId, ConnectPoint>> broadcastSets;
alshabib339a3d92014-09-26 17:54:32 -070081
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080082 private final Supplier<ClusterIndexes> clusterIndexes;
alshabib339a3d92014-09-26 17:54:32 -070083
84 /**
85 * Creates a topology descriptor attributed to the specified provider.
86 *
87 * @param providerId identity of the provider
88 * @param description data describing the new topology
89 */
90 DefaultTopology(ProviderId providerId, GraphDescription description) {
91 super(providerId);
92 this.time = description.timestamp();
93
94 // Build the graph
95 this.graph = new DefaultTopologyGraph(description.vertexes(),
96 description.edges());
97
alshabib339a3d92014-09-26 17:54:32 -070098
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -080099 this.results = Suppliers.memoize(() -> searchForShortestPaths());
100 this.paths = Suppliers.memoize(() -> buildPaths());
alshabib339a3d92014-09-26 17:54:32 -0700101
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800102 this.clusterResults = Suppliers.memoize(() -> searchForClusters());
103 this.clusters = Suppliers.memoize(() -> buildTopologyClusters());
alshabib339a3d92014-09-26 17:54:32 -0700104
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800105 this.clusterIndexes = Suppliers.memoize(() -> buildIndexes());
106
107 this.broadcastSets = Suppliers.memoize(() -> buildBroadcastSets());
108 this.infrastructurePoints = Suppliers.memoize(() -> findInfrastructurePoints());
Thomas Vachuska6b7920d2014-11-25 19:48:39 -0800109 this.computeCost = Math.max(0, System.nanoTime() - time);
alshabib339a3d92014-09-26 17:54:32 -0700110 }
111
112 @Override
113 public long time() {
114 return time;
115 }
116
117 @Override
Thomas Vachuska6b7920d2014-11-25 19:48:39 -0800118 public long computeCost() {
119 return computeCost;
120 }
121
122 @Override
alshabib339a3d92014-09-26 17:54:32 -0700123 public int clusterCount() {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800124 return clusters.get().size();
alshabib339a3d92014-09-26 17:54:32 -0700125 }
126
127 @Override
128 public int deviceCount() {
129 return graph.getVertexes().size();
130 }
131
132 @Override
133 public int linkCount() {
134 return graph.getEdges().size();
135 }
136
137 @Override
138 public int pathCount() {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800139 return paths.get().size();
140 }
141
142 private ImmutableMap<DeviceId, TopologyCluster> clustersByDevice() {
143 return clusterIndexes.get().clustersByDevice;
144 }
145
146 private ImmutableSetMultimap<TopologyCluster, DeviceId> devicesByCluster() {
147 return clusterIndexes.get().devicesByCluster;
148 }
149
150 private ImmutableSetMultimap<TopologyCluster, Link> linksByCluster() {
151 return clusterIndexes.get().linksByCluster;
alshabib339a3d92014-09-26 17:54:32 -0700152 }
153
154 /**
155 * Returns the backing topology graph.
156 *
157 * @return topology graph
158 */
159 TopologyGraph getGraph() {
160 return graph;
161 }
162
163 /**
164 * Returns the set of topology clusters.
165 *
166 * @return set of clusters
167 */
168 Set<TopologyCluster> getClusters() {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800169 return ImmutableSet.copyOf(clusters.get().values());
alshabib339a3d92014-09-26 17:54:32 -0700170 }
171
172 /**
173 * Returns the specified topology cluster.
174 *
175 * @param clusterId cluster identifier
176 * @return topology cluster
177 */
178 TopologyCluster getCluster(ClusterId clusterId) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800179 return clusters.get().get(clusterId);
alshabib339a3d92014-09-26 17:54:32 -0700180 }
181
182 /**
183 * Returns the topology cluster that contains the given device.
184 *
185 * @param deviceId device identifier
186 * @return topology cluster
187 */
188 TopologyCluster getCluster(DeviceId deviceId) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800189 return clustersByDevice().get(deviceId);
alshabib339a3d92014-09-26 17:54:32 -0700190 }
191
192 /**
193 * Returns the set of cluster devices.
194 *
195 * @param cluster topology cluster
196 * @return cluster devices
197 */
198 Set<DeviceId> getClusterDevices(TopologyCluster cluster) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800199 return devicesByCluster().get(cluster);
alshabib339a3d92014-09-26 17:54:32 -0700200 }
201
202 /**
203 * Returns the set of cluster links.
204 *
205 * @param cluster topology cluster
206 * @return cluster links
207 */
208 Set<Link> getClusterLinks(TopologyCluster cluster) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800209 return linksByCluster().get(cluster);
alshabib339a3d92014-09-26 17:54:32 -0700210 }
211
212 /**
213 * Indicates whether the given point is an infrastructure link end-point.
214 *
215 * @param connectPoint connection point
216 * @return true if infrastructure
217 */
218 boolean isInfrastructure(ConnectPoint connectPoint) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800219 return infrastructurePoints.get().contains(connectPoint);
alshabib339a3d92014-09-26 17:54:32 -0700220 }
221
222 /**
223 * Indicates whether the given point is part of a broadcast set.
224 *
225 * @param connectPoint connection point
226 * @return true if in broadcast set
227 */
228 boolean isBroadcastPoint(ConnectPoint connectPoint) {
229 // Any non-infrastructure, i.e. edge points are assumed to be OK.
230 if (!isInfrastructure(connectPoint)) {
231 return true;
232 }
233
234 // Find the cluster to which the device belongs.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800235 TopologyCluster cluster = clustersByDevice().get(connectPoint.deviceId());
alshabib339a3d92014-09-26 17:54:32 -0700236 if (cluster == null) {
237 throw new IllegalArgumentException("No cluster found for device " + connectPoint.deviceId());
238 }
239
240 // If the broadcast set is null or empty, or if the point explicitly
241 // belongs to it, return true;
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800242 Set<ConnectPoint> points = broadcastSets.get().get(cluster.id());
alshabib339a3d92014-09-26 17:54:32 -0700243 return points == null || points.isEmpty() || points.contains(connectPoint);
244 }
245
246 /**
247 * Returns the size of the cluster broadcast set.
248 *
249 * @param clusterId cluster identifier
250 * @return size of the cluster broadcast set
251 */
252 int broadcastSetSize(ClusterId clusterId) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800253 return broadcastSets.get().get(clusterId).size();
alshabib339a3d92014-09-26 17:54:32 -0700254 }
255
256 /**
257 * Returns the set of pre-computed shortest paths between source and
258 * destination devices.
259 *
260 * @param src source device
261 * @param dst destination device
262 * @return set of shortest paths
263 */
264 Set<Path> getPaths(DeviceId src, DeviceId dst) {
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800265 return paths.get().get(new PathKey(src, dst));
alshabib339a3d92014-09-26 17:54:32 -0700266 }
267
268 /**
269 * Computes on-demand the set of shortest paths between source and
270 * destination devices.
271 *
Thomas Vachuskab14c77a2014-11-04 18:08:01 -0800272 * @param src source device
273 * @param dst destination device
274 * @param weight link weight function
alshabib339a3d92014-09-26 17:54:32 -0700275 * @return set of shortest paths
276 */
277 Set<Path> getPaths(DeviceId src, DeviceId dst, LinkWeight weight) {
Yuta HIGUCHI82e53262014-11-27 10:28:51 -0800278 final DefaultTopologyVertex srcV = new DefaultTopologyVertex(src);
279 final DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst);
280 Set<TopologyVertex> vertices = graph.getVertexes();
281 if (!vertices.contains(srcV) || !vertices.contains(dstV)) {
282 // src or dst not part of the current graph
283 return ImmutableSet.of();
284 }
285
alshabib339a3d92014-09-26 17:54:32 -0700286 GraphPathSearch.Result<TopologyVertex, TopologyEdge> result =
Yuta HIGUCHI82e53262014-11-27 10:28:51 -0800287 DIJKSTRA.search(graph, srcV, dstV, weight);
alshabib339a3d92014-09-26 17:54:32 -0700288 ImmutableSet.Builder<Path> builder = ImmutableSet.builder();
289 for (org.onlab.graph.Path<TopologyVertex, TopologyEdge> path : result.paths()) {
290 builder.add(networkPath(path));
291 }
292 return builder.build();
293 }
294
295
296 // Searches the graph for all shortest paths and returns the search results.
297 private ImmutableMap<DeviceId, Result<TopologyVertex, TopologyEdge>> searchForShortestPaths() {
298 ImmutableMap.Builder<DeviceId, Result<TopologyVertex, TopologyEdge>> builder = ImmutableMap.builder();
299
300 // Search graph paths for each source to all destinations.
301 LinkWeight weight = new HopCountLinkWeight(graph.getVertexes().size());
302 for (TopologyVertex src : graph.getVertexes()) {
303 builder.put(src.deviceId(), DIJKSTRA.search(graph, src, null, weight));
304 }
305 return builder.build();
306 }
307
308 // Builds network paths from the graph path search results
309 private ImmutableSetMultimap<PathKey, Path> buildPaths() {
310 Builder<PathKey, Path> builder = ImmutableSetMultimap.builder();
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800311 for (DeviceId deviceId : results.get().keySet()) {
312 Result<TopologyVertex, TopologyEdge> result = results.get().get(deviceId);
alshabib339a3d92014-09-26 17:54:32 -0700313 for (org.onlab.graph.Path<TopologyVertex, TopologyEdge> path : result.paths()) {
314 builder.put(new PathKey(path.src().deviceId(), path.dst().deviceId()),
315 networkPath(path));
316 }
317 }
318 return builder.build();
319 }
320
321 // Converts graph path to a network path with the same cost.
322 private Path networkPath(org.onlab.graph.Path<TopologyVertex, TopologyEdge> path) {
323 List<Link> links = new ArrayList<>();
324 for (TopologyEdge edge : path.edges()) {
325 links.add(edge.link());
326 }
Thomas Vachuska6acd3bb2014-11-09 23:44:22 -0800327 return new DefaultPath(CORE_PROVIDER_ID, links, path.cost());
alshabib339a3d92014-09-26 17:54:32 -0700328 }
329
330
331 // Searches for SCC clusters in the network topology graph using Tarjan
332 // algorithm.
333 private SCCResult<TopologyVertex, TopologyEdge> searchForClusters() {
334 return TARJAN.search(graph, new NoIndirectLinksWeight());
335 }
336
337 // Builds the topology clusters and returns the id-cluster bindings.
338 private ImmutableMap<ClusterId, TopologyCluster> buildTopologyClusters() {
339 ImmutableMap.Builder<ClusterId, TopologyCluster> clusterBuilder = ImmutableMap.builder();
340 SCCResult<TopologyVertex, TopologyEdge> result =
341 TARJAN.search(graph, new NoIndirectLinksWeight());
342
343 // Extract both vertexes and edges from the results; the lists form
344 // pairs along the same index.
345 List<Set<TopologyVertex>> clusterVertexes = result.clusterVertexes();
346 List<Set<TopologyEdge>> clusterEdges = result.clusterEdges();
347
348 // Scan over the lists and create a cluster from the results.
349 for (int i = 0, n = result.clusterCount(); i < n; i++) {
350 Set<TopologyVertex> vertexSet = clusterVertexes.get(i);
351 Set<TopologyEdge> edgeSet = clusterEdges.get(i);
352
353 ClusterId cid = ClusterId.clusterId(i);
354 DefaultTopologyCluster cluster =
355 new DefaultTopologyCluster(cid, vertexSet.size(), edgeSet.size(),
356 findRoot(vertexSet).deviceId());
357 clusterBuilder.put(cid, cluster);
358 }
359 return clusterBuilder.build();
360 }
361
362 // Finds the vertex whose device id is the lexicographical minimum in the
363 // specified set.
364 private TopologyVertex findRoot(Set<TopologyVertex> vertexSet) {
365 TopologyVertex minVertex = null;
366 for (TopologyVertex vertex : vertexSet) {
367 if (minVertex == null ||
368 minVertex.deviceId().toString()
369 .compareTo(minVertex.deviceId().toString()) < 0) {
370 minVertex = vertex;
371 }
372 }
373 return minVertex;
374 }
375
376 // Processes a map of broadcast sets for each cluster.
377 private ImmutableSetMultimap<ClusterId, ConnectPoint> buildBroadcastSets() {
378 Builder<ClusterId, ConnectPoint> builder = ImmutableSetMultimap.builder();
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800379 for (TopologyCluster cluster : clusters.get().values()) {
alshabib339a3d92014-09-26 17:54:32 -0700380 addClusterBroadcastSet(cluster, builder);
381 }
382 return builder.build();
383 }
384
385 // Finds all broadcast points for the cluster. These are those connection
386 // points which lie along the shortest paths between the cluster root and
387 // all other devices within the cluster.
388 private void addClusterBroadcastSet(TopologyCluster cluster,
389 Builder<ClusterId, ConnectPoint> builder) {
390 // Use the graph root search results to build the broadcast set.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800391 Result<TopologyVertex, TopologyEdge> result = results.get().get(cluster.root());
alshabib339a3d92014-09-26 17:54:32 -0700392 for (Map.Entry<TopologyVertex, Set<TopologyEdge>> entry : result.parents().entrySet()) {
393 TopologyVertex vertex = entry.getKey();
394
395 // Ignore any parents that lead outside the cluster.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800396 if (clustersByDevice().get(vertex.deviceId()) != cluster) {
alshabib339a3d92014-09-26 17:54:32 -0700397 continue;
398 }
399
400 // Ignore any back-link sets that are empty.
401 Set<TopologyEdge> parents = entry.getValue();
402 if (parents.isEmpty()) {
403 continue;
404 }
405
406 // Use the first back-link source and destinations to add to the
407 // broadcast set.
408 Link link = parents.iterator().next().link();
409 builder.put(cluster.id(), link.src());
410 builder.put(cluster.id(), link.dst());
411 }
412 }
413
414 // Collects and returns an set of all infrastructure link end-points.
415 private ImmutableSet<ConnectPoint> findInfrastructurePoints() {
416 ImmutableSet.Builder<ConnectPoint> builder = ImmutableSet.builder();
417 for (TopologyEdge edge : graph.getEdges()) {
418 builder.add(edge.link().src());
419 builder.add(edge.link().dst());
420 }
421 return builder.build();
422 }
423
424 // Builds cluster-devices, cluster-links and device-cluster indexes.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800425 private ClusterIndexes buildIndexes() {
alshabib339a3d92014-09-26 17:54:32 -0700426 // Prepare the index builders
427 ImmutableMap.Builder<DeviceId, TopologyCluster> clusterBuilder = ImmutableMap.builder();
428 ImmutableSetMultimap.Builder<TopologyCluster, DeviceId> devicesBuilder = ImmutableSetMultimap.builder();
429 ImmutableSetMultimap.Builder<TopologyCluster, Link> linksBuilder = ImmutableSetMultimap.builder();
430
431 // Now scan through all the clusters
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800432 for (TopologyCluster cluster : clusters.get().values()) {
alshabib339a3d92014-09-26 17:54:32 -0700433 int i = cluster.id().index();
434
435 // Scan through all the cluster vertexes.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800436 for (TopologyVertex vertex : clusterResults.get().clusterVertexes().get(i)) {
alshabib339a3d92014-09-26 17:54:32 -0700437 devicesBuilder.put(cluster, vertex.deviceId());
438 clusterBuilder.put(vertex.deviceId(), cluster);
439 }
440
441 // Scan through all the cluster edges.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800442 for (TopologyEdge edge : clusterResults.get().clusterEdges().get(i)) {
alshabib339a3d92014-09-26 17:54:32 -0700443 linksBuilder.put(cluster, edge.link());
444 }
445 }
446
447 // Finalize all indexes.
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800448 return new ClusterIndexes(clusterBuilder.build(),
449 devicesBuilder.build(),
450 linksBuilder.build());
alshabib339a3d92014-09-26 17:54:32 -0700451 }
452
453 // Link weight for measuring link cost as hop count with indirect links
454 // being as expensive as traversing the entire graph to assume the worst.
455 private static class HopCountLinkWeight implements LinkWeight {
456 private final int indirectLinkCost;
457
458 HopCountLinkWeight(int indirectLinkCost) {
459 this.indirectLinkCost = indirectLinkCost;
460 }
461
462 @Override
463 public double weight(TopologyEdge edge) {
464 // To force preference to use direct paths first, make indirect
465 // links as expensive as the linear vertex traversal.
Thomas Vachuska57126fe2014-11-11 17:13:24 -0800466 return edge.link().state() == ACTIVE ?
467 (edge.link().type() == INDIRECT ? indirectLinkCost : 1) : -1;
alshabib339a3d92014-09-26 17:54:32 -0700468 }
469 }
470
471 // Link weight for preventing traversal over indirect links.
472 private static class NoIndirectLinksWeight implements LinkWeight {
473 @Override
474 public double weight(TopologyEdge edge) {
Thomas Vachuska57126fe2014-11-11 17:13:24 -0800475 return edge.link().state() == INACTIVE || edge.link().type() == INDIRECT ? -1 : 1;
alshabib339a3d92014-09-26 17:54:32 -0700476 }
477 }
478
Yuta HIGUCHIb2e74a02014-11-30 13:54:38 -0800479 static final class ClusterIndexes {
480 final ImmutableMap<DeviceId, TopologyCluster> clustersByDevice;
481 final ImmutableSetMultimap<TopologyCluster, DeviceId> devicesByCluster;
482 final ImmutableSetMultimap<TopologyCluster, Link> linksByCluster;
483
484 public ClusterIndexes(ImmutableMap<DeviceId, TopologyCluster> clustersByDevice,
485 ImmutableSetMultimap<TopologyCluster, DeviceId> devicesByCluster,
486 ImmutableSetMultimap<TopologyCluster, Link> linksByCluster) {
487 this.clustersByDevice = clustersByDevice;
488 this.devicesByCluster = devicesByCluster;
489 this.linksByCluster = linksByCluster;
490 }
491 }
492
alshabib339a3d92014-09-26 17:54:32 -0700493 @Override
494 public String toString() {
495 return toStringHelper(this)
496 .add("time", time)
Thomas Vachuska6b7920d2014-11-25 19:48:39 -0800497 .add("computeCost", computeCost)
alshabib339a3d92014-09-26 17:54:32 -0700498 .add("clusters", clusterCount())
499 .add("devices", deviceCount())
500 .add("links", linkCount())
501 .add("pathCount", pathCount())
502 .toString();
503 }
504}