diff --git a/common/src/main/java/com/dfsek/terra/profiler/Frame.java b/common/src/main/java/com/dfsek/terra/profiler/Frame.java new file mode 100644 index 000000000..7b7be66b5 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/Frame.java @@ -0,0 +1,24 @@ +package com.dfsek.terra.profiler; + +public class Frame { + private final String id; + private final long start; + + public Frame(String id) { + this.id = id; + this.start = System.nanoTime(); + } + + public String getId() { + return id; + } + + public long getStart() { + return start; + } + + @Override + public String toString() { + return id; + } +} diff --git a/common/src/main/java/com/dfsek/terra/profiler/Profiler.java b/common/src/main/java/com/dfsek/terra/profiler/Profiler.java new file mode 100644 index 000000000..ce0fa48c9 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/Profiler.java @@ -0,0 +1,21 @@ +package com.dfsek.terra.profiler; + +import java.util.Map; + +public interface Profiler { + ProfilerImpl INSTANCE = new ProfilerImpl(); + + static Profiler getInstance() { + return INSTANCE; + } + + void push(String frame); + + void pop(String frame); + + void start(); + + void stop(); + + Map getTimings(); +} diff --git a/common/src/main/java/com/dfsek/terra/profiler/ProfilerImpl.java b/common/src/main/java/com/dfsek/terra/profiler/ProfilerImpl.java new file mode 100644 index 000000000..c1410842c --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/ProfilerImpl.java @@ -0,0 +1,70 @@ +package com.dfsek.terra.profiler; + +import com.dfsek.terra.profiler.exception.MalformedStackException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +public class ProfilerImpl implements Profiler { + private static final ThreadLocal> THREAD_STACK = ThreadLocal.withInitial(Stack::new); + private static final ThreadLocal> TIMINGS = ThreadLocal.withInitial(HashMap::new); + private final List> accessibleThreadMaps = new ArrayList<>(); + private volatile boolean running = false; + + + protected ProfilerImpl() { + + } + + @Override + public void push(String frame) { + if(running) THREAD_STACK.get().push(new Frame(frame)); + } + + @Override + public void pop(String frame) { + if(running) { + long time = System.nanoTime(); + Stack stack = THREAD_STACK.get(); + + Map timingsMap = TIMINGS.get(); + + if(timingsMap.size() == 0) { + synchronized(accessibleThreadMaps) { + accessibleThreadMaps.add(timingsMap); + } + } + + Timings bottom = timingsMap.computeIfAbsent(stack.get(0).getId(), id -> new Timings()); + + for(int i = 1; i < stack.size(); i++) { + bottom = bottom.getSubItem(stack.get(i).getId()); + } + + Frame top = stack.pop(); + if(!top.getId().equals(frame)) throw new MalformedStackException("Expected " + frame + ", found " + top); + + bottom.addTime(time - top.getStart()); + } + } + + @Override + public void start() { + running = true; + } + + @Override + public void stop() { + running = false; + } + + @Override + public Map getTimings() { + Map map = new HashMap<>(); + accessibleThreadMaps.forEach(map::putAll); + return map; + } +} diff --git a/common/src/main/java/com/dfsek/terra/profiler/Timings.java b/common/src/main/java/com/dfsek/terra/profiler/Timings.java new file mode 100644 index 000000000..383211ef6 --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/Timings.java @@ -0,0 +1,56 @@ +package com.dfsek.terra.profiler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Timings { + private final Map subItems = new HashMap<>(); + + private final List timings = new ArrayList<>(); + + public void addTime(long time) { + timings.add(time); + } + + public List getTimings() { + return timings; + } + + public double average() { + return (double) timings.stream().reduce(0L, Long::sum) / timings.size(); + } + + public long max() { + return timings.stream().mapToLong(Long::longValue).max().orElse(0L); + } + + public long min() { + return timings.stream().mapToLong(Long::longValue).min().orElse(0L); + } + + public Timings getSubItem(String id) { + return subItems.computeIfAbsent(id, s -> new Timings()); + } + + public String toString(int indent) { + StringBuilder builder = new StringBuilder(); + + builder.append("Avg ").append(average() / 1000000).append("ms"); + + subItems.forEach((id, timings) -> { + builder.append('\n'); + for(int i = 0; i <= indent; i++) { + builder.append('\t'); + } + builder.append(id).append(": ").append(timings.toString(indent + 1)); + }); + return builder.toString(); + } + + @Override + public String toString() { + return toString(0); + } +} diff --git a/common/src/main/java/com/dfsek/terra/profiler/exception/MalformedStackException.java b/common/src/main/java/com/dfsek/terra/profiler/exception/MalformedStackException.java new file mode 100644 index 000000000..0ae345a0f --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/exception/MalformedStackException.java @@ -0,0 +1,17 @@ +package com.dfsek.terra.profiler.exception; + +public class MalformedStackException extends ProfilerException { + private static final long serialVersionUID = -3009539681021691054L; + + public MalformedStackException(String message) { + super(message); + } + + public MalformedStackException(String message, Throwable cause) { + super(message, cause); + } + + public MalformedStackException(Throwable cause) { + super(cause); + } +} diff --git a/common/src/main/java/com/dfsek/terra/profiler/exception/ProfilerException.java b/common/src/main/java/com/dfsek/terra/profiler/exception/ProfilerException.java new file mode 100644 index 000000000..27c6d734f --- /dev/null +++ b/common/src/main/java/com/dfsek/terra/profiler/exception/ProfilerException.java @@ -0,0 +1,17 @@ +package com.dfsek.terra.profiler.exception; + +public class ProfilerException extends RuntimeException { + private static final long serialVersionUID = 8206737998791649002L; + + public ProfilerException(String message) { + super(message); + } + + public ProfilerException(String message, Throwable cause) { + super(message, cause); + } + + public ProfilerException(Throwable cause) { + super(cause); + } +} diff --git a/common/src/test/java/profiler/ProfilerTest.java b/common/src/test/java/profiler/ProfilerTest.java new file mode 100644 index 000000000..1660310f8 --- /dev/null +++ b/common/src/test/java/profiler/ProfilerTest.java @@ -0,0 +1,45 @@ +package profiler; + +import com.dfsek.terra.profiler.Profiler; + +public class ProfilerTest { + //@Test + public static void main(String... a) throws InterruptedException { + Profiler.INSTANCE.start(); + for(int i = 0; i < 100; i++) { + doThing(); + } + + for(int i = 0; i < 100; i++) { + doThirdOtherThing(); + } + + for(int i = 0; i < 100; i++) { + doOtherThing(); + } + Profiler.INSTANCE.stop(); + Profiler.INSTANCE.getTimings().forEach((id, timings) -> { + System.out.println(id + ": " + timings.toString()); + }); + } + + private static void doThing() throws InterruptedException { + Profiler.INSTANCE.push("thing"); + Thread.sleep(1); + doOtherThing(); + Profiler.INSTANCE.pop("thing"); + } + + private static void doOtherThing() throws InterruptedException { + Profiler.INSTANCE.push("thing2"); + Thread.sleep(2); + doThirdOtherThing(); + Profiler.INSTANCE.pop("thing2"); + } + + private static void doThirdOtherThing() throws InterruptedException { + Profiler.INSTANCE.push("thing3"); + Thread.sleep(2); + Profiler.INSTANCE.pop("thing3"); + } +}