zhouziyu02 Cursor commited on
Commit
99ee5de
·
1 Parent(s): 62f5d66

Fix eval countdown to use persisted next_eval_at deadline.

Browse files

Store next_eval_at in eval_state.json after each run and derive the
progress bar from the absolute deadline so page reloads do not reset
the 1-hour cycle to zero.

Co-authored-by: Cursor <cursoragent@cursor.com>

app.py CHANGED
@@ -102,6 +102,7 @@ def maybe_refresh_after_eval() -> tuple:
102
  str(eval_state.get("status", "")),
103
  str(eval_state.get("updated_at", "")),
104
  str(eval_state.get("finished_at", "")),
 
105
  ]
106
  )
107
  if token == _last_seen_eval_token:
 
102
  str(eval_state.get("status", "")),
103
  str(eval_state.get("updated_at", "")),
104
  str(eval_state.get("finished_at", "")),
105
+ str(eval_state.get("next_eval_at_unix", "")),
106
  ]
107
  )
108
  if token == _last_seen_eval_token:
scripts/run_space_eval.py CHANGED
@@ -24,6 +24,8 @@ from tsfm_bench.data.registry import load_data_source, load_dataset_properties
24
  from tsfm_bench.data.ts_bench import TsBenchDataSource
25
  from tsfm_bench.eval.api_predictor import DEFAULT_QUANTILES, TsfmApiConfig, TsfmApiPredictor
26
  from tsfm_bench.eval.online_eval import run_online_eval_for_dataset
 
 
27
 
28
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
29
  logger = logging.getLogger(__name__)
@@ -250,29 +252,37 @@ def run_evaluation() -> int:
250
  },
251
  )
252
  write_eval_state(
253
- {
254
- "status": status,
255
- "started_at": started,
256
- "finished_at": finished,
257
- "failed_models": failed_models,
258
- "task_count": len(datasets),
259
- "model_count": len(model_specs) - len(failed_models),
260
- "message": "Evaluation complete",
261
- "updated_at": finished,
262
- }
 
 
 
 
263
  )
264
  return 0 if status == "ok" else 2
265
  except Exception as exc:
266
  finished = datetime.now(timezone.utc).isoformat()
267
  logger.exception("Evaluation run failed")
268
  write_eval_state(
269
- {
270
- "status": "error",
271
- "started_at": started,
272
- "finished_at": finished,
273
- "message": str(exc),
274
- "updated_at": finished,
275
- }
 
 
 
 
276
  )
277
  return 1
278
 
 
24
  from tsfm_bench.data.ts_bench import TsBenchDataSource
25
  from tsfm_bench.eval.api_predictor import DEFAULT_QUANTILES, TsfmApiConfig, TsfmApiPredictor
26
  from tsfm_bench.eval.online_eval import run_online_eval_for_dataset
27
+ from src.eval_schedule import with_next_eval_fields
28
+ from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS
29
 
30
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
31
  logger = logging.getLogger(__name__)
 
252
  },
253
  )
254
  write_eval_state(
255
+ with_next_eval_fields(
256
+ {
257
+ "status": status,
258
+ "started_at": started,
259
+ "finished_at": finished,
260
+ "failed_models": failed_models,
261
+ "task_count": len(datasets),
262
+ "model_count": len(model_specs) - len(failed_models),
263
+ "message": "Evaluation complete",
264
+ "updated_at": finished,
265
+ },
266
+ finished,
267
+ BENCHMARK_INTERVAL_SECONDS,
268
+ )
269
  )
270
  return 0 if status == "ok" else 2
271
  except Exception as exc:
272
  finished = datetime.now(timezone.utc).isoformat()
273
  logger.exception("Evaluation run failed")
274
  write_eval_state(
275
+ with_next_eval_fields(
276
+ {
277
+ "status": "error",
278
+ "started_at": started,
279
+ "finished_at": finished,
280
+ "message": str(exc),
281
+ "updated_at": finished,
282
+ },
283
+ finished,
284
+ BENCHMARK_INTERVAL_SECONDS,
285
+ )
286
  )
287
  return 1
288
 
src/display/css_html_js.py CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  def build_custom_css() -> str:
2
  return """
3
  .markdown-text { font-size: 16px !important; }
@@ -29,13 +36,27 @@ def build_custom_css() -> str:
29
  """
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  def eval_countdown_markup(next_eval_at_unix: int, interval_seconds: int, running: bool = False) -> str:
33
  if running:
34
  label = "Evaluation in progress…"
35
  progress = 0
36
  else:
37
- label = "Next eval cycle loading…"
38
- progress = 0
 
 
39
 
