16
16
17
17
package com .linecorp .decaton .benchmark ;
18
18
19
+ import java .lang .management .ThreadInfo ;
19
20
import java .time .Duration ;
20
21
import java .util .ArrayList ;
21
22
import java .util .HashMap ;
22
23
import java .util .List ;
23
24
import java .util .Map ;
24
25
import java .util .Properties ;
25
26
import java .util .concurrent .CountDownLatch ;
27
+ import java .util .concurrent .Executors ;
28
+ import java .util .concurrent .ForkJoinPool ;
29
+ import java .util .concurrent .ScheduledExecutorService ;
30
+ import java .util .concurrent .TimeUnit ;
26
31
import java .util .function .Function ;
27
32
28
33
import org .apache .kafka .clients .consumer .ConsumerConfig ;
29
34
35
+ import com .sun .management .ThreadMXBean ;
36
+
30
37
import com .linecorp .decaton .processor .TaskMetadata ;
31
38
import com .linecorp .decaton .processor .metrics .Metrics ;
32
39
import com .linecorp .decaton .processor .runtime .DecatonTask ;
43
50
import io .micrometer .core .instrument .Clock ;
44
51
import io .micrometer .core .instrument .logging .LoggingMeterRegistry ;
45
52
import io .micrometer .core .instrument .logging .LoggingRegistryConfig ;
53
+ import lombok .extern .slf4j .Slf4j ;
46
54
55
+ @ Slf4j
47
56
public class DecatonRunner implements Runner {
48
57
private static final Map <String , Function <String , Object >> propertyConstructors =
49
58
new HashMap <String , Function <String , Object >>() {{
@@ -52,6 +61,7 @@ public class DecatonRunner implements Runner {
52
61
put (ProcessorProperties .CONFIG_LOGGING_MDC_ENABLED .name (), Boolean ::parseBoolean );
53
62
}};
54
63
64
+ private SubPartitionRuntime subPartitionRuntime ;
55
65
private ProcessorSubscription subscription ;
56
66
private LoggingMeterRegistry registry ;
57
67
@@ -71,7 +81,7 @@ public void init(Config config, Recording recording, ResourceTracker resourceTra
71
81
// value than zero with the default "latest" reset policy.
72
82
props .setProperty (ConsumerConfig .AUTO_OFFSET_RESET_CONFIG , "earliest" );
73
83
74
- SubPartitionRuntime subPartitionRuntime = SubPartitionRuntime .THREAD_POOL ;
84
+ subPartitionRuntime = SubPartitionRuntime .THREAD_POOL ;
75
85
List <Property <?>> properties = new ArrayList <>();
76
86
for (Map .Entry <String , String > entry : config .parameters ().entrySet ()) {
77
87
String name = entry .getKey ();
@@ -97,6 +107,8 @@ public String get(String key) {
97
107
}, Clock .SYSTEM );
98
108
Metrics .register (registry );
99
109
110
+ maybeSetupForkJoinPoolDrip ();
111
+
100
112
CountDownLatch startLatch = new CountDownLatch (1 );
101
113
102
114
subscription = SubscriptionBuilder
@@ -113,22 +125,69 @@ public String get(String key) {
113
125
TaskMetadata .builder ().build (), task , bytes );
114
126
})
115
127
.thenProcess (
116
- (ctx , task ) -> {
117
- resourceTracker .track (Thread .currentThread ().getId ());
118
- recording .process (task );
119
- }))
128
+ (ctx , task ) -> recording .process (task )))
120
129
.stateListener (state -> {
121
130
if (state == SubscriptionStateListener .State .RUNNING ) {
122
131
startLatch .countDown ();
123
132
}
124
133
})
125
134
.build ();
126
- resourceTracker .track (subscription .getId ());
127
135
subscription .start ();
128
136
129
137
startLatch .await ();
130
138
}
131
139
140
+ /**
141
+ * This value comes from VirtualThread#createDefaultScheduler(), which defaults keep-alive of ForkJoinPool
142
+ * to 30 seconds (as of Java21).
143
+ */
144
+ private static final long DRIP_INTERVAL = 30 ;
145
+ /**
146
+ * The intention of this method is to setup a scheduled work submitting some VirtualThread instances
147
+ * periodically, in order to make sure the {@link ForkJoinPool} to keep-alive the created carrier thread
148
+ * during warmup phase.
149
+ * It is necessary because {@link #onWarmupComplete(ResourceTracker)} depends on the alive list of
150
+ * ForkJoinPool threads to observe cpu time and memory allocation profile, which cannot be obtained from
151
+ * VirtualThread instances directly.
152
+ */
153
+ private void maybeSetupForkJoinPoolDrip () {
154
+ if (subPartitionRuntime != SubPartitionRuntime .VIRTUAL_THREAD ) {
155
+ return ;
156
+ }
157
+ ScheduledExecutorService executor = Executors .newScheduledThreadPool (
158
+ 1 , Thread .ofPlatform ().daemon ().factory ());
159
+ executor .scheduleAtFixedRate (() -> {
160
+ for (int i = 0 ; i < Runtime .getRuntime ().availableProcessors (); i ++) {
161
+ Thread .startVirtualThread (() -> {
162
+ try {
163
+ TimeUnit .SECONDS .sleep (DRIP_INTERVAL );
164
+ } catch (InterruptedException e ) {
165
+ throw new RuntimeException (e );
166
+ }
167
+ });
168
+ }
169
+ }, 0 , DRIP_INTERVAL , TimeUnit .SECONDS );
170
+ }
171
+
172
+ @ Override
173
+ public void onWarmupComplete (ResourceTracker resourceTracker ) {
174
+ // As we support VIRTUAL_THREAD runtime now, we can't use the technique to obtain the target Thread ID
175
+ // in the execution context, as in VirtualThread gets created and discarded for all tasks separately.
176
+ // Instead we need to observe resource usage based on the carrier thread (platform thread) side, while
177
+ // stdlib doesn't provide a way to resolve the carrier thread's ID.
178
+ ThreadMXBean threadMxBean = ResourceTracker .getSunThreadMxBean ();
179
+ ThreadInfo [] threadInfos = threadMxBean .dumpAllThreads (true , true );
180
+ for (ThreadInfo threadInfo : threadInfos ) {
181
+ String name = threadInfo .getThreadName ();
182
+ log .debug ("Tracking target check for thread name: {}" , name );
183
+ if (name .startsWith ("DecatonSubscriptionThread-" ) || name .startsWith ("PartitionProcessorThread-" ) ||
184
+ subPartitionRuntime == SubPartitionRuntime .VIRTUAL_THREAD && name .startsWith ("ForkJoinPool-" )) {
185
+ log .info ("Tracking resource of thread {} - {}" , threadInfo .getThreadId (), name );
186
+ resourceTracker .track (threadInfo .getThreadId ());
187
+ }
188
+ }
189
+ }
190
+
132
191
@ Override
133
192
public void close () throws Exception {
134
193
if (registry != null ) {
0 commit comments