1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 9 * or http://www.opensolaris.org/os/licensing. 10 * See the License for the specific language governing permissions 11 * and limitations under the License. 12 * 13 * When distributing Covered Code, include this CDDL HEADER in each 14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE. 15 * If applicable, add the following below this CDDL HEADER, with the 16 * fields enclosed by brackets "[]" replaced with your own identifying 17 * information: Portions Copyright [yyyy] [name of copyright owner] 18 * 19 * CDDL HEADER END 20 */ 21 22 /* 23 * Copyright 2006 Sun Microsystems, Inc. All rights reserved. 24 * Use is subject to license terms. 25 * 26 * ident "%Z%%M% %I% %E% SMI" 27 */ 28 package org.opensolaris.os.dtrace; 29 30 import java.util.*; 31 import java.beans.*; 32 import java.io.*; 33 34 /** 35 * A snapshot of a DTrace aggregation. The name of an {@code 36 * Aggregation} instance matches the source declaration, for example 37 * <pre> {@code @a[execname] = count();}</pre> 38 * results in an {@code Aggregation} named "a" (the name does not 39 * include the preceding {@code @}). For convenience, a single 40 * aggregation can remain unnamed (multiple aggregations in the same D 41 * program need distinct names). The unnamed aggregation results in an 42 * {@code Aggregation} instance whose name is the empty string, for 43 * example 44 * <pre> {@code @[execname] = count();}</pre> 45 * An aggregation can list more than one variable in square brackets in 46 * order to accumulate a value for each unique combination, or {@link 47 * Tuple}. Each tuple instance is associated with its accumulated 48 * {@link AggregationValue} in an {@link AggregationRecord}. For 49 * example 50 * <pre> {@code @counts[execname, probefunc, cpu] = count();}</pre> 51 * results in an {@code Aggregation} named "counts" containing records 52 * each pairing a {@link CountValue} to a three-element {@code Tuple}. 53 * It is also possible to omit the square brackets, for example 54 * <pre> {@code @a = count();}</pre> 55 * results in an {@code Aggregation} named "a" with only a single record 56 * keyed to the empty tuple ({@link Tuple#EMPTY}). 57 * <p> 58 * For more information, see the <a 59 * href=http://docs.sun.com/app/docs/doc/817-6223/6mlkidlh7?a=view> 60 * <b>Aggregations</b></a> chapter of the <i>Solaris Dynamic Tracing 61 * Guide</i>. Also, the <a 62 * href=http://docs.sun.com/app/docs/doc/817-6223/6mlkidlfv?a=view> 63 * <b>Built-in Variables</b></a> section of the <b>Variables</b> chapter 64 * describes variables like {@code execname}, {@code probefunc}, and 65 * {@code cpu} useful for aggregating. 66 * <p> 67 * Immutable. Supports persistence using {@link java.beans.XMLEncoder}. 68 * 69 * @see Aggregate 70 * @see PrintaRecord 71 * 72 * @author Tom Erickson 73 */ 74 public final class Aggregation implements Serializable { 75 static final long serialVersionUID = 2340811719178724026L; 76 77 static { 78 try { 79 BeanInfo info = Introspector.getBeanInfo(Aggregation.class); 80 PersistenceDelegate persistenceDelegate = 81 new DefaultPersistenceDelegate( 82 new String[] {"name", "id", "records"}) 83 { 84 @Override 85 protected boolean 86 mutatesTo(Object oldInstance, Object newInstance) 87 { 88 return ((newInstance != null) && (oldInstance != null) && 89 (oldInstance.getClass() == newInstance.getClass())); 90 } 91 }; 92 BeanDescriptor d = info.getBeanDescriptor(); 93 d.setValue("persistenceDelegate", persistenceDelegate); 94 } catch (IntrospectionException e) { 95 e.printStackTrace(); 96 } 97 } 98 99 /** @serial */ 100 private String name; 101 /** @serial */ 102 private final long id; 103 private transient Map <Tuple, AggregationRecord> map; 104 105 /** 106 * Package-level access, called by Aggregate 107 */ 108 Aggregation(String aggregationName, long aggregationID) 109 { 110 name = Aggregate.filterUnnamedAggregationName(aggregationName); 111 id = aggregationID; 112 map = new HashMap <Tuple, AggregationRecord> (); 113 } 114 115 /** 116 * Creates an aggregation with the given name, ID, and records. 117 * Supports XML persistence. 118 * 119 * @param aggregationName the name of this aggregation, empty string 120 * if this aggregation is unnamed 121 * @param aggregationID ID generated from a sequence by the native 122 * DTrace library 123 * @param aggregationRecords unordered collection of records 124 * belonging to this aggregation 125 * @throws NullPointerException if the specified name or list of 126 * records is {@code null} 127 * @throws IllegalArgumentException if any record has an empty 128 * tuple, unless it is the only record in the given collection (only 129 * a singleton generated by an aggregation without square brackets 130 * uses {@link Tuple#EMPTY} as a key) 131 * @see #getRecord(Tuple key) 132 */ 133 public 134 Aggregation(String aggregationName, long aggregationID, 135 Collection <AggregationRecord> aggregationRecords) 136 { 137 name = Aggregate.filterUnnamedAggregationName(aggregationName); 138 id = aggregationID; 139 mapRecords(aggregationRecords); 140 validate(); 141 } 142 143 // assumes map is not yet created 144 private void 145 mapRecords(Collection <AggregationRecord> records) 146 { 147 int capacity = (int)(((float)records.size() * 3.0f) / 2.0f); 148 // avoid rehashing and optimize lookup; will never be modified 149 map = new HashMap <Tuple, AggregationRecord> (capacity, 1.0f); 150 for (AggregationRecord record : records) { 151 map.put(record.getTuple(), record); 152 } 153 } 154 155 private void 156 validate() 157 { 158 if (name == null) { 159 throw new NullPointerException("name is null"); 160 } 161 for (AggregationRecord r : map.values()) { 162 if ((r.getTuple().size() == 0) && (map.size() > 1)) { 163 throw new IllegalArgumentException("empty tuple " + 164 "allowed only in singleton aggregation"); 165 } 166 } 167 } 168 169 /** 170 * Gets the name of this aggregation. 171 * 172 * @return the name of this aggregation exactly as it appears in the 173 * D program minus the preceding {@code @}, or an empty string if 174 * the aggregation is unnamed, for example: 175 * <pre> {@code @[execname] = count();}</pre> 176 */ 177 public String 178 getName() 179 { 180 return name; 181 } 182 183 /** 184 * Gets the D compiler-generated ID of this aggregation. 185 * 186 * @return the D compiler-generated ID 187 */ 188 public long 189 getID() 190 { 191 return id; 192 } 193 194 /** 195 * Gets an unordered list of this aggregation's records. The list 196 * is easily sortable using {@link java.util.Collections#sort(List 197 * list, Comparator c)} provided any user-defined ordering. 198 * Modifying the returned list has no effect on this aggregation. 199 * Supports XML persistence. 200 * 201 * @return a newly created list that copies this aggregation's 202 * records by reference in no particular order 203 */ 204 public List <AggregationRecord> 205 getRecords() 206 { 207 List <AggregationRecord> list = 208 new ArrayList <AggregationRecord> (map.values()); 209 return list; 210 } 211 212 /** 213 * Package level access, called by Aggregate and PrintaRecord. 214 * 215 * @throws IllegalArgumentException if this aggregation already 216 * contains a record with the same tuple key as the given record 217 */ 218 void 219 addRecord(AggregationRecord record) 220 { 221 Tuple key = record.getTuple(); 222 if (map.put(key, record) != null) { 223 throw new IllegalArgumentException("already contains a record " + 224 "with tuple " + key); 225 } 226 } 227 228 /** 229 * Gets a read-only {@code Map} view of this aggregation. 230 * 231 * @return a read-only {@code Map} view of this aggregation 232 */ 233 public Map <Tuple, AggregationRecord> 234 asMap() 235 { 236 return Collections.unmodifiableMap(map); 237 } 238 239 /** 240 * Compares the specified object with this aggregation for equality. 241 * Defines equality as having equal names and equal records. 242 * 243 * @return {@code true} if and only if the specified object is an 244 * {@code Aggregation} with the same name as this aggregation and 245 * the {@code Map} views of both aggregations returned by {@link 246 * #asMap()} are equal as defined by {@link 247 * AbstractMap#equals(Object o) AbstractMap.equals()} 248 */ 249 @Override 250 public boolean 251 equals(Object o) 252 { 253 if (o instanceof Aggregation) { 254 Aggregation a = (Aggregation)o; 255 return (name.equals(a.name) && 256 (map.equals(a.map))); // same mappings 257 } 258 return false; 259 } 260 261 /** 262 * Overridden to ensure that equal aggregations have equal hash 263 * codes. 264 */ 265 @Override 266 public int 267 hashCode() 268 { 269 int hash = 17; 270 hash = (37 * hash) + name.hashCode(); 271 hash = (37 * hash) + map.hashCode(); 272 return hash; 273 } 274 275 /** 276 * Gets the record associated with the given key, or the singleton 277 * record of an aggregation declared without square brackets if 278 * {@code key} is {@code null} or empty. 279 * 280 * @param key The record key, or an empty tuple (see {@link 281 * Tuple#EMPTY}) to obtain the value from a <i>singleton</i> (a 282 * non-keyed instance with only a single value) generated from a 283 * DTrace aggregation declarated without square brackets, for 284 * example: 285 * <pre> {@code @a = count();}</pre> 286 * @return the record associated with the given key, or {@code null} 287 * if no record in this aggregation is associated with the given key 288 */ 289 public AggregationRecord 290 getRecord(Tuple key) 291 { 292 if (key == null) { 293 key = Tuple.EMPTY; 294 } 295 return map.get(key); 296 } 297 298 /** 299 * Serialize this {@code Aggregation} instance. 300 * 301 * @serialData Serialized fields are emitted, followed by a {@link 302 * java.util.List} of {@link AggregationRecord} instances. 303 */ 304 private void 305 writeObject(ObjectOutputStream s) throws IOException 306 { 307 s.defaultWriteObject(); 308 s.writeObject(getRecords()); 309 } 310 311 @SuppressWarnings("unchecked") 312 private void 313 readObject(ObjectInputStream s) 314 throws IOException, ClassNotFoundException 315 { 316 s.defaultReadObject(); 317 // cannot cast to parametric type without compiler warning 318 List <AggregationRecord> records = (List)s.readObject(); 319 // load serialized form into private map as a defensive copy 320 mapRecords(records); 321 // Check class invariants (only after defensive copy) 322 name = Aggregate.filterUnnamedAggregationName(name); 323 validate(); 324 } 325 326 /** 327 * Gets a string representation of this aggregation useful for 328 * logging and not intended for display. The exact details of the 329 * representation are unspecified and subject to change, but the 330 * following format may be regarded as typical: 331 * <pre><code> 332 * class-name[property1 = value1, property2 = value2] 333 * </code></pre> 334 */ 335 @Override 336 public String 337 toString() 338 { 339 StringBuffer buf = new StringBuffer(); 340 buf.append(Aggregation.class.getName()); 341 buf.append("[name = "); 342 buf.append(name); 343 buf.append(", id = "); 344 buf.append(id); 345 buf.append(", records = "); 346 List <AggregationRecord> recordList = getRecords(); 347 // Sort by tuple so that equal aggregations have equal strings 348 Collections.sort(recordList, new Comparator <AggregationRecord> () { 349 public int compare(AggregationRecord r1, AggregationRecord r2) { 350 Tuple t1 = r1.getTuple(); 351 Tuple t2 = r2.getTuple(); 352 return t1.compareTo(t2); 353 } 354 }); 355 buf.append('['); 356 boolean first = true; 357 for (AggregationRecord record : recordList) { 358 if (first) { 359 first = false; 360 } else { 361 buf.append(", "); 362 } 363 buf.append(record); 364 } 365 buf.append(']'); 366 return buf.toString(); 367 } 368 } 369