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  * 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 final 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 is
196      * sortable using {@link java.util.Collections#sort(List list,
197      * Comparator c)} with any user-defined ordering. Modifying the
198      * returned list has no effect on this aggregation. Supports XML
199      * persistence.
200      *
201      * @return a newly created list that copies this aggregation's
202      * records by reference in no particular order
203      * @see Aggregate#getRecords()
204      * @see Aggregate#getOrderedRecords()
205      */
206     public List <AggregationRecord>
207     getRecords()
208     {
209 	List <AggregationRecord> list =
210 		new ArrayList <AggregationRecord> (map.values());
211 	return list;
212     }
213 
214     /**
215      * Package level access, called by Aggregate and PrintaRecord.
216      *
217      * @throws IllegalArgumentException if this aggregation already
218      * contains a record with the same tuple key as the given record
219      */
220     void
221     addRecord(AggregationRecord record)
222     {
223 	Tuple key = record.getTuple();
224 	if (map.put(key, record) != null) {
225 	    throw new IllegalArgumentException("already contains a record " +
226 		    "with tuple " + key);
227 	}
228     }
229 
230     /**
231      * Gets a read-only {@code Map} view of this aggregation.
232      *
233      * @return a read-only {@code Map} view of this aggregation
234      */
235     public Map <Tuple, AggregationRecord>
236     asMap()
237     {
238 	return Collections. <Tuple, AggregationRecord> unmodifiableMap(map);
239     }
240 
241     /**
242      * Compares the specified object with this aggregation for equality.
243      * Defines equality as having equal names and equal records.
244      *
245      * @return {@code true} if and only if the specified object is an
246      * {@code Aggregation} with the same name as this aggregation and
247      * the {@code Map} views of both aggregations returned by {@link
248      * #asMap()} are equal as defined by {@link
249      * AbstractMap#equals(Object o) AbstractMap.equals()}
250      */
251     @Override
252     public boolean
253     equals(Object o)
254     {
255 	if (o instanceof Aggregation) {
256 	    Aggregation a = (Aggregation)o;
257 	    return (name.equals(a.name) &&
258 		    (map.equals(a.map))); // same mappings
259 	}
260 	return false;
261     }
262 
263     /**
264      * Overridden to ensure that equal aggregations have equal hash
265      * codes.
266      */
267     @Override
268     public int
269     hashCode()
270     {
271 	int hash = 17;
272 	hash = (37 * hash) + name.hashCode();
273 	hash = (37 * hash) + map.hashCode();
274 	return hash;
275     }
276 
277     /**
278      * Gets the record associated with the given key, or the singleton
279      * record of an aggregation declared without square brackets if
280      * {@code key} is {@code null} or empty.
281      *
282      * @param key  The record key, or an empty tuple (see {@link
283      * Tuple#EMPTY}) to obtain the value from a <i>singleton</i> (a
284      * non-keyed instance with only a single value) generated from a
285      * DTrace aggregation declarated without square brackets, for
286      * example:
287      * <pre>		{@code @a = count();}</pre>
288      * @return the record associated with the given key, or {@code null}
289      * if no record in this aggregation is associated with the given key
290      */
291     public AggregationRecord
292     getRecord(Tuple key)
293     {
294 	if (key == null) {
295 	    key = Tuple.EMPTY;
296 	}
297 	return map.get(key);
298     }
299 
300     /**
301      * Serialize this {@code Aggregation} instance.
302      *
303      * @serialData Serialized fields are emitted, followed by a {@link
304      * java.util.List} of {@link AggregationRecord} instances.
305      */
306     private void
307     writeObject(ObjectOutputStream s) throws IOException
308     {
309 	s.defaultWriteObject();
310 	s.writeObject(getRecords());
311     }
312 
313     @SuppressWarnings("unchecked")
314     private void
315     readObject(ObjectInputStream s)
316             throws IOException, ClassNotFoundException
317     {
318 	s.defaultReadObject();
319 	// cannot cast to parametric type without compiler warning
320 	List <AggregationRecord> records = (List)s.readObject();
321 	// load serialized form into private map as a defensive copy
322 	mapRecords(records);
323 	// Check class invariants (only after defensive copy)
324 	name = Aggregate.filterUnnamedAggregationName(name);
325 	try {
326 	    validate();
327 	} catch (Exception e) {
328 	    InvalidObjectException x = new InvalidObjectException(
329 		    e.getMessage());
330 	    x.initCause(e);
331 	    throw x;
332 	}
333     }
334 
335     /**
336      * Gets a string representation of this aggregation useful for
337      * logging and not intended for display.  The exact details of the
338      * representation are unspecified and subject to change, but the
339      * following format may be regarded as typical:
340      * <pre><code>
341      * class-name[property1 = value1, property2 = value2]
342      * </code></pre>
343      */
344     @Override
345     public String
346     toString()
347     {
348 	StringBuilder buf = new StringBuilder();
349 	buf.append(Aggregation.class.getName());
350 	buf.append("[name = ");
351 	buf.append(name);
352 	buf.append(", id = ");
353 	buf.append(id);
354 	buf.append(", records = ");
355 	List <AggregationRecord> recordList = getRecords();
356 	// Sort by tuple so that equal aggregations have equal strings
357 	Collections.sort(recordList, new Comparator <AggregationRecord> () {
358 	    public int compare(AggregationRecord r1, AggregationRecord r2) {
359 		Tuple t1 = r1.getTuple();
360 		Tuple t2 = r2.getTuple();
361 		return t1.compareTo(t2);
362 	    }
363 	});
364 	buf.append('[');
365 	boolean first = true;
366 	for (AggregationRecord record : recordList) {
367 	    if (first) {
368 		first = false;
369 	    } else {
370 		buf.append(", ");
371 	    }
372 	    buf.append(record);
373 	}
374 	buf.append(']');
375 	return buf.toString();
376     }
377 }
378