1 /*
  2  * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 package jdk.jfr.internal.util;
 26 
 27 import java.text.NumberFormat;
 28 import java.time.Duration;
 29 import java.time.Instant;
 30 import java.time.LocalTime;
 31 import java.time.ZoneId;
 32 import java.time.format.DateTimeFormatter;
 33 import java.time.temporal.ChronoUnit;
 34 import java.util.ArrayList;
 35 import java.util.List;
 36 import java.util.StringJoiner;
 37 
 38 import jdk.jfr.consumer.RecordedClass;
 39 import jdk.jfr.consumer.RecordedMethod;
 40 
 41 public final class ValueFormatter {
 42     private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance();
 43     private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
 44     private static final Duration MICRO_SECOND = Duration.ofNanos(1_000);
 45     private static final Duration SECOND = Duration.ofSeconds(1);
 46     private static final Duration MINUTE = Duration.ofMinutes(1);
 47     private static final Duration HOUR = Duration.ofHours(1);
 48     private static final Duration DAY = Duration.ofDays(1);
 49     private static final int NANO_SIGNIFICANT_FIGURES = 9;
 50     private static final int MILL_SIGNIFICANT_FIGURES = 3;
 51     private static final int DISPLAY_NANO_DIGIT = 3;
 52     private static final int BASE = 10;
 53 
 54     public static String formatNumber(Number n) {
 55         return NUMBER_FORMAT.format(n);
 56     }
 57 
 58     public static String formatDuration(Duration d) {
 59         Duration roundedDuration = roundDuration(d);
 60         if (roundedDuration.equals(Duration.ZERO)) {
 61             return "0 s";
 62         } else if (roundedDuration.isNegative()) {
 63             return "-" + formatPositiveDuration(roundedDuration.abs());
 64         } else {
 65             return formatPositiveDuration(roundedDuration);
 66         }
 67     }
 68 
 69     private static String formatPositiveDuration(Duration d){
 70         if (d.compareTo(MICRO_SECOND) < 0) {
 71             // 0.000001 ms - 0.000999 ms
 72             double outputMs = (double) d.toNanosPart() / 1_000_000;
 73             return String.format("%.6f ms", outputMs);
 74         } else if (d.compareTo(SECOND) < 0) {
 75             // 0.001 ms - 999 ms
 76             int valueLength = countLength(d.toNanosPart());
 77             int outputDigit = NANO_SIGNIFICANT_FIGURES - valueLength;
 78             double outputMs = (double) d.toNanosPart() / 1_000_000;
 79             return String.format("%." + outputDigit + "f ms", outputMs);
 80         } else if (d.compareTo(MINUTE) < 0) {
 81             // 1.00 s - 59.9 s
 82             int valueLength = countLength(d.toSecondsPart());
 83             int outputDigit = MILL_SIGNIFICANT_FIGURES - valueLength;
 84             double outputSecond = d.toSecondsPart() + (double) d.toMillisPart() / 1_000;
 85             return String.format("%." + outputDigit + "f s", outputSecond);
 86         } else if (d.compareTo(HOUR) < 0) {
 87             // 1 m 0 s - 59 m 59 s
 88             return String.format("%d m %d s", d.toMinutesPart(), d.toSecondsPart());
 89         } else if (d.compareTo(DAY) < 0) {
 90             // 1 h 0 m - 23 h 59 m
 91             return String.format("%d h %d m", d.toHoursPart(), d.toMinutesPart());
 92         } else {
 93             // 1 d 0 h -
 94             return String.format("%d d %d h", d.toDaysPart(), d.toHoursPart());
 95         }
 96     }
 97 
 98     private static int countLength(long value){
 99         return (int) Math.log10(value) + 1;
100     }
101 
102     private static Duration roundDuration(Duration d) {
103         if (d.equals(Duration.ZERO)) {
104             return d;
105         } else if(d.isNegative()) {
106             Duration roundedPositiveDuration = roundPositiveDuration(d.abs());
107             return roundedPositiveDuration.negated();
108         } else {
109             return roundPositiveDuration(d);
110         }
111     }
112 
113     private static Duration roundPositiveDuration(Duration d){
114         if (d.compareTo(MICRO_SECOND) < 0) {
115             // No round
116             return d;
117         } else if (d.compareTo(SECOND) < 0) {
118             // Round significant figures to three digits
119             int valueLength = countLength(d.toNanosPart());
120             int roundValue = (int) Math.pow(BASE, valueLength - DISPLAY_NANO_DIGIT);
121             long roundedNanos = Math.round((double) d.toNanosPart() / roundValue) * roundValue;
122             return d.truncatedTo(ChronoUnit.SECONDS).plusNanos(roundedNanos);
123         } else if (d.compareTo(MINUTE) < 0) {
124             // Round significant figures to three digits
125             int valueLength = countLength(d.toSecondsPart());
126             int roundValue = (int) Math.pow(BASE, valueLength);
127             long roundedMills = Math.round((double) d.toMillisPart() / roundValue) * roundValue;
128             return d.truncatedTo(ChronoUnit.SECONDS).plusMillis(roundedMills);
129         } else if (d.compareTo(HOUR) < 0) {
130             // Round for more than 500 ms or less
131             return d.plusMillis(SECOND.dividedBy(2).toMillisPart()).truncatedTo(ChronoUnit.SECONDS);
132         } else if (d.compareTo(DAY) < 0) {
133             // Round for more than 30 seconds or less
134             return d.plusSeconds(MINUTE.dividedBy(2).toSecondsPart()).truncatedTo(ChronoUnit.MINUTES);
135         } else {
136             // Round for more than 30 minutes or less
137             return d.plusMinutes(HOUR.dividedBy(2).toMinutesPart()).truncatedTo(ChronoUnit.HOURS);
138         }
139     }
140 
141     public static String formatClass(RecordedClass clazz) {
142         String name = clazz.getName();
143         if (name.startsWith("[")) {
144             return decodeDescriptors(name, "").getFirst();
145         }
146         return name;
147     }
148 
149     private static String formatDataAmount(String formatter, long amount) {
150         if (amount == Long.MIN_VALUE) {
151             return "N/A";
152         }
153         int exp = (int) (Math.log(Math.abs(amount)) / Math.log(1024));
154         char unit = "kMGTPE".charAt(exp - 1);
155         return String.format(formatter, amount / Math.pow(1024, exp), unit);
156     }
157 
158     public static String formatBytesCompact(long bytes) {
159         if (bytes < 1024) {
160             return String.valueOf(bytes);
161         }
162         return formatDataAmount("%.1f%cB", bytes);
163     }
164 
165     public static String formatBits(long bits) {
166         if (bits == 1 || bits == -1) {
167             return bits + " bit";
168         }
169         if (bits < 1024 && bits > -1024) {
170             return bits + " bits";
171         }
172         return formatDataAmount("%.1f %cbit", bits);
173     }
174 
175     public static String formatBytes(long bytes) {
176         if (bytes == 1 || bytes == -1) {
177             return bytes + " byte";
178         }
179         if (bytes < 1024 && bytes > -1024) {
180             return bytes + " bytes";
181         }
182         return formatDataAmount("%.1f %cB", bytes);
183     }
184 
185     public static String formatBytesPerSecond(long bytes) {
186         if (bytes < 1024 && bytes > -1024) {
187             return bytes + " byte/s";
188         }
189         return formatDataAmount("%.1f %cB/s", bytes);
190     }
191 
192     public static String formatBitsPerSecond(long bits) {
193         if (bits < 1024 && bits > -1024) {
194             return bits + " bps";
195         }
196         return formatDataAmount("%.1f %cbps", bits);
197     }
198 
199     public static String formatMethod(RecordedMethod m, boolean compact) {
200         StringBuilder sb = new StringBuilder();
201         sb.append(m.getType().getName());
202         sb.append(".");
203         sb.append(m.getName());
204         sb.append("(");
205         StringJoiner sj = new StringJoiner(", ");
206         String md = m.getDescriptor().replace("/", ".");
207         String parameter = md.substring(1, md.lastIndexOf(")"));
208         List<String> parameters = decodeDescriptors(parameter, "");
209         if (!compact) {
210             for (String qualifiedName :parameters) {
211                 String typeName = qualifiedName.substring(qualifiedName.lastIndexOf('.') + 1);
212                 sj.add(typeName);
213             }
214             sb.append(sj.toString());
215         } else {
216             if (!parameters.isEmpty()) {
217                sb.append("...");
218             }
219         }
220         sb.append(")");
221 
222         return sb.toString();
223     }
224 
225     private static List<String> decodeDescriptors(String descriptor, String arraySize) {
226         List<String> descriptors = new ArrayList<>();
227         for (int index = 0; index < descriptor.length(); index++) {
228             String arrayBrackets = "";
229             while (descriptor.charAt(index) == '[') {
230                 arrayBrackets = arrayBrackets + "[" + arraySize + "]";
231                 arraySize = "";
232                 index++;
233             }
234             char c = descriptor.charAt(index);
235             String type;
236             switch (c) {
237             case 'L':
238                 int endIndex = descriptor.indexOf(';', index);
239                 type = descriptor.substring(index + 1, endIndex);
240                 index = endIndex;
241                 break;
242             case 'I':
243                 type = "int";
244                 break;
245             case 'J':
246                 type = "long";
247                 break;
248             case 'Z':
249                 type = "boolean";
250                 break;
251             case 'D':
252                 type = "double";
253                 break;
254             case 'F':
255                 type = "float";
256                 break;
257             case 'S':
258                 type = "short";
259                 break;
260             case 'C':
261                 type = "char";
262                 break;
263             case 'B':
264                 type = "byte";
265                 break;
266             default:
267                 type = "<unknown-descriptor-type>";
268             }
269             descriptors.add(type + arrayBrackets);
270         }
271         return descriptors;
272     }
273 
274     public static String formatTimestamp(Instant instant) {
275         return LocalTime.ofInstant(instant, ZoneId.systemDefault()).format(DATE_FORMAT);
276     }
277 }