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      */
Aggregation(String aggregationName, long aggregationID)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
Aggregation(String aggregationName, long aggregationID, Collection <AggregationRecord> aggregationRecords)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
mapRecords(Collection <AggregationRecord> records)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
validate()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
getName()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
getID()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>
getRecords()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
addRecord(AggregationRecord record)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>
asMap()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
equals(Object o)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
hashCode()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
getRecord(Tuple key)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
writeObject(ObjectOutputStream s)305     writeObject(ObjectOutputStream s) throws IOException
306     {
307 	s.defaultWriteObject();
308 	s.writeObject(getRecords());
309     }
310 
311     @SuppressWarnings("unchecked")
312     private void
readObject(ObjectInputStream s)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
toString()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