40
  return f"""
41
  <div class="eval-countdown-wrap" id="tsfm-eval-countdown"
@@ -44,14 +65,14 @@ def eval_countdown_markup(next_eval_at_unix: int, interval_seconds: int, running
44
  data-running="{'1' if running else '0'}">
45
  <div class="eval-countdown-label" id="tsfm-eval-countdown-label">{label}</div>
46
  <div class="eval-countdown-track">
47
- <div class="eval-countdown-fill" id="tsfm-eval-countdown-fill" style="width: {progress}%;"></div>
48
  </div>
49
  </div>
50
  """
51
 
52
 
53
  def eval_countdown_head_js() -> str:
54
- """Countdown to next eval cycle; progress bar fills as the interval elapses."""
55
  return """
56
  <script>
57
  (function () {
@@ -90,16 +111,30 @@ def eval_countdown_head_js() -> str:
90
  fill.style.width = pct + "%";
91
  }
92
 
93
- function start() {
94
  if (window.__tsfmEvalCountdownId) return;
95
- tick();
96
  window.__tsfmEvalCountdownId = setInterval(tick, 1000);
97
  }
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  if (document.readyState === "loading") {
100
- document.addEventListener("DOMContentLoaded", start);
101
  } else {
102
- start();
103
  }
104
  })();
105
  </script>
 
1
+ """Display helpers for the Gradio leaderboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+
8
  def build_custom_css() -> str:
9
  return """
10
  .markdown-text { font-size: 16px !important; }
 
36
  """
37
 
38
 
39
+ def _format_remaining(total_seconds: int) -> str:
40
+ total_seconds = max(0, total_seconds)
41
+ hours = total_seconds // 3600
42
+ minutes = (total_seconds % 3600) // 60
43
+ seconds = total_seconds % 60
44
+ if hours > 0:
45
+ return f"{hours}h {minutes}m {seconds}s"
46
+ if minutes > 0:
47
+ return f"{minutes}m {seconds}s"
48
+ return f"{seconds}s"
49
+
50
+
51
  def eval_countdown_markup(next_eval_at_unix: int, interval_seconds: int, running: bool = False) -> str:
52
  if running:
53
  label = "Evaluation in progress…"
54
  progress = 0
55
  else:
56
+ remaining = max(0, next_eval_at_unix - int(time.time()))
57
+ elapsed = max(0, interval_seconds - remaining)
58
+ progress = min(100, (elapsed / interval_seconds) * 100) if interval_seconds > 0 else 0
59
+ label = f"Next eval in {_format_remaining(remaining)}"
60
 
61
  return f"""
62
  <div class="eval-countdown-wrap" id="tsfm-eval-countdown"
 
65
  data-running="{'1' if running else '0'}">
66
  <div class="eval-countdown-label" id="tsfm-eval-countdown-label">{label}</div>
67
  <div class="eval-countdown-track">
68
+ <div class="eval-countdown-fill" id="tsfm-eval-countdown-fill" style="width: {progress:.2f}%;"></div>
69
  </div>
70
  </div>
71
  """
72
 
73
 
74
  def eval_countdown_head_js() -> str:
