xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/util/Progress.java (revision 892e85b0307b0e825858bf4da59ab4bc5647c63f)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2007, 2021, Oracle and/or its affiliates. All rights reserved.
22  * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23  */
24 package org.opengrok.indexer.util;
25 
26 import org.opengrok.indexer.configuration.RuntimeEnvironment;
27 
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.concurrent.atomic.AtomicLong;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
33 
34 public class Progress implements AutoCloseable {
35     private final Logger logger;
36     private final long totalCount;
37     private final String suffix;
38 
39     private final AtomicLong currentCount = new AtomicLong();
40     private Thread loggerThread = null;
41     private volatile boolean run;
42 
43     private final Object sync = new Object();
44 
45     /**
46      * @param logger logger instance
47      * @param suffix string suffix to identify the operation
48      * @param totalCount total count
49      */
Progress(Logger logger, String suffix, long totalCount)50     public Progress(Logger logger, String suffix, long totalCount) {
51         this.logger = logger;
52         this.suffix = suffix;
53         this.totalCount = totalCount;
54 
55         // Assuming printProgress configuration setting cannot be changed on the fly.
56         if (totalCount > 0 && RuntimeEnvironment.getInstance().isPrintProgress()) {
57             // spawn a logger thread.
58             run = true;
59             loggerThread = new Thread(this::logLoop,
60                     "progress-thread-" + suffix.replaceAll(" ", "_"));
61             loggerThread.start();
62         }
63     }
64 
65     // for testing
getLoggerThread()66     Thread getLoggerThread() {
67         return loggerThread;
68     }
69 
70     /**
71      * Increment counter. The actual logging will be done eventually.
72      */
increment()73     public void increment() {
74         this.currentCount.incrementAndGet();
75 
76         if (loggerThread != null) {
77             // nag the thread.
78             synchronized (sync) {
79                 sync.notifyAll();
80             }
81         }
82     }
83 
logLoop()84     private void logLoop() {
85         long cachedCount = 0;
86         Map<Level, Long> lastLoggedChunk = new HashMap<>();
87         Map<Level, Integer> levelCount = Map.of(Level.INFO, 100,
88                 Level.FINE, 50,
89                 Level.FINER, 10,
90                 Level.FINEST, 1);
91 
92         while (true) {
93             long currentCount = this.currentCount.get();
94             Level currentLevel = Level.FINEST;
95 
96             // Do not log if there was no progress.
97             if (cachedCount < currentCount) {
98                 if (currentCount <= 1 || currentCount == totalCount) {
99                     currentLevel = Level.INFO;
100                 } else {
101                     if (lastLoggedChunk.getOrDefault(Level.INFO, -1L) <
102                             currentCount / levelCount.get(Level.INFO)) {
103                         currentLevel = Level.INFO;
104                     } else if (lastLoggedChunk.getOrDefault(Level.FINE, -1L) <
105                             currentCount / levelCount.get(Level.FINE)) {
106                         currentLevel = Level.FINE;
107                     } else if (lastLoggedChunk.getOrDefault(Level.FINER, -1L) <
108                             currentCount / levelCount.get(Level.FINER)) {
109                         currentLevel = Level.FINER;
110                     }
111                 }
112 
113                 if (logger.isLoggable(currentLevel)) {
114                     lastLoggedChunk.put(currentLevel, currentCount / levelCount.get(currentLevel));
115                     logger.log(currentLevel, "Progress: {0} ({1}%) for {2}",
116                             new Object[]{currentCount, currentCount * 100.0f / totalCount, suffix});
117                 }
118             }
119 
120             if (!run) {
121                 return;
122             }
123 
124             cachedCount = currentCount;
125 
126             // wait for event
127             try {
128                 synchronized (sync) {
129                     if (!run) {
130                         // Loop once more to do the final logging.
131                         continue;
132                     }
133                     sync.wait();
134                 }
135             } catch (InterruptedException e) {
136                 logger.log(Level.WARNING, "logger thread interrupted");
137             }
138         }
139     }
140 
141     @Override
close()142     public void close() {
143         if (loggerThread == null) {
144             return;
145         }
146 
147         try {
148             run = false;
149             synchronized (sync) {
150                 sync.notifyAll();
151             }
152             loggerThread.join();
153         } catch (InterruptedException e) {
154             logger.log(Level.WARNING, "logger thread interrupted");
155         }
156     }
157 }
158