-
Notifications
You must be signed in to change notification settings - Fork 188
/
TestMonitor.java
238 lines (210 loc) · 9.6 KB
/
TestMonitor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/*
* Copyright (C) 2010-2021 Evolveum and contributors
*
* This work is dual-licensed under the Apache License 2.0
* and European Union Public License. See LICENSE file for details.
*/
package com.evolveum.midpoint.tools.testng;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.javasimon.EnabledManager;
import org.javasimon.Manager;
import org.javasimon.Split;
import org.javasimon.Stopwatch;
/**
* Object can hold various monitors (stopwatches, counters) and dump a report for them.
* Use for a single report unit, e.g. a test class, then throw away.
*/
public class TestMonitor {
/**
* Name of a system property that specifies file name prefix for report.
* If system property is null, report is dumped to standard output.
* Specified file name prefix can be absolute or relative from working directory,
* e.g. `target/perf-report`.
*/
public static final String PERF_REPORT_PREFIX_PROPERTY_NAME = "mp.perf.report.prefix";
/**
* Order of creation/addition is the order in the final report.
* Stopwatches are stored under specific names, but their names are null, always use the key.
*/
private final Map<String, Stopwatch> stopwatches = new LinkedHashMap<>();
/**
* Collection of report sections that will be formatted using {@link #dumpReport(String)}.
* This can be extended in one of these ways:
*
* * Directly by using {@link #addReportSection(String)} and then filling the returned
* {@link TestReportSection}.
* * Using {@link #addReportCallback(ReportCallback)} which can be registered beforehand.
*/
private final List<TestReportSection> reportSections = new ArrayList<>();
/**
* Callbacks that are called during the report dump, see {@link #addReportCallback)}.
*/
private final List<ReportCallback> reportCallbacks = new ArrayList<>();
/** Simon manager used for monitor creations, otherwise ignored. */
private final Manager simonManager = new EnabledManager();
/**
* If you want to register already existing Stopwatch, e.g. from production code.
* Note is taken from the {@link Stopwatch#getNote()}.
*/
public synchronized void register(Stopwatch stopwatch) {
stopwatches.put(stopwatch.getName(), stopwatch);
}
/**
* Returns {@link Stopwatch} for specified name, registers a new one if needed.
*/
public synchronized Stopwatch stopwatch(String name, String description) {
Stopwatch stopwatch = stopwatches.get(name);
if (stopwatch == null) {
// internally the stopwatch is not named to avoid registration with Simon Manager
stopwatch = simonManager.getStopwatch(null);
stopwatches.put(name, stopwatch);
stopwatch.setNote(description);
}
return stopwatch;
}
/**
* Starts measurement (represented by {@link Split}) on a stopwatch with specified name.
* Stopwatch is created and registered with this object if needed (no Simon manager is used).
* Returned {@link Split} is {@link AutoCloseable}, so it can be used in try-with-resource
* and the actual variable can be completely ignored.
*/
public Split stopwatchStart(String name, String description) {
return stopwatch(name, description).start();
}
/**
* This registers the callback that will be executed during the report dump and can be used
* to add new sections.
* The advantage of callback is that it can be prepared during the initialization of the test
* monitor without the need to change the point where the dump occurs (some `@After...` method).
*/
public TestMonitor addReportCallback(ReportCallback reportCallback) {
reportCallbacks.add(reportCallback);
return this;
}
public TestReportSection addReportSection(String sectionName) {
TestReportSection reportSection = new TestReportSection(sectionName);
reportSections.add(reportSection);
return reportSection;
}
public TestReportSection addRawReportSection(String sectionName) {
TestReportSection reportSection = new TestReportSection(sectionName, true);
reportSections.add(reportSection);
return reportSection;
}
public void dumpReport(String testName) {
ReportMetadata reportMetadata = new ReportMetadata(testName);
String perfReportPrefix = System.getProperty(PERF_REPORT_PREFIX_PROPERTY_NAME);
if (perfReportPrefix == null) {
dumpReportToStdout(reportMetadata);
return;
}
// we want to report to a file
String filename = String.format("%s-%s-%s-%s.txt",
perfReportPrefix, testName, reportMetadata.commitIdShort, reportMetadata.timestamp);
try (PrintStream out = new PrintStream(
new BufferedOutputStream(
new FileOutputStream(filename)))) {
dumpReport(reportMetadata, out);
} catch (FileNotFoundException e) {
System.out.println("Creating report file failed with: " + e);
System.out.println("Falling back to stdout dump:");
dumpReportToStdout(reportMetadata);
}
}
private void dumpReportToStdout(ReportMetadata reportMetadata) {
System.out.println(">>>> PERF REPORT");
dumpReport(reportMetadata, System.out);
System.out.println("<<<<");
}
public void dumpReport(String testName, PrintStream out) {
dumpReport(new ReportMetadata(testName), out);
}
private void dumpReport(ReportMetadata reportMetadata, PrintStream out) {
out.println("Commit: " + reportMetadata.commitId);
out.println("Timestamp: " + reportMetadata.timestamp);
out.println("Branch: " + reportMetadata.branch);
out.println("Test: " + reportMetadata.testName);
TestReportSection section = addReportSection("stopwatch")
// millis are more practical, but sometimes too big for avg and min and we don't wanna mix ms/us
.withColumns("monitor", "count", "total(us)", "avg(us)", "min(us)", "max(us)", "note");
for (Map.Entry<String, Stopwatch> stopwatchEntry : stopwatches.entrySet()) {
Stopwatch stopwatch = stopwatchEntry.getValue();
section.addRow(
stopwatchEntry.getKey(),
stopwatch.getCounter(),
TimeUnit.NANOSECONDS.toMicros(stopwatch.getTotal()),
TimeUnit.NANOSECONDS.toMicros((long) stopwatch.getMean()),
TimeUnit.NANOSECONDS.toMicros(stopwatch.getMin()),
TimeUnit.NANOSECONDS.toMicros(stopwatch.getMax()),
stopwatch.getNote());
}
// executing callback to get other report sections for higher level metrics
for (ReportCallback reportCallback : reportCallbacks) {
reportCallback.execute(this);
}
for (TestReportSection reportSection : reportSections) {
reportSection.dump(reportMetadata.testName, out);
}
}
private static class ReportMetadata {
public final String testName;
public final String buildNumber;
public final String branch;
public final String commitId;
public final String commitIdShort;
public final String date;
public final String timestamp;
public ReportMetadata(String testName) {
this.testName = testName;
/*
* Various Jenkins related environment variables are available:
* Git related: BRANCH (local), GIT_BRANCH, GIT_COMMIT (full), GIT_PREVIOUS_COMMIT,
* GIT_PREVIOUS_SUCCESSFUL_COMMIT, GIT_URL
* Job related: BUILD_DISPLAY_NAME, BUILD_ID, BUILD_NUMBER, BUILD_TAG, BUILD_URL,
* JOB_BASE_NAME, JOB_DISPLAY_URL, JOB_NAME, JOB_URL
* Other Jenkins: JENKINS_HOME, JENKINS_SERVER_COOKIE, JENKINS_URL, NODE_LABELS, NODE_NAME
* See also: https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
*/
Map<String, String> env = System.getenv();
buildNumber = env.getOrDefault("BUILD_NUMBER", "unknown");
branch = env.getOrDefault("BRANCH", "unknown");
commitId = env.getOrDefault("GIT_COMMIT", "unknown");
commitIdShort = StringUtils.truncate(commitId, 8);
date = LocalDate.now().toString();
timestamp = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
.withZone(ZoneId.systemDefault())
.format(Instant.now());
}
@Override
public String toString() {
return "ReportMetadata{" +
"testName='" + testName + '\'' +
", buildNumber='" + buildNumber + '\'' +
", commitId='" + commitId + '\'' +
", commitIdShort='" + commitIdShort + '\'' +
", date='" + date + '\'' +
", timestamp='" + timestamp + '\'' +
'}';
}
}
public interface ReportCallback {
/**
* Called during report dump - allows interacting with the monitor,
* typically to add additional report section.
*/
void execute(TestMonitor testMonitor);
}
}