75
+ """Countdown to next eval cycle; uses server-persisted absolute deadline."""
76
  return """
77
  <script>
78
  (function () {
 
111
  fill.style.width = pct + "%";
112
  }
113
 
114
+ function startInterval() {
115
  if (window.__tsfmEvalCountdownId) return;
 
116
  window.__tsfmEvalCountdownId = setInterval(tick, 1000);
117
  }
118
 
119
+ function boot() {
120
+ tick();
121
+ startInterval();
122
+ if (window.__tsfmEvalCountdownObserver) return;
123
+ window.__tsfmEvalCountdownObserver = new MutationObserver(function () {
124
+ if (document.getElementById("tsfm-eval-countdown")) {
125
+ tick();
126
+ }
127
+ });
128
+ window.__tsfmEvalCountdownObserver.observe(document.body, {
129
+ childList: true,
130
+ subtree: true,
131
+ });
132
+ }
133
+
134
  if (document.readyState === "loading") {
135
+ document.addEventListener("DOMContentLoaded", boot);
136
  } else {
137
+ boot();
138
  }
139
  })();
140
  </script>
src/eval_schedule.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Persisted schedule for the next live evaluation cycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+
7
+ from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS
8
+
9
+
10
+ def parse_iso_to_unix(value: str) -> int:
11
+ normalized = value.replace("Z", "+00:00")
12
+ return int(datetime.fromisoformat(normalized).timestamp())
13
+
14
+
15
+ def compute_next_eval_at_iso(finished_at: str, interval_seconds: int | None = None) -> str:
16
+ interval = interval_seconds or BENCHMARK_INTERVAL_SECONDS
17
+ finished_dt = datetime.fromisoformat(finished_at.replace("Z", "+00:00"))
18
+ if finished_dt.tzinfo is None:
19
+ finished_dt = finished_dt.replace(tzinfo=timezone.utc)
20
+ return (finished_dt + timedelta(seconds=interval)).isoformat()
21
+
22
+
23
+ def compute_next_eval_at_unix(
24
+ state: dict,
25
+ interval_seconds: int | None = None,
26
+ ) -> int:
27
+ """Absolute unix time for the next eval; survives process restarts and page reloads."""
28
+ interval = interval_seconds or BENCHMARK_INTERVAL_SECONDS
29
+
30
+ if state.get("status") == "running":
31
+ return int(datetime.now(timezone.utc).timestamp())
32
+
33
+ if raw_unix := state.get("next_eval_at_unix"):
34
+ return int(raw_unix)
35
+
36
+ if next_at := state.get("next_eval_at"):
37
+ return parse_iso_to_unix(next_at)
38
+
39
+ anchor = state.get("finished_at") or state.get("updated_at")
40
+ if anchor:
41
+ return parse_iso_to_unix(anchor) + interval
42
+
43
+ return int(datetime.now(timezone.utc).timestamp()) + interval
44
+
45
+
46
+ def with_next_eval_fields(
47
+ payload: dict,
48
+ finished_at: str,
49
+ interval_seconds: int | None = None,
50
+ ) -> dict:
51
+ interval = interval_seconds or BENCHMARK_INTERVAL_SECONDS
52
+ next_at = compute_next_eval_at_iso(finished_at, interval)
53
+ enriched = dict(payload)
54
+ enriched["next_eval_at"] = next_at
55
+ enriched["next_eval_at_unix"] = parse_iso_to_unix(next_at)
56
+ return enriched
src/eval_scheduler.py CHANGED
@@ -13,6 +13,7 @@ from datetime import datetime, timezone
13
  from pathlib import Path
14
 
15
  from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS
 
16
 
17
  logger = logging.getLogger(__name__)
18
 
@@ -77,10 +78,15 @@ class LiveEvalScheduler:
77
  return max(0, int(self._next_run_at - time.monotonic()))
78
 
79
  def next_eval_at_unix(self) -> int:
 
 
 
80
  remaining = self.seconds_until_next_run()
81
  if remaining is None:
82
- return int(time.time()) + self.interval_seconds
83
- return int(time.time()) + remaining
 
 
84
 
85
  def is_running(self) -> bool:
86
  return self.snapshot().get("status") == "running"
@@ -104,6 +110,23 @@ class LiveEvalScheduler:
104
  finally:
105
  self._lock.release()
106
  self._next_run_at = time.monotonic() + self.interval_seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def _write_idle_state(self, message: str) -> None:
109
  results = SPACE_ROOT / "results"
 
13
  from pathlib import Path
14
 
15
  from src.benchmark_config import BENCHMARK_INTERVAL_SECONDS
16
+ from src.eval_schedule import compute_next_eval_at_unix, with_next_eval_fields
17
 
18
  logger = logging.getLogger(__name__)
19
 
 
78
  return max(0, int(self._next_run_at - time.monotonic()))
79
 
80
  def next_eval_at_unix(self) -> int:
81
+ state = self.snapshot()
82
+ persisted = compute_next_eval_at_unix(state, self.interval_seconds)
83
+
84
  remaining = self.seconds_until_next_run()
85
  if remaining is None:
86
+ return persisted
87
+
88
+ in_memory = int(time.time()) + remaining
89
+ return min(persisted, in_memory)
90
 
91
  def is_running(self) -> bool:
92
  return self.snapshot().get("status") == "running"
 
110
  finally:
111
  self._lock.release()
112
  self._next_run_at = time.monotonic() + self.interval_seconds
113
+ self._persist_next_eval_after_run()
114
+
115
+ def _persist_next_eval_after_run(self) -> None:
116
+ state_path = SPACE_ROOT / "results" / "eval_state.json"
117
+ if not state_path.exists():
118
+ return
119
+ try:
120
+ state = json.loads(state_path.read_text())
121
+ except json.JSONDecodeError:
122
+ return
123
+ if state.get("status") == "running":
124
+ return
125
+ anchor = state.get("finished_at") or state.get("updated_at")
126
+ if not anchor:
127
+ return
128
+ enriched = with_next_eval_fields(state, anchor, self.interval_seconds)
129
+ state_path.write_text(json.dumps(enriched, indent=4) + "\n")
130
 
131
  def _write_idle_state(self, message: str) -> None:
132
  results = SPACE_ROOT / "results"