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 2008 Sun Microsystems, Inc. All rights reserved. 24 * Use is subject to license terms. 25 */ 26 package org.opensolaris.os.dtrace; 27 28 import java.util.*; 29 import java.beans.*; 30 import java.io.*; 31 32 /** 33 * A snapshot of a DTrace aggregation. The name of an {@code 34 * Aggregation} instance matches the source declaration, for example 35 * <pre> {@code @a[execname] = count();}</pre> 36 * results in an {@code Aggregation} named "a" (the name does not 37 * include the preceding {@code @}). For convenience, a single 38 * aggregation can remain unnamed (multiple aggregations in the same D 39 * program need distinct names). The unnamed aggregation results in an 40 * {@code Aggregation} instance whose name is the empty string, for 41 * example 42 * <pre> {@code @[execname] = count();}</pre> 43 * An aggregation can list more than one variable in square brackets in 44 * order to accumulate a value for each unique combination, or {@link 45 * Tuple}. Each tuple instance is associated with its accumulated 46 * {@link AggregationValue} in an {@link AggregationRecord}. For 47 * example 48 * <pre> {@code @counts[execname, probefunc, cpu] = count();}</pre> 49 * results in an {@code Aggregation} named "counts" containing records 50 * each pairing a {@link CountValue} to a three-element {@code Tuple}. 51 * It is also possible to omit the square brackets, for example 52 * <pre> {@code @a = count();}</pre> 53 * results in an {@code Aggregation} named "a" with only a single record 54 * keyed to the empty tuple ({@link Tuple#EMPTY}). 55 * <p> 56 * For more information, see the <a 57 * href=http://dtrace.org/guide/chp-aggs.html> 58 * <b>Aggregations</b></a> chapter of the <i>Dynamic Tracing 59 * Guide</i>. Also, the <a 60 * href=http://dtrace.org/guide/chp-variables.html#chp-variables-5> 61 * <b>Built-in Variables</b></a> section of the <b>Variables</b> chapter 62 * describes variables like {@code execname}, {@code probefunc}, and 63 * {@code cpu} useful for aggregating. 64 * <p> 65 * Immutable. Supports persistence using {@link java.beans.XMLEncoder}. 66 * 67 * @see Aggregate 68 * @see PrintaRecord 69 * 70 * @author Tom Erickson 71 */ 72 public final class Aggregation implements Serializable { 73 static final long serialVersionUID = 2340811719178724026L; 74 75 static { 76 try { 77 BeanInfo info = Introspector.getBeanInfo(Aggregation.class); 78 PersistenceDelegate persistenceDelegate = 79 new DefaultPersistenceDelegate( 80 new String[] {"name", "ID", "records"}) 81 { 82 @Override 83 protected boolean 84 mutatesTo(Object oldInstance, Object newInstance) 85 { 86 return ((newInstance != null) && (oldInstance != null) && 87 (oldInstance.getClass() == newInstance.getClass())); 88 } 89 }; 90 BeanDescriptor d = info.getBeanDescriptor(); 91 d.setValue("persistenceDelegate", persistenceDelegate); 92 } catch (IntrospectionException e) { 93 e.printStackTrace(); 94 } 95 } 96 97 /** @serial */ 98 private String name; 99 /** @serial */ 100 private final long id; 101 private transient Map <Tuple, AggregationRecord> map; 102 103 /** 104 * Package-level access, called by Aggregate 105 */ 106 Aggregation(String aggregationName, long aggregationID) 107 { 108 name = Aggregate.filterUnnamedAggregationName(aggregationName); 109 id = aggregationID; 110 map = new HashMap <Tuple, AggregationRecord> (); 111 } 112 113 /** 114 * Creates an aggregation with the given name, ID, and records. 115 * Supports XML persistence. 116 * 117 * @param aggregationName the name of this aggregation, empty string 118 * if this aggregation is unnamed 119 * @param aggregationID ID generated from a sequence by the native 120 * DTrace library 121 * @param aggregationRecords unordered collection of records 122 * belonging to this aggregation 123 * @throws NullPointerException if the specified name or list of 124 * records is {@code null} 125 * @throws IllegalArgumentException if any record has an empty 126 * tuple, unless it is the only record in the given collection (only 127 * a singleton generated by an aggregation without square brackets 128 * uses {@link Tuple#EMPTY} as a key) 129 * @see #getRecord(Tuple key) 130 */ 131 public 132 Aggregation(String aggregationName, long aggregationID, 133 Collection <AggregationRecord> aggregationRecords) 134 { 135 name = Aggregate.filterUnnamedAggregationName(aggregationName); 136 id = aggregationID; 137 mapRecords(aggregationRecords); 138 validate(); 139 } 140 141 // assumes map is not yet created 142 private void 143 mapRecords(Collection <AggregationRecord> records) 144 { 145 int capacity = (int)(((float)records.size() * 3.0f) / 2.0f); 146 // avoid rehashing and optimize lookup; will never be modified 147 map = new HashMap <Tuple, AggregationRecord> (capacity, 1.0f); 148 for (AggregationRecord record : records) { 149 map.put(record.getTuple(), record); 150 } 151 } 152 153 private final void 154 validate() 155 { 156 if (name == null) { 157 throw new NullPointerException("name is null"); 158 } 159 for (AggregationRecord r : map.values()) { 160 if ((r.getTuple().size() == 0) && (map.size() > 1)) { 161 throw new IllegalArgumentException("empty tuple " + 162 "allowed only in singleton aggregation"); 163 } 164 } 165 } 166 167 /** 168 * Gets the name of this aggregation. 169 * 170 * @return the name of this aggregation exactly as it appears in the 171 * D program minus the preceding {@code @}, or an empty string if 172 * the aggregation is unnamed, for example: 173 * <pre> {@code @[execname] = count();}</pre> 174 */ 175 public String 176 getName() 177 { 178 return name; 179 } 180 181 /** 182 * Gets the D compiler-generated ID of this aggregation. 183 * 184 * @return the D compiler-generated ID 185 */ 186 public long 187 getID() 188 { 189 return id; 190 } 191 192 /** 193 * Gets an unordered list of this aggregation's records. The list is 194 * sortable using {@link java.util.Collections#sort(List list, 195 * Comparator c)} with any user-defined ordering. Modifying the 196 * returned list has no effect on this aggregation. Supports XML 197 * persistence. 198 * 199 * @return a newly created list that copies this aggregation's 200 * records by reference in no particular order 201 * @see Aggregate#getRecords() 202 * @see Aggregate#getOrderedRecords() 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. <Tuple, AggregationRecord> 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 declared 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 try { 324 validate(); 325 } catch (Exception e) { 326 InvalidObjectException x = new InvalidObjectException( 327 e.getMessage()); 328 x.initCause(e); 329 throw x; 330 } 331 } 332 333 /** 334 * Gets a string representation of this aggregation useful for 335 * logging and not intended for display. The exact details of the 336 * representation are unspecified and subject to change, but the 337 * following format may be regarded as typical: 338 * <pre><code> 339 * class-name[property1 = value1, property2 = value2] 340 * </code></pre> 341 */ 342 @Override 343 public String 344 toString() 345 { 346 StringBuilder buf = new StringBuilder(); 347 buf.append(Aggregation.class.getName()); 348 buf.append("[name = "); 349 buf.append(name); 350 buf.append(", id = "); 351 buf.append(id); 352 buf.append(", records = "); 353 List <AggregationRecord> recordList = getRecords(); 354 // Sort by tuple so that equal aggregations have equal strings 355 Collections.sort(recordList, new Comparator <AggregationRecord> () { 356 public int compare(AggregationRecord r1, AggregationRecord r2) { 357 Tuple t1 = r1.getTuple(); 358 Tuple t2 = r2.getTuple(); 359 return t1.compareTo(t2); 360 } 361 }); 362 buf.append('['); 363 boolean first = true; 364 for (AggregationRecord record : recordList) { 365 if (first) { 366 first = false; 367 } else { 368 buf.append(", "); 369 } 370 buf.append(record); 371 } 372 buf.append(']'); 373 return buf.toString(); 374 } 375 } 376