| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.hadoop.yarn.util; |
| |
| import static org.apache.hadoop.yarn.util.ProcfsBasedProcessTree.KB_TO_BYTES; |
| import static org.apache.hadoop.yarn.util.ResourceCalculatorProcessTree.UNAVAILABLE; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeTrue; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileReader; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.Vector; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.commons.io.FileUtils; |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.hadoop.conf.Configuration; |
| import org.apache.hadoop.fs.FileContext; |
| import org.apache.hadoop.fs.FileUtil; |
| import org.apache.hadoop.fs.Path; |
| import org.apache.hadoop.util.Shell; |
| import org.apache.hadoop.util.Shell.ExitCodeException; |
| import org.apache.hadoop.util.Shell.ShellCommandExecutor; |
| import org.apache.hadoop.util.StringUtils; |
| import org.apache.hadoop.yarn.conf.YarnConfiguration; |
| import org.apache.hadoop.yarn.util.ProcfsBasedProcessTree.MemInfo; |
| import org.apache.hadoop.yarn.util.ProcfsBasedProcessTree.ProcessSmapMemoryInfo; |
| import org.apache.hadoop.yarn.util.ProcfsBasedProcessTree.ProcessTreeSmapMemInfo; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| /** |
| * A JUnit test to test ProcfsBasedProcessTree. |
| */ |
| public class TestProcfsBasedProcessTree { |
| |
| private static final Log LOG = LogFactory |
| .getLog(TestProcfsBasedProcessTree.class); |
| protected static File TEST_ROOT_DIR = new File("target", |
| TestProcfsBasedProcessTree.class.getName() + "-localDir"); |
| |
| private ShellCommandExecutor shexec = null; |
| private String pidFile, lowestDescendant, lostDescendant; |
| private String shellScript; |
| |
| private static final int N = 6; // Controls the RogueTask |
| |
| private class RogueTaskThread extends Thread { |
| public void run() { |
| try { |
| Vector<String> args = new Vector<String>(); |
| if (isSetsidAvailable()) { |
| args.add("setsid"); |
| } |
| args.add("bash"); |
| args.add("-c"); |
| args.add(" echo $$ > " + pidFile + "; sh " + shellScript + " " + N |
| + ";"); |
| shexec = new ShellCommandExecutor(args.toArray(new String[0])); |
| shexec.execute(); |
| } catch (ExitCodeException ee) { |
| LOG.info("Shell Command exit with a non-zero exit code. This is" |
| + " expected as we are killing the subprocesses of the" |
| + " task intentionally. " + ee); |
| } catch (IOException ioe) { |
| LOG.info("Error executing shell command " + ioe); |
| } finally { |
| LOG.info("Exit code: " + shexec.getExitCode()); |
| } |
| } |
| } |
| |
| private String getRogueTaskPID() { |
| File f = new File(pidFile); |
| while (!f.exists()) { |
| try { |
| Thread.sleep(500); |
| } catch (InterruptedException ie) { |
| break; |
| } |
| } |
| |
| // read from pidFile |
| return getPidFromPidFile(pidFile); |
| } |
| |
| @Before |
| public void setup() throws IOException { |
| assumeTrue(Shell.LINUX); |
| FileContext.getLocalFSFileContext().delete( |
| new Path(TEST_ROOT_DIR.getAbsolutePath()), true); |
| } |
| |
| @Test(timeout = 30000) |
| @SuppressWarnings("deprecation") |
| public void testProcessTree() throws Exception { |
| try { |
| Assert.assertTrue(ProcfsBasedProcessTree.isAvailable()); |
| } catch (Exception e) { |
| LOG.info(StringUtils.stringifyException(e)); |
| Assert.assertTrue("ProcfsBaseProcessTree should be available on Linux", |
| false); |
| return; |
| } |
| // create shell script |
| Random rm = new Random(); |
| File tempFile = |
| new File(TEST_ROOT_DIR, getClass().getName() + "_shellScript_" |
| + rm.nextInt() + ".sh"); |
| tempFile.deleteOnExit(); |
| shellScript = TEST_ROOT_DIR + File.separator + tempFile.getName(); |
| |
| // create pid file |
| tempFile = |
| new File(TEST_ROOT_DIR, getClass().getName() + "_pidFile_" |
| + rm.nextInt() + ".pid"); |
| tempFile.deleteOnExit(); |
| pidFile = TEST_ROOT_DIR + File.separator + tempFile.getName(); |
| |
| lowestDescendant = |
| TEST_ROOT_DIR + File.separator + "lowestDescendantPidFile"; |
| lostDescendant = |
| TEST_ROOT_DIR + File.separator + "lostDescendantPidFile"; |
| |
| // write to shell-script |
| File file = new File(shellScript); |
| FileUtils.writeStringToFile(file, "# rogue task\n" + "sleep 1\n" + "echo hello\n" |
| + "if [ $1 -ne 0 ]\n" + "then\n" + " sh " + shellScript |
| + " $(($1-1))\n" + "else\n" + " echo $$ > " + lowestDescendant + "\n" |
| + "(sleep 300&\n" |
| + "echo $! > " + lostDescendant + ")\n" |
| + " while true\n do\n" + " sleep 5\n" + " done\n" + "fi"); |
| |
| Thread t = new RogueTaskThread(); |
| t.start(); |
| String pid = getRogueTaskPID(); |
| LOG.info("Root process pid: " + pid); |
| ProcfsBasedProcessTree p = createProcessTree(pid); |
| p.updateProcessTree(); // initialize |
| LOG.info("ProcessTree: " + p.toString()); |
| |
| File leaf = new File(lowestDescendant); |
| // wait till lowest descendant process of Rougue Task starts execution |
| while (!leaf.exists()) { |
| try { |
| Thread.sleep(500); |
| } catch (InterruptedException ie) { |
| break; |
| } |
| } |
| |
| p.updateProcessTree(); // reconstruct |
| LOG.info("ProcessTree: " + p.toString()); |
| |
| // Verify the orphaned pid is In process tree |
| String lostpid = getPidFromPidFile(lostDescendant); |
| LOG.info("Orphaned pid: " + lostpid); |
| Assert.assertTrue("Child process owned by init escaped process tree.", |
| p.contains(lostpid)); |
| |
| // Get the process-tree dump |
| String processTreeDump = p.getProcessTreeDump(); |
| |
| // destroy the process and all its subprocesses |
| destroyProcessTree(pid); |
| |
| boolean isAlive = true; |
| for (int tries = 100; tries > 0; tries--) { |
| if (isSetsidAvailable()) {// whole processtree |
| isAlive = isAnyProcessInTreeAlive(p); |
| } else {// process |
| isAlive = isAlive(pid); |
| } |
| if (!isAlive) { |
| break; |
| } |
| Thread.sleep(100); |
| } |
| if (isAlive) { |
| fail("ProcessTree shouldn't be alive"); |
| } |
| |
| LOG.info("Process-tree dump follows: \n" + processTreeDump); |
| Assert.assertTrue("Process-tree dump doesn't start with a proper header", |
| processTreeDump.startsWith("\t|- PID PPID PGRPID SESSID CMD_NAME " |
| + "USER_MODE_TIME(MILLIS) SYSTEM_TIME(MILLIS) VMEM_USAGE(BYTES) " |
| + "RSSMEM_USAGE(PAGES) FULL_CMD_LINE\n")); |
| for (int i = N; i >= 0; i--) { |
| String cmdLineDump = |
| "\\|- [0-9]+ [0-9]+ [0-9]+ [0-9]+ \\(sh\\)" |
| + " [0-9]+ [0-9]+ [0-9]+ [0-9]+ sh " + shellScript + " " + i; |
| Pattern pat = Pattern.compile(cmdLineDump); |
| Matcher mat = pat.matcher(processTreeDump); |
| Assert.assertTrue("Process-tree dump doesn't contain the cmdLineDump of " |
| + i + "th process!", mat.find()); |
| } |
| |
| // Not able to join thread sometimes when forking with large N. |
| try { |
| t.join(2000); |
| LOG.info("RogueTaskThread successfully joined."); |
| } catch (InterruptedException ie) { |
| LOG.info("Interrupted while joining RogueTaskThread."); |
| } |
| |
| // ProcessTree is gone now. Any further calls should be sane. |
| p.updateProcessTree(); |
| Assert.assertFalse("ProcessTree must have been gone", isAlive(pid)); |
| Assert.assertTrue( |
| "vmem for the gone-process is " + p.getVirtualMemorySize() |
| + " . It should be UNAVAILABLE(-1).", |
| p.getVirtualMemorySize() == UNAVAILABLE); |
| Assert.assertTrue( |
| "vmem (old API) for the gone-process is " + p.getCumulativeVmem() |
| + " . It should be UNAVAILABLE(-1).", |
| p.getCumulativeVmem() == UNAVAILABLE); |
| Assert.assertEquals("[ ]", p.toString()); |
| } |
| |
| protected ProcfsBasedProcessTree createProcessTree(String pid) { |
| return new ProcfsBasedProcessTree(pid); |
| } |
| |
| protected ProcfsBasedProcessTree createProcessTree(String pid, |
| String procfsRootDir, Clock clock) { |
| return new ProcfsBasedProcessTree(pid, procfsRootDir, clock); |
| } |
| |
| protected void destroyProcessTree(String pid) throws IOException { |
| sendSignal("-"+pid, 9); |
| } |
| |
| /** |
| * Get PID from a pid-file. |
| * |
| * @param pidFileName |
| * Name of the pid-file. |
| * @return the PID string read from the pid-file. Returns null if the |
| * pidFileName points to a non-existing file or if read fails from the |
| * file. |
| */ |
| public static String getPidFromPidFile(String pidFileName) { |
| BufferedReader pidFile = null; |
| FileReader fReader = null; |
| String pid = null; |
| |
| try { |
| fReader = new FileReader(pidFileName); |
| pidFile = new BufferedReader(fReader); |
| } catch (FileNotFoundException f) { |
| LOG.debug("PidFile doesn't exist : " + pidFileName); |
| return pid; |
| } |
| |
| try { |
| pid = pidFile.readLine(); |
| } catch (IOException i) { |
| LOG.error("Failed to read from " + pidFileName); |
| } finally { |
| try { |
| if (fReader != null) { |
| fReader.close(); |
| } |
| try { |
| if (pidFile != null) { |
| pidFile.close(); |
| } |
| } catch (IOException i) { |
| LOG.warn("Error closing the stream " + pidFile); |
| } |
| } catch (IOException i) { |
| LOG.warn("Error closing the stream " + fReader); |
| } |
| } |
| return pid; |
| } |
| |
| public static class ProcessStatInfo { |
| // sample stat in a single line : 3910 (gpm) S 1 3910 3910 0 -1 4194624 |
| // 83 0 0 0 0 0 0 0 16 0 1 0 7852 2408448 88 4294967295 134512640 |
| // 134590050 3220521392 3220520036 10975138 0 0 4096 134234626 |
| // 4294967295 0 0 17 1 0 0 |
| String pid; |
| String name; |
| String ppid; |
| String pgrpId; |
| String session; |
| String vmem = "0"; |
| String rssmemPage = "0"; |
| String utime = "0"; |
| String stime = "0"; |
| |
| public ProcessStatInfo(String[] statEntries) { |
| pid = statEntries[0]; |
| name = statEntries[1]; |
| ppid = statEntries[2]; |
| pgrpId = statEntries[3]; |
| session = statEntries[4]; |
| vmem = statEntries[5]; |
| if (statEntries.length > 6) { |
| rssmemPage = statEntries[6]; |
| } |
| if (statEntries.length > 7) { |
| utime = statEntries[7]; |
| stime = statEntries[8]; |
| } |
| } |
| |
| // construct a line that mimics the procfs stat file. |
| // all unused numerical entries are set to 0. |
| public String getStatLine() { |
| return String.format("%s (%s) S %s %s %s 0 0 0" |
| + " 0 0 0 0 %s %s 0 0 0 0 0 0 0 %s %s 0 0" + " 0 0 0 0 0 0 0 0" |
| + " 0 0 0 0 0", pid, name, ppid, pgrpId, session, utime, stime, vmem, |
| rssmemPage); |
| } |
| } |
| |
| public ProcessSmapMemoryInfo constructMemoryMappingInfo(String address, |
| String[] entries) { |
| ProcessSmapMemoryInfo info = new ProcessSmapMemoryInfo(address); |
| info.setMemInfo(MemInfo.SIZE.name(), entries[0]); |
| info.setMemInfo(MemInfo.RSS.name(), entries[1]); |
| info.setMemInfo(MemInfo.PSS.name(), entries[2]); |
| info.setMemInfo(MemInfo.SHARED_CLEAN.name(), entries[3]); |
| info.setMemInfo(MemInfo.SHARED_DIRTY.name(), entries[4]); |
| info.setMemInfo(MemInfo.PRIVATE_CLEAN.name(), entries[5]); |
| info.setMemInfo(MemInfo.PRIVATE_DIRTY.name(), entries[6]); |
| info.setMemInfo(MemInfo.REFERENCED.name(), entries[7]); |
| info.setMemInfo(MemInfo.ANONYMOUS.name(), entries[8]); |
| info.setMemInfo(MemInfo.ANON_HUGE_PAGES.name(), entries[9]); |
| info.setMemInfo(MemInfo.SWAP.name(), entries[10]); |
| info.setMemInfo(MemInfo.KERNEL_PAGE_SIZE.name(), entries[11]); |
| info.setMemInfo(MemInfo.MMU_PAGE_SIZE.name(), entries[12]); |
| return info; |
| } |
| |
| public void createMemoryMappingInfo(ProcessTreeSmapMemInfo[] procMemInfo) { |
| for (int i = 0; i < procMemInfo.length; i++) { |
| // Construct 4 memory mappings per process. |
| // As per min(Shared_Dirty, Pss) + Private_Clean + Private_Dirty |
| // and not including r--s, r-xs, we should get 100 KB per process |
| List<ProcessSmapMemoryInfo> memoryMappingList = |
| procMemInfo[i].getMemoryInfoList(); |
| memoryMappingList.add(constructMemoryMappingInfo( |
| "7f56c177c000-7f56c177d000 " |
| + "rw-p 00010000 08:02 40371558 " |
| + "/grid/0/jdk1.7.0_25/jre/lib/amd64/libnio.so", |
| // Format: size, rss, pss, shared_clean, shared_dirty, private_clean |
| // private_dirty, referenced, anon, anon-huge-pages, swap, |
| // kernel_page_size, mmu_page_size |
| new String[] {"4", "4", "25", "4", "25", "15", "10", "4", "10", "0", |
| "0", "4", "4"})); |
| memoryMappingList.add(constructMemoryMappingInfo( |
| "7fb09382e000-7fb09382f000 r--s 00003000 " + "08:02 25953545", |
| new String[] {"4", "4", "25", "4", "0", "15", "10", "4", "10", "0", |
| "0", "4", "4"})); |
| memoryMappingList.add(constructMemoryMappingInfo( |
| "7e8790000-7e8b80000 r-xs 00000000 00:00 0", new String[] {"4", "4", |
| "25", "4", "0", "15", "10", "4", "10", "0", "0", "4", "4"})); |
| memoryMappingList.add(constructMemoryMappingInfo( |
| "7da677000-7e0dcf000 rw-p 00000000 00:00 0", new String[] {"4", "4", |
| "25", "4", "50", "15", "10", "4", "10", "0", "0", "4", "4"})); |
| } |
| } |
| |
| /** |
| * A basic test that creates a few process directories and writes stat files. |
| * Verifies that the cpu time and memory is correctly computed. |
| * |
| * @throws IOException |
| * if there was a problem setting up the fake procfs directories or |
| * files. |
| */ |
| @Test(timeout = 30000) |
| @SuppressWarnings("deprecation") |
| public void testCpuAndMemoryForProcessTree() throws IOException { |
| |
| // test processes |
| String[] pids = { "100", "200", "300", "400" }; |
| ControlledClock testClock = new ControlledClock(); |
| testClock.setTime(0); |
| // create the fake procfs root directory. |
| File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
| |
| try { |
| setupProcfsRootDir(procfsRootDir); |
| setupPidDirs(procfsRootDir, pids); |
| |
| // create stat objects. |
| // assuming processes 100, 200, 300 are in tree and 400 is not. |
| ProcessStatInfo[] procInfos = new ProcessStatInfo[4]; |
| procInfos[0] = |
| new ProcessStatInfo(new String[]{"100", "proc1", "1", "100", "100", |
| "100000", "100", "1000", "200"}); |
| procInfos[1] = |
| new ProcessStatInfo(new String[]{"200", "process two", "100", "100", |
| "100", "200000", "200", "2000", "400"}); |
| procInfos[2] = |
| new ProcessStatInfo(new String[]{"300", "proc(3)", "200", "100", |
| "100", "300000", "300", "3000", "600"}); |
| procInfos[3] = |
| new ProcessStatInfo(new String[]{"400", "proc4", "1", "400", "400", |
| "400000", "400", "4000", "800"}); |
| |
| ProcessTreeSmapMemInfo[] memInfo = new ProcessTreeSmapMemInfo[4]; |
| memInfo[0] = new ProcessTreeSmapMemInfo("100"); |
| memInfo[1] = new ProcessTreeSmapMemInfo("200"); |
| memInfo[2] = new ProcessTreeSmapMemInfo("300"); |
| memInfo[3] = new ProcessTreeSmapMemInfo("400"); |
| createMemoryMappingInfo(memInfo); |
| writeStatFiles(procfsRootDir, pids, procInfos, memInfo); |
| |
| // crank up the process tree class. |
| Configuration conf = new Configuration(); |
| ProcfsBasedProcessTree processTree = |
| createProcessTree("100", procfsRootDir.getAbsolutePath(), testClock); |
| processTree.setConf(conf); |
| // build the process tree. |
| processTree.updateProcessTree(); |
| |
| // verify virtual memory |
| Assert.assertEquals("Virtual memory does not match", 600000L, |
| processTree.getVirtualMemorySize()); |
| |
| // verify rss memory |
| long cumuRssMem = |
| ProcfsBasedProcessTree.PAGE_SIZE > 0 |
| ? 600L * ProcfsBasedProcessTree.PAGE_SIZE : |
| ResourceCalculatorProcessTree.UNAVAILABLE; |
| Assert.assertEquals("rss memory does not match", cumuRssMem, |
| processTree.getRssMemorySize()); |
| // verify old API |
| Assert.assertEquals("rss memory (old API) does not match", cumuRssMem, |
| processTree.getCumulativeRssmem()); |
| |
| // verify cumulative cpu time |
| long cumuCpuTime = |
| ProcfsBasedProcessTree.JIFFY_LENGTH_IN_MILLIS > 0 |
| ? 7200L * ProcfsBasedProcessTree.JIFFY_LENGTH_IN_MILLIS : 0L; |
| Assert.assertEquals("Cumulative cpu time does not match", cumuCpuTime, |
| processTree.getCumulativeCpuTime()); |
| |
| // verify CPU usage |
| Assert.assertEquals("Percent CPU time should be set to -1 initially", |
| -1.0, processTree.getCpuUsagePercent(), |
| 0.01); |
| |
| // Check by enabling smaps |
| setSmapsInProceTree(processTree, true); |
| // anon (exclude r-xs,r--s) |
| Assert.assertEquals("rss memory does not match", |
| (20 * KB_TO_BYTES * 3), processTree.getRssMemorySize()); |
| // verify old API |
| Assert.assertEquals("rss memory (old API) does not match", |
| (20 * KB_TO_BYTES * 3), processTree.getCumulativeRssmem()); |
| |
| // test the cpu time again to see if it cumulates |
| procInfos[0] = |
| new ProcessStatInfo(new String[]{"100", "proc1", "1", "100", "100", |
| "100000", "100", "2000", "300"}); |
| procInfos[1] = |
| new ProcessStatInfo(new String[]{"200", "process two", "100", "100", |
| "100", "200000", "200", "3000", "500"}); |
| writeStatFiles(procfsRootDir, pids, procInfos, memInfo); |
| |
| long elapsedTimeBetweenUpdatesMsec = 200000; |
| testClock.setTime(elapsedTimeBetweenUpdatesMsec); |
| // build the process tree. |
| processTree.updateProcessTree(); |
| |
| // verify cumulative cpu time again |
| long prevCumuCpuTime = cumuCpuTime; |
| cumuCpuTime = |
| ProcfsBasedProcessTree.JIFFY_LENGTH_IN_MILLIS > 0 |
| ? 9400L * ProcfsBasedProcessTree.JIFFY_LENGTH_IN_MILLIS : 0L; |
| Assert.assertEquals("Cumulative cpu time does not match", cumuCpuTime, |
| processTree.getCumulativeCpuTime()); |
| |
| double expectedCpuUsagePercent = |
| (ProcfsBasedProcessTree.JIFFY_LENGTH_IN_MILLIS > 0) ? |
| (cumuCpuTime - prevCumuCpuTime) * 100.0 / |
| elapsedTimeBetweenUpdatesMsec : 0; |
| // expectedCpuUsagePercent is given by (94000L - 72000) * 100/ |
| // 200000; |
| // which in this case is 11. Lets verify that first |
| Assert.assertEquals(11, expectedCpuUsagePercent, 0.001); |
| Assert.assertEquals("Percent CPU time is not correct expected " + |
| expectedCpuUsagePercent, expectedCpuUsagePercent, |
| processTree.getCpuUsagePercent(), |
| 0.01); |
| } finally { |
| FileUtil.fullyDelete(procfsRootDir); |
| } |
| } |
| |
| private void setSmapsInProceTree(ProcfsBasedProcessTree processTree, |
| boolean enableFlag) { |
| Configuration conf = processTree.getConf(); |
| if (conf == null) { |
| conf = new Configuration(); |
| } |
| conf.setBoolean(YarnConfiguration.PROCFS_USE_SMAPS_BASED_RSS_ENABLED, enableFlag); |
| processTree.setConf(conf); |
| processTree.updateProcessTree(); |
| } |
| |
| /** |
| * Tests that cumulative memory is computed only for processes older than a |
| * given age. |
| * |
| * @throws IOException |
| * if there was a problem setting up the fake procfs directories or |
| * files. |
| */ |
| @Test(timeout = 30000) |
| public void testMemForOlderProcesses() throws IOException { |
| testMemForOlderProcesses(false); |
| testMemForOlderProcesses(true); |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void testMemForOlderProcesses(boolean smapEnabled) throws IOException { |
| // initial list of processes |
| String[] pids = { "100", "200", "300", "400" }; |
| // create the fake procfs root directory. |
| File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
| |
| try { |
| setupProcfsRootDir(procfsRootDir); |
| setupPidDirs(procfsRootDir, pids); |
| |
| // create stat objects. |
| // assuming 100, 200 and 400 are in tree, 300 is not. |
| ProcessStatInfo[] procInfos = new ProcessStatInfo[4]; |
| procInfos[0] = |
| new ProcessStatInfo(new String[]{"100", "proc1", "1", "100", "100", |
| "100000", "100"}); |
| procInfos[1] = |
| new ProcessStatInfo(new String[]{"200", "process two", "100", "100", |
| "100", "200000", "200"}); |
| procInfos[2] = |
| new ProcessStatInfo(new String[]{"300", "proc(3)", "1", "300", "300", |
| "300000", "300"}); |
| procInfos[3] = |
| new ProcessStatInfo(new String[]{"400", "proc4", "100", "100", |
| "100", "400000", "400"}); |
| // write smap information invariably for testing |
| ProcessTreeSmapMemInfo[] memInfo = new ProcessTreeSmapMemInfo[4]; |
| memInfo[0] = new ProcessTreeSmapMemInfo("100"); |
| memInfo[1] = new ProcessTreeSmapMemInfo("200"); |
| memInfo[2] = new ProcessTreeSmapMemInfo("300"); |
| memInfo[3] = new ProcessTreeSmapMemInfo("400"); |
| createMemoryMappingInfo(memInfo); |
| writeStatFiles(procfsRootDir, pids, procInfos, memInfo); |
| |
| // crank up the process tree class. |
| ProcfsBasedProcessTree processTree = |
| createProcessTree("100", procfsRootDir.getAbsolutePath(), |
| SystemClock.getInstance()); |
| setSmapsInProceTree(processTree, smapEnabled); |
| |
| // verify virtual memory |
| Assert.assertEquals("Virtual memory does not match", 700000L, |
| processTree.getVirtualMemorySize()); |
| Assert.assertEquals("Virtual memory (old API) does not match", 700000L, |
| processTree.getCumulativeVmem()); |
| |
| // write one more process as child of 100. |
| String[] newPids = { "500" }; |
| setupPidDirs(procfsRootDir, newPids); |
| |
| ProcessStatInfo[] newProcInfos = new ProcessStatInfo[1]; |
| newProcInfos[0] = |
| new ProcessStatInfo(new String[] { "500", "proc5", "100", "100", |
| "100", "500000", "500" }); |
| ProcessTreeSmapMemInfo[] newMemInfos = new ProcessTreeSmapMemInfo[1]; |
| newMemInfos[0] = new ProcessTreeSmapMemInfo("500"); |
| createMemoryMappingInfo(newMemInfos); |
| writeStatFiles(procfsRootDir, newPids, newProcInfos, newMemInfos); |
| |
| // check memory includes the new process. |
| processTree.updateProcessTree(); |
| Assert.assertEquals("vmem does not include new process", |
| 1200000L, processTree.getVirtualMemorySize()); |
| Assert.assertEquals("vmem (old API) does not include new process", |
| 1200000L, processTree.getCumulativeVmem()); |
| if (!smapEnabled) { |
| long cumuRssMem = |
| ProcfsBasedProcessTree.PAGE_SIZE > 0 |
| ? 1200L * ProcfsBasedProcessTree.PAGE_SIZE : |
| ResourceCalculatorProcessTree.UNAVAILABLE; |
| Assert.assertEquals("rssmem does not include new process", |
| cumuRssMem, processTree.getRssMemorySize()); |
| // verify old API |
| Assert.assertEquals("rssmem (old API) does not include new process", |
| cumuRssMem, processTree.getCumulativeRssmem()); |
| } else { |
| Assert.assertEquals("rssmem does not include new process", |
| 20 * KB_TO_BYTES * 4, processTree.getRssMemorySize()); |
| // verify old API |
| Assert.assertEquals("rssmem (old API) does not include new process", |
| 20 * KB_TO_BYTES * 4, processTree.getCumulativeRssmem()); |
| } |
| |
| // however processes older than 1 iteration will retain the older value |
| Assert.assertEquals( |
| "vmem shouldn't have included new process", 700000L, |
| processTree.getVirtualMemorySize(1)); |
| // verify old API |
| Assert.assertEquals( |
| "vmem (old API) shouldn't have included new process", 700000L, |
| processTree.getCumulativeVmem(1)); |
| if (!smapEnabled) { |
| long cumuRssMem = |
| ProcfsBasedProcessTree.PAGE_SIZE > 0 |
| ? 700L * ProcfsBasedProcessTree.PAGE_SIZE : |
| ResourceCalculatorProcessTree.UNAVAILABLE; |
| Assert.assertEquals( |
| "rssmem shouldn't have included new process", cumuRssMem, |
| processTree.getRssMemorySize(1)); |
| // Verify old API |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new process", cumuRssMem, |
| processTree.getCumulativeRssmem(1)); |
| } else { |
| Assert.assertEquals( |
| "rssmem shouldn't have included new process", |
| 20 * KB_TO_BYTES * 3, processTree.getRssMemorySize(1)); |
| // Verify old API |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new process", |
| 20 * KB_TO_BYTES * 3, processTree.getCumulativeRssmem(1)); |
| } |
| |
| // one more process |
| newPids = new String[] { "600" }; |
| setupPidDirs(procfsRootDir, newPids); |
| |
| newProcInfos = new ProcessStatInfo[1]; |
| newProcInfos[0] = |
| new ProcessStatInfo(new String[] { "600", "proc6", "100", "100", |
| "100", "600000", "600" }); |
| newMemInfos = new ProcessTreeSmapMemInfo[1]; |
| newMemInfos[0] = new ProcessTreeSmapMemInfo("600"); |
| createMemoryMappingInfo(newMemInfos); |
| writeStatFiles(procfsRootDir, newPids, newProcInfos, newMemInfos); |
| |
| // refresh process tree |
| processTree.updateProcessTree(); |
| |
| // processes older than 2 iterations should be same as before. |
| Assert.assertEquals( |
| "vmem shouldn't have included new processes", 700000L, |
| processTree.getVirtualMemorySize(2)); |
| // verify old API |
| Assert.assertEquals( |
| "vmem (old API) shouldn't have included new processes", 700000L, |
| processTree.getCumulativeVmem(2)); |
| if (!smapEnabled) { |
| long cumuRssMem = |
| ProcfsBasedProcessTree.PAGE_SIZE > 0 |
| ? 700L * ProcfsBasedProcessTree.PAGE_SIZE : |
| ResourceCalculatorProcessTree.UNAVAILABLE; |
| Assert.assertEquals( |
| "rssmem shouldn't have included new processes", |
| cumuRssMem, processTree.getRssMemorySize(2)); |
| // Verify old API |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new processes", |
| cumuRssMem, processTree.getCumulativeRssmem(2)); |
| } else { |
| Assert.assertEquals( |
| "rssmem shouldn't have included new processes", |
| 20 * KB_TO_BYTES * 3, processTree.getRssMemorySize(2)); |
| // Verify old API |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new processes", |
| 20 * KB_TO_BYTES * 3, processTree.getCumulativeRssmem(2)); |
| } |
| |
| // processes older than 1 iteration should not include new process, |
| // but include process 500 |
| Assert.assertEquals( |
| "vmem shouldn't have included new processes", 1200000L, |
| processTree.getVirtualMemorySize(1)); |
| // verify old API |
| Assert.assertEquals( |
| "vmem (old API) shouldn't have included new processes", 1200000L, |
| processTree.getCumulativeVmem(1)); |
| if (!smapEnabled) { |
| long cumuRssMem = |
| ProcfsBasedProcessTree.PAGE_SIZE > 0 |
| ? 1200L * ProcfsBasedProcessTree.PAGE_SIZE : |
| ResourceCalculatorProcessTree.UNAVAILABLE; |
| Assert.assertEquals( |
| "rssmem shouldn't have included new processes", |
| cumuRssMem, processTree.getRssMemorySize(1)); |
| // verify old API |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new processes", |
| cumuRssMem, processTree.getCumulativeRssmem(1)); |
| } else { |
| Assert.assertEquals( |
| "rssmem shouldn't have included new processes", |
| 20 * KB_TO_BYTES * 4, processTree.getRssMemorySize(1)); |
| Assert.assertEquals( |
| "rssmem (old API) shouldn't have included new processes", |
| 20 * KB_TO_BYTES * 4, processTree.getCumulativeRssmem(1)); |
| } |
| |
| // no processes older than 3 iterations |
| Assert.assertEquals( |
| "Getting non-zero vmem for processes older than 3 iterations", |
| 0, processTree.getVirtualMemorySize(3)); |
| // verify old API |
| Assert.assertEquals( |
| "Getting non-zero vmem (old API) for processes older than 3 iterations", |
| 0, processTree.getCumulativeVmem(3)); |
| Assert.assertEquals( |
| "Getting non-zero rssmem for processes older than 3 iterations", |
| 0, processTree.getRssMemorySize(3)); |
| // verify old API |
| Assert.assertEquals( |
| "Getting non-zero rssmem (old API) for processes older than 3 iterations", |
| 0, processTree.getCumulativeRssmem(3)); |
| } finally { |
| FileUtil.fullyDelete(procfsRootDir); |
| } |
| } |
| |
| /** |
| * Verifies ProcfsBasedProcessTree.checkPidPgrpidForMatch() in case of |
| * 'constructProcessInfo() returning null' by not writing stat file for the |
| * mock process |
| * |
| * @throws IOException |
| * if there was a problem setting up the fake procfs directories or |
| * files. |
| */ |
| @Test(timeout = 30000) |
| public void testDestroyProcessTree() throws IOException { |
| // test process |
| String pid = "100"; |
| // create the fake procfs root directory. |
| File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
| |
| try { |
| setupProcfsRootDir(procfsRootDir); |
| |
| // crank up the process tree class. |
| createProcessTree(pid, procfsRootDir.getAbsolutePath(), |
| SystemClock.getInstance()); |
| |
| // Let us not create stat file for pid 100. |
| Assert.assertTrue(ProcfsBasedProcessTree.checkPidPgrpidForMatch(pid, |
| procfsRootDir.getAbsolutePath())); |
| } finally { |
| FileUtil.fullyDelete(procfsRootDir); |
| } |
| } |
| |
| /** |
| * Test the correctness of process-tree dump. |
| * |
| * @throws IOException |
| */ |
| @Test(timeout = 30000) |
| public void testProcessTreeDump() throws IOException { |
| |
| String[] pids = { "100", "200", "300", "400", "500", "600" }; |
| |
| File procfsRootDir = new File(TEST_ROOT_DIR, "proc"); |
| |
| try { |
| setupProcfsRootDir(procfsRootDir); |
| setupPidDirs(procfsRootDir, pids); |
| |
| int numProcesses = pids.length; |
| // Processes 200, 300, 400 and 500 are descendants of 100. 600 is not. |
| ProcessStatInfo[] procInfos = new ProcessStatInfo[numProcesses]; |
| procInfos[0] = |
| new ProcessStatInfo(new String[]{"100", "proc1", "1", "100", "100", |
| "100000", "100", "1000", "200"}); |
| procInfos[1] = |
| new ProcessStatInfo(new String[]{"200", "process two", "100", "100", |
| "100", "200000", "200", "2000", "400"}); |
| procInfos[2] = |
| new ProcessStatInfo(new String[]{"300", "proc(3)", "200", "100", |
| "100", "300000", "300", "3000", "600"}); |
| procInfos[3] = |
| new ProcessStatInfo(new String[]{"400", "proc4", "200", "100", |
| "100", "400000", "400", "4000", "800"}); |
| procInfos[4] = |
| new ProcessStatInfo(new String[]{"500", "proc5", "400", "100", |
| "100", "400000", "400", "4000", "800"}); |
| procInfos[5] = |
| new ProcessStatInfo(new String[]{"600", "proc6", "1", "1", "1", |
| "400000", "400", "4000", "800"}); |
| |
| ProcessTreeSmapMemInfo[] memInfos = new ProcessTreeSmapMemInfo[6]; |
| memInfos[0] = new ProcessTreeSmapMemInfo("100"); |
| memInfos[1] = new ProcessTreeSmapMemInfo("200"); |
| memInfos[2] = new ProcessTreeSmapMemInfo("300"); |
| memInfos[3] = new ProcessTreeSmapMemInfo("400"); |
| memInfos[4] = new ProcessTreeSmapMemInfo("500"); |
| memInfos[5] = new ProcessTreeSmapMemInfo("600"); |
| |
| String[] cmdLines = new String[numProcesses]; |
| cmdLines[0] = "proc1 arg1 arg2"; |
| cmdLines[1] = "process two arg3 arg4"; |
| cmdLines[2] = "proc(3) arg5 arg6"; |
| cmdLines[3] = "proc4 arg7 arg8"; |
| cmdLines[4] = "proc5 arg9 arg10"; |
| cmdLines[5] = "proc6 arg11 arg12"; |
| |
| createMemoryMappingInfo(memInfos); |
| writeStatFiles(procfsRootDir, pids, procInfos, memInfos); |
| writeCmdLineFiles(procfsRootDir, pids, cmdLines); |
| |
| ProcfsBasedProcessTree processTree = |
| createProcessTree("100", procfsRootDir.getAbsolutePath(), |
| SystemClock.getInstance()); |
| // build the process tree. |
| processTree.updateProcessTree(); |
| |
| // Get the process-tree dump |
| String processTreeDump = processTree.getProcessTreeDump(); |
| |
| LOG.info("Process-tree dump follows: \n" + processTreeDump); |
| Assert.assertTrue("Process-tree dump doesn't start with a proper header", |
| processTreeDump.startsWith("\t|- PID PPID PGRPID SESSID CMD_NAME " |
| + "USER_MODE_TIME(MILLIS) SYSTEM_TIME(MILLIS) VMEM_USAGE(BYTES) " |
| + "RSSMEM_USAGE(PAGES) FULL_CMD_LINE\n")); |
| for (int i = 0; i < 5; i++) { |
| ProcessStatInfo p = procInfos[i]; |
| Assert.assertTrue( |
| "Process-tree dump doesn't contain the cmdLineDump of process " |
| + p.pid, |
| processTreeDump.contains("\t|- " + p.pid + " " + p.ppid + " " |
| + p.pgrpId + " " + p.session + " (" + p.name + ") " + p.utime |
| + " " + p.stime + " " + p.vmem + " " + p.rssmemPage + " " |
| + cmdLines[i])); |
| } |
| |
| // 600 should not be in the dump |
| ProcessStatInfo p = procInfos[5]; |
| Assert.assertFalse( |
| "Process-tree dump shouldn't contain the cmdLineDump of process " |
| + p.pid, |
| processTreeDump.contains("\t|- " + p.pid + " " + p.ppid + " " |
| + p.pgrpId + " " + p.session + " (" + p.name + ") " + p.utime + " " |
| + p.stime + " " + p.vmem + " " + cmdLines[5])); |
| } finally { |
| FileUtil.fullyDelete(procfsRootDir); |
| } |
| } |
| |
| protected static boolean isSetsidAvailable() { |
| ShellCommandExecutor shexec = null; |
| boolean setsidSupported = true; |
| try { |
| String[] args = { "setsid", "bash", "-c", "echo $$" }; |
| shexec = new ShellCommandExecutor(args); |
| shexec.execute(); |
| } catch (IOException ioe) { |
| LOG.warn("setsid is not available on this machine. So not using it."); |
| setsidSupported = false; |
| } finally { // handle the exit code |
| LOG.info("setsid exited with exit code " + shexec.getExitCode()); |
| } |
| return setsidSupported; |
| } |
| |
| /** |
| * Is the root-process alive? Used only in tests. |
| * |
| * @return true if the root-process is alive, false otherwise. |
| */ |
| private static boolean isAlive(String pid) { |
| try { |
| final String sigpid = isSetsidAvailable() ? "-" + pid : pid; |
| try { |
| sendSignal(sigpid, 0); |
| } catch (ExitCodeException e) { |
| return false; |
| } |
| return true; |
| } catch (IOException ignored) { |
| } |
| return false; |
| } |
| |
| private static void sendSignal(String pid, int signal) throws IOException { |
| ShellCommandExecutor shexec = null; |
| String[] arg = { "kill", "-" + signal, "--", pid }; |
| shexec = new ShellCommandExecutor(arg); |
| shexec.execute(); |
| } |
| |
| /** |
| * Is any of the subprocesses in the process-tree alive? Used only in tests. |
| * |
| * @return true if any of the processes in the process-tree is alive, false |
| * otherwise. |
| */ |
| private static boolean isAnyProcessInTreeAlive( |
| ProcfsBasedProcessTree processTree) { |
| for (String pId : processTree.getCurrentProcessIDs()) { |
| if (isAlive(pId)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Create a directory to mimic the procfs file system's root. |
| * |
| * @param procfsRootDir |
| * root directory to create. |
| * @throws IOException |
| * if could not delete the procfs root directory |
| */ |
| public static void setupProcfsRootDir(File procfsRootDir) throws IOException { |
| // cleanup any existing process root dir. |
| if (procfsRootDir.exists()) { |
| Assert.assertTrue(FileUtil.fullyDelete(procfsRootDir)); |
| } |
| |
| // create afresh |
| Assert.assertTrue(procfsRootDir.mkdirs()); |
| } |
| |
| /** |
| * Create PID directories under the specified procfs root directory |
| * |
| * @param procfsRootDir |
| * root directory of procfs file system |
| * @param pids |
| * the PID directories to create. |
| * @throws IOException |
| * If PID dirs could not be created |
| */ |
| public static void setupPidDirs(File procfsRootDir, String[] pids) |
| throws IOException { |
| for (String pid : pids) { |
| File pidDir = new File(procfsRootDir, pid); |
| pidDir.mkdir(); |
| if (!pidDir.exists()) { |
| throw new IOException("couldn't make process directory under " |
| + "fake procfs"); |
| } else { |
| LOG.info("created pid dir"); |
| } |
| } |
| } |
| |
| /** |
| * Write stat files under the specified pid directories with data setup in the |
| * corresponding ProcessStatInfo objects |
| * |
| * @param procfsRootDir |
| * root directory of procfs file system |
| * @param pids |
| * the PID directories under which to create the stat file |
| * @param procs |
| * corresponding ProcessStatInfo objects whose data should be written |
| * to the stat files. |
| * @throws IOException |
| * if stat files could not be written |
| */ |
| public static void writeStatFiles(File procfsRootDir, String[] pids, |
| ProcessStatInfo[] procs, ProcessTreeSmapMemInfo[] smaps) |
| throws IOException { |
| for (int i = 0; i < pids.length; i++) { |
| File statFile = |
| new File(new File(procfsRootDir, pids[i]), |
| ProcfsBasedProcessTree.PROCFS_STAT_FILE); |
| BufferedWriter bw = null; |
| try { |
| FileWriter fw = new FileWriter(statFile); |
| bw = new BufferedWriter(fw); |
| bw.write(procs[i].getStatLine()); |
| LOG.info("wrote stat file for " + pids[i] + " with contents: " |
| + procs[i].getStatLine()); |
| } finally { |
| // not handling exception - will throw an error and fail the test. |
| if (bw != null) { |
| bw.close(); |
| } |
| } |
| if (smaps != null) { |
| File smapFile = |
| new File(new File(procfsRootDir, pids[i]), |
| ProcfsBasedProcessTree.SMAPS); |
| bw = null; |
| try { |
| FileWriter fw = new FileWriter(smapFile); |
| bw = new BufferedWriter(fw); |
| bw.write(smaps[i].toString()); |
| bw.flush(); |
| LOG.info("wrote smap file for " + pids[i] + " with contents: " |
| + smaps[i].toString()); |
| } finally { |
| // not handling exception - will throw an error and fail the test. |
| if (bw != null) { |
| bw.close(); |
| } |
| } |
| } |
| } |
| } |
| |
| private static void writeCmdLineFiles(File procfsRootDir, String[] pids, |
| String[] cmdLines) throws IOException { |
| for (int i = 0; i < pids.length; i++) { |
| File statFile = |
| new File(new File(procfsRootDir, pids[i]), |
| ProcfsBasedProcessTree.PROCFS_CMDLINE_FILE); |
| BufferedWriter bw = null; |
| try { |
| bw = new BufferedWriter(new FileWriter(statFile)); |
| bw.write(cmdLines[i]); |
| LOG.info("wrote command-line file for " + pids[i] + " with contents: " |
| + cmdLines[i]); |
| } finally { |
| // not handling exception - will throw an error and fail the test. |
| if (bw != null) { |
| bw.close(); |
| } |
| } |
| } |
| } |
| } |