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 consistent snapshot of all aggregations requested by a single
34  * {@link Consumer}.
35  * <p>
36  * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
37  *
38  * @see Consumer#getAggregate()
39  *
40  * @author Tom Erickson
41  */
42 public final class Aggregate implements Serializable
43 {
44     static final long serialVersionUID = 3180340417154076628L;
45 
46     static {
47 	try {
48 	    BeanInfo info = Introspector.getBeanInfo(Aggregate.class);
49 	    PersistenceDelegate persistenceDelegate =
50 		    new DefaultPersistenceDelegate(
51 		    new String[] {"snaptime", "aggregations"});
52 	    BeanDescriptor d = info.getBeanDescriptor();
53 	    d.setValue("persistenceDelegate", persistenceDelegate);
54 	} catch (IntrospectionException e) {
55 	    System.out.println(e);
56 	}
57     }
58 
59     /** @serial */
60     private final long snaptime;
61 
62     // Map must not have same name as named PersistenceDelegate property
63     // ("aggregations"), otherwise it gets confused for a bean property
64     // and XMLDecoder calls the constructor with a Map instead of the
65     // value of the getAggregations() method.
66 
67     private transient Map <String, Aggregation> map;
68     private transient int recordSequence;
69 
70     /**
71      * Called by native code.
72      */
73     private
Aggregate(long snaptimeNanos)74     Aggregate(long snaptimeNanos)
75     {
76 	snaptime = snaptimeNanos;
77 	map = new HashMap <String, Aggregation> ();
78     }
79 
80     /**
81      * Creates an aggregate with the given snaptime and aggregations.
82      * Supports XML persistence.
83      *
84      * @param snaptimeNanos nanosecond timestamp when this aggregate was
85      * snapped
86      * @param aggregations unordered collection of aggregations
87      * belonging to this aggregate
88      * @throws NullPointerException if the given collection of
89      * aggregations is {@code null}
90      * @throws IllegalArgumentException if the record ordinals of the
91      * given aggregations are invalid
92      */
93     public
Aggregate(long snaptimeNanos, Collection <Aggregation> aggregations)94     Aggregate(long snaptimeNanos, Collection <Aggregation> aggregations)
95     {
96 	snaptime = snaptimeNanos;
97 	mapAggregations(aggregations);
98 	validate();
99     }
100 
101     // assumes map is not yet created
102     private void
mapAggregations(Collection <Aggregation> aggregations)103     mapAggregations(Collection <Aggregation> aggregations)
104     {
105 	int capacity = (int)(((float)aggregations.size() * 3.0f) / 2.0f);
106 	// avoid rehashing and optimize lookup; will never be modified
107 	map = new HashMap <String, Aggregation> (capacity, 1.0f);
108 	for (Aggregation a : aggregations) {
109 	    map.put(a.getName(), a);
110 	    recordSequence += a.asMap().size();
111 	}
112     }
113 
114     private void
validate()115     validate()
116     {
117 	int capacity = (int)(((float)recordSequence * 3.0f) / 2.0f);
118 	Set <Integer> ordinals = new HashSet <Integer> (capacity, 1.0f);
119 	int ordinal, max = 0;
120 	for (Aggregation a : map.values()) {
121 	    for (AggregationRecord r : a.asMap().values()) {
122 		// Allow all ordinals to be zero for backward
123 		// compatibility (allows XML decoding of aggregates that
124 		// were encoded before the ordinal property was added).
125 		if (!ordinals.add(ordinal = r.getOrdinal()) && (ordinal > 0)) {
126 		    throw new IllegalArgumentException(
127 			    "duplicate record ordinal: " + ordinal);
128 		}
129 		if (ordinal > max) {
130 		    max = ordinal;
131 		}
132 	    }
133 	}
134 	if ((max > 0) && (max != (recordSequence - 1))) {
135 	    throw new IllegalArgumentException(
136 		    "The maximum record ordinal (" + max + ") does not " +
137 		    "equal the number of records (" + recordSequence +
138 		    ") minus one.");
139 	}
140     }
141 
142     /**
143      * Gets the nanosecond timestamp of this aggregate snapshot.
144      *
145      * @return nanosecond timestamp of this aggregate snapshot
146      */
147     public long
getSnaptime()148     getSnaptime()
149     {
150 	return snaptime;
151     }
152 
153     /**
154      * Gets an unordered list of all aggregations in this aggregate
155      * snapshot.  The list is easily sortable using {@link
156      * java.util.Collections#sort(List list, Comparator c)} provided any
157      * user-defined ordering.  Modifying the returned list has no effect
158      * on this aggregate.  Supports XML persistence.
159      *
160      * @return modifiable unordered list of all aggregations in this
161      * aggregate snapshot; list is non-null and possibly empty
162      */
163     public List <Aggregation>
getAggregations()164     getAggregations()
165     {
166 	// Must return an instance of a public, mutable class in order
167 	// to support XML persistence.
168 	List <Aggregation> list = new ArrayList <Aggregation> (map.size());
169 	list.addAll(map.values());
170 	return list;
171     }
172 
173     /**
174      * Gets the aggregation with the given name if it exists in this
175      * aggregate snapshot.
176      *
177      * @param name  the name of the desired aggregation, or empty string
178      * to request the unnamed aggregation.  In D, the unnamed
179      * aggregation is used anytime a name does not follow the
180      * aggregation symbol '{@code @}', for example:
181      * <pre>		{@code @ = count();}</pre> as opposed to
182      * <pre>		{@code @counts = count()}</pre> resulting in an
183      * {@code Aggregation} with the name "counts".
184      *
185      * @return {@code null} if no aggregation by the given name exists
186      * in this aggregate
187      * @see Aggregation#getName()
188      */
189     public Aggregation
getAggregation(String name)190     getAggregation(String name)
191     {
192 	// This was decided March 18, 2005 in a meeting with the DTrace
193 	// team that calling getAggregation() with underbar should
194 	// return the unnamed aggregation (same as calling with empty
195 	// string).  Underbar is used to identify the unnamed
196 	// aggregation in libdtrace; in the java API it is identified by
197 	// the empty string.  The API never presents underbar but
198 	// accepts it as input (just converts underbar to empty string
199 	// everywhere it sees it).
200 	name = Aggregate.filterUnnamedAggregationName(name);
201 	return map.get(name);
202     }
203 
204     /**
205      * Gets an unordered list of this aggregate's records. The list is
206      * sortable using {@link java.util.Collections#sort(List list,
207      * Comparator c)} with any user-defined ordering. Modifying the
208      * returned list has no effect on this aggregate.
209      *
210      * @return a newly created list that copies this aggregate's records
211      * by reference in no particular order
212      */
213     public List <AggregationRecord>
getRecords()214     getRecords()
215     {
216 	List <AggregationRecord> list =
217 		new ArrayList <AggregationRecord> (recordSequence);
218 	for (Aggregation a : map.values()) {
219 	    list.addAll(a.asMap().values());
220 	}
221 	return list;
222     }
223 
224     /**
225      * Gets an ordered list of this aggregate's records sequenced by
226      * their {@link AggregationRecord#getOrdinal() ordinal} property.
227      * Note that the unordered list returned by {@link #getRecords()}
228      * can easily be sorted by any arbitrary criteria, for example by
229      * key ascending:
230      * <pre><code>
231      * List &lt;AggregationRecord&gt; records = aggregate.getRecords();
232      * Collections.sort(records, new Comparator &lt;AggregationRecord&gt; () {
233      *  public int compare(AggregationRecord r1, AggregationRecord r2) {
234      *   return r1.getTuple().compareTo(r2.getTuple());
235      *  }
236      * });
237      * </code></pre>
238      * Use {@code getOrderedRecords()} instead of {@code getRecords()}
239      * when you want to list records as they would be ordered by {@code
240      * dtrace(8)}.
241      *
242      * @return a newly created list of this aggregate's records
243      * in the order used by the native DTrace library
244      */
245     public List <AggregationRecord>
getOrderedRecords()246     getOrderedRecords()
247     {
248 	List <AggregationRecord> list = getRecords();
249 	Collections.sort(list, new Comparator <AggregationRecord> () {
250 	    public int compare(AggregationRecord r1, AggregationRecord r2) {
251 		int n1 = r1.getOrdinal();
252 		int n2 = r2.getOrdinal();
253 		return (n1 < n2 ? -1 : (n1 > n2 ? 1 : 0));
254 	    }
255 	});
256 	return list;
257     }
258 
259     /**
260      * In the native DTrace library, the unnamed aggregation {@code @}
261      * is given the name {@code _} (underbar).  The Java DTrace API does
262      * not expose this implementation detail but instead identifies the
263      * unnamed aggregation with the empty string.  Here we convert the
264      * name of the unnamed aggregation at the earliest opportunity.
265      * <p>
266      * Package level access.  Called by this class and PrintaRecord when
267      * adding the Aggregation abstraction on top of native aggregation
268      * records.
269      */
270     static String
filterUnnamedAggregationName(String name)271     filterUnnamedAggregationName(String name)
272     {
273 	if ((name != null) && name.equals("_")) {
274 	    return "";
275 	}
276 	return name;
277     }
278 
279     /**
280      * Gets a read-only {@code Map} view of this aggregate.
281      *
282      * @return a read-only {@code Map} view of this aggregate keyed by
283      * aggregation name
284      */
285     public Map <String, Aggregation>
asMap()286     asMap()
287     {
288 	return Collections. <String, Aggregation> unmodifiableMap(map);
289     }
290 
291     /**
292      * Called by native code.
293      *
294      * @throws IllegalStateException if the aggregation with the given
295      * name already has a record with the same tuple key as the given
296      * record.
297      */
298     private void
addRecord(String aggregationName, long aggid, AggregationRecord rec)299     addRecord(String aggregationName, long aggid, AggregationRecord rec)
300     {
301 	rec.setOrdinal(recordSequence++);
302 	aggregationName = Aggregate.filterUnnamedAggregationName(
303 		aggregationName);
304 	Aggregation aggregation = getAggregation(aggregationName);
305 	if (aggregation == null) {
306 	    aggregation = new Aggregation(aggregationName, aggid);
307 	    map.put(aggregationName, aggregation);
308 	}
309 	aggregation.addRecord(rec);
310     }
311 
312     /**
313      * Serialize this {@code Aggregate} instance.
314      *
315      * @serialData Serialized fields are emitted, followed by a {@link
316      * java.util.List} of {@link Aggregation} instances.
317      */
318     private void
writeObject(ObjectOutputStream s)319     writeObject(ObjectOutputStream s) throws IOException
320     {
321 	s.defaultWriteObject();
322 	s.writeObject(getAggregations());
323     }
324 
325     @SuppressWarnings("unchecked")
326     private void
readObject(ObjectInputStream s)327     readObject(ObjectInputStream s)
328             throws IOException, ClassNotFoundException
329     {
330 	s.defaultReadObject();
331 	// cannot cast to parametric type without compiler warning
332 	List <Aggregation> aggregations = (List)s.readObject();
333 	// load serialized form into private map as a defensive copy
334 	mapAggregations(aggregations);
335 	// check class invariants after defensive copy
336 	try {
337 	    validate();
338 	} catch (Exception e) {
339 	    InvalidObjectException x = new InvalidObjectException(
340 		    e.getMessage());
341 	    x.initCause(e);
342 	    throw x;
343 	}
344     }
345 
346     /**
347      * Gets a string representation of this aggregate snapshot useful
348      * for logging and not intended for display.  The exact details of
349      * the representation are unspecified and subject to change, but the
350      * following format may be regarded as typical:
351      * <pre><code>
352      * class-name[property1 = value1, property2 = value2]
353      * </code></pre>
354      */
355     public String
toString()356     toString()
357     {
358 	StringBuilder buf = new StringBuilder();
359 	buf.append(Aggregate.class.getName());
360 	buf.append("[snaptime = ");
361 	buf.append(snaptime);
362 	buf.append(", aggregations = ");
363 	List <Aggregation> a = getAggregations();
364 	Collections.sort(a, new Comparator <Aggregation> () {
365 	    public int compare(Aggregation a1, Aggregation a2) {
366 		return a1.getName().compareTo(a2.getName());
367 	    }
368 	});
369 	buf.append(Arrays.toString(a.toArray()));
370 	buf.append(']');
371 	return buf.toString();
372     }
373 }
374