This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new b6f1119a6689 chore: camel-jbang - timers and animations use a
monotonic clock, not the wall clock (#27720)
b6f1119a6689 is described below
commit b6f1119a668973529b150d428b8d4d121406b8f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 16:44:07 2026 +0200
chore: camel-jbang - timers and animations use a monotonic clock, not the
wall clock (#27720)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 3 +-
.../camel/dsl/jbang/core/commands/tui/AiPanel.java | 39 +++++----
.../jbang/core/commands/tui/ArchitectureView.java | 2 +-
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 14 ++--
.../jbang/core/commands/tui/CaptionOverlay.java | 32 +++++--
.../core/commands/tui/DataRefreshService.java | 19 +++--
.../dsl/jbang/core/commands/tui/DrawOverlay.java | 3 +-
.../dsl/jbang/core/commands/tui/EditReplay.java | 4 +-
.../dsl/jbang/core/commands/tui/HttpHelper.java | 4 +-
.../dsl/jbang/core/commands/tui/InfraInfo.java | 1 +
.../jbang/core/commands/tui/IntegrationInfo.java | 3 +-
.../core/commands/tui/IntegrationSummaryHints.java | 4 +-
.../dsl/jbang/core/commands/tui/LaunchManager.java | 8 +-
.../dsl/jbang/core/commands/tui/McpFacade.java | 26 +++---
.../dsl/jbang/core/commands/tui/MemoryLeakTab.java | 5 +-
.../jbang/core/commands/tui/MetricsCollector.java | 16 ++--
.../jbang/core/commands/tui/MonitorContext.java | 2 +-
.../jbang/core/commands/tui/MonotonicClock.java | 43 ++++++++++
.../dsl/jbang/core/commands/tui/OllamaMonitor.java | 39 +++++----
.../dsl/jbang/core/commands/tui/OllamaTab.java | 2 +-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 15 ++--
.../core/commands/tui/ProjectOverviewAssist.java | 4 +-
.../jbang/core/commands/tui/RecordingManager.java | 14 ++--
.../dsl/jbang/core/commands/tui/RouteGroups.java | 6 +-
.../dsl/jbang/core/commands/tui/RouteKeys.java | 6 +-
.../jbang/core/commands/tui/RunOptionsForm.java | 2 +-
.../jbang/core/commands/tui/SourceEditAssist.java | 4 +-
.../dsl/jbang/core/commands/tui/SourceTab.java | 2 +-
.../dsl/jbang/core/commands/tui/SourceViewer.java | 6 +-
.../dsl/jbang/core/commands/tui/TapeRecorder.java | 15 ++--
.../dsl/jbang/core/commands/tui/ThreadsTab.java | 4 +-
.../dsl/jbang/core/commands/tui/TuiHelper.java | 4 +-
.../dsl/jbang/core/commands/tui/TuiMcpServer.java | 10 +--
.../jbang/core/commands/tui/TuiToolRegistry.java | 30 +++----
.../dsl/jbang/core/commands/tui/AiPanelTest.java | 6 +-
.../core/commands/tui/LaunchManagerInfraTest.java | 30 +++----
.../core/commands/tui/MonotonicClockTest.java | 54 ++++++++++++
.../core/commands/tui/MonotonicTimersTest.java | 97 ++++++++++++++++++++++
.../jbang/core/commands/tui/OllamaMonitorTest.java | 17 ++--
.../commands/tui/OpenedProjectLifecycleTest.java | 6 +-
.../core/commands/tui/RecordingManagerTest.java | 2 +-
.../core/commands/tui/RunOptionsFormTest.java | 2 +-
42 files changed, 423 insertions(+), 182 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 4a2fdb261e6f..3ec314ff7dda 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -981,6 +981,7 @@ class ActionsPopup {
}
}
+ /** Watches the launches; now is a {@link MonotonicClock} time. */
void tick(long now) {
launchManager.tick(now);
}
@@ -1331,7 +1332,7 @@ class ActionsPopup {
runOptionsForm.close();
// the project stays listed (Starting) until its app shows up and
stands in for it
- phantom.startingSince = System.currentTimeMillis();
+ phantom.startingSince = MonotonicClock.millis();
// the app is found by the process it runs in: a folder runs from
elsewhere, and reports another directory
phantom.launchedProcess = phantom.projectType != null
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 504137b65c51..4f1e0e0d81b6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -574,7 +574,7 @@ class AiPanel {
// it waits until the panel is idle, so a question asked right
away is answered first
Thread t = new Thread(
() -> projectOverview.autoExplain(dir, c, mcpFacade,
this::addOverviewEntry,
- () -> idleFor(OVERVIEW_IDLE_MS,
System.currentTimeMillis())),
+ () -> idleFor(OVERVIEW_IDLE_MS,
MonotonicClock.millis())),
"tui-ai-overview-check");
t.setDaemon(true);
t.start();
@@ -584,7 +584,10 @@ class AiPanel {
/** How long the panel is left alone (nothing typed, no question running)
before the summary starts. */
static final long OVERVIEW_IDLE_MS = 3000;
- /** Whether the panel has been idle for a while: no question or command
running, nothing typed. */
+ /**
+ * Whether the panel has been idle for a while: no question or command
running, nothing typed. Now is a
+ * {@link MonotonicClock} time.
+ */
boolean idleFor(long ms, long now) {
return !thinking.get() && activeCliCommand == null &&
inputBuffer.isEmpty() && now - lastInputMs >= ms;
}
@@ -863,7 +866,7 @@ class AiPanel {
}
boolean handleKeyEvent(KeyEvent ke) {
- lastInputMs = System.currentTimeMillis();
+ lastInputMs = MonotonicClock.millis();
if (permissionPopup.isVisible()) {
if (ke.isCtrlC()) {
interruptBusyOperation();
@@ -1581,7 +1584,7 @@ class AiPanel {
noteQuestionStarted(question);
log(LogLevel.QUESTION, "Question", question);
thinkingVerb =
THINKING_VERBS.get(ThreadLocalRandom.current().nextInt(THINKING_VERBS.size()));
- thinkingStartTime = System.currentTimeMillis();
+ thinkingStartTime = MonotonicClock.millis();
thinking.set(true);
// re-read the tool mode so a change made in F2 -> Settings applies to
the next question, and rebuild the
@@ -1650,10 +1653,10 @@ class AiPanel {
throw new InterruptedException();
}
- long callStart = System.currentTimeMillis();
+ long callStart = MonotonicClock.millis();
drainClientOutput();
LlmClient.ChatResponse response =
client.chatWithTools(systemPrompt, messages, tools);
- long callLatency = System.currentTimeMillis() - callStart;
+ long callLatency = MonotonicClock.millis() - callStart;
turnAiMs += callLatency;
if (response == null) {
String err = "No response from LLM";
@@ -1692,7 +1695,7 @@ class AiPanel {
String key = toolCall.name() + " " + arguments;
int repeats = callCounts.merge(key, 1, Integer::sum);
String result;
- long toolStart = System.currentTimeMillis();
+ long toolStart = MonotonicClock.millis();
if (repeats > MAX_IDENTICAL_TOOL_CALLS) {
result = "You have already called " + toolCall.name()
+ " with these exact arguments "
+ (repeats - 1) + " times in this turn and
the result will not change. Stop calling "
@@ -1700,7 +1703,7 @@ class AiPanel {
} else {
result = executeTuiTool(toolCall.name(),
toolCall.arguments());
}
- long toolElapsed = System.currentTimeMillis() - toolStart;
+ long toolElapsed = MonotonicClock.millis() - toolStart;
if (toolRegistry != null) {
// waiting for the user to confirm a file write is not
tool time
toolElapsed = Math.max(0, toolElapsed -
toolRegistry.consumeConfirmWaitMs());
@@ -1740,10 +1743,10 @@ class AiPanel {
"You have used all " + MAX_ITERATIONS + " tool calls available
for this question and cannot call any"
+ " more; do not call a tool.
Answer now: summarize what you did, what"
+ " you found, what failed, and
what the user could try next."));
- long wrapUpStart = System.currentTimeMillis();
+ long wrapUpStart = MonotonicClock.millis();
drainClientOutput();
LlmClient.ChatResponse wrapUp = client.chatWithTools(systemPrompt,
messages, tools);
- long wrapUpLatency = System.currentTimeMillis() - wrapUpStart;
+ long wrapUpLatency = MonotonicClock.millis() - wrapUpStart;
turnAiMs += wrapUpLatency;
if (wrapUp != null) {
turnRequests++;
@@ -1752,10 +1755,10 @@ class AiPanel {
}
if (wrapUp == null || wrapUp.text() == null ||
wrapUp.text().isBlank()) {
// the model called a tool regardless: once more without any tool
to call, at the price of the prefix
- wrapUpStart = System.currentTimeMillis();
+ wrapUpStart = MonotonicClock.millis();
drainClientOutput();
wrapUp = client.chatWithTools(systemPrompt, messages, List.of());
- wrapUpLatency = System.currentTimeMillis() - wrapUpStart;
+ wrapUpLatency = MonotonicClock.millis() - wrapUpStart;
turnAiMs += wrapUpLatency;
if (wrapUp != null) {
turnRequests++;
@@ -1798,7 +1801,7 @@ class AiPanel {
List<LlmClient.Message> messages) {
sessionTotalTokens += totalUsage.totalTokens();
if (text != null && !text.isBlank()) {
- long elapsed = System.currentTimeMillis() - thinkingStartTime;
+ long elapsed = MonotonicClock.millis() - thinkingStartTime;
String note = problems.isEmpty() ? null : answerNote(problems);
ConversationEntry entry = new ConversationEntry(
AiRole.ASSISTANT, text, elapsed, turnAiMs, turnToolMs,
turnToolCalls,
@@ -2011,7 +2014,7 @@ class AiPanel {
conversation.add(new ConversationEntry(AiRole.USER, question));
log(LogLevel.QUESTION, "Question", question);
thinkingVerb =
THINKING_VERBS.get(ThreadLocalRandom.current().nextInt(THINKING_VERBS.size()));
- thinkingStartTime = System.currentTimeMillis();
+ thinkingStartTime = MonotonicClock.millis();
thinking.set(true);
// The panel keeps showing exactly what was typed (including an
explicit /agent: escape), but the agent
// itself only ever sees its own command name: /agent:clear reaches it
as /clear.
@@ -2269,7 +2272,7 @@ class AiPanel {
}
synchronized void finish(String stopReason) {
- long elapsed = System.currentTimeMillis() - thinkingStartTime;
+ long elapsed = MonotonicClock.millis() - thinkingStartTime;
// the agent reports what its context holds now, not what the turn
spent: the turn is the growth, and a
// context that shrank because the agent compacted spends nothing
long previous = acpContextUsed;
@@ -2681,15 +2684,15 @@ class AiPanel {
}
private void renderThinkingStatus(Frame frame, Rect area) {
- long elapsed = (System.currentTimeMillis() - thinkingStartTime) / 1000;
+ long elapsed = (MonotonicClock.millis() - thinkingStartTime) / 1000;
Spinner spinner = Spinner.builder()
.spinnerStyle(SpinnerStyle.DOTS)
.style(Style.EMPTY.fg(Theme.accent()).bold())
.build();
Rect spinnerArea = new Rect(area.left(), area.top(), 2, 1);
- frame.renderStatefulWidget(spinner, spinnerArea, new
SpinnerState(System.currentTimeMillis() / 100));
+ frame.renderStatefulWidget(spinner, spinnerArea, new
SpinnerState(MonotonicClock.millis() / 100));
- long dots = (System.currentTimeMillis() / 500) % 4;
+ long dots = (MonotonicClock.millis() / 500) % 4;
String text = " " + (thinkingVerb != null ? thinkingVerb :
THINKING_VERBS.get(0));
if (elapsed > 0) {
text += " (" + elapsed + "s)";
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
index 2677a14ac04a..99398cfb1abb 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ArchitectureView.java
@@ -215,7 +215,7 @@ final class ArchitectureView {
* summary /overview has just written, without a reload key.
*/
void checkForChanges() {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (!isActive() || loading || checking || dir == null || now -
lastCheck < RECHECK_MS) {
return;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index e259a7a4ff8b..edd4b522cd23 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -312,7 +312,7 @@ public class CamelMonitor extends CamelCommand {
TapePlayer.Driver driver = new TapePlayer.Driver() {
@Override
public void key(KeyEvent event) {
- pendingKeys.add(new McpFacade.PendingKey(event,
System.currentTimeMillis()));
+ pendingKeys.add(new McpFacade.PendingKey(event,
MonotonicClock.millis()));
}
@Override
@@ -2030,10 +2030,10 @@ public class CamelMonitor extends CamelCommand {
}
private boolean handleTickEvent(TuiRunner runner) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
boolean keyProcessed = false;
McpFacade.PendingKey pk;
- while ((pk = pendingKeys.peek()) != null && now >= pk.fireAt()) {
+ while ((pk = pendingKeys.peek()) != null && pk.isDue(now)) {
pendingKeys.poll();
mcpInjectedKey = true;
handleEvent(pk.event(), runner);
@@ -2249,7 +2249,7 @@ public class CamelMonitor extends CamelCommand {
}
}
if (monitorNotification != null) {
- if (System.currentTimeMillis() > monitorNotificationExpiry) {
+ if (MonotonicClock.millis() > monitorNotificationExpiry) {
monitorNotification = null;
}
}
@@ -2798,7 +2798,7 @@ public class CamelMonitor extends CamelCommand {
private void setNotification(String message, boolean error) {
monitorNotification = message;
monitorNotificationError = error;
- monitorNotificationExpiry = System.currentTimeMillis() + (error ?
20000 : 10000);
+ monitorNotificationExpiry = MonotonicClock.millis() + (error ? 20000 :
10000);
notificationWaveState.reset();
}
@@ -2981,12 +2981,12 @@ public class CamelMonitor extends CamelCommand {
List<Span> rightSpans = new ArrayList<>();
if (recordingManager.isRecording() &&
!recordingManager.getRecentKeys().isEmpty()) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
List<RecordingManager.KeyRecord> recentKeys =
recordingManager.getRecentKeys();
int maxKeys = Math.min(recentKeys.size(), 8);
List<RecordingManager.KeyRecord> visible =
recentKeys.subList(recentKeys.size() - maxKeys, recentKeys.size());
for (RecordingManager.KeyRecord kr : visible) {
- long age = now - kr.timestamp();
+ long age = now - kr.pressedAt();
Style style = age < 1000
? Theme.selectionBg()
: Theme.muted();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
index cbf8cd83245a..a1e1a29f42c2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.LongSupplier;
import dev.tamboui.layout.Rect;
import dev.tamboui.style.Style;
@@ -40,6 +41,9 @@ class CaptionOverlay {
private static final long FADE_DURATION_MS = 1000;
private static final long INLINE_IDLE_TIMEOUT_MS = 3000;
+ // the clock of the times below: the MonotonicClock, or a fake one in tests
+ private final LongSupplier clock;
+
private boolean inlineMode;
private StringBuilder inlineBuffer;
private long inlineLastKeystroke;
@@ -49,6 +53,15 @@ class CaptionOverlay {
private long captionFullyTypedTime;
private long captionAutoDismissTime;
+ CaptionOverlay() {
+ this(MonotonicClock::millis);
+ }
+
+ /** With a clock, for tests that drive the time. */
+ CaptionOverlay(LongSupplier clock) {
+ this.clock = clock;
+ }
+
boolean isInlineMode() {
return inlineMode;
}
@@ -69,25 +82,25 @@ class CaptionOverlay {
void openInline() {
inlineMode = true;
inlineBuffer = new StringBuilder();
- inlineLastKeystroke = System.currentTimeMillis();
+ inlineLastKeystroke = clock.getAsLong();
captionText = "";
- captionStartTime = System.currentTimeMillis();
+ captionStartTime = clock.getAsLong();
captionFullyTypedTime = 0;
}
void showCaption(String text) {
captionText = text.replace("\\n", "\n");
- captionStartTime = System.currentTimeMillis();
+ captionStartTime = clock.getAsLong();
captionFullyTypedTime = 0;
captionAutoDismissTime = 0;
}
void showCaption(String text, int durationSeconds) {
captionText = text.replace("\\n", "\n");
- captionStartTime = System.currentTimeMillis();
+ captionStartTime = clock.getAsLong();
captionFullyTypedTime = 0;
if (durationSeconds > 0) {
- captionAutoDismissTime = System.currentTimeMillis() +
(durationSeconds * 1000L);
+ captionAutoDismissTime = clock.getAsLong() + (durationSeconds *
1000L);
} else {
captionAutoDismissTime = 0;
}
@@ -109,12 +122,12 @@ class CaptionOverlay {
// the last character, both chars of an emoji
inlineBuffer.setLength(inlineBuffer.offsetByCodePoints(inlineBuffer.length(),
-1));
captionText = inlineBuffer.toString();
- inlineLastKeystroke = System.currentTimeMillis();
+ inlineLastKeystroke = clock.getAsLong();
}
} else if (FormHelper.typedText(ke) != null) {
inlineBuffer.append(ke.string());
captionText = inlineBuffer.toString();
- inlineLastKeystroke = System.currentTimeMillis();
+ inlineLastKeystroke = clock.getAsLong();
}
return true;
}
@@ -129,6 +142,7 @@ class CaptionOverlay {
return false;
}
+ /** Finishes an idle inline caption and dismisses an expired one; now is a
time of the caption's clock. */
void tick(long now) {
if (inlineMode && !inlineBuffer.isEmpty()
&& now - inlineLastKeystroke > INLINE_IDLE_TIMEOUT_MS) {
@@ -170,7 +184,7 @@ class CaptionOverlay {
}
captionText = inlineBuffer.toString().replace("\\n", "\n");
inlineBuffer = null;
- captionFullyTypedTime = System.currentTimeMillis();
+ captionFullyTypedTime = clock.getAsLong();
}
void render(Frame frame, Rect area) {
@@ -220,7 +234,7 @@ class CaptionOverlay {
}
private void renderCaption(Frame frame, Rect area) {
- long now = System.currentTimeMillis();
+ long now = clock.getAsLong();
long elapsed = now - captionStartTime;
// typed a character at a time, an emoji (two chars) at once
int charsToShow = Math.min((int) (elapsed / CHAR_DELAY_MS),
captionText.codePointCount(0, captionText.length()));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index 67e75eaca3dd..ec0d8f7ef0bd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -158,6 +158,7 @@ class DataRefreshService {
return stoppingPids;
}
+ /** When the last refresh started, a {@link MonotonicClock} time. */
long lastRefresh() {
return lastRefresh;
}
@@ -165,11 +166,11 @@ class DataRefreshService {
// ---- Burst mode ----
void enableBurstMode() {
- burstModeUntil = System.currentTimeMillis() + 20_000;
+ burstModeUntil = MonotonicClock.millis() + 20_000;
}
void forceFullScan() {
- forceFullScanUntil = System.currentTimeMillis() + 20_000;
+ forceFullScanUntil = MonotonicClock.millis() + 20_000;
}
/** How many scans of every integration have published their data. */
@@ -178,7 +179,7 @@ class DataRefreshService {
}
boolean isBurstMode() {
- return System.currentTimeMillis() < burstModeUntil;
+ return MonotonicClock.millis() < burstModeUntil;
}
// ---- Refresh orchestration ----
@@ -199,7 +200,7 @@ class DataRefreshService {
if (!refreshInProgress.compareAndSet(false, true)) {
return;
}
- lastRefresh = System.currentTimeMillis();
+ lastRefresh = MonotonicClock.millis();
ctx.backgroundExecutor.execute(() -> {
try {
refreshSync(logRefresher, conditionalRefresher);
@@ -213,7 +214,7 @@ class DataRefreshService {
* Perform a synchronous data refresh cycle: log data, integration scan,
auto-select, and conditional tab data.
*/
void refreshSync(Runnable logRefresher, Runnable conditionalRefresher) {
- lastRefresh = System.currentTimeMillis();
+ lastRefresh = MonotonicClock.millis();
try {
logRefresher.run();
boolean fullScan = scanIntegrations();
@@ -282,7 +283,7 @@ class DataRefreshService {
private boolean scanIntegrations() {
List<IntegrationInfo> infos = new ArrayList<>();
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
boolean wantFullScan = refreshCtx.selectedTab() ==
TabRegistry.TAB_OVERVIEW || refreshCtx.isSwitchPopupVisible()
|| cachedPids.isEmpty()
|| forceFullScanUntil > now;
@@ -387,7 +388,7 @@ class DataRefreshService {
if (wasExplicitStop) {
metrics.removeVanished(prev.pid);
} else {
- vanishing.put(prev.pid, new VanishingInfo(prev,
System.currentTimeMillis()));
+ vanishing.put(prev.pid, new VanishingInfo(prev,
MonotonicClock.millis()));
}
}
}
@@ -588,10 +589,10 @@ class DataRefreshService {
List<InfraInfo> previousInfra = infraData.get();
for (InfraInfo prev : previousInfra) {
if (!prev.vanishing && !liveInfraPids.contains(prev.pid) &&
!vanishingInfra.containsKey(prev.pid)) {
- vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev,
System.currentTimeMillis()));
+ vanishingInfra.put(prev.pid, new VanishingInfraInfo(prev,
MonotonicClock.millis()));
}
}
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
Iterator<Map.Entry<String, VanishingInfraInfo>> infraIt =
vanishingInfra.entrySet().iterator();
while (infraIt.hasNext()) {
Map.Entry<String, VanishingInfraInfo> entry = infraIt.next();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
index 6e94f254002d..25508321717e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DrawOverlay.java
@@ -47,7 +47,7 @@ class DrawOverlay {
void setDrawing(List<DrawCell> newCells, int durationSeconds) {
this.cells = new ArrayList<>(newCells);
if (durationSeconds > 0) {
- this.autoDismissTime = System.currentTimeMillis() +
(durationSeconds * 1000L);
+ this.autoDismissTime = MonotonicClock.millis() + (durationSeconds
* 1000L);
} else {
this.autoDismissTime = 0;
}
@@ -66,6 +66,7 @@ class DrawOverlay {
autoDismissTime = 0;
}
+ /** Dismisses the drawing once its duration is over; now is a {@link
MonotonicClock} time. */
void tick(long now) {
if (autoDismissTime > 0 && now > autoDismissTime) {
clear();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EditReplay.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EditReplay.java
index 4cbd83e2f0ec..7c0aa3ba5cab 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EditReplay.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EditReplay.java
@@ -98,7 +98,7 @@ final class EditReplay {
private Phase askedFrom = Phase.PAUSED;
EditReplay() {
- this(System::currentTimeMillis);
+ this(MonotonicClock::millis);
}
/**
@@ -214,7 +214,7 @@ final class EditReplay {
phase = Phase.FINISHED;
}
- /** Advances the replay; call on every UI tick. */
+ /** Advances the replay; call on every UI tick, with a time of the
replay's clock (the {@link MonotonicClock}). */
void tick(long now) {
if (phase != Phase.TYPING) {
return;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpHelper.java
index dc036f3cde12..40ed56a24817 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpHelper.java
@@ -59,10 +59,10 @@ final class HttpHelper {
}
}
- long start = System.currentTimeMillis();
+ long start = MonotonicClock.millis();
HttpResponse<String> response = client.send(reqBuilder.build(),
HttpResponse.BodyHandlers.ofString());
- long elapsed = System.currentTimeMillis() - start;
+ long elapsed = MonotonicClock.millis() - start;
int statusCode = response.statusCode();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraInfo.java
index eb063c2f37c7..4abfa99633b5 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/InfraInfo.java
@@ -27,5 +27,6 @@ class InfraInfo {
Map<String, Object> properties = new TreeMap<>();
boolean alive;
boolean vanishing;
+ // when it was found gone, a MonotonicClock time: it fades out from then
long vanishStart;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index 899da1619885..77743bb194db 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -37,7 +37,7 @@ class IntegrationInfo {
String linkedPid;
// a running app: the name of the project it was opened as, when it
differs from the app's own name
String openedAs;
- // an opened project: when it was asked to run, until its app shows up (or
the launch fails)
+ // an opened project: when it was asked to run (a MonotonicClock time),
until its app shows up (or the launch fails)
long startingSince;
// the process F10 launched for an opened project: its app is that process
or a descendant of it (CAMEL-25417)
ProcessHandle launchedProcess;
@@ -91,6 +91,7 @@ class IntegrationInfo {
int threadCount;
int peakThreadCount;
boolean vanishing;
+ // when it was found gone, a MonotonicClock time: it fades out from then
long vanishStart;
final List<RouteInfo> routes = new ArrayList<>();
final List<ConsumerInfo> consumers = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryHints.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryHints.java
index 3972690dbb12..a9cc8e089f5f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryHints.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationSummaryHints.java
@@ -137,7 +137,7 @@ final class IntegrationSummaryHints {
}
private static Entry entry(Path dir) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
Entry e = CACHE.get(dir);
if (e != null && now - e.checkedAt() < RECHECK_MS) {
return e;
@@ -179,7 +179,7 @@ final class IntegrationSummaryHints {
/** Whether the AI overview setting is not off, whatever the ai view
setting. */
static boolean settingEnabled() {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - modeCheckedAt > MODE_RECHECK_MS) {
modeCheckedAt = now;
enabled =
!ProjectOverviewAssist.MODE_OFF.equals(ProjectOverviewAssist.mode());
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
index 0dded3a0874d..d79c01c88e39 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
@@ -188,7 +188,7 @@ class LaunchManager {
}
void addPendingLaunch(String name, Process process, Path outputFile) {
- pendingLaunches.add(new PendingLaunch(name, process, outputFile,
System.currentTimeMillis()));
+ pendingLaunches.add(new PendingLaunch(name, process, outputFile,
MonotonicClock.millis()));
pendingAutoSelect = name;
launched.add(process);
}
@@ -208,6 +208,7 @@ class LaunchManager {
launched.clear();
}
+ /** Watches the launches; now is a {@link MonotonicClock} time, as their
start times. */
void tick(long now) {
monitorPendingLaunches(now);
checkDeferredLaunch(now);
@@ -276,7 +277,7 @@ class LaunchManager {
pb.redirectErrorStream(true);
pb.redirectOutput(outputFile.toFile());
Process process = pb.start();
- PendingLaunch pl = new PendingLaunch(alias, process, outputFile,
System.currentTimeMillis());
+ PendingLaunch pl = new PendingLaunch(alias, process, outputFile,
MonotonicClock.millis());
pl.infraAlias = serviceOf(alias);
pendingLaunches.add(pl);
}
@@ -292,7 +293,7 @@ class LaunchManager {
/** Runs the launch once the given infra services are up, or drops it when
they fail to start. */
void deferUntilInfra(List<String> infra, String displayName, Runnable
launchAction) {
- deferredLaunch = new DeferredLaunch(displayName, infra,
System.currentTimeMillis(), launchAction);
+ deferredLaunch = new DeferredLaunch(displayName, infra,
MonotonicClock.millis(), launchAction);
}
void startMissingInfraAndDefer(List<String> missingInfra, String
displayName, Runnable launchAction) {
@@ -620,6 +621,7 @@ class LaunchManager {
final String name;
final Process process;
final Path outputFile;
+ // a MonotonicClock time
final long startTime;
// the service of an infra launch: it has started once the service is
up; null for an integration
String infraAlias;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 941a18b7ba71..d1109eb60c6a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -59,9 +59,15 @@ import static
org.apache.camel.dsl.jbang.core.commands.tui.TuiHelper.hintLast;
class McpFacade {
/**
- * Pending key event queued for injection into the TUI event loop.
+ * Pending key event queued for injection into the TUI event loop, by an
MCP client or a tape. It fires on the first
+ * tick at or after fireAt, a {@link MonotonicClock} time.
*/
record PendingKey(KeyEvent event, long fireAt) {
+
+ /** Whether the key fires at the given {@link MonotonicClock} time. */
+ boolean isDue(long now) {
+ return now >= fireAt;
+ }
}
/**
@@ -283,8 +289,8 @@ class McpFacade {
}
long before = done.getAsLong();
request.run();
- long deadline = System.currentTimeMillis() + FULL_SCAN_TIMEOUT_MS;
- while (done.getAsLong() <= before && System.currentTimeMillis() <
deadline) {
+ long deadline = MonotonicClock.millis() + FULL_SCAN_TIMEOUT_MS;
+ while (done.getAsLong() <= before && MonotonicClock.millis() <
deadline) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
@@ -619,7 +625,7 @@ class McpFacade {
// ---- Key injection ----
int injectKeys(List<String> keys, int delayMs) {
- long fireAt = System.currentTimeMillis();
+ long fireAt = MonotonicClock.millis();
int count = 0;
for (String key : keys) {
KeyEvent ke = parseKey(key);
@@ -739,8 +745,8 @@ class McpFacade {
if (data != null || !tab.ensureDataLoaded()) {
return data;
}
- long deadline = System.currentTimeMillis() + timeoutMs;
- while (data == null && System.currentTimeMillis() < deadline &&
tab.dataLoadError() == null) {
+ long deadline = MonotonicClock.millis() + timeoutMs;
+ while (data == null && MonotonicClock.millis() < deadline &&
tab.dataLoadError() == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
@@ -1348,18 +1354,18 @@ class McpFacade {
}
}
if (writeMode == WriteMode.LIVE && exists && bridge != null) {
- long waitStart = System.currentTimeMillis();
+ long waitStart = MonotonicClock.millis();
ReplayOutcome outcome = bridge.replayFileWrite(request);
- lastConfirmWaitMs = System.currentTimeMillis() - waitStart;
+ lastConfirmWaitMs = MonotonicClock.millis() - waitStart;
if (outcome != null) {
return replayResult(target, dir, file, content, outcome);
}
// could not replay (for example unsaved edits in the editor):
fall back to the dialog
}
if (confirm || writeMode != WriteMode.AUTO) {
- long waitStart = System.currentTimeMillis();
+ long waitStart = MonotonicClock.millis();
boolean confirmed = bridge != null &&
bridge.confirmFileWrite(request);
- lastConfirmWaitMs = System.currentTimeMillis() - waitStart;
+ lastConfirmWaitMs = MonotonicClock.millis() - waitStart;
if (!confirmed) {
JsonObject result = new JsonObject();
result.put("status", "rejected");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
index c7b3d6835da1..e6625e1f6053 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MemoryLeakTab.java
@@ -73,6 +73,7 @@ class MemoryLeakTab extends AbstractTab {
private State state = State.IDLE;
private RecordingMode recordingMode = RecordingMode.DUAL;
private int duration = 60;
+ // the wall clock (epoch millis): the start of a recording may come from
the integration's status
private long recordingStartTime;
private int currentRecordingDuration;
@@ -1521,8 +1522,8 @@ class MemoryLeakTab extends AbstractTab {
return;
}
startDaemonThread("jfr-poll-" + pid, () -> {
- long deadline = System.currentTimeMillis() + (dur + 30) * 1000L;
- while (System.currentTimeMillis() < deadline) {
+ long deadline = MonotonicClock.millis() + (dur + 30) * 1000L;
+ while (MonotonicClock.millis() < deadline) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index 0df55e2b752e..18bbcd30847e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -201,7 +201,7 @@ class MetricsCollector {
void updateThroughputHistory(IntegrationInfo info) {
long currentFailed = info.failed;
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
String pid = info.pid;
LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k
-> new LinkedList<>());
@@ -260,7 +260,7 @@ class MetricsCollector {
.filter(ep -> "out".equals(ep.direction) && (ep.remote ||
ep.stub))
.mapToLong(ep -> ep.hits).sum();
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
String pid = info.pid;
recordEndpointSample(pid, now, inTotal, outTotal,
@@ -332,7 +332,7 @@ class MetricsCollector {
.filter(s -> "out".equals(s.direction))
.mapToLong(s -> s.hits).sum();
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
recordEndpointSample(info.pid, now, inTotal, outTotal,
serviceSamples, previousServiceTime, serviceInHistory,
serviceOutHistory);
@@ -406,7 +406,7 @@ class MetricsCollector {
}
void updateCbHistory(IntegrationInfo info) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
for (CircuitBreakerInfo cb : info.circuitBreakers) {
if (cb.id == null) {
continue;
@@ -423,7 +423,7 @@ class MetricsCollector {
void updateHeapHistory(IntegrationInfo info) {
if (info.heapMemUsed > 0) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
Long lastTime = previousHeapTime.get(info.pid);
if (lastTime == null || now - lastTime >= HEAP_SAMPLE_INTERVAL_MS)
{
previousHeapTime.put(info.pid, now);
@@ -438,17 +438,17 @@ class MetricsCollector {
Optional<Duration> durOpt = ph.info().totalCpuDuration();
if (durOpt.isPresent()) {
long cpuNanos = durOpt.get().toNanos();
- long wallMs = System.currentTimeMillis();
+ long nowMs = MonotonicClock.millis();
long[] prev = prevCpuSample.get(pid);
if (prev != null) {
long deltaCpuNanos = cpuNanos - prev[0];
- long deltaWallNanos = (wallMs - prev[1]) * 1_000_000L;
+ long deltaWallNanos = (nowMs - prev[1]) * 1_000_000L;
if (deltaWallNanos > 0) {
double cpuPct = (double) deltaCpuNanos / deltaWallNanos *
100.0;
cpuLoadAvg.computeIfAbsent(pid, k -> new
LoadAvg()).update(Math.max(0, cpuPct));
}
}
- prevCpuSample.put(pid, new long[] { cpuNanos, wallMs });
+ prevCpuSample.put(pid, new long[] { cpuNanos, nowMs });
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index 1a422ca3c3bb..de03b305970e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -202,7 +202,7 @@ class MonitorContext {
Object lock = actionLocks.computeIfAbsent(pid, k -> new Object());
synchronized (lock) {
CachedAnswer cached = jvmInfo.get(pid);
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (cached != null && now - cached.time() < JVM_INFO_TTL_MS) {
return cached.answer();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClock.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClock.java
new file mode 100644
index 000000000000..b0d4ab15e663
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClock.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The clock of the TUI's timers, deadlines and animations: milliseconds that
only move forward, read from
+ * {@link System#nanoTime()}. The wall clock ({@link
System#currentTimeMillis()}) can be set back or forward (an NTP
+ * step, a manual change, a VM resume), and a timer measured with it then
freezes until the clock catches up, or fires
+ * at once.
+ * <p/>
+ * A time of this clock is no time of day: compare it only with another time
of this clock, never with an epoch time
+ * from the wall clock, a file or another process.
+ */
+final class MonotonicClock {
+
+ // the clock starts a day in, so a time still at 0 (never set) reads as
long ago, as it does on the wall clock
+ private static final long ORIGIN = System.nanoTime() -
TimeUnit.DAYS.toNanos(1);
+
+ private MonotonicClock() {
+ }
+
+ /** The time in milliseconds: at least a day, and never less than a time
read before. */
+ static long millis() {
+ // nanoTime is compared only by its difference, which is right even
when its value overflows
+ return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - ORIGIN);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitor.java
index 4c42db936cca..31a5015ae815 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitor.java
@@ -373,12 +373,13 @@ final class OllamaMonitor {
/**
* The AI panel question being answered right now, so the tab can list it
from the moment it was asked: before the
* first request returns there is no {@link RequestEntry} for it, and
while tools run its last step says
- * {@code tool_calls} like a finished question's would.
+ * {@code tool_calls} like a finished question's would. It was asked at
startedAt, the time of day shown, and at
+ * monotonicStart, the {@link MonotonicClock} time it is timed from.
*/
- record ActiveQuestion(int question, String text, Instant startedAt) {
+ record ActiveQuestion(int question, String text, Instant startedAt, long
monotonicStart) {
long elapsedMs() {
- return Math.max(0, System.currentTimeMillis() -
startedAt.toEpochMilli());
+ return Math.max(0, MonotonicClock.millis() - monotonicStart);
}
boolean matches(QuestionGroup g) {
@@ -498,6 +499,7 @@ final class OllamaMonitor {
private int panelBudget;
private ActiveQuestion activeQuestion;
+ // MonotonicClock times of the last look at each source; the rate windows
and CPU samples are on this clock too
private long lastProbe;
private long lastVersion;
private long lastPs;
@@ -529,7 +531,7 @@ final class OllamaMonitor {
return;
}
try {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - lastProbe >= DETECT_INTERVAL_MS) {
lastProbe = now;
lastVersion = now;
@@ -677,7 +679,7 @@ final class OllamaMonitor {
/** True once every few seconds: the GenAI spans of a route are worth
re-reading. */
boolean wantsSpans() {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - lastSpanIngest >= SPAN_INTERVAL_MS) {
lastSpanIngest = now;
return true;
@@ -696,7 +698,7 @@ final class OllamaMonitor {
/** The AI panel started working on a question; it is listed as in
progress until {@link #questionFinished()}. */
void questionStarted(int question, String text) {
synchronized (lock) {
- activeQuestion = new ActiveQuestion(question, text, Instant.now());
+ activeQuestion = new ActiveQuestion(question, text, Instant.now(),
MonotonicClock.millis());
}
}
@@ -784,12 +786,16 @@ final class OllamaMonitor {
/** Applies a runner slot sample; the decode and prefill rate windows
advance from its counters. */
void updateSlot(SlotState state) {
+ updateSlot(state, MonotonicClock.millis());
+ }
+
+ /** Applies a runner slot sample read at the given {@link MonotonicClock}
time. */
+ void updateSlot(SlotState state, long readAt) {
synchronized (lock) {
slot = state;
if (state != null) {
- long now = state.sampledAt() != null ?
state.sampledAt().toEpochMilli() : System.currentTimeMillis();
- decodeWindow.sample(now, state.decoded());
- prefillWindow.sample(now, state.promptProcessed());
+ decodeWindow.sample(readAt, state.decoded());
+ prefillWindow.sample(readAt, state.promptProcessed());
}
}
}
@@ -800,19 +806,22 @@ final class OllamaMonitor {
}
}
- /** Ends one poll: appends the current live decode rate to the history and
stamps the poll time. */
+ /**
+ * Ends one poll: appends the current live decode rate to the history and
stamps the poll time (of day). Now is a
+ * {@link MonotonicClock} time.
+ */
void tick(long nowMillis) {
synchronized (lock) {
long rate = Math.round(decodeWindow.ratePerSecond(nowMillis));
System.arraycopy(decodeHistory, 1, decodeHistory, 0,
decodeHistory.length - 1);
decodeHistory[decodeHistory.length - 1] = rate;
- lastPoll = Instant.ofEpochMilli(nowMillis);
+ lastPoll = Instant.now();
}
}
Snapshot snapshot() {
synchronized (lock) {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
return new Snapshot(
server, models, installed, slot, runner, host,
List.copyOf(requests),
decodeWindow.ratePerSecond(now),
prefillWindow.ratePerSecond(now),
@@ -844,7 +853,7 @@ final class OllamaMonitor {
return;
}
try {
- doPoll(System.currentTimeMillis());
+ doPoll(MonotonicClock.millis());
} catch (Exception e) {
LOG.debug("Ollama poll failed", e);
} finally {
@@ -945,7 +954,7 @@ final class OllamaMonitor {
lastSlotPoll = now;
JsonArray slots = getJsonArray("http://127.0.0.1:" +
current.port() + "/slots");
if (slots != null) {
- updateSlot(OllamaParsers.parseSlots(slots,
Instant.ofEpochMilli(now)));
+ updateSlot(OllamaParsers.parseSlots(slots, Instant.now()));
} else {
updateRunner(null);
current = null;
@@ -1064,7 +1073,7 @@ final class OllamaMonitor {
if (gpu == null && serverStats == null && runnerStats == null) {
return null;
}
- return new HostStats(gpu, serverStats, runnerStats,
Instant.ofEpochMilli(now));
+ return new HostStats(gpu, serverStats, runnerStats, Instant.now());
}
private GpuStats probeGpu() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
index 1e5878154455..d1d211040088 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaTab.java
@@ -642,7 +642,7 @@ class OllamaTab extends AbstractTab {
private static final String[] SPINNER = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴",
"⠦", "⠧", "⠇", "⠏" };
private static Span workingSpan() {
- String frame = SPINNER[(int) ((System.currentTimeMillis() / 100) %
SPINNER.length)];
+ String frame = SPINNER[(int) ((MonotonicClock.millis() / 100) %
SPINNER.length)];
return Span.styled(" " + frame + " working", Theme.info());
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index f7d566650535..50dbd260e8d8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -405,7 +405,7 @@ class OverviewTab extends AbstractTab {
Style rowBg = isEven ? Style.EMPTY.bg(Theme.zebra()) : Style.EMPTY;
if (info.vanishing) {
- long elapsed = System.currentTimeMillis() - info.vanishStart;
+ long elapsed = MonotonicClock.millis() - info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
int gray = (int) (100 * fade);
Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
@@ -555,7 +555,7 @@ class OverviewTab extends AbstractTab {
Cell.from(Span.styled(TuiHelper.truncate(nameText,
nameWidth), Theme.info())),
Cell.from(Span.styled("", Theme.muted())),
Cell.from(Span.styled("", Theme.muted())),
- Cell.from(projectStatus(info,
System.currentTimeMillis())),
+ Cell.from(projectStatus(info,
MonotonicClock.millis())),
Cell.from(Span.styled("", Theme.muted())),
Cell.from(Span.styled("", Theme.muted())),
Cell.from(Span.styled("", Theme.muted())),
@@ -563,7 +563,7 @@ class OverviewTab extends AbstractTab {
Cell.from(Span.styled("", Theme.muted())),
Cell.from(Span.styled("",
Theme.muted()))).style(rowBg));
} else if (info.vanishing) {
- long elapsed = System.currentTimeMillis() - info.vanishStart;
+ long elapsed = MonotonicClock.millis() - info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
int gray = (int) (100 * fade);
Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
@@ -915,7 +915,10 @@ class OverviewTab extends AbstractTab {
/** How long an opened project shows as Starting when its app does not
show up. */
static final long PROJECT_START_MS = 5 * 60_000;
- /** The status of an opened project: Starting while it was asked to run
and its app is not up yet, else Stopped. */
+ /**
+ * The status of an opened project: Starting while it was asked to run and
its app is not up yet, else Stopped. Now
+ * is a {@link MonotonicClock} time, as the time it was asked to run.
+ */
static Span projectStatus(IntegrationInfo project, long now) {
if (project.startingSince > 0 && now - project.startingSince <
PROJECT_START_MS) {
return Span.styled(TuiIcons.GEAR + " Starting", Theme.warning());
@@ -942,7 +945,7 @@ class OverviewTab extends AbstractTab {
Span.raw(TuiHelper.truncateStart(project.sourceDir,
Math.max(1, width - 9)))));
}
lines.add(Line.from(Span.raw("")));
- lines.add(Line.from(projectStatus(project,
System.currentTimeMillis())));
+ lines.add(Line.from(projectStatus(project, MonotonicClock.millis())));
lines.add(Line.from(Span.raw("")));
if (project.startingSince == 0) {
lines.add(Line.from(Span.styled(" F10 ", Theme.hintKey()),
Span.styled(" runs it", dim)));
@@ -1151,7 +1154,7 @@ class OverviewTab extends AbstractTab {
Style statusStyle = info.alive ? Theme.success() : Theme.error();
if (info.vanishing) {
- long elapsed = System.currentTimeMillis() - info.vanishStart;
+ long elapsed = MonotonicClock.millis() - info.vanishStart;
float fade = 1.0f - Math.min(1.0f, (float) elapsed /
VANISH_DURATION_MS);
int gray = (int) (100 * fade);
Style dimStyle = Style.EMPTY.fg(Color.indexed(232 +
Math.min(gray / 4, 23)));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
index 6a9f5b2616cd..a6e4a1e3fc61 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProjectOverviewAssist.java
@@ -181,9 +181,9 @@ final class ProjectOverviewAssist {
/** Waits until {@code idle} holds, checking a few times a second; false
when it never did in time. */
static boolean awaitIdle(BooleanSupplier idle, long maxWaitMs) {
- long until = System.currentTimeMillis() + maxWaitMs;
+ long until = MonotonicClock.millis() + maxWaitMs;
while (!idle.getAsBoolean()) {
- if (System.currentTimeMillis() >= until) {
+ if (MonotonicClock.millis() >= until) {
return false;
}
try {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
index 383a77bcea6c..0c7dd4b7f76d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManager.java
@@ -36,7 +36,8 @@ import dev.tamboui.tui.event.KeyEvent;
*/
class RecordingManager {
- record KeyRecord(String label, long timestamp) {
+ /** A key shown while recording; pressedAt is a {@link MonotonicClock}
time. */
+ record KeyRecord(String label, long pressedAt) {
}
private final CaptionOverlay captionOverlay;
@@ -70,7 +71,7 @@ class RecordingManager {
eventLog.record(label, label);
}
if (recording) {
- recentKeys.add(new KeyRecord(label,
System.currentTimeMillis()));
+ recentKeys.add(new KeyRecord(label, MonotonicClock.millis()));
}
TapeRecorder tr = tapeRecorderRef.get();
if (tr != null && tr.isActive() && !mcpInjected) {
@@ -119,10 +120,10 @@ class RecordingManager {
ExportRequest.export(buf).text().toFile(txtPath);
ExportRequest.export(buf).text().options(o ->
o.styles(true)).toFile(ansPath);
screenshotMessage = "Screenshot saved to " +
svgPath.toAbsolutePath() + " (and .txt, .ans)";
- screenshotMessageTime = System.currentTimeMillis();
+ screenshotMessageTime = MonotonicClock.millis();
} catch (IOException e) {
screenshotMessage = "Screenshot failed: " + e.getMessage();
- screenshotMessageTime = System.currentTimeMillis();
+ screenshotMessageTime = MonotonicClock.millis();
}
}
@@ -150,7 +151,7 @@ class RecordingManager {
String screenshotFlashMessage() {
String msg = screenshotMessage;
- if (msg != null && System.currentTimeMillis() - screenshotMessageTime
< 5000) {
+ if (msg != null && MonotonicClock.millis() - screenshotMessageTime <
5000) {
return msg;
}
screenshotMessage = null;
@@ -234,10 +235,11 @@ class RecordingManager {
return recentKeys;
}
+ /** Drops the keys shown for two seconds; now is a {@link MonotonicClock}
time. */
void tickRecentKeys(long now) {
if (recording && !recentKeys.isEmpty()) {
long cutoff = now - 2000;
- recentKeys.removeIf(k -> k.timestamp() < cutoff);
+ recentKeys.removeIf(k -> k.pressedAt() < cutoff);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteGroups.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteGroups.java
index 6e95a72ad36a..eac7eee00008 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteGroups.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteGroups.java
@@ -71,15 +71,15 @@ final class RouteGroups {
}
Path key = dir.toAbsolutePath().normalize();
Entry e = CACHE.get(key);
- if ((e == null || System.currentTimeMillis() - e.checkedAt() >
RECHECK_MS) && LOADING.add(key)) {
+ if ((e == null || MonotonicClock.millis() - e.checkedAt() >
RECHECK_MS) && LOADING.add(key)) {
Thread t = new Thread(() -> {
try {
ProjectOverview.Overview overview =
ProjectOverview.analyze(key, ProjectOverviewAssist.catalog());
CACHE.put(key, new Entry(
- System.currentTimeMillis(), tags(overview,
IntegrationSummary.read(key)),
+ MonotonicClock.millis(), tags(overview,
IntegrationSummary.read(key)),
tags(overview, null)));
} catch (RuntimeException ex) {
- CACHE.put(key, new Entry(System.currentTimeMillis(),
Map.of(), Map.of()));
+ CACHE.put(key, new Entry(MonotonicClock.millis(),
Map.of(), Map.of()));
} finally {
LOADING.remove(key);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteKeys.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteKeys.java
index f5dd3ea22008..a86c6730005f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteKeys.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RouteKeys.java
@@ -76,15 +76,15 @@ final class RouteKeys {
List<Running> running = RUNNING.getOrDefault(key, List.of());
Entry e = CACHE.get(key);
boolean stale = e == null || !e.running().equals(running)
- || System.currentTimeMillis() - e.checkedAt() > RECHECK_MS;
+ || MonotonicClock.millis() - e.checkedAt() > RECHECK_MS;
if (stale && !running.isEmpty() && LOADING.add(key)) {
Thread t = new Thread(() -> {
try {
CamelCatalog catalog = ProjectOverviewAssist.catalog();
ProjectOverview.Overview o = ProjectOverview.analyze(key,
catalog);
- CACHE.put(key, new Entry(System.currentTimeMillis(),
running, match(o, running, catalog)));
+ CACHE.put(key, new Entry(MonotonicClock.millis(), running,
match(o, running, catalog)));
} catch (RuntimeException ex) {
- CACHE.put(key, new Entry(System.currentTimeMillis(),
running, Map.of()));
+ CACHE.put(key, new Entry(MonotonicClock.millis(), running,
Map.of()));
} finally {
LOADING.remove(key);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index 4d70ba9fa0ff..b562236e6a57 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -547,7 +547,7 @@ class RunOptionsForm {
private void renderOptionsPage(Frame frame, Rect area) {
// wide enough for the runtime cycler to show all runtimes (Camel
Main, Spring Boot, Quarkus, JBang)
int popupW = Math.min(80, area.width() - 4);
- String warning = portWarning(System.currentTimeMillis());
+ String warning = portWarning(MonotonicClock.millis());
int popupH = PAGE1_HEIGHT + (errorMessage != null ? 1 : 0) + (warning
!= null ? 1 : 0);
Rect popup = DialogHelper.centered(area, popupW, popupH);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index aafa6fa6e42e..c421f9ab93f9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -1542,7 +1542,7 @@ final class SourceEditAssist {
return List.of();
}
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (placeholderCache != null && rootDir.equals(placeholderCacheDir) &&
(now - placeholderCacheTime) < 5000) {
return placeholderCache;
}
@@ -1950,7 +1950,7 @@ final class SourceEditAssist {
/** The Java sources of the project, for the constants a route refers to
in another class; kept for a while. */
private Map<String, Supplier<String>> javaSources() {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (javaSourcesCache == null || now - javaSourcesCacheTime >
JAVA_SOURCES_TTL_MS
|| !Objects.equals(javaSourcesCacheDir, rootDir)) {
javaSourcesCache = RouteAssist.javaSources(rootDir);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 1b5f6b89a8db..9b7a302843bb 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -1582,7 +1582,7 @@ class SourceTab extends AbstractTab {
/** The beans and classes the project declares, read again when a file of
the project changed. */
ProjectBeans projectBeans() {
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - projectBeansChecked < 2000) {
// the quick doc asks on every frame: look at the files at most
every 2 seconds
return projectBeans;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 64956d5f4689..1df5455c890b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -444,7 +444,7 @@ class SourceViewer {
if (liveRunData == null || loadedFilePath == null) {
return Collections.emptyMap();
}
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - liveLinesTime > 1000) {
refreshLiveLines(now);
}
@@ -453,7 +453,7 @@ class SourceViewer {
/** Package-private for tests: reads the live run data again now, as the
render does once a second. */
void refreshLiveLinesForTesting() {
- refreshLiveLines(System.currentTimeMillis());
+ refreshLiveLines(MonotonicClock.millis());
}
/** Package-private for tests: the text of the live column of a line. */
@@ -2362,7 +2362,7 @@ class SourceViewer {
if (!dirty || validationErrors != null) {
return;
}
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (now - lastBackgroundValidationTime <
BACKGROUND_VALIDATION_INTERVAL_MS) {
return;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
index 08a72553ef33..e825e08b11c1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
@@ -29,6 +29,7 @@ class TapeRecorder {
"pgup", "pgdn");
private final List<String> lines = new ArrayList<>();
+ // MonotonicClock times: the sleeps between the recorded events
private long startTime;
private long lastEventTime;
private int keyCount;
@@ -47,7 +48,7 @@ class TapeRecorder {
lines.add("Set CursorBlink false");
lines.add("Set Theme \"Aardvark Blue\"");
lines.add("");
- startTime = System.currentTimeMillis();
+ startTime = MonotonicClock.millis();
lastEventTime = startTime;
keyCount = 0;
active = true;
@@ -58,7 +59,7 @@ class TapeRecorder {
return;
}
preKeyPosition = lines.size();
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
long elapsed = now - lastEventTime;
if (elapsed > 200) {
lines.add("Sleep " + formatSleep(elapsed));
@@ -80,7 +81,7 @@ class TapeRecorder {
void trimTo(int position) {
if (position >= 0 && position < lines.size()) {
lines.subList(position, lines.size()).clear();
- lastEventTime = System.currentTimeMillis();
+ lastEventTime = MonotonicClock.millis();
}
}
@@ -88,7 +89,7 @@ class TapeRecorder {
if (!active) {
return;
}
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
long elapsed = now - lastEventTime;
if (elapsed > 200) {
lines.add("Sleep " + formatSleep(elapsed));
@@ -120,7 +121,7 @@ class TapeRecorder {
return;
}
lines.add("Sleep " + formatSleep(ms));
- lastEventTime = System.currentTimeMillis();
+ lastEventTime = MonotonicClock.millis();
}
void recordCaption(String text, int durationSeconds) {
@@ -138,14 +139,14 @@ class TapeRecorder {
if (durationSeconds > 0) {
lines.add("Sleep " + durationSeconds + "s");
}
- lastEventTime = System.currentTimeMillis();
+ lastEventTime = MonotonicClock.millis();
}
void resetClock() {
if (!active) {
return;
}
- lastEventTime = System.currentTimeMillis();
+ lastEventTime = MonotonicClock.millis();
}
String stop() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
index 15b93e9c7e1a..376e391aaa99 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ThreadsTab.java
@@ -149,7 +149,7 @@ class ThreadsTab extends AbstractTableTab {
@Override
protected void renderContent(Frame frame, Rect area, IntegrationInfo info)
{
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
if (!allThreads.isEmpty() && now - lastRefreshTime >=
AUTO_REFRESH_INTERVAL_MS) {
loadThreads();
}
@@ -426,7 +426,7 @@ class ThreadsTab extends AbstractTableTab {
threadCount = tc;
peakThreadCount = peak;
lastPid = pid;
- lastRefreshTime = System.currentTimeMillis();
+ lastRefreshTime = MonotonicClock.millis();
});
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
index 0b89c26f32f2..ce4d9bf8884d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiHelper.java
@@ -657,8 +657,8 @@ final class TuiHelper {
};
static JsonObject pollJsonResponse(Path outputFile, long timeout) {
- long start = System.currentTimeMillis();
- while (System.currentTimeMillis() - start < timeout) {
+ long start = MonotonicClock.millis();
+ while (MonotonicClock.millis() - start < timeout) {
try {
Thread.sleep(100);
if (Files.exists(outputFile) && outputFile.toFile().length() >
0) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index 82b3e279469d..0fa62c2479e4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -134,7 +134,7 @@ class TuiMcpServer {
}
boolean isRecentActivity() {
- return System.currentTimeMillis() - lastToolCallTime < 2000;
+ return MonotonicClock.millis() - lastToolCallTime < 2000;
}
int getToolCallCount() {
@@ -146,7 +146,7 @@ class TuiMcpServer {
* client that never says initialize, one that sent a request in the last
minute. Null when there is none.
*/
String getConnectedClient() {
- if (sessionOpen || System.currentTimeMillis() - lastActivity <
CLIENT_TIMEOUT_MS) {
+ if (sessionOpen || MonotonicClock.millis() - lastActivity <
CLIENT_TIMEOUT_MS) {
return clientName != null ? clientName : "connected";
}
return null;
@@ -154,12 +154,12 @@ class TuiMcpServer {
/** For tests: as if the last request came that many milliseconds ago. */
void idleFor(long millis) {
- lastActivity = System.currentTimeMillis() - millis;
+ lastActivity = MonotonicClock.millis() - millis;
}
private void handleMcp(HttpExchange exchange) throws IOException {
try {
- lastActivity = System.currentTimeMillis();
+ lastActivity = MonotonicClock.millis();
String method = exchange.getRequestMethod();
if ("DELETE".equals(method)) {
// the client closes its session: it is gone
@@ -527,7 +527,7 @@ class TuiMcpServer {
args = Map.of();
}
- lastToolCallTime = System.currentTimeMillis();
+ lastToolCallTime = MonotonicClock.millis();
toolCallCount.incrementAndGet();
String text;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 83010b29f77a..db43992f1617 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -503,8 +503,8 @@ class TuiToolRegistry {
}
long beforeGen = facade.getRenderGeneration();
- long deadline = System.currentTimeMillis() + 2000;
- while (System.currentTimeMillis() < deadline) {
+ long deadline = MonotonicClock.millis() + 2000;
+ while (MonotonicClock.millis() < deadline) {
if (facade.getRenderGeneration() >= beforeGen + 2) {
break;
}
@@ -569,12 +569,12 @@ class TuiToolRegistry {
return "Queued " + sent + " key(s) with " + delay + "ms delay";
}
- long lastKeyFireAt = System.currentTimeMillis() + (long) (sent - 1) *
delay;
+ long lastKeyFireAt = MonotonicClock.millis() + (long) (sent - 1) *
delay;
long waitDeadline = lastKeyFireAt + 5000;
- long start = System.currentTimeMillis();
+ long start = MonotonicClock.millis();
- while (System.currentTimeMillis() < waitDeadline) {
- long now = System.currentTimeMillis();
+ while (MonotonicClock.millis() < waitDeadline) {
+ long now = MonotonicClock.millis();
if (now >= lastKeyFireAt) {
long gen = facade.getRenderGeneration();
if (gen >= beforeGen + sent + 2) {
@@ -593,7 +593,7 @@ class TuiToolRegistry {
JsonObject result = new JsonObject();
result.put("sent", sent);
result.put("delay", delay);
- result.put("waitedMs", System.currentTimeMillis() - start);
+ result.put("waitedMs", MonotonicClock.millis() - start);
if (buf != null) {
result.put("screen", ExportRequest.export(buf).text().toString());
}
@@ -655,16 +655,16 @@ class TuiToolRegistry {
}
long startGeneration = facade.getRenderGeneration();
- long start = System.currentTimeMillis();
+ long start = MonotonicClock.millis();
long deadline = start + timeout;
- while (System.currentTimeMillis() < deadline) {
+ while (MonotonicClock.millis() < deadline) {
long current = facade.getRenderGeneration();
if (current >= startGeneration + requiredFrames) {
Buffer buf = facade.getLastBuffer();
JsonObject result = new JsonObject();
result.put("settled", true);
- result.put("waitedMs", System.currentTimeMillis() - start);
+ result.put("waitedMs", MonotonicClock.millis() - start);
result.put("frames", current - startGeneration);
if (buf != null) {
result.put("screen",
ExportRequest.export(buf).text().toString());
@@ -682,7 +682,7 @@ class TuiToolRegistry {
JsonObject result = new JsonObject();
result.put("settled", false);
- result.put("waitedMs", System.currentTimeMillis() - start);
+ result.put("waitedMs", MonotonicClock.millis() - start);
result.put("reason", "timeout");
return Jsoner.serialize(result);
}
@@ -1111,9 +1111,9 @@ class TuiToolRegistry {
/** Calls the lookup until it finds something or the time is up; the last
answer (null when not found). */
static String retryUntilFound(Supplier<String> lookup, BooleanSupplier
settled, long maxWaitMs) {
- long until = System.currentTimeMillis() + maxWaitMs;
+ long until = MonotonicClock.millis() + maxWaitMs;
String found = lookup.get();
- while (found == null && !settled.getAsBoolean() &&
System.currentTimeMillis() < until) {
+ while (found == null && !settled.getAsBoolean() &&
MonotonicClock.millis() < until) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
@@ -1973,8 +1973,8 @@ class TuiToolRegistry {
// an example can fail a few seconds after it is launched (a missing
API key, a bad route): wait for Camel to
// say it started, or for the failure, so the agent learns how it went
LaunchManager.LaunchOutcome outcome = null;
- long deadline = System.currentTimeMillis() + RUN_EXAMPLE_WAIT_MS;
- while ((outcome = lm.outcome(name)) == null &&
System.currentTimeMillis() < deadline) {
+ long deadline = MonotonicClock.millis() + RUN_EXAMPLE_WAIT_MS;
+ while ((outcome = lm.outcome(name)) == null && MonotonicClock.millis()
< deadline) {
Thread.sleep(250);
}
JsonObject result = new JsonObject();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 9aba88c5c5ee..e3cef1f7aca9 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -71,13 +71,13 @@ class AiPanelTest {
@Test
void thePanelIsIdleOnlyWhenNothingIsTypedOrAsked() {
AiPanel panel = new AiPanel();
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
assertTrue(panel.idleFor(AiPanel.OVERVIEW_IDLE_MS, now), "nothing
typed yet");
type(panel, "why");
- assertFalse(panel.idleFor(AiPanel.OVERVIEW_IDLE_MS,
System.currentTimeMillis()), "typing a question");
+ assertFalse(panel.idleFor(AiPanel.OVERVIEW_IDLE_MS,
MonotonicClock.millis()), "typing a question");
// still typed, even long after the last key
- assertFalse(panel.idleFor(AiPanel.OVERVIEW_IDLE_MS,
System.currentTimeMillis() + 60_000));
+ assertFalse(panel.idleFor(AiPanel.OVERVIEW_IDLE_MS,
MonotonicClock.millis() + 60_000));
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
index 583e079227f5..7bd8a8bcc9ad 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
@@ -18,12 +18,14 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.camel.util.json.JsonObject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
/**
* The infra services an example needs may name the implementation too ({@code
aws sqs}): it is started with both, and
@@ -60,12 +62,12 @@ class LaunchManagerInfraTest {
lm.launchInfra("kafka", List.of("sleep", "30"));
lm.deferUntilInfra(List.of("kafka"), "kafka-orders", () ->
launched.set(true));
- lm.tick(System.currentTimeMillis());
+ lm.tick(MonotonicClock.millis());
assertThat(launched).isFalse();
assertThat(notices).isEmpty();
running.add(infra("kafka"));
- lm.tick(System.currentTimeMillis());
+ lm.tick(MonotonicClock.millis());
assertThat(launched).isTrue();
assertThat(notices).contains("Started: kafka");
ProcessHandle.current().children()
@@ -80,11 +82,11 @@ class LaunchManagerInfraTest {
lm.launchInfra("aws sqs", List.of("sleep", "30"));
lm.deferUntilInfra(List.of("aws sqs"), "aws-sqs", () ->
launched.set(true));
- lm.tick(System.currentTimeMillis());
+ lm.tick(MonotonicClock.millis());
assertThat(launched).isFalse();
running.add(infra("aws"));
- lm.tick(System.currentTimeMillis());
+ lm.tick(MonotonicClock.millis());
assertThat(launched).isTrue();
ProcessHandle.current().children()
.filter(p -> p.info().command().map(c ->
c.endsWith("sleep")).orElse(false))
@@ -101,11 +103,10 @@ class LaunchManagerInfraTest {
// the container cannot bind its port: camel infra run ends with an
error
lm.launchInfra("mosquitto", List.of("sh", "-c", "echo port is already
allocated; exit 1"));
lm.deferUntilInfra(List.of("mosquitto"), "mqtt", () ->
launched.set(true));
- long deadline = System.currentTimeMillis() + 10_000;
- while (failures.isEmpty() && System.currentTimeMillis() < deadline) {
- lm.tick(System.currentTimeMillis());
- Thread.sleep(50);
- }
+ await().atMost(10, TimeUnit.SECONDS).pollInterval(50,
TimeUnit.MILLISECONDS).pollInSameThread().until(() -> {
+ lm.tick(MonotonicClock.millis());
+ return !failures.isEmpty();
+ });
assertThat(launched).isFalse();
assertThat(failures).containsExactly("mosquitto");
@@ -138,15 +139,14 @@ class LaunchManagerInfraTest {
lm.setNotificationCallback((msg, error) -> notices.add(msg));
lm.launchInfra("ollama", List.of("sh", "-c",
"echo 'Pulling image layers: 3 pending, 2 downloaded, 0
extracted, (1.2 GB/3.4 GB)'; sleep 30"));
- long deadline = System.currentTimeMillis() + 10_000;
- while (notices.isEmpty() && System.currentTimeMillis() < deadline) {
- lm.tick(System.currentTimeMillis());
- Thread.sleep(50);
- }
+ await().atMost(10, TimeUnit.SECONDS).pollInterval(50,
TimeUnit.MILLISECONDS).pollInSameThread().until(() -> {
+ lm.tick(MonotonicClock.millis());
+ return !notices.isEmpty();
+ });
assertThat(notices).containsExactly("Pulling image of ollama: 2 of 5
layers, 1.2 GB of 3.4 GB (35%)");
// the same progress is not shown again
- lm.tick(System.currentTimeMillis());
+ lm.tick(MonotonicClock.millis());
assertThat(notices).hasSize(1);
ProcessHandle.current().children()
.filter(p -> p.info().command().map(c ->
c.endsWith("sh")).orElse(false))
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClockTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClockTest.java
new file mode 100644
index 000000000000..a79cbcf78895
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicClockTest.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+class MonotonicClockTest {
+
+ @Test
+ void neverGoesBack() {
+ long previous = MonotonicClock.millis();
+ for (int i = 0; i < 100_000; i++) {
+ long now = MonotonicClock.millis();
+ assertThat(now).isGreaterThanOrEqualTo(previous);
+ previous = now;
+ }
+ }
+
+ @Test
+ void movesOnWithTheTimeThatPasses() {
+ long start = MonotonicClock.millis();
+ long startNanos = System.nanoTime();
+ await().atMost(5, TimeUnit.SECONDS)
+ .until(() -> System.nanoTime() - startNanos >=
TimeUnit.MILLISECONDS.toNanos(50));
+
+ assertThat(MonotonicClock.millis() - start).isGreaterThanOrEqualTo(50);
+ }
+
+ @Test
+ void aTimeNeverSetReadsAsLongAgo() {
+ // a time field starts at 0: an interval since then has passed, as it
has on the wall clock
+ long neverSet = 0;
+ assertThat(MonotonicClock.millis() -
neverSet).isGreaterThanOrEqualTo(TimeUnit.DAYS.toMillis(1));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicTimersTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicTimersTest.java
new file mode 100644
index 000000000000..196be71bde73
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonotonicTimersTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import dev.tamboui.style.Style;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The timers run on the {@link MonotonicClock}, so a wall clock set back or
forward (an NTP step, a manual change, a VM
+ * resume) neither freezes them nor fires them at once. The times here are far
from any wall-clock time: a timer that
+ * read the wall clock would fire at once, or never.
+ */
+class MonotonicTimersTest {
+
+ /** A clock the tests set by hand. */
+ private static final class FakeClock {
+ long now;
+ }
+
+ @Test
+ void aPendingMcpKeyFiresOnTime() {
+ Queue<McpFacade.PendingKey> keys = new ConcurrentLinkedQueue<>();
+ McpFacade facade = new McpFacade(null, null, null, null, null, null,
null, null, null, null, null, keys, null);
+
+ long before = MonotonicClock.millis();
+ assertThat(facade.injectKeys(List.of("a", "Enter"), 500)).isEqualTo(2);
+ long after = MonotonicClock.millis();
+
+ // the tick compares the keys with the monotonic time: the first fires
at once, the next half a second later
+ McpFacade.PendingKey first = keys.poll();
+ McpFacade.PendingKey second = keys.poll();
+ assertThat(first.fireAt()).isBetween(before, after);
+ assertThat(first.isDue(after)).isTrue();
+ assertThat(second.isDue(first.fireAt() + 499)).isFalse();
+ assertThat(second.isDue(first.fireAt() + 500)).isTrue();
+ }
+
+ @Test
+ void aCaptionExpiresOnItsClock() {
+ FakeClock clock = new FakeClock();
+ clock.now = 5_000;
+ CaptionOverlay caption = new CaptionOverlay(() -> clock.now);
+
+ caption.showCaption("Hello", 2);
+ clock.now += 2_000;
+ caption.tick(clock.now);
+ assertThat(caption.isCaptionVisible()).as("shown for two
seconds").isTrue();
+ clock.now += 1;
+ caption.tick(clock.now);
+ assertThat(caption.isCaptionVisible()).isFalse();
+
+ // without a duration: typed a character every 50 ms, then held for
three seconds and faded out in one
+ caption.showCaption("Hi");
+ clock.now += 100;
+ caption.tick(clock.now);
+ clock.now += 4_000;
+ caption.tick(clock.now);
+ assertThat(caption.isCaptionVisible()).as("held and fading").isTrue();
+ clock.now += 1;
+ caption.tick(clock.now);
+ assertThat(caption.isCaptionVisible()).isFalse();
+ }
+
+ @Test
+ void aDrawingExpiresOnTime() {
+ DrawOverlay overlay = new DrawOverlay();
+ long before = MonotonicClock.millis();
+ overlay.setDrawing(List.of(new DrawOverlay.DrawCell(0, 0, "x",
Style.EMPTY)), 1);
+ long after = MonotonicClock.millis();
+
+ // the tick passes the monotonic time: the drawing shows for its
second, then goes
+ overlay.tick(before + 1_000);
+ assertThat(overlay.isVisible()).isTrue();
+ overlay.tick(after + 1_001);
+ assertThat(overlay.isVisible()).isFalse();
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitorTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitorTest.java
index aeef31603617..0591699e51fc 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitorTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OllamaMonitorTest.java
@@ -170,7 +170,7 @@ class OllamaMonitorTest {
assertEquals(12, first.contextPercent());
// the runner's slot wins over the model list when present
- monitor.updateSlot(slot(false, 0, System.currentTimeMillis()));
+ monitor.updateSlot(slot(false, 0));
monitor.recordRequest("m", new LlmClient.TokenUsage(20_000, 60,
20_060, 0, 900, 800, 0, 1800), 0, "stop");
assertEquals(262144, monitor.snapshot().lastRequest().contextSize());
assertEquals(7, monitor.snapshot().lastRequest().contextPercent());
@@ -188,10 +188,11 @@ class OllamaMonitorTest {
@Test
void slotSamplesDriveTheLiveDecodeRateAndHistory() {
OllamaMonitor monitor = new OllamaMonitor();
- long t0 = System.currentTimeMillis();
- monitor.updateSlot(slot(true, 0, t0));
- monitor.updateSlot(slot(true, 25, t0 + 500));
- monitor.updateSlot(slot(true, 50, t0 + 1000));
+ // read half a second apart, by the monotonic clock the rates are
measured with
+ long t0 = MonotonicClock.millis();
+ monitor.updateSlot(slot(true, 0), t0);
+ monitor.updateSlot(slot(true, 25), t0 + 500);
+ monitor.updateSlot(slot(true, 50), t0 + 1000);
monitor.tick(t0 + 1000);
Snapshot s = monitor.snapshot();
@@ -422,7 +423,7 @@ class OllamaMonitorTest {
List.of("completion", "tools")))));
monitor.updateInstalled(List.of("qwen3.6:35b-a3b", "llama3.2:latest"));
monitor.updateRunner(new OllamaMonitor.RunnerInfo(23629, 58237,
262144, 1, "/blob", "llama-server"));
- monitor.updateSlot(slot(false, 6, System.currentTimeMillis()));
+ monitor.updateSlot(slot(false, 6));
monitor.updateHost(new OllamaMonitor.HostStats(
new OllamaMonitor.GpuStats("Apple GPU", 25, 31463079936L,
34170552320L, 1),
new OllamaMonitor.ProcessStats(70566, "ollama serve", 0.1,
82176L * 1024),
@@ -476,8 +477,8 @@ class OllamaMonitorTest {
assertEquals(0, ((JsonArray) json.get("requests")).size());
}
- private static SlotState slot(boolean processing, long decoded, long
atMillis) {
- return new SlotState(processing, 25, 25, 9, decoded, 262144,
"draft-mtp", 1, Instant.ofEpochMilli(atMillis));
+ private static SlotState slot(boolean processing, long decoded) {
+ return new SlotState(processing, 25, 25, 9, decoded, 262144,
"draft-mtp", 1, Instant.now());
}
private static SpanEntry genAiSpan(
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OpenedProjectLifecycleTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OpenedProjectLifecycleTest.java
index 804241b47acf..dc21943f9b18 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OpenedProjectLifecycleTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OpenedProjectLifecycleTest.java
@@ -73,7 +73,7 @@ class OpenedProjectLifecycleTest {
IntegrationInfo project = new IntegrationInfo();
project.name = "custom-kamelet";
project.sourceDir = "/work/custom-kamelet";
- project.startingSince = System.currentTimeMillis();
+ project.startingSince = MonotonicClock.millis();
project.launchedProcess = ProcessHandle.current();
ctx.addPhantom(project);
ctx.selectedPid = project.pid;
@@ -97,7 +97,7 @@ class OpenedProjectLifecycleTest {
IntegrationInfo project = new IntegrationInfo();
project.name = "orders";
project.sourceDir = "/work/orders";
- project.startingSince = System.currentTimeMillis();
+ project.startingSince = MonotonicClock.millis();
// camel run pom.xml ended (its Maven build failed) before the app
showed up
Process run = new ProcessBuilder("true").start();
run.waitFor();
@@ -115,7 +115,7 @@ class OpenedProjectLifecycleTest {
project.name = "metrics";
project.sourceDir = "/work/metrics";
ctx.addPhantom(project);
- long now = System.currentTimeMillis();
+ long now = MonotonicClock.millis();
assertEquals("✖ Stopped", OverviewTab.projectStatus(project,
now).content());
project.startingSince = now;
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java
index c14680b49259..30020113ecca 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RecordingManagerTest.java
@@ -266,7 +266,7 @@ class RecordingManagerTest {
assertFalse(manager.getRecentKeys().isEmpty());
// Tick with a time far in the future to expire all entries
- manager.tickRecentKeys(System.currentTimeMillis() + 10_000);
+ manager.tickRecentKeys(MonotonicClock.millis() + 10_000);
assertTrue(manager.getRecentKeys().isEmpty(),
"Old key records should be removed after tick");
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
index 70f87f09c609..ee254414a8d1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
@@ -116,7 +116,7 @@ class RunOptionsFormTest {
List<String> inner = renderPopupRows(form);
// Name .. JFR: one row each, nothing left empty below the last
checkbox
- assertThat(inner).hasSize(14 +
(form.portWarning(System.currentTimeMillis()) != null ? 1 : 0));
+ assertThat(inner).hasSize(14 +
(form.portWarning(MonotonicClock.millis()) != null ? 1 : 0));
assertThat(inner).noneMatch(String::isBlank);
}