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