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 2007 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     static final long serialVersionUID = -4174277639915895694L;
80 
81     static {
82 	try {
83 	    BeanInfo info = Introspector.getBeanInfo(PrintaRecord.class);
84 	    PersistenceDelegate persistenceDelegate =
85 		    new DefaultPersistenceDelegate(
86 		    new String[] {"snaptime", "aggregations",
87 		    "formattedStrings", "tuples", "output"});
88 	    BeanDescriptor d = info.getBeanDescriptor();
89 	    d.setValue("persistenceDelegate", persistenceDelegate);
90 	} catch (IntrospectionException e) {
91 	    System.out.println(e);
92 	}
93     }
94 
95     /** @serial */
96     private final long snaptime;
97     /** @serial */
98     private List <Aggregation> aggregations;
99     /** @serial */
100     private Map <Tuple, String> formattedStrings;
101     /** @serial */
102     private List <Tuple> tuples;
103     private transient StringBuilder outputBuffer;
104     private transient String output;
105     private transient boolean formatted;
106 
107     /**
108      * Package level access, called by ProbeData.
109      */
110     PrintaRecord(long snaptimeNanos, boolean isFormatString)
111     {
112 	snaptime = snaptimeNanos;
113 	aggregations = new ArrayList <Aggregation> ();
114 	formattedStrings = new HashMap <Tuple, String> ();
115 	tuples = new ArrayList <Tuple> ();
116 	outputBuffer = new StringBuilder();
117 	formatted = isFormatString;
118 	validate();
119     }
120 
121     /**
122      * Creates a record with the given snaptime, aggregations, and
123      * formatted output.
124      *
125      * @param snaptimeNanos  nanosecond timestamp of the snapshot used
126      * to create this {@code printa()} record
127      * @param aggs  aggregations passed to the {@code printa()} action
128      * that generated this record
129      * @param formattedOutput  the formatted output, if any, associated
130      * with each {@code Tuple} occurring in the aggregations belonging
131      * to this record, one formatted string per {@code Tuple}, or an
132      * empty or {@code null} map if an incomplete {@code printa()}
133      * format string caused aggregation tuples to be omitted from this
134      * record
135      * @param orderedTuples list of aggregation tuples in the same order
136      * generated by the native DTrace library (determined by the various
137      * "aggsort" options such as {@link Option#aggsortkey})
138      * @param formattedOutputString {@code printa()} formatted string
139      * output in the same order generated by the native DTrace library
140      * (determined by the various "aggsort" options such as
141      * {@link Option#aggsortkey})
142      * @throws NullPointerException if the given collection of
143      * aggregations is {@code null}, or if the given ordered lists of
144      * tuples or formatted strings are {@code null}
145      * @throws IllegalArgumentException if the given snaptime is
146      * negative
147      */
148     public
149     PrintaRecord(long snaptimeNanos, Collection <Aggregation> aggs,
150 	    Map <Tuple, String> formattedOutput,
151 	    List <Tuple> orderedTuples,
152 	    String formattedOutputString)
153     {
154 	snaptime = snaptimeNanos;
155 	if (aggs != null) {
156 	    aggregations = new ArrayList <Aggregation> (aggs.size());
157 	    aggregations.addAll(aggs);
158 	}
159 	if (formattedOutput != null) {
160 	    formattedStrings = new HashMap <Tuple, String>
161 		    (formattedOutput);
162 	}
163 	if (orderedTuples != null) {
164 	    tuples = new ArrayList <Tuple> (orderedTuples.size());
165 	    tuples.addAll(orderedTuples);
166 	}
167 	output = formattedOutputString;
168 	validate();
169     }
170 
171     private final void
172     validate()
173     {
174 	if (snaptime < 0) {
175 	    throw new IllegalArgumentException("snaptime is negative");
176 	}
177 	if (aggregations == null) {
178 	    throw new NullPointerException("aggregations list is null");
179 	}
180 	Aggregation a;
181 	for (int i = 0, len = aggregations.size(); i < len; ++i) {
182 	    a = aggregations.get(i);
183 	    if (a == null) {
184 		throw new NullPointerException(
185 			"null aggregation at index " + i);
186 	    }
187 	}
188 	if (tuples == null) {
189 	    throw new NullPointerException("ordered tuple list is null");
190 	}
191 	if (output == null && outputBuffer == null) {
192 	    throw new NullPointerException("formatted output is null");
193 	}
194     }
195 
196     /**
197      * Gets the nanosecond timestamp of the aggregate snapshot used to
198      * create this {@code printa()} record.
199      *
200      * @return nanosecond timestamp
201      */
202     public long
203     getSnaptime()
204     {
205 	return snaptime;
206     }
207 
208     private Aggregation
209     getAggregationImpl(String name)
210     {
211 	if (name == null) {
212 	    return null;
213 	}
214 	for (Aggregation a : aggregations) {
215 	    if (name.equals(a.getName())) {
216 		return a;
217 	    }
218 	}
219 	return null;
220     }
221 
222     /**
223      * Gets the named aggregation.
224      *
225      * @return the named aggregation passed to {@code printa()}, or
226      * {@code null} if the named aggregation is not passed to {@code
227      * printa()}, or if it is omitted due to an incomplete {@code
228      * printa()} format string, or if it is empty (a future release of
229      * this API may represent an empty DTrace aggregation as a non-null
230      * {@code Aggregation} with no records; users of this API should not
231      * rely on a non-null return value to indicate a non-zero record
232      * count)
233      */
234     public Aggregation
235     getAggregation(String name)
236     {
237 	name = Aggregate.filterUnnamedAggregationName(name);
238 	return getAggregationImpl(name);
239     }
240 
241     /**
242      * Gets a list of the aggregations passed to the {@code printa()}
243      * action that generated this record.  The returned list is a copy,
244      * and modifying it has no effect on this record.  Supports XML
245      * persistence.
246      *
247      * @return non-null, possibly empty list of aggregations belonging
248      * to this record (empty aggregations are excluded)
249      */
250     public List <Aggregation>
251     getAggregations()
252     {
253 	return new ArrayList <Aggregation> (aggregations);
254     }
255 
256     /**
257      * Gets the formatted string, if any, associated with the given
258      * aggregation tuple.
259      *
260      * @param key aggregation tuple
261      * @return the formatted string associated with the given
262      * aggregation tuple, or {@code null} if the given tuple does not
263      * exist in the aggregations belonging to this record or if it
264      * is omitted from this record due to an incomplete {@code printa()}
265      * format string
266      * @see #getFormattedStrings()
267      * @see #getOutput()
268      */
269     public String
270     getFormattedString(Tuple key)
271     {
272 	if (formattedStrings == null) {
273 	    return null;
274 	}
275 	return formattedStrings.get(key);
276     }
277 
278     /**
279      * Gets the formatted output, if any, associated with each {@code
280      * Tuple} occurring in the aggregations belonging to this record,
281      * one formatted string per {@code Tuple}.  Gets an empty map if
282      * aggregation tuples are omitted from this record due to an
283      * incomplete {@code printa()} format string.  The returned map is a
284      * copy and modifying it has no effect on this record.  Supports XML
285      * persistence.
286      *
287      * @return a map of aggregation tuples and their associated
288      * formatted output strings, empty if aggregation tuples are omitted
289      * from this record due to an incomplete {@code printa(}) format
290      * string
291      * @see #getFormattedString(Tuple key)
292      * @see #getOutput()
293      */
294     public Map <Tuple, String>
295     getFormattedStrings()
296     {
297 	if (formattedStrings == null) {
298 	    return new HashMap <Tuple, String> ();
299 	}
300 	return new HashMap <Tuple, String> (formattedStrings);
301     }
302 
303     /**
304      * Gets an ordered list of this record's aggregation tuples.  The
305      * returned list is a copy, and modifying it has no effect on this
306      * record.  Supports XML persistence.
307      *
308      * @return a non-null list of this record's aggregation tuples in
309      * the order they were generated by the native DTrace library, as
310      * determined by the {@link Option#aggsortkey}, {@link
311      * Option#aggsortrev}, {@link Option#aggsortpos}, and {@link
312      * Option#aggsortkeypos} options
313      */
314     public List <Tuple>
315     getTuples()
316     {
317 	return new ArrayList <Tuple> (tuples);
318     }
319 
320     /**
321      * Gets this record's formatted output.  Supports XML persistence.
322      *
323      * @return non-null formatted output in the order generated by the
324      * native DTrace library, as determined by the {@link
325      * Option#aggsortkey}, {@link Option#aggsortrev}, {@link
326      * Option#aggsortpos}, and {@link Option#aggsortkeypos} options
327      */
328     public String
329     getOutput()
330     {
331 	if (output == null) {
332 	    output = outputBuffer.toString();
333 	    outputBuffer = null;
334 	    if ((output.length() == 0) && !formatted) {
335 		output = "\n";
336 	    }
337 	}
338 	return output;
339     }
340 
341     /**
342      * Package level access, called by ProbeData.
343      *
344      * @throws NullPointerException if aggregationName is null
345      * @throws IllegalStateException if this PrintaRecord has an
346      * aggregation matching the given name and it already has an
347      * AggregationRecord with the same tuple key as the given record.
348      */
349     void
350     addRecord(String aggregationName, long aggid, AggregationRecord record)
351     {
352 	if (formattedStrings == null) {
353 	    // printa() format string does not completely specify tuple
354 	    return;
355 	}
356 
357 	aggregationName = Aggregate.filterUnnamedAggregationName(
358 		aggregationName);
359 	Aggregation aggregation = getAggregationImpl(aggregationName);
360 	if (aggregation == null) {
361 	    aggregation = new Aggregation(aggregationName, aggid);
362 	    aggregations.add(aggregation);
363 	}
364 	try {
365 	    aggregation.addRecord(record);
366 	} catch (IllegalArgumentException e) {
367 	    Map <Tuple, AggregationRecord> map = aggregation.asMap();
368 	    AggregationRecord r = map.get(record.getTuple());
369 	    //
370 	    // The printa() format string may specify the value of the
371 	    // aggregating action multiple times.  While that changes
372 	    // the resulting formatted string associated with the tuple,
373 	    // we ignore the attempt to add the redundant record to the
374 	    // aggregation.
375 	    //
376 	    if (!r.equals(record)) {
377 		throw e;
378 	    }
379 	}
380     }
381 
382     //
383     // Called from native code when the tuple is not completely
384     // specified in the printa() format string.
385     //
386     void
387     invalidate()
388     {
389 	formattedStrings = null;
390 	aggregations.clear();
391 	tuples.clear();
392     }
393 
394     void
395     addFormattedString(Tuple tuple, String formattedString)
396     {
397 	if (tuple != null && formattedStrings != null) {
398 	    if (formattedStrings.containsKey(tuple)) {
399 		throw new IllegalArgumentException("A formatted string " +
400 			"for tuple " + tuple + " already exists.");
401 	    } else {
402 		formattedStrings.put(tuple, formattedString);
403 		tuples.add(tuple);
404 	    }
405 	}
406 	outputBuffer.append(formattedString);
407     }
408 
409     /**
410      * Serialize this {@code PrintaRecord} instance.
411      *
412      * @serialData Serialized fields are emitted, followed by the
413      * formatted output string.
414      */
415     private void
416     writeObject(ObjectOutputStream s) throws IOException
417     {
418 	s.defaultWriteObject();
419 	if (output == null) {
420 	    s.writeObject(outputBuffer.toString());
421 	} else {
422 	    s.writeObject(output);
423 	}
424     }
425 
426     private void
427     readObject(ObjectInputStream s)
428             throws IOException, ClassNotFoundException
429     {
430 	s.defaultReadObject();
431 	output = (String)s.readObject();
432 	// make defensive copy
433 	if (aggregations != null) {
434 	    List <Aggregation> copy = new ArrayList <Aggregation>
435 		    (aggregations.size());
436 	    copy.addAll(aggregations);
437 	    aggregations = copy;
438 	}
439 	if (formattedStrings != null) {
440 	    formattedStrings = new HashMap <Tuple, String> (formattedStrings);
441 	}
442 	if (tuples != null) {
443 	    List <Tuple> copy = new ArrayList <Tuple> (tuples.size());
444 	    copy.addAll(tuples);
445 	    tuples = copy;
446 	}
447 	// check constructor invariants only after defensize copy
448 	try {
449 	    validate();
450 	} catch (Exception e) {
451 	    InvalidObjectException x = new InvalidObjectException(
452 		    e.getMessage());
453 	    x.initCause(e);
454 	    throw x;
455 	}
456     }
457 
458     /**
459      * Gets a string representation of this instance useful for logging
460      * and not intended for display.  The exact details of the
461      * representation are unspecified and subject to change, but the
462      * following format may be regarded as typical:
463      * <pre><code>
464      * class-name[property1 = value1, property2 = value2]
465      * </code></pre>
466      */
467     public String
468     toString()
469     {
470 	StringBuilder buf = new StringBuilder();
471 	buf.append(PrintaRecord.class.getName());
472 	buf.append("[snaptime = ");
473 	buf.append(snaptime);
474 	buf.append(", aggregations = ");
475 	buf.append(aggregations);
476 	buf.append(", formattedStrings = ");
477 	buf.append(formattedStrings);
478 	buf.append(", tuples = ");
479 	buf.append(tuples);
480 	buf.append(", output = ");
481 	buf.append(getOutput());
482 	buf.append(']');
483 	return buf.toString();
484     }
485 }
486