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.io.*;
29 import java.beans.*;
30 import java.util.*;
31 
32 /**
33  * A record generated by the DTrace {@code printa()} action.  Lists the
34  * aggregations passed to {@code printa()} and records the formatted
35  * output associated with each {@link Tuple}.  If multiple aggregations
36  * were passed to the {@code printa()} action that generated this
37  * record, then the DTrace library tabulates the output, using a default
38  * format if no format string was specified.  By default, the output
39  * string associated with a given {@code Tuple} includes a value from
40  * each aggregation, or zero wherever an aggregation has no value
41  * associated with that {@code Tuple}.  For example, the D statements
42  * <pre><code>
43  *     &#64;a[123] = sum(1);
44  *     &#64;b[456] = sum(2);
45  *     printa(&#64;a, &#64;b, &#64;c);
46  * </code></pre>
47  * produce output for the tuples "123" and "456" similar to the
48  * following:
49  * <pre><code>
50  *	123	1	0	0
51  *	456	0	2	0
52  * </code></pre>
53  * The first column after the tuple contains values from {@code @a},
54  * the next column contains values from {@code @b}, and the last
55  * column contains zeros because {@code @c} has neither a value
56  * associated with "123" nor a value associated with "456".
57  * <p>
58  * If a format string is passed to {@code printa()}, it may limit the
59  * aggregation data available in this record.  For example, if the
60  * format string specifies a value placeholder for only one of two
61  * aggregations passed to {@code printa()}, then the resulting {@code
62  * PrintaRecord} will contain only one {@code Aggregation}.  If no value
63  * placeholder is specified, or if the aggregation tuple is not
64  * completely specified, the resulting {@code PrintaRecord} will contain
65  * no aggregation data.  However, the formatted output generated by the
66  * DTrace library is available in all cases.  For details about
67  * {@code printa()} format strings, see the <a
68  * href=http://dtrace.org/guide/chp-fmt.html#chp-fmt-printa>
69  * <b>{@code printa()}</b></a> section of the <b>Output
70  * Formatting</b> chapter of the <i>Dynamic Tracing Guide</i>.
71  * <p>
72  * Immutable.  Supports persistence using {@link java.beans.XMLEncoder}.
73  *
74  * @author Tom Erickson
75  */
76 public final class PrintaRecord implements Record, Serializable,
77 	Comparable <PrintaRecord> {
78     static final long serialVersionUID = -4174277639915895694L;
79 
80     static {
81 	try {
82 	    BeanInfo info = Introspector.getBeanInfo(PrintaRecord.class);
83 	    PersistenceDelegate persistenceDelegate =
84 		    new DefaultPersistenceDelegate(
85 		    new String[] {"snaptime", "aggregations",
86 		    "formattedStrings", "tuples", "output"})
87 	    {
88 		/*
89 		 * Need to prevent DefaultPersistenceDelegate from using
90 		 * overridden equals() method, resulting in a
91 		 * StackOverFlowError.  Revert to PersistenceDelegate
92 		 * implementation.  See
93 		 * http://forum.java.sun.com/thread.jspa?threadID=
94 		 * 477019&tstart=135
95 		 */
96 		protected boolean
97 		mutatesTo(Object oldInstance, Object newInstance)
98 		{
99 		    return (newInstance != null && oldInstance != null &&
100 			    oldInstance.getClass() == newInstance.getClass());
101 		}
102 	    };
103 	    BeanDescriptor d = info.getBeanDescriptor();
104 	    d.setValue("persistenceDelegate", persistenceDelegate);
105 	} catch (IntrospectionException e) {
106 	    System.out.println(e);
107 	}
108     }
109 
110     /** @serial */
111     private final long snaptime;
112     /** @serial */
113     private List <Aggregation> aggregations;
114     /** @serial */
115     private Map <Tuple, String> formattedStrings;
116     /** @serial */
117     private List <Tuple> tuples;
118     private transient StringBuilder outputBuffer;
119     private transient String output;
120     private transient boolean formatted;
121 
122     /**
123      * Package level access, called by ProbeData.
124      */
PrintaRecord(long snaptimeNanos, boolean isFormatString)125     PrintaRecord(long snaptimeNanos, boolean isFormatString)
126     {
127 	snaptime = snaptimeNanos;
128 	aggregations = new ArrayList <Aggregation> ();
129 	formattedStrings = new HashMap <Tuple, String> ();
130 	tuples = new ArrayList <Tuple> ();
131 	outputBuffer = new StringBuilder();
132 	formatted = isFormatString;
133 	validate();
134     }
135 
136     /**
137      * Creates a record with the given snaptime, aggregations, and
138      * formatted output.
139      *
140      * @param snaptimeNanos  nanosecond timestamp of the snapshot used
141      * to create this {@code printa()} record
142      * @param aggs  aggregations passed to the {@code printa()} action
143      * that generated this record
144      * @param formattedOutput  the formatted output, if any, associated
145      * with each {@code Tuple} occurring in the aggregations belonging
146      * to this record, one formatted string per {@code Tuple}, or an
147      * empty or {@code null} map if an incomplete {@code printa()}
148      * format string caused aggregation tuples to be omitted from this
149      * record
150      * @param orderedTuples list of aggregation tuples in the same order
151      * generated by the native DTrace library (determined by the various
152      * "aggsort" options such as {@link Option#aggsortkey})
153      * @param formattedOutputString {@code printa()} formatted string
154      * output in the same order generated by the native DTrace library
155      * (determined by the various "aggsort" options such as
156      * {@link Option#aggsortkey})
157      * @throws NullPointerException if the given collection of
158      * aggregations is {@code null}, or if the given ordered lists of
159      * tuples or formatted strings are {@code null}
160      * @throws IllegalArgumentException if the given snaptime is
161      * negative
162      */
163     public
PrintaRecord(long snaptimeNanos, Collection <Aggregation> aggs, Map <Tuple, String> formattedOutput, List <Tuple> orderedTuples, String formattedOutputString)164     PrintaRecord(long snaptimeNanos, Collection <Aggregation> aggs,
165 	    Map <Tuple, String> formattedOutput,
166 	    List <Tuple> orderedTuples,
167 	    String formattedOutputString)
168     {
169 	snaptime = snaptimeNanos;
170 	if (aggs != null) {
171 	    aggregations = new ArrayList <Aggregation> (aggs.size());
172 	    aggregations.addAll(aggs);
173 	}
174 	if (formattedOutput != null) {
175 	    formattedStrings = new HashMap <Tuple, String>
176 		    (formattedOutput);
177 	}
178 	if (orderedTuples != null) {
179 	    tuples = new ArrayList <Tuple> (orderedTuples.size());
180 	    tuples.addAll(orderedTuples);
181 	}
182 	output = formattedOutputString;
183 	validate();
184     }
185 
186     private final void
validate()187     validate()
188     {
189 	if (snaptime < 0) {
190 	    throw new IllegalArgumentException("snaptime is negative");
191 	}
192 	if (aggregations == null) {
193 	    throw new NullPointerException("aggregations list is null");
194 	}
195 	Aggregation a;
196 	for (int i = 0, len = aggregations.size(); i < len; ++i) {
197 	    a = aggregations.get(i);
198 	    if (a == null) {
199 		throw new NullPointerException(
200 			"null aggregation at index " + i);
201 	    }
202 	}
203 	if (tuples == null) {
204 	    throw new NullPointerException("ordered tuple list is null");
205 	}
206 	if (output == null && outputBuffer == null) {
207 	    throw new NullPointerException("formatted output is null");
208 	}
209     }
210 
211     /**
212      * Gets the nanosecond timestamp of the aggregate snapshot used to
213      * create this {@code printa()} record.
214      *
215      * @return nanosecond timestamp
216      */
217     public long
getSnaptime()218     getSnaptime()
219     {
220 	return snaptime;
221     }
222 
223     private Aggregation
getAggregationImpl(String name)224     getAggregationImpl(String name)
225     {
226 	if (name == null) {
227 	    return null;
228 	}
229 	for (Aggregation a : aggregations) {
230 	    if (name.equals(a.getName())) {
231 		return a;
232 	    }
233 	}
234 	return null;
235     }
236 
237     /**
238      * Gets the named aggregation.
239      *
240      * @return the named aggregation passed to {@code printa()}, or
241      * {@code null} if the named aggregation is not passed to {@code
242      * printa()}, or if it is omitted due to an incomplete {@code
243      * printa()} format string, or if it is empty (a future release of
244      * this API may represent an empty DTrace aggregation as a non-null
245      * {@code Aggregation} with no records; users of this API should not
246      * rely on a non-null return value to indicate a non-zero record
247      * count)
248      */
249     public Aggregation
getAggregation(String name)250     getAggregation(String name)
251     {
252 	name = Aggregate.filterUnnamedAggregationName(name);
253 	return getAggregationImpl(name);
254     }
255 
256     /**
257      * Gets a list of the aggregations passed to the {@code printa()}
258      * action that generated this record.  The returned list is a copy,
259      * and modifying it has no effect on this record.  Supports XML
260      * persistence.
261      *
262      * @return non-null, possibly empty list of aggregations belonging
263      * to this record (empty aggregations are excluded)
264      */
265     public List <Aggregation>
getAggregations()266     getAggregations()
267     {
268 	return new ArrayList <Aggregation> (aggregations);
269     }
270 
271     /**
272      * Gets the formatted string, if any, associated with the given
273      * aggregation tuple.
274      *
275      * @param key aggregation tuple
276      * @return the formatted string associated with the given
277      * aggregation tuple, or {@code null} if the given tuple does not
278      * exist in the aggregations belonging to this record or if it
279      * is omitted from this record due to an incomplete {@code printa()}
280      * format string
281      * @see #getFormattedStrings()
282      * @see #getOutput()
283      */
284     public String
getFormattedString(Tuple key)285     getFormattedString(Tuple key)
286     {
287 	if (formattedStrings == null) {
288 	    return null;
289 	}
290 	return formattedStrings.get(key);
291     }
292 
293     /**
294      * Gets the formatted output, if any, associated with each {@code
295      * Tuple} occurring in the aggregations belonging to this record,
296      * one formatted string per {@code Tuple}.  Gets an empty map if
297      * aggregation tuples are omitted from this record due to an
298      * incomplete {@code printa()} format string.  The returned map is a
299      * copy and modifying it has no effect on this record.  Supports XML
300      * persistence.
301      *
302      * @return a map of aggregation tuples and their associated
303      * formatted output strings, empty if aggregation tuples are omitted
304      * from this record due to an incomplete {@code printa(}) format
305      * string
306      * @see #getFormattedString(Tuple key)
307      * @see #getOutput()
308      */
309     public Map <Tuple, String>
getFormattedStrings()310     getFormattedStrings()
311     {
312 	if (formattedStrings == null) {
313 	    return new HashMap <Tuple, String> ();
314 	}
315 	return new HashMap <Tuple, String> (formattedStrings);
316     }
317 
318     /**
319      * Gets an ordered list of this record's aggregation tuples.  The
320      * returned list is a copy, and modifying it has no effect on this
321      * record.  Supports XML persistence.
322      *
323      * @return a non-null list of this record's aggregation tuples in
324      * the order they were generated by the native DTrace library, as
325      * determined by the {@link Option#aggsortkey}, {@link
326      * Option#aggsortrev}, {@link Option#aggsortpos}, and {@link
327      * Option#aggsortkeypos} options
328      */
329     public List <Tuple>
getTuples()330     getTuples()
331     {
332 	return new ArrayList <Tuple> (tuples);
333     }
334 
335     /**
336      * Gets this record's formatted output.  Supports XML persistence.
337      *
338      * @return non-null formatted output in the order generated by the
339      * native DTrace library, as determined by the {@link
340      * Option#aggsortkey}, {@link Option#aggsortrev}, {@link
341      * Option#aggsortpos}, and {@link Option#aggsortkeypos} options
342      */
343     public String
getOutput()344     getOutput()
345     {
346 	if (output == null) {
347 	    output = outputBuffer.toString();
348 	    outputBuffer = null;
349 	    if ((output.length() == 0) && !formatted) {
350 		output = "\n";
351 	    }
352 	}
353 	return output;
354     }
355 
356     /**
357      * Package level access, called by ProbeData.
358      *
359      * @throws NullPointerException if aggregationName is null
360      * @throws IllegalStateException if this PrintaRecord has an
361      * aggregation matching the given name and it already has an
362      * AggregationRecord with the same tuple key as the given record.
363      */
364     void
addRecord(String aggregationName, long aggid, AggregationRecord record)365     addRecord(String aggregationName, long aggid, AggregationRecord record)
366     {
367 	if (formattedStrings == null) {
368 	    // printa() format string does not completely specify tuple
369 	    return;
370 	}
371 
372 	aggregationName = Aggregate.filterUnnamedAggregationName(
373 		aggregationName);
374 	Aggregation aggregation = getAggregationImpl(aggregationName);
375 	if (aggregation == null) {
376 	    aggregation = new Aggregation(aggregationName, aggid);
377 	    aggregations.add(aggregation);
378 	}
379 	try {
380 	    aggregation.addRecord(record);
381 	} catch (IllegalArgumentException e) {
382 	    Map <Tuple, AggregationRecord> map = aggregation.asMap();
383 	    AggregationRecord r = map.get(record.getTuple());
384 	    //
385 	    // The printa() format string may specify the value of the
386 	    // aggregating action multiple times.  While that changes
387 	    // the resulting formatted string associated with the tuple,
388 	    // we ignore the attempt to add the redundant record to the
389 	    // aggregation.
390 	    //
391 	    if (!r.equals(record)) {
392 		throw e;
393 	    }
394 	}
395     }
396 
397     //
398     // Called from native code when the tuple is not completely
399     // specified in the printa() format string.
400     //
401     void
invalidate()402     invalidate()
403     {
404 	formattedStrings = null;
405 	aggregations.clear();
406 	tuples.clear();
407     }
408 
409     void
addFormattedString(Tuple tuple, String formattedString)410     addFormattedString(Tuple tuple, String formattedString)
411     {
412 	if (tuple != null && formattedStrings != null) {
413 	    if (formattedStrings.containsKey(tuple)) {
414 		throw new IllegalArgumentException("A formatted string " +
415 			"for tuple " + tuple + " already exists.");
416 	    } else {
417 		formattedStrings.put(tuple, formattedString);
418 		tuples.add(tuple);
419 	    }
420 	}
421 	outputBuffer.append(formattedString);
422     }
423 
424     /**
425      * Compares the specified object with this {@code PrintaRecord} for
426      * equality. Returns {@code true} if and only if the specified
427      * object is also a {@code PrintaRecord} and both records have the
428      * same aggregations and the same formatted strings in the same
429      * order (by aggregation tuple).
430      *
431      * @return {@code true} if and only if the specified object is also
432      * a {@code PrintaRecord} and both records have the same
433      * aggregations and the same formatted strings in the same order (by
434      * aggregation tuple)
435      */
436     @Override
437     public boolean
equals(Object o)438     equals(Object o)
439     {
440 	if (o instanceof PrintaRecord) {
441 	    PrintaRecord r = (PrintaRecord)o;
442 	    return (aggregations.equals(r.aggregations) &&
443 		    ((formattedStrings == null || formattedStrings.isEmpty())
444 		    ? (r.formattedStrings == null ||
445 			r.formattedStrings.isEmpty())
446 		    : formattedStrings.equals(r.formattedStrings)) &&
447 		    tuples.equals(r.tuples));
448 	}
449 
450 	return false;
451     }
452 
453     /**
454      * Overridden to ensure that equal instances have equal hash codes.
455      */
456     @Override
457     public int
hashCode()458     hashCode()
459     {
460 	int hash = 17;
461 	hash = (hash * 37) + aggregations.hashCode();
462 	hash = (hash * 37) + ((formattedStrings == null ||
463 	    formattedStrings.isEmpty()) ? 0 :
464 	    formattedStrings.hashCode());
465 	hash = (hash * 37) + tuples.hashCode();
466 	return hash;
467     }
468 
469     /**
470      * Compares the formatted {@link #getOutput() output} of this record
471      * with that of the given record. Note that ordering {@code printa}
472      * records by their output string values is incompatible with {@link
473      * #equals(Object o) equals()}, which also checks the underlying
474      * aggregation data for equality.
475      *
476      * @return a negative number, 0, or a positive number as this
477      * record's formatted output is lexicographically less than, equal
478      * to, or greater than the given record's formatted output
479      */
480     public int
compareTo(PrintaRecord r)481     compareTo(PrintaRecord r)
482     {
483 	return getOutput().compareTo(r.getOutput());
484     }
485 
486     /**
487      * Serialize this {@code PrintaRecord} instance.
488      *
489      * @serialData Serialized fields are emitted, followed by the
490      * formatted output string.
491      */
492     private void
writeObject(ObjectOutputStream s)493     writeObject(ObjectOutputStream s) throws IOException
494     {
495 	s.defaultWriteObject();
496 	if (output == null) {
497 	    s.writeObject(outputBuffer.toString());
498 	} else {
499 	    s.writeObject(output);
500 	}
501     }
502 
503     private void
readObject(ObjectInputStream s)504     readObject(ObjectInputStream s)
505             throws IOException, ClassNotFoundException
506     {
507 	s.defaultReadObject();
508 	output = (String)s.readObject();
509 	// make defensive copy
510 	if (aggregations != null) {
511 	    List <Aggregation> copy = new ArrayList <Aggregation>
512 		    (aggregations.size());
513 	    copy.addAll(aggregations);
514 	    aggregations = copy;
515 	}
516 	if (formattedStrings != null) {
517 	    formattedStrings = new HashMap <Tuple, String> (formattedStrings);
518 	}
519 	if (tuples != null) {
520 	    List <Tuple> copy = new ArrayList <Tuple> (tuples.size());
521 	    copy.addAll(tuples);
522 	    tuples = copy;
523 	}
524 	// check constructor invariants only after defensive copy
525 	try {
526 	    validate();
527 	} catch (Exception e) {
528 	    InvalidObjectException x = new InvalidObjectException(
529 		    e.getMessage());
530 	    x.initCause(e);
531 	    throw x;
532 	}
533     }
534 
535     /**
536      * Gets a string representation of this instance useful for logging
537      * and not intended for display.  The exact details of the
538      * representation are unspecified and subject to change, but the
539      * following format may be regarded as typical:
540      * <pre><code>
541      * class-name[property1 = value1, property2 = value2]
542      * </code></pre>
543      */
544     public String
toString()545     toString()
546     {
547 	StringBuilder buf = new StringBuilder();
548 	buf.append(PrintaRecord.class.getName());
549 	buf.append("[snaptime = ");
550 	buf.append(snaptime);
551 	buf.append(", aggregations = ");
552 	buf.append(aggregations);
553 	buf.append(", formattedStrings = ");
554 	buf.append(formattedStrings);
555 	buf.append(", tuples = ");
556 	buf.append(tuples);
557 	buf.append(", output = ");
558 	buf.append(getOutput());
559 	buf.append(']');
560 	return buf.toString();
561     }
562 }
563