1 /*
   2  * Copyright (c) 2017, 2018, 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 
  26 package jdk.jfr.event.runtime;
  27 
  28 import com.sun.management.ThreadMXBean;
  29 import jdk.jfr.Recording;
  30 import jdk.jfr.consumer.RecordedEvent;
  31 import jdk.jfr.consumer.RecordedThread;
  32 import jdk.test.lib.Asserts;
  33 import jdk.test.lib.jfr.EventNames;
  34 import jdk.test.lib.jfr.Events;
  35 
  36 import java.lang.management.ManagementFactory;
  37 import java.time.Duration;
  38 import java.time.Instant;
  39 import java.util.Comparator;
  40 import java.util.Iterator;
  41 import java.util.List;
  42 import java.util.concurrent.BrokenBarrierException;
  43 import java.util.concurrent.CyclicBarrier;
  44 import java.util.stream.Collectors;
  45 
  46 /**
  47  * @test
  48  * @key jfr
  49  *
  50  * @library /lib /
  51  *
  52  *
  53  * @run main/othervm jdk.jfr.event.runtime.TestThreadCpuTimeEvent
  54  */
  55 
  56 /**
  57  */
  58 public class TestThreadCpuTimeEvent {
  59 
  60     public static void main(String[] args) throws Throwable {
  61         testSimple();
  62         testCompareWithMXBean();
  63         testEventAtThreadExit();
  64     }
  65 
  66     private static final long eventPeriodMillis = 50;
  67     private static final String cpuConsumerThreadName = "cpuConsumer";
  68 
  69     // The cpu consumer will run for eventPeriodMillis times this factor to ensure that we see some
  70     // events even if the scheduler isn't cooperating.
  71     private static final long cpuConsumerRunFactor = 10;
  72 
  73     // The cpu consumer will run at least this number of loops, even if it takes longer than
  74     // the requested period of time (in case the thread didn't get scheduled within the allotted time).
  75     private static final long cpuConsumerMinCount = 1000000;
  76 
  77     static class CpuConsumingThread extends Thread {
  78 
  79         Duration runTime;
  80         CyclicBarrier barrier;
  81         volatile long counter;
  82 
  83         CpuConsumingThread(Duration runTime, CyclicBarrier barrier, String threadName) {
  84             super(threadName);
  85             this.runTime = runTime;
  86             this.barrier = barrier;
  87         }
  88 
  89         CpuConsumingThread(Duration runTime, CyclicBarrier barrier) {
  90             this(runTime, barrier, cpuConsumerThreadName);
  91         }
  92 
  93         @Override
  94         public void run() {
  95             try {
  96                 while (true) {
  97                     barrier.await();
  98                     Instant start = Instant.now();
  99                     counter = 0;
 100                     while ((Duration.between(start, Instant.now()).compareTo(runTime) < 0) ||
 101                             (counter < cpuConsumerMinCount)) {
 102                         counter++;
 103                     }
 104                     barrier.await();
 105                 }
 106             } catch (BrokenBarrierException e) {
 107                 // Another thread has been interrupted - wait for us to be interrupted as well
 108                 while (!interrupted()) {
 109                     yield();
 110                 }
 111             } catch (InterruptedException e) {
 112                 // Normal way of stopping the thread
 113             }
 114         }
 115     }
 116 
 117     // For a given thread, check that accumulated processTime >= cpuTime >= userTime.
 118     // This may not hold for a single event instance due to differences in counter resolution
 119     static void verifyPerThreadInvariant(List<RecordedEvent> events, String threadName) {
 120         List<RecordedEvent> filteredEvents = events.stream()
 121                 .filter(e -> e.getThread().getJavaName().equals(threadName))
 122                 .sorted(Comparator.comparing(RecordedEvent::getStartTime))
 123                 .collect(Collectors.toList());
 124 
 125         int numCpus = Runtime.getRuntime().availableProcessors();
 126         Iterator<RecordedEvent> i = filteredEvents.iterator();
 127         while (i.hasNext()) {
 128             RecordedEvent event = i.next();
 129 
 130             Float systemLoad = (Float)event.getValue("system");
 131             Float userLoad = (Float)event.getValue("user");
 132 
 133             Asserts.assertLessThan(systemLoad + userLoad, 1.01f / numCpus); // 100% + rounding errors
 134         }
 135     }
 136 
 137     static Duration getAccumulatedTime(List<RecordedEvent> events, String threadName, String fieldName) {
 138         List<RecordedEvent> filteredEvents = events.stream()
 139                 .filter(e -> e.getThread().getJavaName().equals(threadName))
 140                 .sorted(Comparator.comparing(RecordedEvent::getStartTime))
 141                 .collect(Collectors.toList());
 142 
 143         int numCpus = Runtime.getRuntime().availableProcessors();
 144         Iterator<RecordedEvent> i = filteredEvents.iterator();
 145         RecordedEvent cur = i.next();
 146         Duration totalTime = Duration.ZERO;
 147         while (i.hasNext()) {
 148             RecordedEvent prev = cur;
 149             cur = i.next();
 150 
 151             Duration sampleTime = Duration.between(prev.getStartTime(), cur.getStartTime());
 152             Float load = (Float)cur.getValue(fieldName);
 153 
 154             // Adjust load to be thread-relative (fully loaded thread would give 100%)
 155             Float totalLoadForThread = load * numCpus;
 156             Duration threadTime = Duration.ofMillis((long) (sampleTime.toMillis() * totalLoadForThread));
 157             totalTime = totalTime.plus(threadTime);
 158         }
 159 
 160         return totalTime;
 161     }
 162 
 163     static List<RecordedEvent> generateEvents(int minimumEventCount, CyclicBarrier barrier) throws Throwable {
 164         int retryCount = 0;
 165 
 166         while (true) {
 167             Recording recording = new Recording();
 168 
 169             // Default period is once per chunk
 170             recording.enable(EventNames.ThreadCPULoad).withPeriod(Duration.ofMillis(eventPeriodMillis));
 171             recording.start();
 172 
 173             // Run a single pass
 174             barrier.await();
 175             barrier.await();
 176 
 177             recording.stop();
 178             List<RecordedEvent> events = Events.fromRecording(recording);
 179 
 180             long numEvents = events.stream()
 181                     .filter(e -> e.getThread().getJavaName().equals(cpuConsumerThreadName))
 182                     .count();
 183 
 184             // If the JFR periodicals thread is really starved, we may not get enough events.
 185             // In that case, we simply retry the operation.
 186             if (numEvents < minimumEventCount) {
 187                 System.out.println("Not enough events recorded, trying again...");
 188                 if (retryCount++ > 10) {
 189                     Asserts.fail("Retry count exceeded");
 190                     throw new RuntimeException();
 191                 }
 192             } else {
 193                 return events;
 194             }
 195         }
 196     }
 197 
 198     static void testSimple() throws Throwable {
 199         Duration testRunTime = Duration.ofMillis(eventPeriodMillis * cpuConsumerRunFactor);
 200         CyclicBarrier barrier = new CyclicBarrier(2);
 201         CpuConsumingThread thread = new CpuConsumingThread(testRunTime, barrier);
 202         thread.start();
 203 
 204         List<RecordedEvent> events = generateEvents(1, barrier);
 205         verifyPerThreadInvariant(events, cpuConsumerThreadName);
 206 
 207         thread.interrupt();
 208         thread.join();
 209     }
 210 
 211     static void testCompareWithMXBean() throws Throwable {
 212         Duration testRunTime = Duration.ofMillis(eventPeriodMillis * cpuConsumerRunFactor);
 213         CyclicBarrier barrier = new CyclicBarrier(2);
 214         CpuConsumingThread thread = new CpuConsumingThread(testRunTime, barrier);
 215         thread.start();
 216 
 217         List<RecordedEvent> beforeEvents = generateEvents(2, barrier);
 218         verifyPerThreadInvariant(beforeEvents, cpuConsumerThreadName);
 219 
 220         // Run a second single pass
 221         barrier.await();
 222         barrier.await();
 223 
 224         ThreadMXBean bean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
 225         Duration cpuTime = Duration.ofNanos(bean.getThreadCpuTime(thread.getId()));
 226         Duration userTime = Duration.ofNanos(bean.getThreadUserTime(thread.getId()));
 227 
 228         // Check something that should hold even in the presence of unfortunate scheduling
 229         Asserts.assertGreaterThanOrEqual(cpuTime.toMillis(), eventPeriodMillis);
 230         Asserts.assertGreaterThanOrEqual(userTime.toMillis(), eventPeriodMillis);
 231 
 232         Duration systemTimeBefore = getAccumulatedTime(beforeEvents, cpuConsumerThreadName, "system");
 233         Duration userTimeBefore = getAccumulatedTime(beforeEvents, cpuConsumerThreadName, "user");
 234         Duration cpuTimeBefore = userTimeBefore.plus(systemTimeBefore);
 235 
 236         Asserts.assertLessThan(cpuTimeBefore, cpuTime);
 237         Asserts.assertLessThan(userTimeBefore, userTime);
 238         Asserts.assertGreaterThan(cpuTimeBefore, Duration.ZERO);
 239 
 240         thread.interrupt();
 241         thread.join();
 242     }
 243 
 244     static void testEventAtThreadExit() throws Throwable {
 245         Recording recording = new Recording();
 246 
 247         recording.enable(EventNames.ThreadCPULoad).withPeriod(Duration.ofHours(10));
 248         recording.start();
 249 
 250         Duration testRunTime = Duration.ofMillis(eventPeriodMillis * cpuConsumerRunFactor);
 251         CyclicBarrier barrier = new CyclicBarrier(2);
 252         CpuConsumingThread thread = new CpuConsumingThread(testRunTime, barrier);
 253 
 254         // Run a single pass
 255         thread.start();
 256         barrier.await();
 257         barrier.await();
 258 
 259         thread.interrupt();
 260         thread.join();
 261 
 262         recording.stop();
 263 
 264         List<RecordedEvent> events = Events.fromRecording(recording);
 265         verifyPerThreadInvariant(events, cpuConsumerThreadName);
 266 
 267         int exitingCount = 0;
 268         for (RecordedEvent event : events) {
 269             RecordedThread eventThread = event.getThread();
 270             if (eventThread.getJavaName().equals(cpuConsumerThreadName)) {
 271                 exitingCount++;
 272             }
 273         }
 274         Asserts.assertEquals(exitingCount, 1);
 275     }
 276 }