View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty5.testsuite.util;
17  
18  import io.netty5.util.CharsetUtil;
19  import io.netty5.util.internal.logging.InternalLogger;
20  import io.netty5.util.internal.logging.InternalLoggerFactory;
21  import org.junit.jupiter.api.TestInfo;
22  import org.tukaani.xz.LZMA2Options;
23  import org.tukaani.xz.XZOutputStream;
24  
25  import javax.management.MBeanServer;
26  import java.io.File;
27  import java.io.FileInputStream;
28  import java.io.FileOutputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStream;
32  import java.lang.management.ManagementFactory;
33  import java.lang.management.ThreadInfo;
34  import java.lang.reflect.Method;
35  import java.text.SimpleDateFormat;
36  import java.util.Date;
37  import java.util.concurrent.TimeUnit;
38  
39  import static java.util.Objects.requireNonNull;
40  
41  public final class TestUtils {
42  
43      private static final InternalLogger logger = InternalLoggerFactory.getInstance(TestUtils.class);
44  
45      private static final Method hotspotMXBeanDumpHeap;
46      private static final Object hotspotMXBean;
47  
48      private static final long DUMP_PROGRESS_LOGGING_INTERVAL = TimeUnit.SECONDS.toNanos(5);
49  
50      static {
51          // Retrieve the hotspot MXBean and its class if available.
52          Object mxBean;
53          Method mxBeanDumpHeap;
54          try {
55              Class<?> clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
56              MBeanServer server = ManagementFactory.getPlatformMBeanServer();
57              mxBean = ManagementFactory.newPlatformMXBeanProxy(
58                      server, "com.sun.management:type=HotSpotDiagnostic", clazz);
59              mxBeanDumpHeap = clazz.getMethod("dumpHeap", String.class, boolean.class);
60          } catch (Exception ignored) {
61              mxBean = null;
62              mxBeanDumpHeap = null;
63          }
64  
65          hotspotMXBean = mxBean;
66          hotspotMXBeanDumpHeap = mxBeanDumpHeap;
67      }
68  
69      /**
70       * Returns the method name of the current test.
71       */
72      public static String testMethodName(TestInfo testInfo) {
73          String testMethodName = testInfo.getTestMethod().map(Method::getName).orElse("[unknown method]");
74          if (testMethodName.contains("[")) {
75              testMethodName = testMethodName.substring(0, testMethodName.indexOf('['));
76          }
77          return testMethodName;
78      }
79  
80      public static void dump(String filenamePrefix) throws IOException {
81          requireNonNull(filenamePrefix, "filenamePrefix");
82  
83          final String timestamp = timestamp();
84          final File heapDumpFile = new File(filenamePrefix + '.' + timestamp + ".hprof");
85          if (heapDumpFile.exists()) {
86              if (!heapDumpFile.delete()) {
87                  throw new IOException("Failed to remove the old heap dump: " + heapDumpFile);
88              }
89          }
90  
91          final File threadDumpFile = new File(filenamePrefix + '.' + timestamp + ".threads");
92          if (threadDumpFile.exists()) {
93              if (!threadDumpFile.delete()) {
94                  throw new IOException("Failed to remove the old thread dump: " + threadDumpFile);
95              }
96          }
97  
98          dumpHeap(heapDumpFile);
99          dumpThreads(threadDumpFile);
100     }
101 
102     public static void compressHeapDumps() throws IOException {
103         final File[] files = new File(System.getProperty("user.dir")).listFiles((dir, name) -> name.endsWith(".hprof"));
104         if (files == null) {
105             logger.warn("failed to find heap dump due to I/O error!");
106             return;
107         }
108 
109         final byte[] buf = new byte[65536];
110         final LZMA2Options options = new LZMA2Options(LZMA2Options.PRESET_DEFAULT);
111 
112         for (File file: files) {
113             final String filename = file.toString();
114             final String xzFilename = filename + ".xz";
115             final long fileLength = file.length();
116 
117             logger.info("Compressing the heap dump: {}", xzFilename);
118 
119             long lastLogTime = System.nanoTime();
120             long counter = 0;
121 
122             try (InputStream in = new FileInputStream(filename);
123                  OutputStream out = new XZOutputStream(new FileOutputStream(xzFilename), options)) {
124                 for (;;) {
125                     int readBytes = in.read(buf);
126                     if (readBytes < 0) {
127                         break;
128                     }
129                     if (readBytes == 0) {
130                         continue;
131                     }
132 
133                     out.write(buf, 0, readBytes);
134                     counter += readBytes;
135 
136                     long currentTime = System.nanoTime();
137                     if (currentTime - lastLogTime > DUMP_PROGRESS_LOGGING_INTERVAL) {
138                         logger.info("Compressing the heap dump: {} ({}%)",
139                                     xzFilename, counter * 100 / fileLength);
140                         lastLogTime = currentTime;
141                     }
142                 }
143             } catch (Throwable t) {
144                 logger.warn("Failed to compress the heap dump: {}", xzFilename, t);
145             }
146 
147             // Delete the uncompressed dump in favor of the compressed one.
148             if (!file.delete()) {
149                 logger.warn("Failed to delete the uncompressed heap dump: {}", filename);
150             }
151         }
152     }
153 
154     private static String timestamp() {
155         return new SimpleDateFormat("HHmmss.SSS").format(new Date());
156     }
157 
158     private static void dumpHeap(File file) {
159         if (hotspotMXBean == null) {
160             logger.warn("Can't dump heap: HotSpotDiagnosticMXBean unavailable");
161             return;
162         }
163 
164         final String filename = file.toString();
165         logger.info("Dumping heap: {}", filename);
166         try {
167             hotspotMXBeanDumpHeap.invoke(hotspotMXBean, filename, true);
168         } catch (Exception e) {
169             logger.warn("Failed to dump heap: {}", filename, e);
170         }
171     }
172 
173     private static void dumpThreads(File file) {
174         final String filename = file.toString();
175         OutputStream out = null;
176         try {
177             logger.info("Dumping threads: {}", filename);
178             final StringBuilder buf = new StringBuilder(8192);
179             try {
180                 for (ThreadInfo info : ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)) {
181                     buf.append(info);
182                 }
183                 buf.append('\n');
184             } catch (UnsupportedOperationException ignored) {
185                 logger.warn("Can't dump threads: ThreadMXBean.dumpAllThreads() unsupported");
186                 return;
187             }
188 
189             out = new FileOutputStream(file);
190             out.write(buf.toString().getBytes(CharsetUtil.UTF_8));
191         } catch (Exception e) {
192             logger.warn("Failed to dump threads: {}", filename, e);
193         } finally {
194             if (out != null) {
195                 try {
196                     out.close();
197                 } catch (IOException ignored) {
198                     // Ignore.
199                 }
200             }
201         }
202     }
203 
204     private TestUtils() { }
205 }