Skip to content

Commit 2e58bd8

Browse files
committed
1480: introduce timing logger
1 parent 3c1306b commit 2e58bd8

File tree

3 files changed

+475
-0
lines changed

3 files changed

+475
-0
lines changed

lib/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ tasks.named("check").configure {
5656

5757
dependencies {
5858
compileOnly 'org.slf4j:slf4j-api:2.0.0'
59+
testCommonImplementation 'org.slf4j:slf4j-api:2.0.0'
60+
5961
// zero runtime reqs is a hard requirements for spotless-lib
6062
// if you need a dep, put it in lib-extra
6163
testCommonImplementation "org.junit.jupiter:junit-jupiter:$VER_JUNIT"
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless;
17+
18+
import static com.diffplug.spotless.LazyArgLogger.lazy;
19+
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.stream.Stream;
23+
24+
import javax.annotation.Nonnull;
25+
26+
import org.slf4j.Logger;
27+
28+
/**
29+
* A logger that logs the time it took to execute a block of code.
30+
*/
31+
public class TimedLogger {
32+
33+
public static final String MESSAGE_PREFIX_BEGIN = "[BEGIN] ";
34+
35+
public static final String MESSAGE_PREFIX_END = "[END] ";
36+
37+
public static final String MESSAGE_SUFFIX_TOOK = " (took {})";
38+
39+
private final Logger logger;
40+
private final Ticker ticker;
41+
42+
private TimedLogger(@Nonnull Logger logger, Ticker ticker) {
43+
this.logger = Objects.requireNonNull(logger);
44+
this.ticker = ticker;
45+
}
46+
47+
public static TimedLogger forLogger(@Nonnull Logger logger) {
48+
return forLogger(logger, Ticker.systemTicker());
49+
}
50+
51+
public static TimedLogger forLogger(@Nonnull Logger logger, Ticker ticker) {
52+
return new TimedLogger(logger, ticker);
53+
}
54+
55+
public TimedExec withInfo(@Nonnull String message, Object... args) {
56+
return new TimedExec(logger::isInfoEnabled, logger::info, ticker, message, args);
57+
}
58+
59+
public TimedExec withDebug(@Nonnull String message, Object... args) {
60+
return new TimedExec(logger::isDebugEnabled, logger::debug, ticker, message, args);
61+
}
62+
63+
public TimedExec withTrace(@Nonnull String message, Object... args) {
64+
return new TimedExec(logger::isTraceEnabled, logger::trace, ticker, message, args);
65+
}
66+
67+
public TimedExec withWarn(@Nonnull String message, Object... args) {
68+
return new TimedExec(logger::isWarnEnabled, logger::warn, ticker, message, args);
69+
}
70+
71+
public TimedExec withError(@Nonnull String message, Object... args) {
72+
return new TimedExec(logger::isErrorEnabled, logger::error, ticker, message, args);
73+
}
74+
75+
public static class Timed implements AutoCloseable {
76+
77+
@Nonnull
78+
private final String msg;
79+
80+
@Nonnull
81+
private final List<Object> params;
82+
@Nonnull
83+
private final LogToLevelMethod delegatedLogger;
84+
@Nonnull
85+
private final Ticker ticker;
86+
87+
private final long startedAt;
88+
89+
public Timed(@Nonnull Ticker ticker, @Nonnull String msg, @Nonnull List<Object> params, @Nonnull LogToLevelMethod delegatedLogger) {
90+
this.ticker = Objects.requireNonNull(ticker);
91+
this.msg = Objects.requireNonNull(msg);
92+
this.params = List.copyOf(Objects.requireNonNull(params));
93+
this.delegatedLogger = Objects.requireNonNull(delegatedLogger);
94+
this.startedAt = ticker.read();
95+
logStart();
96+
}
97+
98+
private void logStart() {
99+
delegatedLogger.log(MESSAGE_PREFIX_BEGIN + msg, params.toArray());
100+
}
101+
102+
private void logEnd() {
103+
delegatedLogger.log(MESSAGE_PREFIX_END + msg + MESSAGE_SUFFIX_TOOK, paramsForEnd());
104+
}
105+
106+
@Override
107+
public final void close() {
108+
logEnd();
109+
}
110+
111+
private Object[] paramsForEnd() {
112+
if (params.isEmpty() || !(params.get(params.size() - 1) instanceof Throwable)) {
113+
// if the last element is not a throwable, we can add the duration as the last element
114+
return Stream.concat(params.stream(), Stream.of(lazy(this::durationString))).toArray();
115+
}
116+
// if the last element is a throwable, we have to add the duration before the last element
117+
return Stream.concat(
118+
params.stream().limit(params.size() - 1),
119+
Stream.of(lazy(this::durationString),
120+
params.get(params.size() - 1)))
121+
.toArray();
122+
}
123+
124+
private String durationString() {
125+
long duration = ticker.read() - startedAt;
126+
if (duration < 1000) {
127+
return duration + "ms";
128+
} else if (duration < 1000 * 60) {
129+
return (duration / 1000) + "s";
130+
} else {
131+
// output in the format 3m 4.321s
132+
long minutes = duration / (1000 * 60);
133+
long seconds = (duration - minutes * 1000 * 60) / 1000;
134+
long millis = duration - minutes * 1000 * 60 - seconds * 1000;
135+
return minutes + "m" + (seconds + millis > 0 ? " " + seconds + "." + millis + "s" : "");
136+
}
137+
}
138+
}
139+
140+
public static final class NullStopWatchLogger extends Timed {
141+
private static final NullStopWatchLogger INSTANCE = new NullStopWatchLogger();
142+
143+
private NullStopWatchLogger() {
144+
super(Ticker.systemTicker(), "", List.of(), (m, a) -> {});
145+
}
146+
}
147+
148+
interface Ticker {
149+
long read();
150+
151+
static Ticker systemTicker() {
152+
return System::currentTimeMillis;
153+
}
154+
}
155+
156+
static class TestTicker implements Ticker {
157+
private long time = 0;
158+
159+
@Override
160+
public long read() {
161+
return time;
162+
}
163+
164+
public void tickMillis(long millis) {
165+
time += millis;
166+
}
167+
}
168+
169+
public static class TimedExec {
170+
@Nonnull
171+
private final LogActiveMethod logActiveMethod;
172+
@Nonnull
173+
private final LogToLevelMethod logMethod;
174+
@Nonnull
175+
private final Ticker ticker;
176+
@Nonnull
177+
private final String message;
178+
@Nonnull
179+
private final Object[] args;
180+
181+
public TimedExec(LogActiveMethod logActiveMethod, LogToLevelMethod logMethod, Ticker ticker, String message, Object... args) {
182+
this.logActiveMethod = Objects.requireNonNull(logActiveMethod);
183+
this.logMethod = Objects.requireNonNull(logMethod);
184+
this.ticker = Objects.requireNonNull(ticker);
185+
this.message = Objects.requireNonNull(message);
186+
this.args = Objects.requireNonNull(args);
187+
}
188+
189+
public void run(ThrowingEx.Runnable r) {
190+
try (Timed ignore = timed()) {
191+
ThrowingEx.run(r);
192+
}
193+
}
194+
195+
public void runChecked(ThrowingEx.Runnable r) throws Exception {
196+
try (Timed ignore = timed()) {
197+
r.run();
198+
}
199+
}
200+
201+
private Timed timed() {
202+
if (logActiveMethod.isLogLevelActive()) {
203+
return new Timed(ticker, message, List.of(args), logMethod);
204+
}
205+
return NullStopWatchLogger.INSTANCE;
206+
}
207+
}
208+
209+
@FunctionalInterface
210+
private interface LogActiveMethod {
211+
boolean isLogLevelActive();
212+
}
213+
214+
@FunctionalInterface
215+
private interface LogToLevelMethod {
216+
void log(String message, Object... args);
217+
}
218+
}

0 commit comments

Comments
 (0)