ThreadInfoController.java

/*
 * Copyright 2022 Global Crop Diversity Trust
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.gringlobal.api.admin.v1;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.gringlobal.api.v1.ApiBaseController;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

@RestController("threadInfoAPIV1")
@RequestMapping(ThreadInfoController.API_URL)
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@Api(tags = { "threadinfov1" })
@Slf4j
public class ThreadInfoController {

	/** The Constant API_URL. */
	public static final String API_URL = ApiBaseController.APIv1_BASE + "/admin";

	@GetMapping(value = "/threaddump", produces = { MediaType.TEXT_PLAIN_VALUE })
	public String threadDump() throws IOException {
		StringBuilder dump = new StringBuilder();
		log.warn("Dumping thread info");
		dump.append("Thread dump:\t").append(new Date()).append(System.lineSeparator());
		dump.append("Hostname:\t").append(java.net.InetAddress.getLocalHost().getHostName()).append(System.lineSeparator());

		dump.append("CPUs:\t").append(Runtime.getRuntime().availableProcessors()).append(System.lineSeparator());
		dump.append("Free memory:\t").append(Runtime.getRuntime().freeMemory()).append(System.lineSeparator());
		dump.append("Max memory:\t").append(Runtime.getRuntime().maxMemory()).append(System.lineSeparator());
		dump.append("Total memory:\t").append(Runtime.getRuntime().totalMemory()).append(System.lineSeparator());
		dump.append(System.lineSeparator());

		Map<Thread, StackTraceElement[]> threadSet = Thread.getAllStackTraces();

		ArrayList<Thread> sortedThreads = new ArrayList<>(threadSet.keySet());
		sortedThreads.sort(Comparator.comparing(Thread::getName));

		dump.append("\n\n*** Thread list ***\n").append(System.lineSeparator());

		for (Thread t : sortedThreads) {
			dump.append(t.getName()).append(System.lineSeparator());
		}
		dump.append(System.lineSeparator());

		dump.append("\n\n*** Threads ***\n").append(System.lineSeparator());
		dump.append("ID\tState\tName\tGroup").append(System.lineSeparator());
		for (Thread t : sortedThreads) {
			dump.append(t.getId());
			dump.append("\t");
			dump.append(t.getState());
			dump.append("\t");
			dump.append(t.getName());
			dump.append("\t");
			ThreadGroup threadGroup = t.getThreadGroup();
			dump.append(threadGroup == null ? "N/A" : threadGroup.getName());
			dump.append(System.lineSeparator());

			StackTraceElement[] ste = threadSet.get(t);
			Arrays.stream(ste).forEach((st) -> {
				dump.append(t.getId());
				dump.append("\t");
				dump.append(st.getClassName());
				dump.append(":");
				dump.append(st.getMethodName());
				dump.append("\t");
				dump.append(StringUtils.defaultIfBlank(st.getFileName(), "---"));
				dump.append("\t");
				dump.append(System.lineSeparator());
			});

			dump.append(System.lineSeparator());
		}


		ThreadMXBean tmxb = ManagementFactory.getThreadMXBean();
		if (tmxb != null && tmxb.isThreadCpuTimeSupported()) {
			dump.append("\n\n*** CPU Usage ***\n").append(System.lineSeparator());
			dump.append("ID\tCPU%\tCPU[ms]\tUser[ms]\tState\tName\tGroup").append(System.lineSeparator());
			Map<Long, ThreadCpuUsage> cpuUsage = findCpuUsage(tmxb, sortedThreads);

			sortedThreads.sort((t1, t2) -> (int)(cpuUsage.get(t2.getId()).cpuTime - cpuUsage.get(t1.getId()).cpuTime));
			for (Thread t : sortedThreads) {
				dump.append(t.getId());
				dump.append("\t");
				ThreadCpuUsage cpu = cpuUsage.get(t.getId());
				dump.append(String.format("%.4f", cpu.utilization * 100));
				dump.append("\t");
				dump.append(String.format("%.2f", cpu.cpuTime / 1000f));
				dump.append("\t");
				dump.append(String.format("%.2f", cpu.userTime / 1000f));
				dump.append("\t");

				dump.append(t.getState());
				dump.append("\t");
				dump.append(t.getName());
				dump.append("\t");
				ThreadGroup threadGroup = t.getThreadGroup();
				dump.append(threadGroup == null ? "N/A" : threadGroup.getName());

				dump.append(System.lineSeparator());
			}
		}
		return dump.toString();
	}

	private Map<Long, ThreadCpuUsage> findCpuUsage(ThreadMXBean tmxb, List<? extends Thread> threads) {
		Map<Long, ThreadCpuUsage> cpu = new Hashtable<>();
		for (int i = 0; i < threads.size(); i++) {
			Thread thread = threads.get(i);
			long threadId = thread.getId();
			ThreadCpuUsage cpuUsage = new ThreadCpuUsage();
			cpuUsage.cpuTime1 = tmxb.getThreadCpuTime(threadId);
			cpuUsage.userTime1 = tmxb.getThreadUserTime(threadId);
			cpuUsage.time1 = System.currentTimeMillis();
			cpu.put(threadId, cpuUsage);
		}
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			log.warn("Interrupted from sleep: " + e.getMessage());
		}

		for (int i = 0; i < threads.size(); i++) {
			Thread thread = threads.get(i);
			long threadId = thread.getId();
			ThreadCpuUsage cpuUsage = cpu.get(threadId);
			cpuUsage.cpuTime2 = tmxb.getThreadCpuTime(threadId);
			cpuUsage.userTime2 = tmxb.getThreadUserTime(threadId);
			cpuUsage.time2 = System.currentTimeMillis();

			cpuUsage.cpuTime = cpuUsage.cpuTime2 - cpuUsage.cpuTime1;
			cpuUsage.userTime = cpuUsage.userTime2 - cpuUsage.userTime1;
			cpuUsage.time = cpuUsage.time2 - cpuUsage.time1;

//			if (cpuUsage.cpuTime > 0) {
//				cpuUsage.threadInfo = tmxb.getThreadInfo(threadId, 50);
//			}

			cpuUsage.utilization = (cpuUsage.cpuTime) / ((cpuUsage.time) * 1000000F);
		}

		return cpu;
	}

	private static class ThreadCpuUsage {
		//		public ThreadInfo threadInfo;
		public long userTime1, userTime2, userTime;
		public long cpuTime1, cpuTime2, cpuTime;
		public long time1, time2, time;
		public double utilization;
	}
}